第一次正确逆向

小记一下第一次成功的逆向,eshard培训作业ARE-4中获取key程序的序列号。
练习目标是通过使用panda-re工具(QEMU的套件),通过查找QEMU中寄存器状态,获得输入字符匹配时,PC指针的值,顺着找到代码中的程序行,接着找到程序验证序列号的代码,逆向后破解。
具体过程:

0x00 下载编译panda-re

根据 官方repo 可以在Ubuntu 16.04上轻松的编译成功。不过Ubuntu 18.04会因为GCC版本过高等问题出现兼容性问题(2018年8月)。编译需要大约10分钟,编的时候可以出去遛一遛。

0x01 获取QEMU运行快照

先玩一下,看看什么效果。规则是输入4段每段16字符,中间短杠作为分隔。

(panda_env) enigma001@ubuntu:~/eshard-re/4-ARE/workspace/key-analysis$ ./key 20E806AF-05376811-38D34527-638DD79A
serial bad
(panda_env) enigma001@ubuntu:~/eshard-re/4-ARE/workspace/key-analysis$ ./key 20E806AF-05376811-38D34527-638DD79B
serial bad
(panda_env) enigma001@ubuntu:~/eshard-re/4-ARE/workspace/key-analysis$ ./key 00000000-00000000-00000000-00000000
serial bad

返回的值都是serial bad,看起来应该就是在程序中校验了一下值,比对错误就返回错误信号。
玩完了开始用QEMU跑快照,序列号是瞎给的。

  1 #!/bin/bash
  2 
  3 ~/eshard-re/4-ARE/panda/panda/scripts/run_debian.py key 20E806AF-05376811-38D34527-638DD79A

创建快照之前关闭QEMU中的ASLR,不然之后字符串搜索时,PC地址会不准确,且无法找到基址的值
方法是在 https://github.com/eshard/panda/blob/learn/panda/scripts/run_guest.py 的create_recording中添加:
qemu.run_console("echo 0 > /proc/sys/kernel/randomize_va_space")

为了加快进度,可以先下载快照必要文件到~/.panda目录
- 必要文件: initrd.img-3.2.0-4-versatile, vmlinuz-3.2.0-4-versatile
- 结构相关文件: wheezy_panda2.qcow2, wheezy_x64.qcow2(需要在录制时加--arch x86_64)

跑完之后在当前路径下出现replays文件夹

(panda_env) enigma001@ubuntu:~/eshard-re/4-ARE/workspace/key-analysis$ tree replays
replays
└── key
    ├── cdrom
    │   └── key
    ├── cdrom.iso
    ├── key-rr-nondet.log
    └── key-rr-snp

接着就可以replay(使用panda-re版本r-18-06)

  1 #!/bin/bash
  2 
  3 ~/eshard-re/4-ARE/panda/build/i386-softmmu/qemu-system-i386 -replay replays/key/key -panda osi -os linux-32-. -panda osi_linux:kconf_group=debian-3.2.81-686-pae:32 -panda asidstory
  4 
  5 mv asidstory asidstory_key

