SWPU2019-ReverseMe

方法一

IDA打开如下图所示(部分分析在图上):

基本可以确定,我们要让v16=0才是正确的,而能使v16=0有两条路径,第一条是伪代码中第89103行的 while 循环,第二条是伪代码中第105111行的if语句。显然,第二条路径是走不通的,因为不可能存在同时满足如下条件:

1
2
3
v17 = v18 < *((_BYTE *)v13 + 1), v18 == *((_BYTE *)v13 + 1)
v17 = v19 < *((_BYTE *)v13 + 2), v19 == *((_BYTE *)v13 + 2)
v17 = v20 < *((_BYTE *)v13 + 3), v20 == *((_BYTE *)v13 + 3)

所以唯一能行的是通过while循环的路径使得v16=0

现在我们从头开始分析伪代码。首先是让我们输入长度为32bytes的字符串,存放于Block中,然后将字符串”SWPU_2019_CTF”复制给v33,接着是一个while循环使Block循环异或v33。之后有一个比较重要的函数sub_D625C0(约第77行),稍后详细说明。在这之后,又是while循环,里面让v12v13作比较,v12来自数组v34(已知),v13来自数组v28,目前来看数组v28为0,显然v12v13不可能相等,但在刚才的分析中,又必须是这里使得v16=0,所以极有可能是刚刚提到的函数sub_D625C0(约第77行)修改了数组v28

这里使用动态调试来分析。首先在jnz处下断点,如下图所示:

随便输入长度为32的字符串。断点触发后,我们可以知道eax(数组v12)和edx(数组v13)的值,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned char eax[] =
{
0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A,
0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16,
0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B,
0xC8, 0x67
};

unsigned char edx[] =
{
0xE4, 0x6A, 0x5F, 0xAE, 0xF6, 0xD4, 0xAF, 0x19, 0xEA, 0x19,
0x19, 0xC3, 0x1D, 0xC3, 0x11, 0xD1, 0x0D, 0xFF, 0x34, 0x04,
0x7A, 0xF1, 0x15, 0x42, 0x26, 0x2D, 0x29, 0x76, 0xE7, 0x19,
0xBA, 0x2B
};

由于数组v12已知,数组v13未知,所以我们需要跟踪v13是怎么产生的。

显然应证了我们的猜测,v13是由函数sub_D625C0所产生的。

再进行新的动调,去除原先的断点,在call处下断点,运行后,再给ebp+var_88开始的32bytes的内存下内存断点(选中后按F2)。然后按F9,程序触发内存断点后会停下来。

可以看到,这里将ecx移动到目标内存中,而ecx[esi+eax]异或[eax -4]得到的,esi+eaxeax -4开始的内存中的值如下(因为触发断点的时候已经执行了一步,所以不要少了前面4bytes):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned char esi_eax[] =
{
0x86, 0x0C, 0x3E, 0xCA, 0x98, 0xD7, 0xAE, 0x19, 0xE2, 0x77,
0x6B, 0xA6, 0x6A, 0xA1, 0x77, 0xB0, 0x69, 0x91, 0x37, 0x05,
0x7A, 0xF9, 0x7B, 0x30, 0x43, 0x5A, 0x4B, 0x10, 0x86, 0x7D,
0xD4, 0x28
};

unsigned char eax_4[] =
{
0x62, 0x66, 0x61, 0x64, 0x6E, 0x03, 0x01, 0x00, 0x08, 0x6E,
0x72, 0x65, 0x77, 0x62, 0x66, 0x61, 0x64, 0x6E, 0x03, 0x01,
0x00, 0x08, 0x6E, 0x72, 0x65, 0x77, 0x62, 0x66, 0x61, 0x64,
0x6E, 0x03
};

当然,现在无法确定这些值是不是固定的。

还能利用的应该就是Block(输入字符串)跟v33(已知字符串)异或后的结果,毕竟我们最终就是获取输入,而跟输入有关的就是异或后的结果(没有的话干嘛要这么做呢?)。

