LUOJIABASUO

  • 半个月前参加了一场比赛,我主要是负责的Re但是只解出了一个比较简单的那就是Pyre其他的我打开来看了一下就关了甚至没调试。后来发现还是挺简单的一个题目

RE

反调试

  • 首先老规矩IDA打开静态分析看看,发现压根无法分析。主要是这里使用了虚表调用。虚表调用的子程序(函数)是在每次运行的时候才计算出来子程序的地址。这导致IDA分析的时候是无法对齐静态的分析的。所以

  • 按照老规矩动态调试挂起来,发现IDA没有控制权无法控制程序当程序运行起来的 时候没有响应切不能在IDA结束远程的调试目标。那说明存在反调试。

  • 如何寻找反调试的地方在哪呢?也挺简单的只要在入口处下个断点不断的运行程序看看哪里IDA没有控制权了那么说明该地方具有反调试
  • 要注意的 是main函数不是最开始的入口,”__scrt_common_main_seh“ 这个子程序才是最开始执行的

引用来自https://www.dazhuanlan.com/relic256/topics/1212274

虽然 VS 系列入口函数均为mainCRTStartup,但不同版本的实现仍然有些许不同。 在 VC6.0 中,可通过 APIGetVersionGetCommandLine来找到入口函数。而main函数是其中唯一一个 3 个参数的函数,故可以通过找 3 个 push 来找到main函数 (WinMain为 4 个 push)。 在 VS2013 中,开始先使用__security_init_cookie函数初始化 cookie(/GS 选项需要使用的,详见),再进入_tmainCRTStartup中进行类似 VC6 中的类似初始化。这在 Debug 版的汇编中体现为一个 jmp 后两个连续的 call,而main函数在第二个 call 当中。在 Release 版中,因为优化而入口处直接便是一个 call 后接一个 jmp,而main函数在 jmp 中。 在 VS2015 中,mainCRTStartup函数中只有__scrt_common_main函数,在这个函数中先调用__security_init_cookie,再调用__scrt_common_main_seh,在__scrt_common_main_seh中进行类似 VC6 的初始化,其后使用invoke_main来调用main函数 (或者其他版本的 main)。这些在 Debug 版的汇编中体现为一个 jmp 后一个 call,再后两个连续的 call,而main函数在第二个 call 中的invoke_main当中。

  • 所以我们不能直接在main函数的入口下断,而是在__scrt_common_main_seh的领空下断

  • 发现调用了_initterm后就就是去了控制权,这个函数看样子应该是标准库的函数

引用来自https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/initterm-initterm-e?view=msvc-170

Internal methods that walk a table of function pointers and initialize them. The first pointer is the starting location in the table and the second pointer is the ending location.

  • 看不懂没关系,因为我也看不懂哈哈哈哈。我猜测应该就是初始化。直接重新打开程序挂起动态跟进程序看看。可以看到循环调用了一个类似IAT表一样的表里的函数。这里可以单步调试看看RAX值为啥的时候IDA失去控制权也可以用跟踪。这里我用单步调试
  • 在call地方下个断点然后运行程序查看每次断下来后RAX的变化。发现当RAX为0x00007FF729431170的时候IDA失去了控制权
  • 直接跟进发现调用了一个子程序

  • F7跟进后在该子程序中发现了反调试主要是NtSetInformationThread

引用来自https://anti-debug.checkpoint.com/techniques/interactive.html#ntsetinformationthread

The function ntdll!NtSetInformationThread() can be used to hide a thread from a debugger. It is possible with a help of the undocumented value THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11). This is intended to be used by an external process, but any thread can use it on itself. After the thread is hidden from the debugger, it will continue running but the debugger won’t receive events related to this thread. This thread can perform anti-debugging checks such as code checksum, debug flags verification, etc. However, if there is a breakpoint in the hidden thread or if we hide the main thread from the debugger, the process will crash and the debugger will be stuck. In the example, we hide the current thread from the debugger. This means that if we trace this code in the debugger or put a breakpoint to any instruction of this thread, the debugging will be stuck once ntdll!NtSetInformationThread() is called.

  • 意思是程序对调试器隐藏了线程,导致调试器无法找到线程来实现反调试
  • 知道了反调试在这后可以直接对其NOP,BUT!!!我发现NOP后每当程序运行起来会把该地方NOP的指令进行还原执行

  • 可以直接修改属性达到执行了函数并不会隐藏线程,把所有断点去掉后程序可以正常运行,IDA也获得了程序的控制权
  • 但以为这就完了吗?No!这个程序很多地方都有反调试主要是调用了IsDebuggerPresent检测调试器的标志位,直接在IAT导入表查看IsDebuggerPresent的交叉引用

  • 发现有三个地方调用了该函数,解决方法和上面那个反调试的方法一样把返回值改了就行。
  • 记得在函数执行后修改EAX = 0即可

  • 对所有引用的地方都设置好了之后回到main中下个断运行程序开始正题

