软件漏洞分析入门(四)

逆向例题分析

我们通过三道例题来探索一下逆向具体的过程

SecondReverse.exe

我们先正常运行一下这个程序看看他的程序逻辑

image-20220606004846503

可以看出这个程序需要输入一串字符串来通过验证,而我们的目标就是通过逆向找到我们需要输入的字符串,直接拖到IDA里面

image-20220606005019662

可以看出,这个程序的逻辑流程并不复杂,接受了一次字符串的输入之后进行判断,然后一个是正确分支,一个是错误分支,我们直接F5反编译一下

image-20220606015038710

int __cdecl main(int argc, const char **argv, const char **envp)
{
    
    
  signed int v3; // esi@1
  char v4; // al@2
  char v6[52]; // [sp+Ch] [bp-38h]@3

  printf("Please give me your input: ");
  v3 = 0;
  do
  {
    
    
    v4 = getchar();
    if ( v4 == 10 )  //回车符就结束
      break;
    v6[v3++] = v4;
  }
  while ( v3 < 40 );
  v6[v3] = 0;
  if ( sub_401000() )
    printf("Good! The flag is your input!!!\n");
  else
    printf("Sorry! You are wrong!!!\n");
  system("pause");
  return 0;
}

这段是main函数,分析可知,程序循环读取我们输入的字符,输入回车就结束读取,然后进入 sub_401000() 函数判断字符串是否正确

我们继续进入这个 sub_401000()

image-20220606015524934

首先这个字符串长度得是37位,然后进行一段最关键的循环判断

while ( *(&aGmbhXfmd1nfU1U[v1 - aGmbhXfmd1nfU1U] + result) + 1 == aGmbhXfmd1nfU1U[result] )
    {
    
    
      if ( ++result >= 37 )
      {
    
    
        if ( result == 37 )
          result = 1;
        return result;
      }
    }

这段的意思就是把我们输入的字符串每个字符都分别 +1 之后要和 aGmbhXfmd1nfU1U 这个里面的字符串相等

我们直接看看这段字符串是什么

image-20220606025125435

知道了验证函数的逻辑,也知道了字符串,我们直接写脚本

aa 就是程序里面需要对比的字符串,由于我们需要得到的 flag 需要 +1 才能得到 aa,所以我们循环 aa 然后都 -1 就得到了正确的 flag

image-20220606025329132

把这个flag输入程序,正确

image-20220606041559562

DecryptPlayer.exe

依然是先打开程序,我们随便输入一些字符,可以看到这次的程序验证分为了两步,先输入一个 key 然后还需要输入一个 activation code 进行验证,判断验证不通过之后显示 wrong code

image-20220606030054481

用IDA打开程序开始逆向分析

image-20220606030550749

整体分支也不算复杂,第一步先判断有没有发生这个 code broken 如果没有发生则正常运行,先输入 key 然后后面call了一个自定义函数之后根据结果判断成功或者失败,我们F5反编译一下看看

image-20220606031106971

在程序开头,先声明了一批变量,这些后面肯定用得到

  v14 = 11;
  v15 = 45;
  v16 = 667;
  memset(&v17, 0, 0x184u);
  v8 = 0;
  memset(&v9, 0, 0x18Cu);
  v10 = -1929852969;
  v11 = -1929852943;
  v12 = -1929853625;

忽略掉中间一段对我们没用的代码,这个程序最核心的代码就是

printf("please input your key:\n");
  scanf("%x", &v7);
  sub_401040(&v8, v7, (int)&v14);
  v5 = 0;
  while ( *(int *)((char *)&v8 + v5 * 4) == *(int *)((char *)&v10 + v5 * 4) )
  {
    
    
    ++v5;
    if ( v5 >= 3 )
    {
    
    
      printf("Success\n");
      goto LABEL_10;
    }
  }
  printf("Failed\n");
LABEL_10:
  system("pause");
  return 0;

在要求用户输入字符串之后,程序把这段输入的字符串放到了v7里面,然后函数sub_401040被传入了三个参数并执行了,分别是v8v7以及v14,其中v8初始化为0,v7是用户输入的字符串,v14是11,我们继续进入这个函数看看