重新下断点,在cmp [ebp+var_8C],10h处下断点(即do…while…一开始的地方),如下图所示:

执行后,shift+F12搜索我们输入的字符串,可能有多个,以\0结尾的才是,情况如下图所示:

依图所示,就是倒数第二个。给这片内存下内存断点,F9执行触发断点(多执行几次),可以发现输入字符串所在的内存发生改变,即进行了异或加密。

在该部分汇编代码中,可以知道edi是字符串下表,ecx是输入字符串的起始地址,并且后面还将异或后的字符串复制到另一片内存中,即ebp+var_64开始的内存。

这一部分清楚后,我们清除原来的所有断点,然后给 call sub_D625C0下断点(就是伪代码中第79行的函数),执行到该处后(此时已将异或后的字符串复制到了ebp+var_64开始的内存),再给ebp+var_64开始的内存下断点。之后再F9触发内存断点,

还是原来那个地方,eax-4开始的内存存储的就是之前异或得到的字符串。因此可以确定的是esi+eax开始的内存中的值是固定的,而eax -4开始的内存中的值是异或得到的字符串。

总结一下,整个过程就是,输入的字符串循环异或 “SWPU_2019_CTF” ,得到的结果再跟

1
2
3
4
5
6
7
unsigned char esi_eax[] =
{
0x86, 0x0C, 0x3E, 0xCA, 0x98, 0xD7, 0xAE, 0x19, 0xE2, 0x77,
0x6B, 0xA6, 0x6A, 0xA1, 0x77, 0xB0, 0x69, 0x91, 0x37, 0x05,
0x7A, 0xF9, 0x7B, 0x30, 0x43, 0x5A, 0x4B, 0x10, 0x86, 0x7D,
0xD4, 0x28
};

异或,得到的结果为

1
2
3
4
5
6
7
unsigned char eax[] =
{
0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A,
0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16,
0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B,
0xC8, 0x67
};

整个伪代码很复杂,但是最终我们分析出来的加密过程却这么简单。脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>

int main() {

unsigned char esi_eax[] =
{
0x86, 0x0C, 0x3E, 0xCA, 0x98, 0xD7, 0xAE, 0x19, 0xE2, 0x77,
0x6B, 0xA6, 0x6A, 0xA1, 0x77, 0xB0, 0x69, 0x91, 0x37, 0x05,
0x7A, 0xF9, 0x7B, 0x30, 0x43, 0x5A, 0x4B, 0x10, 0x86, 0x7D,
0xD4, 0x28
};

unsigned char eax[] =
{
0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A,
0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16,
0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B,
0xC8, 0x67
};

char v33[] = "SWPU_2019_CTF";

for (int i = 0; i < 32; i++) {
char c = (eax[i] ^ esi_eax[i]) ^ v33[i % 13];
printf("%c", c);
}

}

方法二

使用 Ponce 插件,参考IDA插件Ponce的使用-软件逆向-看雪-安全社区|安全招聘|kanxue.com

使用该插件,我们需要找到待求解字符串的地址和长度,以及距离正确路径最近的一个条件跳转。下面详细说明一下怎么使用。

首先搜索输入字符串,找到后,选中32bytes大小(不要合并成一个字符串),右键 -> Symbolic -> Symbolize memory。

然后找到距离正确路径最近的一个条件跳转,经过我们前面的分析,可以确定的是伪代码中第94行,如图所示:

F9执行到该处后,右键该处分支断点 -> SMT Solver -> Negate & Inject,然后就可以得到部分正确字符。

重复粗体字的操作(F9,右键…)直至全部解出。最后在待求解字符串所在的内存中读取即可。


SWPU2019-ReverseMe
http://example.com/2023/09/04/CTF/SWPU2019ReverseMe/
作者
gla2xy
发布于
2023年9月4日
许可协议