bugku-逆向-12、Mountain climbing(Win32位逆向、UPX脱壳)

首先下载运行ConsoleApplication2.exe:
在这里插入图片描述

发现是输入一个字符串或者数字,错误就输出“error”.

1、脱UPX壳

然后用PEiD查壳:
在这里插入图片描述

发现有UPX的壳,再用Upx静态脱壳机脱壳:
在这里插入图片描述
在这里插入图片描述

2、IDA反汇编静态分析

接下来用IDA打开脱壳后的ConsoleApplication2.exe,找到main函数:
在这里插入图片描述

进入main_0()函数查看:
在这里插入图片描述

main_0()函数的伪代码是:

__int64 main_0()
{
  int v0; // edx
  __int64 v1; // ST04_8
  char v3; // [esp+0h] [ebp-160h]
  int v4; // [esp+D0h] [ebp-90h]
  int j; // [esp+DCh] [ebp-84h]
  int i; // [esp+E8h] [ebp-78h]
  char Str[104]; // [esp+F4h] [ebp-6Ch]

  srand(0xCu);
  j_memset(&unk_423D80, 0, 0x9C40u);
  for ( i = 1; i <= 20; ++i )
  {
    for ( j = 1; j <= i; ++j )
      dword_41A138[100 * i + j] = rand() % 100000;
  }
  sub_41134D("input your key with your operation can get the maximum:", v3);
  sub_411249("%s", Str);
  if ( j_strlen(Str) == 19 )
  {
    sub_41114F(Str);
    v4 = 0;
    j = 1;
    i = 1;
    dword_423D78 += dword_41A138[101];
    while ( v4 < 19 )
    {
      if ( Str[v4] == 76 )
      {
        dword_423D78 += dword_41A138[100 * ++i + j];
      }
      else
      {
        if ( Str[v4] != 82 )
        {
          sub_41134D("error\n", v3);
          system("pause");
          goto LABEL_18;
        }
        dword_423D78 += dword_41A138[100 * ++i + ++j];
      }
      ++v4;
    }
    sub_41134D("your operation can get %d points\n", dword_423D78);
    system("pause");
  }
  else
  {
    sub_41134D("error\n", v3);
    system("pause");
  }
LABEL_18:
  HIDWORD(v1) = v0;
  LODWORD(v1) = 0;
  return v1;
}

程序的逻辑就是先初始化了一个随机数种子srand(0xCu),之后生成了一个特定的数组元素大小在100000以内的伪随机数数组dword_41A138,之后我们要输入一个长度为19的字符串,然后逐个判断字符串的字符是不是L(76)或者R(82),不正确就输出error,最后都正确就输出“your operation can get %d points”。

3、伪随机数组

接下来开始破解,我们首先根据随机数种子srand(0xCu)得到伪随机数数组dword_41A138:
源代码:

#include<stdio.h>
#include <stdlib.h>

int main(){
    /// 随机数种子
    srand(0xCu);
    /// 存放随机数的数组,数组下标最大有100 * 20 + 20
    int dword_41A138[2100];
    int i , j;
    ///当前数组下标
    int lab = 0;
    for ( i = 1; i <= 20; ++i ){
        ///%2d:对长度为2以内的数字右对齐
        printf("%2d ", i );
        for ( j = 1; j <= i; ++j ){
            lab = 100 * i + j;
            dword_41A138[lab] = rand() % 100000;
            ///%5d:对长度为5以内的数字右对齐
            printf("%5d ",dword_41A138[lab]);
        }
        printf("\n");
    }
    return 0;
}

运行结果:
在这里插入图片描述

结合题目Mountain climbing(爬山),所以这个数组dword_41A138就相当于一座山,而我们输入19个字符‘L’和‘R’就是爬山的方向,从下面的代码可以看出:

if ( Str[v4] == 76 )
      {
        dword_423D78 += dword_41A138[100 * ++i + j];
      }
      else
      {
        if ( Str[v4] != 82 )
        {
          sub_41134D("error\n", v3);
          system("pause");
          goto LABEL_18;
        }
        dword_423D78 += dword_41A138[100 * ++i + ++j];
      }