逻辑分析

  • 运行后发现首先判断了输入的flag的长度是否为36(这里有个技巧如果看到的不是十进制的值可以点击十六进制按H互相转换),重新运行输入36位flag

  • 首先分配大小为0x10大小的内存空间,把内存空间的地址存放到栈里

  • 继续执行调用了一个函数跟进分析发现其主要是把用户输入的flag所存放的位置的地址和一个 Base::vftable子程序的地址分别存放到一个连续的内存中(可理解为数组)
  • 继续发现调用了刚刚的子程序

  • 对比数据加密前后发现是反置了比特位,如13的二进制是1011反置后为1101。不理解就当作是加密,把用户输入的每个字符进行了比特位反置
  • 继续跟进发现了一个暂时不知道干嘛的函数,先记一下他就是对数据进行了赋值操作。了解一下后面需要的时候再回来分析

  • 当直接执行了sub_7FF729432110子程序后发现程序直接闪退了,跟进看看。猜测可能又是反调试

  • 又发现了一个call直接跟进,发现是解密用用户输入的前4位解密数据存放到lpAddress中

  • 有函数继续跟进,跟进发现又是赋值先看一眼后面需要用到参数再回来分析。但里面还调用了子程序逐个走进去看看。发现开辟了一个线程执行子程序查看一下子程序主要的作用是啥
  • 发现是对解密的数据进行逐个检查判断是否有0xCC如果有就退出程序

  • 有函数直接跟进去分析发现把刚刚解密的数据当成子程序执行
  • 那么可以猜测这是SMC(Self Modify Code)

  • 现在就需要把该数据正确的解密出来才可以知道算法是啥。唯一能想到的就是爆破。
  • 如果是子程序那么有一下几个特征
    • 开头必须是movpush
    • 如果是push的时候必须是push rbp
    • 结尾必须是ret
  • 其中根据结尾是ret可以直接把加密后的最后一个字节和ret指令码进行异或得到0xE密钥是4位已经解出了一位但是哪一位不知道
  • 根据下图可得知用231 % 4后就是最后一次的下标为3

  • 现在知道了最后一位了还有3位直接根据前面的规则写出爆破。这里需要借助Capstone
  • 本来是使用Python编写的但发现性能真的太慢了直接用C艹,下面贴出代码
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

#include <iostream>
#include <inttypes.h>
#include <capstone/capstone.h>

using namespace std;

uint8_t smc_code[] =
{
0x27, 0x44, 0x7F, 0xEB, 0x3A, 0x8F, 0x1A, 0x2E, 0xFB, 0x41,
0xE6, 0x46, 0xFB, 0x59, 0xEE, 0x42, 0xFB, 0x49, 0xD6, 0x46,
0xF9, 0x49, 0xEE, 0x85, 0x72, 0x85, 0xB3, 0xF6, 0x3A, 0x87,
0xB3, 0x16, 0xF9, 0x4C, 0xF2, 0x87, 0x37, 0xF8, 0x31, 0x4B,
0x82, 0x0C, 0xF6, 0x0E, 0x72, 0xCB, 0xB3, 0xE2, 0x0D, 0xA0,
0x9C, 0x89, 0xB5, 0x49, 0x0A, 0x0E, 0x72, 0x0C, 0xF6, 0x85,
0x37, 0xF0, 0xCD, 0x4B, 0x62, 0x03, 0x75, 0x8A, 0x72, 0x0C,
0xF6, 0x85, 0x37, 0xF8, 0x7B, 0x1A, 0xB7, 0x0C, 0xF6, 0x0E,
0x72, 0x87, 0xB3, 0xFA, 0xB3, 0xE4, 0xF0, 0x3F, 0xB0, 0x87,
0xB3, 0xFA, 0x73, 0xCE, 0x7D, 0x4B, 0x82, 0x8F, 0x16, 0x0D,
0x3A, 0x81, 0xFA, 0x8B, 0x72, 0x0C, 0xF6, 0x0E, 0x3A, 0x87,
0xB3, 0x2E, 0x3A, 0x0D, 0x3E, 0x85, 0x7A, 0x87, 0xB3, 0xFE,
0x73, 0xC4, 0xC7, 0xDE, 0x73, 0x49, 0x0E, 0x85, 0x37, 0xE0,
0xF7, 0x4B, 0x82, 0x87, 0xB3, 0xF6, 0xFF, 0x18, 0x33, 0x0E,
0x72, 0x0C, 0xF6, 0x85, 0x37, 0xF4, 0x37, 0xE6, 0x74, 0x3D,
0x34, 0x85, 0x37, 0xF4, 0xF7, 0xCC, 0xF9, 0x49, 0x06, 0xCF,
0x9A, 0x07, 0x7F, 0xCE, 0xF1, 0xEC, 0xF5, 0x46, 0xFF, 0x00,
0x73, 0x0E, 0x72, 0x0C, 0xF6, 0x46, 0xF9, 0x49, 0xD6, 0x46,
0x73, 0xC4, 0x7D, 0x06, 0xF9, 0x49, 0x06, 0x0F, 0xBA, 0x3D,
0x26, 0x0F, 0x37, 0xF8, 0x75, 0x4B, 0x8E, 0x0D, 0x1F, 0x7E,
0x8D, 0xF3, 0x09, 0x46, 0xF9, 0x49, 0xEE, 0x85, 0x27, 0xF4,
0x7F, 0x1E, 0x3A, 0x87, 0xB3, 0x16, 0x3A, 0x8F, 0x36, 0x0A,
0xF9, 0x59, 0x02, 0x87, 0x62, 0x9C, 0xBE, 0x8D, 0xB6, 0x2C,
0xAB, 0xCD
};