image-20220606032111630

我们发现,程序验证的第二步原来是在这个函数里面,而且这个 activation code 如果不为 51 的话就直接回显 wrong code 了,所以这一步我们直接知道了通过后半段验证的关键

printf("please enter activation code:");
  scanf("%i", &v11);
  if ( v11 != 51 )
    sub_401010((int)"wrong code");

我们继续往下看,这个函数的后半段对v3指针的地方进行了编辑,而v3也就是函数传入的第一个参数,即主函数里面的v8,也就是说这个函数的作用是对v8进行编辑

  v5 = 0;
  v6 = a3 - (_DWORD)v3;
  dword_42DE28 = v4 ^ 0x5F5B884;
  for ( i = a3 - (_DWORD)v3; ; v6 = i )
  {
    
    
    v7 = dword_42DE28 ^ *(int *)((char *)v3 + v6);
    result = _time64(0);
    if ( result > 1667658196 )
    {
    
    
      v9 = _time64(0);
      printf("%#lu", v9);
      sub_401010((int)"expired");
    }
    *v3 = v7;
    ++v5;
    ++v3;
    if ( v5 >= 3 )
      break;
  }

我们再回去看看主函数执行了这个函数之后会发生什么

  v5 = 0;
  while ( *(int *)((char *)&v8 + v5 * 4) == *(int *)((char *)&v10 + v5 * 4) )
  {
    
    
    ++v5;
    if ( v5 >= 3 )
    {
    
    
      printf("Success\n");
      goto LABEL_10;
    }
  }
  printf("Failed\n");

这段的意思就是从v8首地址开始连续三块以 4char 为单位的内存需要和从v10首地址开始内存存放的东西一模一样才能输出 Success ,知道了要求之后我们再去看那个sub_401040函数,因为就是这个函数改变了v8

  v5 = 0;
  v6 = a3 - (_DWORD)v3;
  dword_42DE28 = v4 ^ 0x5F5B884;
  for ( i = a3 - (_DWORD)v3; ; v6 = i )
  {
    
    
    v7 = dword_42DE28 ^ *(int *)((char *)v3 + v6);
    result = _time64(0);
    if ( result > 1667658196 )
    {
    
    
      v9 = _time64(0);
      printf("%#lu", v9);
      sub_401010((int)"expired");
    }
    *v3 = v7;
    ++v5;
    ++v3;
    if ( v5 >= 3 )
      break;
  }

分析这段代码,dword_42DE28就是v40x5F5B884 按位异或的结果,而v4就是用户的输入。v6a3v3地址的偏移量,而v3就是a1,下面的for循环的意思就是,v8首地址开始写入一个dword_42DE28a3首地址开始的数据异或的结果,而a3也就是主函数里面的v14,这样的循环一共三次

知道这个逻辑之后,我们还需要知道几个信息,第一个是函数里面的a3首地址开始到底是什么样的数据,实际上我们可以从主函数的开头声明看出来,a3在主函数里面对应的v14以及后面的v15v16在内存上都是连续的

image-20220606035206870

即使没注意到没看出来,我们也可以动态调试

我们在IDA里面找到异或的这段汇编

image-20220606035713441

同时在 x32dbg 里面找到对应位置

image-20220606035829472

我们进入此时的 esi 对应的内存看看,发现对应的就是那三个变量v14,v15,v16

image-20220606040030028

我们需要知道的第二个信息是主函数里面v10开始对应的内存是什么

  v5 = 0;
  while ( *(int *)((char *)&v8 + v5 * 4) == *(int *)((char *)&v10 + v5 * 4) )
  {
    
    
    ++v5;
    if ( v5 >= 3 )
    {
    
    
      printf("Success\n");
      goto LABEL_10;
    }
  }
  printf("Failed\n");

只看IDA的开头声明依然可以直接看出v10v11v12这三个变量是连续的

image-20220606040418172

当然不放心的话我们也去动态调试一下