当输入的字符为L=76时数组下标100 * ++i表示往下走(例如从77走到5628),当输入的字符为R=82时数组下标100 * ++i + ++j表示表示往右下角走(例如从77走到6232)。因为输入的是19个字符,所以不管它是到底是L还是R,最终都会从第一行第一个数77走到第20行。

4、第一次错误的思路(没有考虑完整)

结合最后输出的“your operation can get %d points”(你的操作可以获得%d分),我们可以知道这个游戏相当于一个从左上角走到右下角的爬山游戏,走过的路径的每个位置上的数字加起来就是总得分。
由此我们可以逆推出最大的得分和对应的字符串:
源代码:

#include<stdio.h>
#include <stdlib.h>

int main(){
    /// 随机数种子
    srand(0xCu);
    /// 存放随机数的数组,数组下标最大有100 * 20 + 20
    int dword_41A138[2100];
    int i , j;
    ///当前数组下标
    int lab = 0;
    for ( i = 1; i <= 20; ++i ){
        ///%2d:对长度为2以内的数字右对齐
        printf("%2d ", i );
        for ( j = 1; j <= i; ++j ){
            lab = 100 * i + j;
            dword_41A138[lab] = rand() % 100000;
            ///%5d:对长度为5以内的数字右对齐
            printf("%5d ",dword_41A138[lab]);
        }
        printf("\n");
    }
    ///分数
    int dword_423D78 = 0;
    ///分数的初值默认为第一行的第一个数
    dword_423D78 += dword_41A138[101];
    ///输入的字符串
    char Str[19];
    i = 1 ;
    j = 1 ;
    while(i < 20){
        printf("%d:", i);
        ///右下角的值大于下面的值
        if(dword_41A138[100 * (i + 1) + (j + 1)] > dword_41A138[100 * (i + 1) + j]){
            Str[i-1] = 'R';
            printf("%c  ",Str[i-1]);
            dword_423D78 += dword_41A138[100 * (++i) + (++j)];
        }else{
            Str[i-1] = 'L';
            printf("%c  ",Str[i-1]);
            dword_423D78 += dword_41A138[100 * (++i) + j];
        }
    }
    Str[19] = '\0';
    ///444740
    printf("\n最终的分数是:%d\n", dword_423D78);
    ///RRRRRLLRRRLRLRRRLRL
    printf("输入的字符串为:%s\n", Str);
    system("pause");
    return 0;
}

运行结果:
在这里插入图片描述

但是把得到的字符串输入到ConsoleApplication2.exe,结果竟然是error:

5、没有考虑的sub_41114F函数

我们再查看源码,发现最开始有个sub_41114F(Str)函数:
在这里插入图片描述

开始点进去查看以为它是没有用的,现在逐个点进去查看,这部分的函数调用是sub_41114F函数—>sub_411900函数->sub_4110A5函数->sub_411750函数:
sub_41114F函数:
在这里插入图片描述

sub_411900函数:
在这里插入图片描述

上面这个函数的参数都是没法反编译的,只能查看汇编代码:
在这里插入图片描述
在这里插入图片描述

继续查看函数:
sub_4110A5函数:
在这里插入图片描述

最终进入sub_411750函数:
在这里插入图片描述

其中VirtualQuery和VirtualProtect都是虚拟内存API函数,LPCVOID lpAddress指的就是虚拟内存,int a2 应该是用来控制循环的,int a3的值就是在前面的函数输入的参数是4。
源代码:

BOOL __cdecl sub_411750(LPCVOID lpAddress, int a2, int a3)
{
  int v3; // ST1C_4
  DWORD flOldProtect; // [esp+D4h] [ebp-2Ch]
  struct _MEMORY_BASIC_INFORMATION Buffer; // [esp+E0h] [ebp-20h]

  VirtualQuery(lpAddress, &Buffer, 0x1Cu);      // 该函数用来查询内存中指定页的特性。参数1指向希望查询的虚拟地址;参数2是指向内存基本信息结构的指针;参数3指定查询的长度。
  // 该函数用来把已经分配的页改变成保护页。参数1指定分配页的基地址;参数2指定保护页的长度;参数3指定页的保护属性,取值PAGE_READ、PAGE_WRITE、PAGE_READWRITE等等;参数4用来返回原来的保护属性。
  VirtualProtect(Buffer.BaseAddress, Buffer.RegionSize, 0x40u, &Buffer.Protect);
  while ( 1 )
  {
    v3 = a2--;                                  // a2控制循环次数
    if ( !v3 )
      break;
    *(_BYTE *)lpAddress ^= a3;                  // 虚拟地址上的值  异或 4 
    lpAddress = (char *)lpAddress + 1;          // 虚拟地址+1,到下一个
  }
  return VirtualProtect(Buffer.BaseAddress, Buffer.RegionSize, Buffer.Protect, &flOldProtect);
}

还是没太看明白这个函数对Str字符串做了什么操作,所以我们接下来结合OD来对它动态调试。

6、OD动态分析

用OD打开ConsoleApplication2.exe:
搜索字符串,找到题目输出的字符串“input your key with your operation can get the maximum:”:
在这里插入图片描述

双击字符串跳转到它所在的位置
在这里插入图片描述

ConsoleA.00417B30就是字符串input your key with your operation can get the maximum:
在这里插入图片描述

ConsoleA.0041134D就是输出函数printf
在这里插入图片描述

ConsoleA.00417B74就是 %s
在这里插入图片描述

ConsoleA.00411249就是输入函数scanf
在这里插入图片描述

这些我们可以在IDA中得到验证。
接下来我们在字符串input your key with your operation can get the maximum:和输入函数scanf后面下断点:
输入字符串:RRRRRRRRRRRRRRRRRRR
在这里插入图片描述

我们可以看到输入的(ASCII “RRRRRRRRRRRRRRRRRRR”),保存在堆栈地址=0019FE98上:
在这里插入图片描述

我们继续单步调试,发现经过:
00411C4F E8 79F4FFFF call ConsoleA.004110CD
这一行,之后EAX = 0x00000013,正好是10进制的19:
在这里插入图片描述

然后下面的一行:
00411C57 83F8 13 cmp eax,0x13
将eax的值和0x13比较大小。结合之前反编译出来的源码:
if ( j_strlen(Str) == 19 )
我们可以知道ConsoleA.004110CD就是j_strlen函数。
接下来继续单步运行,因为我们输入的字符串长度是19,所以能够通过判断,继续跳转运行:
在这里插入图片描述

之后我们在
00411C8B E8 BFF4FFFF call ConsoleA.0041114F
后面一行下个断点

在这里插入图片描述
运行到这一行,我们就可以知道ConsoleA.0041114F函数对Str字符串产生了什么影响了:
在这里插入图片描述

我们可以看到RRRRRRRRRRRRRRRRRRR经过了ConsoleA.0041114F函数之后变成了RVRVRVRVRVRVRVRVRVR,字符串Str的偶数位上的字符都从R变成了V,这就是ConsoleA.0041114F函数的作用。

7、得到真正的flag

我们再重新运行,把输入改成RRRRRLLRRRLRLRRRLRL:
在这里插入图片描述

运行到刚刚ConsoleA.0041114F函数之后的断点位置:
在这里插入图片描述

我们可以看到RRRRRLLRRRLRLRRRLRL变成了RVRVRHLVRVLVLVRVLVL,就是把字符串Str的偶数位置上的R变成V、L变成H,结合之前IDAfan编译出来的0041114F函数的伪代码,我们可以知道这应该就是*(_BYTE *)lpAddress ^= a3; // 虚拟地址上的值 异或 4
的结果,其中V = R ^ 4 ;H = L ^ 4 。
其实,到这里我们就已经可以知道正确的输入字符串就是RVRVRHLVRVLVLVRVLVL:
在这里插入图片描述

即flag为:zsctf{RVRVRHLVRVLVLVRVLVL}。

8、继续的仔细分析(sub_411750函数)