void reverseBit(uint8_t* data, size_t size) {
for (size_t i = 0; i < size; i++)
{
uint8_t v = *(data + i);
v = (v & 0xAA) >> 1 (v & 0x55) << 1;
v = (v & 0xCC) >> 2 (v & 0x33) << 2;
v = (v & 0xF0) >> 4 (v & 0x0F) << 4;
*(data + i) = v;
}
}

void checkIsValid(uint8_t* key, uint8_t *oriKey) {
uint8_t buff[sizeof(smc_code)];
for (size_t i = 0; i < sizeof(smc_code); i++)
{
buff[i] = smc_code[i] ^ *(key + (i % 4));
if (buff[i] == 0xCC)
return;
}

csh handle;
cs_insn* insn;
size_t count;
if (cs_open(CS_ARCH_X86, CS_MODE_64, &handle) != CS_ERR_OK)
return;

count = cs_disasm(handle, buff, sizeof(buff), 0x1000, 0, &insn);
if (count <= 0)
{
cs_free(insn, count);
return;
}

if (insn[0].id != X86_INS_PUSH && insn[0].id != X86_INS_MOV && insn[count - 1].id != X86_INS_RET)
{
cs_free(insn, count);
return;
}

int sum = 0;
for (size_t i = 0; i < count; i++)sum += insn[i].size;

if (sum == 232 && !\s\t\r\c\m\p(insn[0].op_str, rbp)) {
reverseBit(oriKey+3, 1);
printf(key: %s\t\t0x%x:\t%s\t\t%s\n, oriKey , (int)insn[0].address, insn[0].mnemonic, insn[0].op_str);
}
cs_free(insn, count);
}

int main() {
unsigned char key[5] = { 0 };
key[3] = 0x0E;
unsigned char waitKey[0x7F - 0x21];
int sum = 0;
for (unsigned char i = 0x21; i < 0x7F; i++)
waitKey[i - 0x21] = i;

printf(smc len: %u\n, sizeof(smc_code));
for (size_t i = 0; i < sizeof(waitKey); i++)
{

for (size_t j = 0; j < sizeof(waitKey); j++)
{

for (size_t k = 0; k < sizeof(waitKey); k++)
{
key[0] = waitKey[i];
key[1] = waitKey[j];
key[2] = waitKey[k];
unsigned char oriKey[5] = {0};
strcpy((char *)oriKey, (const char*)key);
reverseBit(key, 3);
checkIsValid(key,oriKey);
}

}

}

}
  • 爆破出几个一个个尝试发现是N0op,代入运行可以看到是一个魔改的TEA加密

  • 继续分析发现加密后使用strncmp进行数据的比较

  • 值得注意的是这里调用了两次分别对输入的不同位置进行了加密,使用了不同的密钥
  • 下下面直接上代码

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
void teaDecrypt(uint32_t*data, uint32_t *key, size_t size)
{

uint32_t v0 = *data;
uint32_t v1 = *(data+1);
uint32_t delta = 0x876AAC7F;
uint32_t sum = delta * size;

for (size_t i = 0; i < size; ++i)
{
v1 -= (v0 + ((v0 >> 6) ^ (8 * v0))) ^ (*(((sum >> 11) & 3) + key) + sum);
sum -= delta;
v0 -= (v1 + ((v1 >> 6) ^ (8 * v1))) ^ (*((sum & 3) + key) + sum);
}
*data = v0;
*(data+1) = v1;
}

