qdam 发表于 2019-3-20 20:24

分享一道有意思的CTF逆向题

本人刚刚接触CTF不到半年,还是属于一个萌新,最近解出一道比较有意思逆向题,想和大家分享一下,还希望各位大佬能够指点一下。
先奉上附件
这道题是通过击败boss获得flag

先看一下界面

可以看到初始时自己只有100HP,而boss有999HP,有点悬殊
而题目中也有提示,要根据游戏内的bug获得胜利,因此接下来就是逆向分析了,推荐各位可以在分析之前先玩一玩,看看每个道具有什么效果,有助于解题
这也是我第一次用IDA解题吧。先看源代码,有些部分注释可能表达的不清楚,还望理解。
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3; // al
unsigned int input_num_sub1; // eax
int v5; // eax
int decrease_blood_num; // eax
int v7; // eax
int input_num; //
char v9; //

sub_4024F0();
sub_401911();
sub_401936();
while ( 1 )
{
    while ( 1 )
    {
      input_num = if_attack_me();
      if ( !input_num )
      break;
      decrease_count((int *)&unk_403188);
      v7 = decreasing_blood((int)&boss_blood, &my_blood);
      input_num = v7;
      if ( my_blood <= 0 )
      {
      puts("You died!");
      exit(0);
      }
      sprintf(&v9, "You were attacked by BOSS, and your HP decreases by %d.", v7);
      sub_4016F4(&v9);
    }
    decrease_count(&myblood_add8);            // 调整状态栏
    sub_40181E();                               // 无关函数,输出固定字符
    sub_401766();
    sub_4017B8();                               // 无关函数
    input_num = -1;
    while ( 1 )
    {
      printf("Please input your choice number: ");
      fflush(&iob);
      if ( scanf("%d", &input_num) <= 0 )       // 不能小于0
      {
      do
          v3 = getchar();
      while ( v3 != 10 && v3 != -1 );         // =-1?
      }
      input_num_sub1 = input_num - 1;
      input_num = input_num_sub1;               // 输入的数值减一
      if ( input_num_sub1 <= 4 )                // 不能大于5
      break;
      input_num = -1;
      puts("Error number!");
LABEL_12:
      v5 = input_num;
      if ( input_num != -1 )
      goto LABEL_13;
    }
    if ( input_num_sub1 != 4 )                  // 使用物品
      break;
    v5 = input_num;
LABEL_13:
    if ( v5 == 4 )
    {
      decrease_blood_num = decreasing_blood((int)&my_blood, &boss_blood);
      input_num = decrease_blood_num;
      if ( boss_blood <= 0 )
      {
      puts("You win!");
      sub_401955();
      exit(0);
      }
      sprintf(&v9, "You attacked BOSS, and it's HP decreases by %d.", decrease_blood_num);
      sub_4016F4(&v9);
    }
    else
    {
      --dword_403080;                  // 物品数量减一
      add_state(&myblood_add8, v5, (v5 != 3) + 2);// 物品4的可执行次数为2次,其余为3次
      sprintf(&v9, "You used item %s and gain a state \"%s\".", &aReporter, off_403260);
      sub_4016F4(&v9);
    }
}
if ( !dword_403080 )
{
    input_num = -1;
    puts("You don't have this item!");
}
goto LABEL_12;
}


这里面最重要的应该就是while语句里面的代码。
先来看第一个部分
input_num = if_attack_me();
      if ( !input_num )
      break;
      decrease_count((int *)&unk_403188);
      v7 = decreasing_blood((int)&boss_blood, &my_blood);
      input_num = v7;
      if ( my_blood <= 0 )
      {
      puts("You died!");
      exit(0);
      }
      sprintf(&v9, "You were attacked by BOSS, and your HP decreases by %d.", v7);
      sub_4016F4(&v9);
在sprintf中可以看到,这应该是boss攻击,而里面还有个函数if_attack_me(),这是我根据代码分析后重新命名的,源码是
signed int if_attack_me()
{
signed int result; // eax

if ( dword_403204 > dword_403184 )            // 如果我数值的更大
{
    dword_403204 -= dword_403184;
    dword_403184 = 10;
    result = 1;
}
else
{
    dword_403184 -= dword_403204;
    result = select1_if_exist(&myblood_add8, 0);
    if ( result )
    {
      dword_403204 = 2;
      result = 0;
    }
    else
    {
      dword_403204 = 5;
    }
}
return result;
}
可以发现这里关键是两个数之间的比较,我们来看一下里面的内存值