于是可以获得这样的结果

  1    Count   Pid                  Name      Asid     First          Last
  2      388   743                 udevd  c76e8000    260129  ->  67698264
  3       97  2487                 blkid  c5182000   5947088  ->  13122162
  4       66  2433                 bash2  c7ae4000       248  ->  12824086
  5       41  2488                   key  c62fa000  13363004  ->  19256272
  6       40   123           kworker/0:2         0    145380  ->  15194483
  7       35  2487                udevd2  c5bab000    294682  ->   4687204
  8       13  2487                udevd3  c5182000   4687550  ->   5858025
  9       12   337             flush-8:0         0    284803  ->   3062779
 10        9  2487                blkid2         0  13204146  ->  13459162
 11        6  2488                 bash3  c5202000  12914158  ->  13055670
 12        6   291                udevd4  c53bb000  19261480  ->  19334033
 13        3  2488                 bash4  c62fa000  13242438  ->  13362904
 14 
 15                bash2 : [## ############                                                            ]
 16          kworker/0:2 : [# ##         ####                                                          ]
 17                udevd : [##            #############################################################]
 18            flush-8:0 : [## #                                                                       ]
 19               udevd2 : [## ###                                                                     ]
 20               udevd3 : [     ##                                                                    ]
 21                blkid : [      #########                                                            ]
 22                bash3 : [              #                                                            ]
 23               blkid2 : [              #                                                            ]
 24                bash4 : [              #                                                            ]
 25                  key : [              ### ## #                                                     ]
 26               udevd4 : [                     #                                                     ]

为了提高之后的字符串搜索效率,把key程序的运行时段截取出来,起始点在13363004,再跑一次replay

  1 #!/bin/bash
  2 
  3 
  4 ~/eshard-re/4-ARE/panda/build/i386-softmmu/qemu-system-i386 -replay replays/key/key -panda 'scissors:name=key-cut,start=13363000'
  5 
  6 
  7 ~/eshard-re/4-ARE/panda/build/i386-softmmu/qemu-system-i386 -replay key-cut -panda osi -os linux-32-. -panda osi_linux:kconf_group=debian-3.2.81-686-pae:32 -panda asidstory
  8 
  9 mv asidstory asidstory_key-cut2

获得新的运行结果

  1    Count   Pid                  Name      Asid     First          Last
  2      465   743                 udevd  c76e8000     96261  ->  54335263
  3       43  2488                   key  c62fa000         3  ->   5893271
  4       17   123           kworker/0:2         0    245453  ->   1831482
  5        6   291                udevd2  c53bb000   5898479  ->   5971032
  6        3  2487                 blkid         0     78967  ->     96161
  7 
  8                  key : [### # # #                                                                  ]
  9                blkid : [#                                                                          ]
 10                udevd : [###########################################################################]
 11          kworker/0:2 : [###                                                                        ]
 12               udevd2 : [        #                                                                  ]

这下key程序的起始位置在3,基本就是开头位置了。其实也可以切后面,把start=1336300写成start=1336300,end=xxxxx就行,记得前后时间稍留一些空间比较好。

0x02 在运行快照里查找输入字符串

这一步感觉就像是用修改器改李逍遥的经验以至于在客栈带着残废赵灵儿打苗族黑脸就可以用天剑随便穿穿穿……

  1 #!/bin/bash
  2 
  3 ~/eshard-re/4-ARE/panda/build/i386-softmmu/qemu-system-i386 -replay key-cut -panda callstack_instr -panda stringsearch:name=key,callers=4

其中,-replay后面接之前record的快照名,stringsearch:name接程序名,callers的值代表需要输出的最多栈调用层次。搜索需要设定一个文本文件,名字以<程序名>_search_string.txt命名,里面的内容以双引号括起需要搜索的内容

1 "20E806AF-05376811-38D34527-638DD79A"

运行脚本后,会输出一个<程序名>_string_matches.txt的文件,如下

  1 c10282ed 80012031 b7e70e46 8001267a 80012261 062fa000  1
  2 c10282ed 80012031 b7e70e46 8001267a 80012264 062fa000  1
  3 c10282ed c10fd883 c1165b8d 00000000  1

其中最后一列是搜索字符串击中的次数,倒数第二列是地址空间(不明含义),倒数第三列为需要找到的PC指针的值,再向前的列是栈调用使用的地址。
所以可以看到之前输入的字符串,出现在了PC指针为0x80012261和0x80012264的位置。默认基址0x80000000,因此现在就应该去二进制文件中找到0x12261和0x12264两行指令对应的内容。

0x03 二进制逆向

用IDA Pro打开二进制文件,相应地址附近的内容如下

.text:00012236 _sn_cpy         proc near               ; CODE XREF: sub_12275+117p
.text:00012236
.text:00012236 var_4           = dword ptr -4
.text:00012236 arg_0           = dword ptr  8
.text:00012236 arg_4           = dword ptr  0Ch
.text:00012236 arg_8           = dword ptr  10h
.text:00012236
.text:00012236                 push    ebp
.text:00012237                 mov     ebp, esp
.text:00012239                 sub     esp, 10h
.text:0001223C                 call    sub_123D5
.text:00012241                 add     eax, 1CDDBFh
.text:00012246                 mov     [ebp+var_4], 0
.text:0001224D                 jmp     short loc_1226A
.text:0001224F ; ---------------------------------------------------------------------------
.text:0001224F
.text:0001224F loc_1224F:                              ; CODE XREF: _sn_cpy+3Aj
.text:0001224F                 mov     edx, [ebp+arg_4]
.text:00012252                 lea     eax, [edx+1]
.text:00012255                 mov     [ebp+arg_4], eax
.text:00012258                 mov     eax, [ebp+arg_0]
.text:0001225B                 lea     ecx, (dword_1E0000+1 - 1E0000h)[eax]
.text:0001225E                 mov     [ebp+arg_0], ecx
.text:00012261                 movzx   edx, byte ptr [edx]
.text:00012264                 mov     byte ptr ds:(dword_1E0000 - 1E0000h)[eax], dl
.text:00012266                 add     [ebp+var_4], 1
.text:0001226A
.text:0001226A loc_1226A:                              ; CODE XREF: _sn_cpy+17j
.text:0001226A                 mov     eax, [ebp+var_4]
.text:0001226D                 cmp     eax, [ebp+arg_8]
.text:00012270                 jl      short loc_1224F
.text:00012272                 nop
.text:00012273                 leave
.text:00012274                 retn
.text:00012274 _sn_cpy         endp

F5党当然还是看C代码舒服

int __cdecl sn_cpy(int a1, int a2, int a3)
{
  int v3; // edx@2
  int v4; // eax@2
  int result; // eax@3
  int i; // [sp+Ch] [bp-4h]@1

  sub_123D5();
  for ( i = 0; ; ++i )
  {
    result = i;
    if ( i >= a3 )
      break;
    v3 = a2++;
    v4 = a1;
    a1 += (int)((char *)&dword_1E0000 + 0xFFE20001);
    *(_BYTE *)v4 = *(_BYTE *)v3;
  }
  return result;
}

可以看到这个函数主要就是做了一个从a2到a1的复制,长度a3。按x查找交叉调用,看到

BOOL __cdecl sub_12275(signed int a1, int a2)
{
  int v2; // ST00_4@14
  int v3; // eax@14
  BOOL result; // eax@15
  unsigned int i; // [sp+4h] [bp-14h]@1
  unsigned int j; // [sp+8h] [bp-10h]@7
  int v7; // [sp+Ch] [bp-Ch]@13

  for ( i = 0; i <= 8; ++i )
  {
    if ( aVhuldoRn[i] - 3 > 96 && aVhuldoRn[i] - 3 <= 122 )
      aVhuldoRn[i] -= 3;
  }
  for ( j = 0; j <= 9; ++j )
  {
    if ( aVhuldoEdg[j] - 3 > 96 && aVhuldoEdg[j] - 3 <= 122 )
      aVhuldoEdg[j] -= 3;
  }
  v7 = 0;
  if ( a1 > 1 )
  {
    v2 = *(_DWORD *)(a2 + 4);
    v3 = local_strlen_(0x1E0000);
    sn_cpy((int)&sn_cpy_dst, *(_DWORD *)(a2 + 4), v3);
    v7 = cmp_sn_with_deadbeef_xor((int)&sn_cpy_dst);
  }
  puts(off_1E0060[v7 != 0]);
  *(_DWORD *)&result = v7 == 0;
  return result;
}

函数cmp_sn_with_deadbeef_xor就是核心函数了。

int __cdecl cmp_sn_with_deadbeef_xor(int *a1)
{
  int result; // eax@2
  unsigned int v2; // ST38_4@6
  signed int i; // [sp+1Ch] [bp-2Ch]@3
  int v4[3]; // [sp+2Ch] [bp-1Ch]@4
  int v5; // [sp+38h] [bp-10h]@6
  int v6; // [sp+3Ch] [bp-Ch]@1

  v6 = *MK_FP(__GS__, 20);
  if ( local_strlen_(0x1E0000) == 35 ) //strlen of input is 35
  {
    for ( i = 0; i <= 3; ++i )
      v4[i] = strtoll((const char *)a1 + 9 * i, 0, 16);// hex convert. a1=sn_cpy_dst
                                                // convert 4 blocks of sn strings into hex value
    v2 = sub_1213D(v4);                         // 0xdeadbeef xor
    result = v2 == v5;              //compare if 0 then win
  }
  else
  {
    result = 0;
  }
  if ( *MK_FP(__GS__, 20) != v6 )
    sub_136430();
  return result;
}

对于[sp+3Ch] [bp-Ch]@1这种标识, 看雪 上有个很好的解释
用sp寄存器寻址的话是'[sp+18h]'
用bp寄存器寻址的话是'[bp-158h]'
C编译器有时候会编译成'[esp+ *** ]'的访问局部变量代码。
@1表示是第一个使用这个地址的变量

result = v2 == v5;可知,当sub_1213D的结果为0时,校验通过。

unsigned int __cdecl sub_1213D(int *a1)
{
  unsigned int v2; // [sp+8h] [bp-8h]@1
  signed int i; // [sp+Ch] [bp-4h]@1

  sub_123D5();
  v2 = 0xDEADBEEF;
  for ( i = 0; i <= 2; ++i )
    v2 ^= a1[i];
  return v2;
}

而sub_1213D为异或函数,由此反推只要每段序列号异或后为0xDEADBEEF即可。
测试证明

(panda_env) enigma001@ubuntu:~/eshard-re/4-ARE/workspace/key-analysis$ ./key deadbeef-00000000-00000001-00000001
serial ok

0x04 小trick

一般偷懒静态分析"比较-验证"模式的程序,最快的方法是找输出的信息字符串,比如此例中的"serial bad"或"serial ok"。用Linux中的strings命令或IDA Pro中的Strings View可以进行此类检索。然而在本例的key程序中,结果信息字符串被进行了混淆,通过正常查找"serial bad"完全没有任何信息。
事后发现字符串做了一个简单的凯撒密码的替换,替换实现在上面的sub_12275函数,替换规则为3个字母差。原始字符存储如下:

.data:001E0048 aVhuldoRn       db 'vhuldo rn',0        ; DATA XREF: sub_12275:loc_12290o
.data:001E0048                                         ; sub_12275+34o ...
.data:001E0052                 align 4
.data:001E0054 aVhuldoEdg      db 'vhuldo edg',0       ; DATA XREF: sub_12275:loc_122F7o
.data:001E0054                                         ; sub_12275+9Bo ...
.data:001E005F                 align 10h

有意思,真有意思。

Comments
Write a Comment