这个就是主函数比较那里的代码对应的汇编

image-20220606040612288

x32dbg 相同的位置设断点并查看对应的内存

image-20220606040842829

能明显看出这个对应的就是v10,v11,v12三个变量对应的数据

image-20220606040937055

知道了逻辑也知道了所有内存的信息,我们可以开始编写脚本进行逆向

image-20220606041425317

其中D1就是dword_42DE28,实际上只需要循环中的一次来逆向得到密码,这里f1,f2,f3得到的结果都是一样的

我们把密码输入程序,activation code 就是之前已经知道的 51,显示成功

image-20220606042124492

reversePractice.exe

最后一个程序,依然是先打开

image-20220606042329164

逻辑就是用户输入字符串然后进行验证,失败就直接关闭窗口

我们用IDA打开他,可以看出这次的程序分支还是比较多比较复杂的

image-20220606042516571

F5反编译之后发现其实没有那么复杂

image-20220606042621341

int __cdecl main(int argc, const char **argv, const char **envp)
{
    
    
  int v3; // eax@1
  signed int v4; // ecx@1
  char v5; // dl@2
  signed int i; // esi@5
  int result; // eax@8
  char v8; // [sp+4h] [bp-64h]@1
  char v9; // [sp+5h] [bp-63h]@1
  __int16 v10; // [sp+65h] [bp-3h]@1
  char v11; // [sp+67h] [bp-1h]@1

  v8 = 0;
  memset(&v9, 0, 0x60u);
  v10 = 0;
  v11 = 0;
  puts(aPleaseInputASt);
  scanf(aS, &v8);
  v3 = 0;
  v4 = strlen(&v8);
  if ( v4 <= 0 )
  {
    
    
LABEL_5:
    for ( i = 0; i < v4; ++i )
      *(&v8 + i) = (3 * *(&v8 + i) - 284) % 26 + 97;
    if ( !strcmp(&v8, aQxbxpluxvwhuzj) )
      puts(aOkYouReallyKno);
    else
      puts(aSorry);
    result = system(aPause);
  }
  else
  {
    
    
    while ( 1 )
    {
    
    
      v5 = *(&v8 + v3);
      if ( v5 < 97 || v5 > 122 )
        break;
      if ( ++v3 >= v4 )
        goto LABEL_5;
    }
    result = -1;
  }
  return result;
}

这个程序的大致逻辑就是,用户输入的字符串进入了v8,然后逐字符验证,必须要满足每个字符值 ≥97 并且 ≤122

while ( 1 )
{
    
    
    v5 = *(&v8 + v3);
    if ( v5 < 97 || v5 > 122 )
        break;
    if ( ++v3 >= v4 )
        goto LABEL_5;
}

然后v8开始进行逐字符的一个自变化算法,变化完成之后要和 aQxbxpluxvwhuzj 这个字符串进行比对,如果一样就正确,不一致则错误

for ( i = 0; i < v4; ++i )
    *(&v8 + i) = (3 * *(&v8 + i) - 284) % 26 + 97;
if ( !strcmp(&v8, aQxbxpluxvwhuzj) )
    puts(aOkYouReallyKno);
else
    puts(aSorry);

我们点进这个 aQxbxpluxvwhuzj 字符串

image-20220606043412685

现在知道了字符串也知道了程序的逻辑,直接开始编写脚本进行逆向

image-20220606043510534

由于这里的算法设计到模运算,如果纯进行逆向算法,会产生很多同时满足的结果再根据条件进行筛选,这样代码量比较大,所以既然程序逻辑给了字符值的限制,我们可以直接在范围内循环然后找到在这个字符值条件区间里面符合算法的那个字符,这样一个一个拼接,最终就得到了正确的flag

我们把flag输入原程序,正确

image-20220606043828882


这三题逆向题在加密的算法上各不相同,有字符值直接加减,有异或运算,也有模运算,在逆向题的大类中,掌握了这些属于最基本的算法,逆向的大门已经朝我们敞开了

猜你喜欢

转载自blog.csdn.net/SimoSimoSimo/article/details/125229605