可以发现初始状态是一个10,一个是5,结合代码分析后可以知道,程序第一次的返回结果,也就是result一定-1,那么也就是说,第一次boss不会攻击我,这符合实际情况
然后在函数if_attack_me()中还有一个函数是select1_if_exist(),这个函数之后会提到,因为在这里还不好得出函数的功能
然后我们来看一下,假如boss攻击我,那么又会进入另一个函数decreasing_blood(),这个函数在后面我攻击boss中也出现了,而且通过传入的参数可以分析出这个是计算每次攻击所扣除的血量值。
然后我们进入这个函数看一下
int __cdecl decreasing_blood(int A_blood, _DWORD *B_blood)
{
signed int v2; // ebx
int result; // eax

v2 = (unsigned int)select1_if_exist((_DWORD *)(A_blood + 8), 2) < 1 ? 20 : 25;
if ( select1_if_exist((_DWORD *)(A_blood + 8), 3) )
    v2 *= 10;
if ( select1_if_exist(B_blood + 2, 1) )
    v2 /= 2;
result = v2 + rand() % 5;
*B_blood -= result;
return result;
}
函数比较短,同时出现了3个if语句,而且当逻辑值为1时都会改变result的值,回想在玩游戏的时候是有道具,可以猜测一下,这个if语句是对当前我身上有无特定的道具进行判断
可以看到三个判断语句中有两个是关于A_blood,一个是B_blood,而关于A的都是增加伤害,关于B的是减少伤害,那么回想游戏中道具2是Defense,也就是防御,减少伤害,也就是当boss攻击时,假如你有Defense的状态,那么就会减少伤害,这时第三个if语句就会执行,而当你攻击时,就只会去判断第一个和第二个if语句是否成立,这符合之前的猜测。也就是说,这个select1_if_exist()函数应该就是判定我有无特定道具的。
进入该函数看一下
signed int __cdecl select1_if_exist(_DWORD *state, int a2)
{
int v2; // eax

if ( *state <= 0 )
    return 0;
if ( state == a2 )
    return 1;
v2 = 0;
do
{
    if ( ++v2 == *state )
      return 0;
}
while ( state != a2 );
return 1;
}
基本上可以解释通,这里的state应该存放的就是相应道具所对应的值了
接下来看main()函数中后面的程序
decrease_count(&myblood_add8);            // 调整状态栏
    sub_40181E();                               // 无关函数,输出固定字符
    sub_401766();
    sub_4017B8();                               // 无关函数
这几行是boss攻击语句外的最初几条语句,点进第二个和第四个都可以发现这是输出语句的函数,因此基本判定第2,3,4条语句是无关的
那么再来看第一条语句的函数
// 将状态中的可执行次数减一
int __cdecl decrease_count(int *a1)
{
int v1; // ebx
int result; // eax

if ( *a1 > 0 )                              // 如果状态个数大于0
{
    v1 = 0;
    do
    {
      result = a1 - 1;
      a1 = result;
      if ( !result )
      result = sub_40154C(a1, v1);            // 删除该状态并进行调整
      ++v1;
    }
    while ( *a1 > v1 );                         // 循环a1次
}
return result;
}
通过while语句判断,这里传入的a1地址上的内容应该是当前身上状态的个数,而这里我们发现对a1地址后面位移为偶数的内存进行减一操作,同时当减完后为0时,又进入了一个函数sub_40154C()
// a2值状态栏中的位置
int __cdecl sub_40154C(int *myblood_add8, int cur)
{
int result; // eax
int v3; // ebx
int v4; // esi

result = cur;
v3 = *myblood_add8 - 1;                     // 状态个数减一
*myblood_add8 = v3;
if ( v3 > cur )
{
    do                                          // 状态向前移动
    {
      v4 = myblood_add8;
      myblood_add8 = myblood_add8;
      myblood_add8 = v4;
    }
    while ( result != v3 );
}
return result;}
这里在do语句中做的是循环位移操作,而且似乎是2个内层单元为一个部分,这样猜测奇数地址放的是道具对应的编号,而偶数地址放的是该道具的剩余有效回合数,也就是当其值为0时会消失,然后放在后面的道具的数据就会填充到前面
理解完这个最关键的部分后,再到后面,使用道具的程序部分,中间比较简单就跳过了
--dword_403080;                  // 物品数量减一
      add_state(&myblood_add8, v5, (v5 != 3) + 2);// 物品4的可执行次数为2次,其余为3次
也就是这两行代码,第一行代码指的是相应的道具数量减一,点开data数据区就可得出该结论
然后是下一个函数add_state()也就是当你选择道具后会添加该道具到你的状态处,看具体代码
int __cdecl add_state(int *myblood_add8, int input_num, int logic_numisnot3_add_2)
{
int v3; // ecx
int result; // eax

v3 = *myblood_add8;
if ( *myblood_add8 <= 0 )                     // 状态个数小于等于0
{
LABEL_9:                                        // 状态个数加一
    result = v3 + 1;
    *myblood_add8 = v3 + 1;
    myblood_add8 = input_num;       // 增加该状态到末尾
    myblood_add8 = logic_numisnot3_add_2;// 可执行次数
    return result;
}
if ( input_num != myblood_add8 )         // 如果不等于当前第一个状态
{
    result = 0;
    while ( ++result != v3 )                  // 循环状态个数的次数
    {
      if ( input_num == myblood_add8 )// 等于其中某个状态
      goto LABEL_7;
    }
    goto LABEL_9;                               // 最后再增加一个状态
}
result = 0;
LABEL_7:
myblood_add8 += logic_numisnot3_add_2;// 刷新可执行次数
return result;
}
这个函数中最重要的逻辑就是假如你选择的是道具1,2,3,那么对应增加的剩余有效回合数就是3(这里的循环就是找当前身上有没有该道具,如果有就增加,没有就添加并置剩余有效回合数为3),而道具4(也就是能让你伤害*10的道具)剩余有效回合次数只有2(因为传入该函数的参数 logic_numisnot3_add_2为 (v5 != 3) + 2,而v5是道具对应的值-1,比如道具1对应0)
分析到这里以后基本上游戏的逻辑都已经讲完了,那么游戏的bug在哪里呢?