我们再继续分析一下最内层的sub_411750函数,可以直接再OD中按快捷键Ctrl + G地址跟随,输入411750就可以跟随到sub_411750函数了:
在这里插入图片描述

接下来我们继续单步运行,可以看到VirtualQuery和VirtualProtect函数并不会改变Str字符串的值:
在这里插入图片描述

但是,我们发现运行完sub_411750函数这部分的代码,Str字符串的值并没有改变:
在这里插入图片描述

也就是我们刚刚分析的由sub_411750函数来改变Str字符串的值是错误的!!!
可是运行完sub_41114F函数之后Str字符串的值是被改变了的,调用流程:sub_41114F函数—>sub_411900函数->sub_4110A5函数->sub_411750函数,所以真正改变Str字符串的函数,不是sub_411750函数,而是sub_411900函数或者sub_4110A5函数。
继续单步运行,从sub_411750函数返回到sub_4110A5函数,发现sub_4110A5函数只是一个简单的调用,不可能改变Str字符串的值:
在这里插入图片描述

9、关键的sub_411900函数

接着单步运行,从sub_4110A5函数返回到sub_411900函数:
在这里插入图片描述

在返回sub_411900函数后,
1、先把堆栈 ss:[0019FD54]初始化为0用来控制循环的跳转,之后每次循环加1,当它大于等于19时跳出循环;
2、然后eax=ss:[0019FD54]大小范围是018,接着让eax与1,偶数与1后eax为0,奇数与1后eax为1,这样就可以区分开018中的奇数和偶数;
3、当eax为0、2、……等偶数时,这两条汇编
test eax,eax
je short 00411992
会跳过下面的改变字符的代码,当eax为1、3、……等奇数时,这两条汇编不跳转,继续运行下面的改变字符的代码,
4、之后继续运行,把字符串"RVRVRHLVRVLVLVRVLVL"首地址堆栈 ss:[0019FDA0]=0019FE98赋值给eax,之后eax+1获得第二个字符‘V’的地址,再地址上的‘V’取出来给ecx,之后ecx异或4,把异或后的结果赋值给原来字符串的对应位置;
通过这部分的代码实现了改变字符串RVRVRHLVRVLVLVRVLVL到RRRRRLLRRRLRLRRRLRL的变化。

加上注释的汇编代码如下:

00411953    C745 BC 0000000>mov dword ptr ss:[ebp-0x44],0x0   ; 先把堆栈 ss:[0019FD54]初始化为0
0041195A    EB 09           jmp short ConsoleA.00411965       ; 跳转到411965
0041195C    8B45 BC         mov eax,dword ptr ss:[ebp-0x44]   ;               eax = 堆栈 ss:[0019FD54]
0041195F    83C0 01         add eax,0x1                       ;               eax + 1
00411962    8945 BC         mov dword ptr ss:[ebp-0x44],eax   ;               堆栈 ss:[0019FD54] = eax
00411965    837D BC 13      cmp dword ptr ss:[ebp-0x44],0x13  ; 比较堆栈 ss:[0019FD54]19
00411969    7D 29           jge short ConsoleA.00411994       ; JGE: 大于或等于转移指令,跳出循环
0041196B    8B45 BC         mov eax,dword ptr ss:[ebp-0x44]   ;               eax = 堆栈 ss:[0019FD54]
0041196E    25 01000080     and eax,0x80000001                ; eax与1   ;改变eax的值,偶数与1后eax为0,奇数与1后eax为1
00411973    79 05           jns short ConsoleA.0041197A       ; 如果符号位S为0,就跳转到41197A  ;SF用来反映运算结果的符号位,它与运算结果的最高位相同
00411975    48              dec eax                           ;            ;上面eax与1的结果是0或者1,最高位始终为0,所以这个跳转一直成立
00411976    83C8 FE         or eax,-0x2
00411979    40              inc eax
0041197A    85C0            test eax,eax                      ; eax为0时与的结果是0,ZF置1跳转,为其他数字与的结果都是1
0041197C    74 14           je short ConsoleA.00411992        ; ZF等于1时进行跳转411992
0041197E    8B45 08         mov eax,dword ptr ss:[ebp+0x8]    ; eax赋值为堆栈 ss:[0019FDA0]=0019FE98,(字符串"RVRVRHLVRVLVLVRVLVL"首地址)
00411981    0345 BC         add eax,dword ptr ss:[ebp-0x44]   ; ss:[0019FD54]=00000001、……,eax+1 、……  ;eax的地址为第二个字符的地址19FE99
00411984    0FBE08          movsx ecx,byte ptr ds:[eax]       ; 把eax的当前地址上的值,送给ecx  ;因为上面的eax+1,所以取出来的是第二个字符'V'
00411987    83F1 04         xor ecx,0x4                       ; ecx异或4       ;第二个字符'V'异或4
0041198A    8B55 08         mov edx,dword ptr ss:[ebp+0x8]    ; 把字符串首地址ss:[0019FDA0]=0019FE98,赋值给edx
0041198D    0355 BC         add edx,dword ptr ss:[ebp-0x44]   ; edx加上00000001、……    ;获得当前被改变的那个字符的地址
00411990    880A            mov byte ptr ds:[edx],cl          ; d把异或后的值'R',赋值给原来字符串的对应位置
00411992  ^ EB C8           jmp short ConsoleA.0041195C       ; 跳转到上面41195C继续循环