void reverseBit(uint8_t* data, size_t size) {
for (size_t i = 0; i < size; i++)
{
uint8_t v = *(data + i);
v = (v & 0xAA) >> 1 (v & 0x55) << 1;
v = (v & 0xCC) >> 2 (v & 0x33) << 2;
v = (v & 0xF0) >> 4 (v & 0x0F) << 4;
*(data + i) = v;
}
}

void checkIsValid(uint8_t* key, uint8_t *oriKey) {

int main() {

uint8_t data[] =
{
0x72, 0x0C, 0xF6, 0x0E, 0x8C, 0x69, 0x23, 0x69, 0x59, 0xA8,
0x06, 0xEF, 0x2A, 0x1A, 0x56, 0xB6, 0x96, 0xAC, 0xEE, 0x92,
0x5C, 0xF2, 0xED, 0x0A, 0x5F, 0x36, 0x8E, 0x41, 0xA6, 0x36,
0x86, 0x72, 0x56, 0xD2, 0x54, 0xC2, 0x00
};

uint8_t key[] = Welcome to the game!\nYour key: ;

teaDecrypt((uint32_t*)(data + 4), key, 0xC);
teaDecrypt((uint32_t*)(data + 20), key + 16, 0xC);
reverseBit(data, 36);
puts((char *)data);
}
  • 解得Flag
    • N0opa_G3Ey#zTXjmi5wIHd&5pRN2elaNjK*C

最后鸣谢

  • “#” 提供思路
  • 以及以上所有参考的文章的博主!!!

PyRE

程序分析

  • 直接下载程序,根绝标题和题目描述
  • ”这种exe文件怎么调用py的库?要怎么逆呢,小明同学,你来试试吧?“
  • 不难得知此程序是Python的一道Re。
  • 使用010Editor查看一下当前程序的版本号。由下图不难看出使用了python3.7进行程序的编译

环境的切换

环境的切换是非必要的,但不切换在反汇编的时候可能会产生一些错误

  • 如果安装的是python3.7的环境即可跳过本步骤
  • 如果安装了conda可以看下面快速切换,如果没有则需要自己去官网下载对应的版本进行切换
1
2
3
4
5
#安装python3.7
conda create -n py37 python=3.7

#切换python3.7环境
conda activate py37

程序的逆向

1
python pyinstxtractor.py [ProgramFilePath]
  • 逆向之后会在pyinstxtractor脚本所在的目录生成一个xxx.exe_extracted的文件夹 。在文件夹中找到一个名字看起来最奇怪的.pyc结尾的文件

  • 打开 在线PYC反编译
  • 把文件丢上去直接反编译出源码
  • But!!!这样做会发现源码中有对b这个变量进行了运算,但是 b 这个变量却是None
  • 使用python标准库自带的marsha和dis库对pyc二进制文件进行反汇编。运行后会自动在窗口中打开bytecode将其复制到文件中分析得知
  • b的值是179
1
2
3
4
import marshal
import dis
with open(rFilePath\pyre.exe_extracted\1.pyc,'rb') as f:
dis.dis(marshal.loads(f.read()[16:]))

编写解密脚本

  • 根据上面的分析不难看出其实就是一个简单的算法
    • ord(a[i]) * 33 % b != c[i]
  • 拿数组中的每一个数值和输入的flag进行运算后的值对比判断是否一致,如果都一致则验证成功
  • 那么我们可以尝试直接使用爆破
  • 首先下面是已知信息
    • 数组c
    • 变量b的值
    • 立即数33
  • 根据flag都是可见字符得知,可以使用ASCII编码表中0x21-0x7E的值对flag进行模拟判断是否和数组c中的每个元素相等即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
c = [
144, 163, 158, 177, 121, 39, 58, 58, 91, 111, 25, 158, 72, 53, 152, 78, 171, 12, 53, 105, 45, 12, 12, 53, 12, 171, 111, 91, 53, 152, 105, 45, 152, 144, 39, 171, 45, 91, 78, 45, 158, 8
]

b = 179

flags = []
for i in range(len(c)):
for j in range(0x21,0x7F):
if j * 33 % b == c[i]:
flags.append(chr(j))

print('.join(flags))