让我们回到decrease_count()函数中
// 将状态中的可执行次数减一
int __cdecl decrease_count(int *a1)
{
int v1; // ebx
int result; // eax

if ( *a1 > 0 )                              // 如果状态个数大于0
{
    v1 = 0;
    do
    {
      result = a1 - 1;
      a1 = result;
      if ( !result )
      result = sub_40154C(a1, v1);            // 删除该状态并进行调整
      ++v1;
    }
    while ( *a1 > v1 );                         // 循环a1次
}
return result;
}

在这里我们看到,当某一个道具还剩1次时(也就是减完一后变成了0),程序会先将后面的所有道具向前移动一格,然后呢,继续将后面的道具次数减一,有没有发现什么?
没错,在移动过程中,之前删掉的道具格子中填入了后面的道具,于是他就逃避了检测(比如说当前有两个状态,剩余次数分别为1 2 2,正常理解下经过该函数后会变成 1 1, 但是由于第一个删掉后,填入了第二个道具的数据,第二个填入了第三个道具的数据,然后检测第二个道具,注意这里的第二个已经是原来的第三个了,因此将其减一,函数就结束了,因此最后就会是 2 1),这个就是程序的bug。
然后我们便要想办法让道具4尽可能的使用多的次数,因为道具4能让伤害*10,如果结合道具3,那么伤害至少是250,如果能执行4次,那么就成功了。
于是我希望在我选择道具4前,目前的状态栏中剩余有效回合次数是4 2 1,那么我加入道具4后就会变成3 1 2,然后按5攻击后变成 2 2,再按5攻击变成1 1,再攻击变成1(道具4的次数),最后在攻击一次,这里我攻击了4次,其中前三次伤害250-254,最后一次200-204(这里可以令第一个状态栏的道具是道具3,也就是基础伤害值变成25),最后一次因为没有道具3,因此伤害值减少,此时boss还剩下5最多49HP,基本宣告胜利,而我通过4 2 1逆向推出我每一个回合我的输入值,最终是2 3 3 2 2 1 3 4 5 5 5 5 2 3 5 5(这里应该不止一种方法,有兴趣的可以尝试一下)。


当然本题的flag肯定不是放在程序里面的啦,要通过与服务器交互得到哦。(第一次发帖,有点紧张,不知道写的好不好,还希望大佬们多多指点)

qdam 发表于 2019-3-22 00:15

bitpeach 发表于 2019-3-21 11:17
赞。我也是刚入门菜鸟,学习了这个有意思的游戏。此题目中服务器交互后拿到FLAG。意思是,胜利了,服务器才 ...

额,这个应该是没有的吧,当你知道如何击败boss后,你需要在服务器上再玩一遍,然后当你胜利了才会给出flag

qdam 发表于 2019-3-20 20:34

这里补充一下,在函数if_attack_me(),也就是boss是否攻击我的函数中,假如我有道具1的话,那么我的数值,对应内存为dword_403204就会变成2,否则会变成5。而每一次boss的数值会减去我的数值,当我的数值更大时,boss就会攻击我,这里可以理解为速度,值越小则速度越快,所以可以选择道具1来减少boss的攻击次数。

Mr.Eleven 发表于 2019-3-21 10:04

突然发现评不了分{:1_925:}今天还没评就剩余0了   感谢分享

NotSoGood 发表于 2019-3-21 10:47

写的还不错

bitpeach 发表于 2019-3-21 11:17

赞。我也是刚入门菜鸟,学习了这个有意思的游戏。此题目中服务器交互后拿到FLAG。意思是,胜利了,服务器才发放FLAG是么?那是否有空隙可钻?谢谢您指点。

cdevil 发表于 2019-3-21 14:15

楼主的分析好详细,好评,好评!

229836680 发表于 2019-3-21 14:18

表示,看不懂。

preneur 发表于 2019-3-21 20:46

虽然看不懂 但是支持楼主

players 发表于 2019-3-21 21:02

虽然看不懂 但是支持楼主
页: [1] 2 3 4 5
查看完整版本: 分享一道有意思的CTF逆向题