运行完第2轮循环,第二个字符‘V’被改成了‘R’:
在这里插入图片描述

运行完18轮循环,字符串RVRVRHLVRVLVLVRVLVL已经变成了RRRRRLLRRRLRLRRRLRL:
在这里插入图片描述

到这我们就明白了Str字符串被修改的具体原理了。
所以我们只要在源代码加上Str字符串被修改的代码就可以了:

for(i = 1 ; i < 20 ; i++){
        ///当前i是偶数位
        if(i % 2 == 0){
            Str[i-1] ^= 4;
        }
    }

10、正确完整的代码

源代码:

#include<stdio.h>
#include <stdlib.h>

int main(){
    /// 随机数种子
    srand(0xCu);
    /// 存放随机数的数组,数组下标最大有100 * 20 + 20
    int dword_41A138[2100];
    int i , j;
    ///当前数组下标
    int lab = 0;
    for ( i = 1; i <= 20; ++i ){
        ///%2d:对长度为2以内的数字右对齐
        printf("%2d ", i );
        for ( j = 1; j <= i; ++j ){
            lab = 100 * i + j;
            dword_41A138[lab] = rand() % 100000;
            ///%5d:对长度为5以内的数字右对齐
            printf("%5d ",dword_41A138[lab]);
        }
        printf("\n");
    }
    ///分数
    int dword_423D78 = 0;
    ///分数的初值默认为第一行的第一个数
    dword_423D78 += dword_41A138[101];
    ///输入的字符串
    char Str[19];
    i = 1 ;
    j = 1 ;
    while(i < 20){
        printf("%d:", i);
        ///右下角的值大于下面的值
        if(dword_41A138[100 * (i + 1) + (j + 1)] > dword_41A138[100 * (i + 1) + j]){
            Str[i-1] = 'R';
            printf("%c  ",Str[i-1]);
            dword_423D78 += dword_41A138[100 * (++i) + (++j)];
        }else{
            Str[i-1] = 'L';
            printf("%c  ",Str[i-1]);
            dword_423D78 += dword_41A138[100 * (++i) + j];
        }

    }
    Str[19] = '\0';
    ///444740
    printf("\n最终的分数是:%d\n", dword_423D78);
    ///RRRRRLLRRRLRLRRRLRL
    for(i = 1 ; i < 20 ; i++){
        ///当前i是偶数位
        if(i % 2 == 0){
            Str[i-1] ^= 4;
        }
    }
    ///RVRVRHLVRVLVLVRVLVL
    printf("输入的字符串为:%s\n", Str);
    system("pause");
    return 0;
}

运行结果:
在这里插入图片描述

得到正确的flag:RVRVRHLVRVLVLVRVLVL。

  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值