Zj_W1nd's BLOG

Windows下的简单逆向分析入门——从PE开始

2024/09/11

PE 文件格式简析

相比于ELF,PE文件中包含了相当多的可读性非常强(更加直观)的信息。包括预设的程序加载基址0x400000, 导入导出表等等都是直接写明文字符串而且独立于section之外单独存储的。用010editor结合一个exe程序简单分析一下。这里记得分清FOA和RVA的转换与记录区别,windows下的PE文件不区分内存和文件视图,而是用两种不同的地址来进行标识。和linux还是有一点差别的。

PE文件头

一个没什么用的DOS头

以5a4d开头,记录了一些DOS16位模式运行需要的信息,兼容的问题保留了。最后一个字段AddressOfNewExeHeader指向PE头(NtHeader)的FOA地址。

NtHeader

以0x4550(PE)开头。记录了很多关键信息,分为文件头FileHeader和可选头OptionalHeader。

FileHeader

记录运行架构,创建时间戳,符号表指针,optionalheader的大小,seciton数目和一个程序相关的属性flag字。包括是否32位等等信息,flag bit形式保存。
FileHeader

OptionalHeader

magic字段,然后一堆记录了各种东西的主版本号和最低版本号的字段,包括子系统,操作系统等等,略过。还有很多别的对齐信息,checksum,预留栈空间、dll属性flag等等乱七八糟的东西。

重点是这里记录了imagebase(内存镜像基地址,一般exe的都是0x400000),入口点rva地址,以及一个datadir结构体数组指向各个section的RVA地址。OptionalHeader
这个datadir数组里面的成员只有一个地址和一个大小,没有对应节就置空。寻址对应节的方法是,这个数组里面都是按顺序排好的,下标0(第一个)存放export,第二个存放import等等,获取的时候用宏就行,按顺序寻址因此不用别的连接方法。

导入表和导出表

import代表程序要导入哪些外部函数,export则代表要向外提供哪些函数。一般一个dll的导出表会比较多,而exe一般没有导出表(空)。

值得一提的是,我们其实不需要导入表也能在程序运行的时候导入动态库,使用对应的api动态的进行装载即可。这一点在加壳保护等等会有点用

INT和IAT

IAT(Import Address Table)和INT(Import Name Table)这俩玩意在加载前是类似的,都是引入函数的信息。对于INT,其是IMAGE_IMPORT_BY_NAME的结构数组,里面就放函数名和函数序号。这个表即使在程序加载入内存后也不变化。

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //可能为0,编译器决定,如果不为0,是函数在导出表中的索引
BYTE Name[1]; //函数名称,以0结尾,由于不知道到底多长,所以干脆只给出第一个字符,找到0结束
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

而对于IAT,其是IMAGE_THUNK_DATA32的结构数组,他是一个4字节联合体

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; //RVA 指向_IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

这个结构决定了函数可以索引名字也可以索引序号。如果最高位为0,则这里存放对应INT的RVA,如果最高位为1,则直接用31位索引导出序号。程序加载后,IAT就会更换为函数的地址信息。

导入表Import

有点复杂,感觉不如linux的got和plt
首先,对于每个库,都有一个IMAGE_IMPORT_DESCRIPTOR的结构进行维护,导入表存放的是IMAGE_IMPORT_DESCRIPTOR数组,一个库在导入表中占一个条目。

导入表的descriptor结构体只是一个引导,还没有到函数地址。它维护dll的名字(也是RVA),时间戳,ForwarderChain标志(可以不管)以及比较关键的两个指针:指向INT RVA的OriginalFirstThunk和指向IAT RVA的FirstChunk。
ImportTable

导出表

导出表就可以说的比较简单,它维护一个IMAGE_EXPORT_DIRECTORY数组,每个条目里面保存了时间戳、版本号、模块名的RVA、api基数(从1开始),函数的数目,函数名数目(很多函数没有名字),以及导出地址表、导出名称表、导出序列号的rva。

重定位表

参考https://www.cnblogs.com/Chary/p/12981261.html
linux的地址在链接过程中就已经将符号全部重定位好了,而且全部都是偏移地址,base在装载的时候再说。而windows我们知道pe头中每个imagebase绝对地址都是写死的而且dll大概率都一样,偏移也写死的话那地址冲突怎么办?那就需要重定位表。他本质存放的是一堆需要修改的代码地址。感觉不如linux的延迟绑定但是似乎是更直观的解法。为了节约,重定位表是按页记录重定位的信息的。每个条目管1000字节的程序,就两个字段(有用),第一个字段指向要重定位的RVA(0x1000 page align),第二个字段sizeofblock则是记录重定位块的大小,后面跟的都是具体偏移什么的。

TLS表

什么是TLS

TLS全程是Thread Local Storage,即线程局部存储,它关联到具体的线程而非进程。顾名思义,它是为了实现相同进程下不同线程间互相独立的地址空间而存在的,总之它能为线程提供一个拥有全局变量或者静态数据的视图,但其实是副本这种感觉。简单来讲它的实现,它为每个线程分配了一个单独的数组,然后根据索引去查找数组的值。对于不同的线程,索引一样但实际地址不同值也不同。

TLS回调函数在main函数之前就会运行,会阻截调试。这是反调试的一个关键。参见反调试技术。

TLS表

在windows下,TLS的使用分为动态和静态两种。动态使用指的是通过tlsalloctlssetvalue等动态函数,在程序运行期间对tls相关的结构和空间进行初始化,而静态使用则会为pe文件生成tls表。

tls表会保存通过tls访问的数据的初始化其实地址和结束地址,范围里存储程序中tls所使用的全局变量的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;//TLS初始化数据的起始地址
DWORD EndAddressOfRawData; //TLS初始化数据的结束地址 两个正好定位一个范围,范围放初始化的值
DWORD AddressOfIndex; // TLS 索引的位置
DWORD AddressOfCallBacks; // Tls回调函数的数组指针
DWORD SizeOfZeroFill;  //填充0的个数
union {
DWORD Characteristics; //保留
struct {
DWORD Reserved0 : 20;
DWORD Alignment : 4;
DWORD Reserved1 : 8;
} DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;

} IMAGE_TLS_DIRECTORY32;

多的不讲了,反调试细说。

延迟导入表

加速加载速度用,这个descriptor结构体每个存储了属性,以及指向dll名字,iat,int和一堆东西的rva。这玩意可以延迟导入不常用的dll函数,类似于linux下的延迟绑定,一次绑定后IAT就被写好后面就不用再绑了。

这个功能需要编译的时候手动开启然后自己制定延迟导入的dll名称。虽然条目也是按dll写,延迟导入是一次一个函数的。
Delay loader

资源和其他表

windows将位图,鼠标指针,各种消息等等都作为资源存储。也是用表存储,这里不再多说,看不下去了没啥意思。

PE的装载和内存视图

反调试技术

TLS反调试

参考https://blog.csdn.net/Joyce_hjll/article/details/136732526
我们知道,自己注册的tls函数会在主线程调用main之前就调用,回调函数触发的时机总共有下面几个:

  • 主线程调用main之前

  • 子线程启动前

  • 子线程结束后

  • 主线程结束后一般是用主线程调用main前启动回调函数来阻截调试让程序退出,需要patch。

入口点识别

一般的main函数

32位下从入口开始寻找3个push一个call的函数(三个参数)。release版本主函数一般就在地址开始附近,拉到顶就行了。

WMCTF2024 Ezlearn

第一次看windows的简单逆向分析。程序也是用了自定义section代码数据共存,函数调用直接jmp混淆call reference分析等等的神秘方法。介绍下几个关键点。

破坏堆栈平衡的花指令

刚开始不懂,IDA报错sp分析失败,跟进看一下发现是程序采用了这么一套代码来进行反汇编混淆:

1
2
3
4
5
6
7
8
call $+5
pop ebp
dec eax ; useless
add ebp (offset a-offset b) ; 就是8
push ebp
retn

..next code

这样一套流程下来,其实就是代码顺序往下执行,利用call和$符号将代码地址压栈,pop之后借助ebp的push+ret再恢复控制流。但是堆栈的分析就会被破坏。这样的指令有很多,把这种花指令要全nop掉就能反汇编了。

TLS反调试函数

直接用ida动调的时候一开就会退出,用x64dbg看了一眼退出前的最后一个call,发现指向这个函数:

1
2
3
4
5
.text:001A10FF
.text:001A10FF public TlsCallback_0
.text:001A10FF TlsCallback_0 proc near ; DATA XREF: .rdata:TlsCallbacks↓o
.text:001A10FF 000 jmp TlsCallback_0_0
.text:001A10FF TlsCallback_0 endp

跟进发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
HANDLE CurrentProcess; // eax
int result; // eax
BOOL pbDebuggerPresent; // [esp+DCh] [ebp-Ch] BYREF

pbDebuggerPresent = 0;
CurrentProcess = GetCurrentProcess();
CheckRemoteDebuggerPresent(CurrentProcess, &pbDebuggerPresent);
if ( pbDebuggerPresent )
exit(0);
dword_1AD724 = (int)&loc_1A1855;
result = &loc_1A1896 - &loc_1A1855;
dword_1AD728 = &loc_1A1896 - &loc_1A1855;
return result;
}

这里利用了TLS回调函数加载先于主模块main入口的特性,检查到调试直接退出了。我们将PE头datadir中的tls部分全部patch掉,然后将文件中rdata部分的tls回调函数表也全patch掉(都写成0),程序就能调试了。

另外,x64dbg首选项开了在tls回调中断,但是这里没有检测,估计是因为用了一个jmp直接跳隐藏call tls函数。

加密函数

加密函数也是用了上面的混淆指令和jmp隐藏的混淆,首先看主函数逻辑,它读取一个长32的字符串然后按照前16和后16分别加密两次,每次加密完后和预设的结果比较。然后patch堆栈花指令后跟进加密函数。

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
__int64 __cdecl encrypt(int arr1, char *input_0_16_32, _DWORD *arr0)
{
int i; // [esp+DCh] [ebp-C8h]
int v5[3]; // [esp+E8h] [ebp-BCh] BYREF
int v6[3]; // [esp+F4h] [ebp-B0h] BYREF
char v7[12]; // [esp+100h] [ebp-A4h] BYREF
int int0; // [esp+10Ch] [ebp-98h] BYREF
int int4; // [esp+110h] [ebp-94h] BYREF
int int8; // [esp+114h] [ebp-90h] BYREF
_DWORD int16[34]; // [esp+118h] [ebp-8Ch] BYREF

__CheckForDebuggerJustMyCode(byte_D8F0A2);
int0 = *(_DWORD *)input_0_16_32;
int4 = *((_DWORD *)input_0_16_32 + 1);
int8 = *((_DWORD *)input_0_16_32 + 2);
int16[0] = *((_DWORD *)input_0_16_32 + 3);
j_init_enc(arr1);
for ( i = 0; i < 32; ++i )
{
j_xor_0x12(&int4 + i, &int16[i - 1], &int16[i], &result[i], v7, 4);
j_Sbox_substitution((int)v7, (int)v6, 4);
v5[0] = ((v6[0] << 24) | ((unsigned __int64)(unsigned int)v6[0] >> 8)) ^ ((v6[0] << 18) | ((unsigned __int64)(unsigned int)v6[0] >> 14)) ^ ((v6[0] << 10) | ((unsigned __int64)(unsigned int)v6[0] >> 22)) ^ v6[0] ^ ((4 * v6[0]) | ((unsigned __int64)(unsigned int)v6[0] >> 30));
j_xor_0x34((int)(&int0 + i), (int)v5, (int)&int16[i + 1], 4);
}
*arr0 = int16[32];
arr0[1] = int16[31];
arr0[2] = int16[30];
arr0[3] = int16[29];
return 0xC00000000i64;
}

在循环之前,主加密函数还调用了另一个函数,跟进去再看,发现是个类似的加密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl init_enc(int arr1)
{
int i; // [esp+DCh] [ebp-C8h]
int v3[3]; // [esp+E8h] [ebp-BCh] BYREF
int v4[3]; // [esp+F4h] [ebp-B0h] BYREF
char tmp_arr2[12]; // [esp+100h] [ebp-A4h] BYREF
int tmp_arr[37]; // [esp+10Ch] [ebp-98h] BYREF

__CheckForDebuggerJustMyCode(byte_D8F0A2);
j_xor_0x34(arr1, (int)data_16bytes, (int)tmp_arr, 16);// 初始轮密钥
for ( i = 0; i < 32; ++i )
{
j_xor_0x12(&tmp_arr[i + 1], &tmp_arr[i + 2], &tmp_arr[i + 3], &data_128bytes[i], tmp_arr2, 4);
j_Sbox_substitution((int)tmp_arr2, (int)v4, 4);
v3[0] = ((v4[0] << 23) | ((unsigned __int64)(unsigned int)v4[0] >> 9)) ^ v4[0] ^ ((v4[0] << 13) | ((unsigned __int64)(unsigned int)v4[0] >> 19));
j_xor_0x34((int)&tmp_arr[i], (int)v3, (int)&tmp_arr[i + 4], 4);
result[i] = tmp_arr[i + 4];
}
return 0;
}

中间的小函数一路跟进去就能看懂功能,xor_0x12是将参数123和一个预设的数组按字节异或之后再异或0x12存放至参数4,里面有个异或0没用。我们可以注意到最后一个参数永远是4,其实就是按int进行4字节的异或。

1
2
3
4
5
6
7
void __cdecl xor_0x12(int a1, int a2, int a3, int a4, int a5, int const_4)
{
int i; // [esp+D0h] [ebp-8h]

for ( i = 0; i < const_4; ++i )
*(_BYTE *)(i + a5) = const_0_d8d720 ^ *(_BYTE *)(i + a4) ^ *(_BYTE *)(i + a3) ^ *(_BYTE *)(i + a2) ^ *(_BYTE *)(i + a1) ^ 0x12;
}

第二个函数很明显是一个代换,S盒写好了:

1
2
3
4
5
6
7
8
int __cdecl Sbox_substitution(int a1, int a2, int a3)
{
int i; // [esp+D0h] [ebp-38h]
__CheckForDebuggerJustMyCode(byte_D8F0A2);
for ( i = 0; i < a3; ++i )
*(_BYTE *)(i + a2) = Sbox[16 * ((int)*(unsigned __int8 *)(i + a1) >> 4) + (*(_BYTE *)(i + a1) & 0xF)];
return 0;
}

第三步是代换后的结果进行一系列位运算

1
v3[0] = ((v4[0] << 23) | ((unsigned __int64)(unsigned int)v4[0] >> 9)) ^ v4[0] ^ ((v4[0] << 13) | ((unsigned __int64)(unsigned int)v4[0] >> 19));

最后异或0x34:

1
2
3
4
5
6
7
void __cdecl xor_0x34(int arr1, int arr_d8d700, int arr_enc5, int const_16)
{
int i; // [esp+D0h] [ebp-8h]

for ( i = 0; i < const_16; ++i )
*(_BYTE *)(i + arr_enc5) = const_0_d8d720 ^ *(_BYTE *)(i + arr_d8d700) ^ *(_BYTE *)(i + arr1) ^ 0x34;
}

然后结果存放到一个数组里。

控制流分析

我们可以注意到,第一个调用的相似结构的加密函数里面没有涉及任何我们的输入操作,其结果存放在数组中后再进行外层的加密处理。因此patch掉反调试之后,我们可以直接过掉这个函数然后把数组里面的东西直接导出来看外层。动调时候获得的:

1
2
3
4
5
6
7
8
9
10
.data:00D8D730 ; int result[32]
.data:00D8D730 result dd 0FF055A4Eh, 529CC66Ah, 18E3DA43h, 465E0E4h, 0FD58BCB6h
.data:00D8D730 ; DATA XREF: init_enc+15F↑w
.data:00D8D730 ; encrypt+FB↑o
.data:00D8D744 dd 2C4E97ECh, 48A234B8h, 842EE158h, 7C55AA74h, 9BEF3AFEh
.data:00D8D758 dd 8779FE09h, 1685B020h, 95794366h, 7AC1501Fh, 7FB0D538h
.data:00D8D76C dd 38980B16h, 33D37C51h, 5FAACC9Bh, 0D47351CCh, 48CEEAB2h
.data:00D8D780 dd 7C296054h, 0F163D1EFh, 0DDB1E47h, 9DA4F767h, 0FAF4B1E0h
.data:00D8D794 dd 0FC5A5FB1h, 0D3FED672h, 264B1A75h, 0EFA7E6C4h, 94B344A4h
.data:00D8D7A8 dd 0ED19375Fh, 58AA0CEDh

函数功能逆完之后流程就比较简单了,他是一个流式加密的方法,一共用到36字节空间,但是每次异或加密等操作的时候都按int型处理(连续4字节一个单位),取输入的前4个字节做初始向量,经过以下步骤循环32轮后得到密文:

  1. 从输入的input[1]算起, 当前的连续三个单位(滚动向前,123)和result数组轮数对应位置的值进行异或得到中间结果a

  2. 以a为索引(高4位行低4位列)进行S盒代换得到结果b

  3. 对b进行指定的移位、按位或和异或操作得到结果c

  4. 将c,input[0](滚动向前,0)算起的一个单位异或,存放至单位4,完成一轮操作

  5. 循环32次直至拿到最后的4个4字节int值,倒序赋值并返回获得16字节加密结果

解密

我们知道异或操作是可逆的,破解这个加密算法其实并不困难,只需要从最后四字节倒着推回去就行了。我们知道最后新的字节的获得是0号单位和s盒替换后又做了位运算的值异或得到的,有后四个单位的值和运算,我们就要拿到S盒的下标。因此只需要用前三个值和导出的result数组先异或0x12拿到下标,S盒代换再位运算,最后获得的值和0x34与最后一个值异或就能往前推一位,倒着重复32次就能拿到输入了。

EXP如下,很多导出的数据都没有用,因为动调直接跳过了一个大的初始加密环节。

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# 由一个encrypt函数定义一轮加密,一轮加密分几个步骤
# 首先按int取出输入(int)input[0], input[1], input[2], input[3]

# enc3: 接收3个数组和一个常量16,将12数组的0-16下标(按char)对应异或后再异或0x34存至3,3为上级
# 调用者创建的局部数组,12来自更早,16个char,4个int,反正都一样

# 然后刚刚得到了16个字节的结果,按照4,8,12和一个固定数组的顺序传入,全部异或再异或0x12
#

from struct import *
# const1_16bytes=[0xC6, 0xBA, 0xB1, 0xA3, 0x50, 0x33, 0xAA, 0x56, 0x97, 0x91,0x7D, 0x67, 0xDC, 0x22, 0x70, 0xB2]
# const2_128bytes=[ 0x15, 0x0E, 0x07, 0x00, 0x31, 0x2A, 0x23, 0x1C, 0x4D, 0x46,
# 0x3F, 0x38, 0x69, 0x62, 0x5B, 0x54, 0x85, 0x7E, 0x77, 0x70,
# 0xA1, 0x9A, 0x93, 0x8C, 0xBD, 0xB6, 0xAF, 0xA8, 0xD9, 0xD2,
# 0xCB, 0xC4, 0xF5, 0xEE, 0xE7, 0xE0, 0x11, 0x0A, 0x03, 0xFC,
# 0x2D, 0x26, 0x1F, 0x18, 0x49, 0x42, 0x3B, 0x34, 0x65, 0x5E,
# 0x57, 0x50, 0x81, 0x7A, 0x73, 0x6C, 0x9D, 0x96, 0x8F, 0x88,
# 0xB9, 0xB2, 0xAB, 0xA4, 0xD5, 0xCE, 0xC7, 0xC0, 0xF1, 0xEA,
# 0xE3, 0xDC, 0x0D, 0x06, 0xFF, 0xF8, 0x29, 0x22, 0x1B, 0x14,
# 0x45, 0x3E, 0x37, 0x30, 0x61, 0x5A, 0x53, 0x4C, 0x7D, 0x76,
# 0x6F, 0x68, 0x99, 0x92, 0x8B, 0x84, 0xB5, 0xAE, 0xA7, 0xA0,
# 0xD1, 0xCA, 0xC3, 0xBC, 0xED, 0xE6, 0xDF, 0xD8, 0x09, 0x02,
# 0xFB, 0xF4, 0x25, 0x1E, 0x17, 0x10, 0x41, 0x3A, 0x33, 0x2C,
# 0x5D, 0x56, 0x4F, 0x48, 0x79, 0x72, 0x6B, 0x64]

Sbox=[0xD6, 0x90, 0xE9, 0xFE, 0xCC, 0xE1, 0x3D, 0xB7, 0x16, 0xB6,
0x14, 0xC2, 0x28, 0xFB, 0x2C, 0x05, 0x2B, 0x67, 0x9A, 0x76,
0x2A, 0xBE, 0x04, 0xC3, 0xAA, 0x44, 0x13, 0x26, 0x49, 0x86,
0x06, 0x99, 0x9C, 0x42, 0x50, 0xF4, 0x91, 0xEF, 0x98, 0x7A,
0x33, 0x54, 0x0B, 0x43, 0xED, 0xCF, 0xAC, 0x62, 0xE4, 0xB3,
0x1C, 0xA9, 0xC9, 0x08, 0xE8, 0x95, 0x80, 0xDF, 0x94, 0xFA,
0x75, 0x8F, 0x3F, 0xA6, 0x47, 0x07, 0xA7, 0xFC, 0xF3, 0x73,
0x17, 0xBA, 0x83, 0x59, 0x3C, 0x19, 0xE6, 0x85, 0x4F, 0xA8,
0x68, 0x6B, 0x81, 0xB2, 0x71, 0x64, 0xDA, 0x8B, 0xF8, 0xEB,
0x0F, 0x4B, 0x70, 0x56, 0x9D, 0x35, 0x1E, 0x24, 0x0E, 0x5E,
0x63, 0x58, 0xD1, 0xA2, 0x25, 0x22, 0x7C, 0x3B, 0x01, 0x21,
0x78, 0x87, 0xD4, 0x00, 0x46, 0x57, 0x9F, 0xD3, 0x27, 0x52,
0x4C, 0x36, 0x02, 0xE7, 0xA0, 0xC4, 0xC8, 0x9E, 0xEA, 0xBF,
0x8A, 0xD2, 0x40, 0xC7, 0x38, 0xB5, 0xA3, 0xF7, 0xF2, 0xCE,
0xF9, 0x61, 0x15, 0xA1, 0xE0, 0xAE, 0x5D, 0xA4, 0x9B, 0x34,
0x1A, 0x55, 0xAD, 0x93, 0x32, 0x30, 0xF5, 0x8C, 0xB1, 0xE3,
0x1D, 0xF6, 0xE2, 0x2E, 0x82, 0x66, 0xCA, 0x60, 0xC0, 0x29,
0x23, 0xAB, 0x0D, 0x53, 0x4E, 0x6F, 0xD5, 0xDB, 0x37, 0x45,
0xDE, 0xFD, 0x8E, 0x2F, 0x03, 0xFF, 0x6A, 0x72, 0x6D, 0x6C,
0x5B, 0x51, 0x8D, 0x1B, 0xAF, 0x92, 0xBB, 0xDD, 0xBC, 0x7F,
0x11, 0xD9, 0x5C, 0x41, 0x1F, 0x10, 0x5A, 0xD8, 0x0A, 0xC1,
0x31, 0x88, 0xA5, 0xCD, 0x7B, 0xBD, 0x2D, 0x74, 0xD0, 0x12,
0xB8, 0xE5, 0xB4, 0xB0, 0x89, 0x69, 0x97, 0x4A, 0x0C, 0x96,
0x77, 0x7E, 0x65, 0xB9, 0xF1, 0x09, 0xC5, 0x6E, 0xC6, 0x84,
0x18, 0xF0, 0x7D, 0xEC, 0x3A, 0xDC, 0x4D, 0x20, 0x79, 0xEE,
0x5F, 0x3E, 0xD7, 0xCB, 0x39, 0x48]

arr1=[0x22313,0x821DEF,0x123128,0x43434310]

init_arr=[-16426418,
1386006122,
417585731,
73785572,
-44516170,
743348204,
1218589880,
-2077302440,
2085988980,
-1678820610,
-2022048247,
377860128,
-1787215002,
2059489311,
2142295352,
949488406,
869497937,
1605029019,
-730639924,
1221520050,
2083086420,
-245116433,
232463943,
-1650133145,
-84626976,
-61186127,
-738273678,
642456181,
-274209084,
-1800190812,
-317114529,
1487539437]

init_arr2=[0x4E, 0x5A, 0x05, 0xFF, 0x6A, 0xC6, 0x9C, 0x52, 0x43, 0xDA,
0xE3, 0x18, 0xE4, 0xE0, 0x65, 0x04, 0xB6, 0xBC, 0x58, 0xFD,
0xEC, 0x97, 0x4E, 0x2C, 0xB8, 0x34, 0xA2, 0x48, 0x58, 0xE1,
0x2E, 0x84, 0x74, 0xAA, 0x55, 0x7C, 0xFE, 0x3A, 0xEF, 0x9B,
0x09, 0xFE, 0x79, 0x87, 0x20, 0xB0, 0x85, 0x16, 0x66, 0x43,
0x79, 0x95, 0x1F, 0x50, 0xC1, 0x7A, 0x38, 0xD5, 0xB0, 0x7F,
0x16, 0x0B, 0x98, 0x38, 0x51, 0x7C, 0xD3, 0x33, 0x9B, 0xCC,
0xAA, 0x5F, 0xCC, 0x51, 0x73, 0xD4, 0xB2, 0xEA, 0xCE, 0x48,
0x54, 0x60, 0x29, 0x7C, 0xEF, 0xD1, 0x63, 0xF1, 0x47, 0x1E,
0xDB, 0x0D, 0x67, 0xF7, 0xA4, 0x9D, 0xE0, 0xB1, 0xF4, 0xFA,
0xB1, 0x5F, 0x5A, 0xFC, 0x72, 0xD6, 0xFE, 0xD3, 0x75, 0x1A,
0x4B, 0x26, 0xC4, 0xE6, 0xA7, 0xEF, 0xA4, 0x44, 0xB3, 0x94,
0x5F, 0x37, 0x19, 0xED, 0xED, 0x0C, 0xAA, 0x58]

def sbox_sub(x): ## 高4位寻址行,低4位寻址列
return Sbox[16*(x>>4)+(x&0xf)]

def MyBitCalc(x):
return (((x<<24)|(x>>8)) ^ ((x<<18)|(x>>14)) ^ ((x<<10)|(x>>22)) ^ ((x<<2)|(x>>30)) ^ x) & 0xffffffff

def decrypt(result):
input_flag = [0]*36;
for i in range(4):
input_flag[35-i]=result[i]

for i in range(31,-1,-1):
idx = 0x12121212 ^ input_flag[i+1] ^ input_flag[i+2] ^ input_flag[i+3] ^ init_arr[i]
# 字节寻址sbox
sub0 = sbox_sub(idx&0xff)
sub1 = sbox_sub((idx>>8)&0xff)
sub2 = sbox_sub((idx>>16)&0xff)
sub3 = sbox_sub((idx>>24)&0xff)
sub = sub0 | (sub1<<8) | (sub2<<16) | (sub3<<24)
input_flag[i]=MyBitCalc(sub)^0x34343434^input_flag[i+4]

flag = b""
for i in range(4):
flag += pack("<I",input_flag[i])

print(flag)

# 动调导出前16字节加密后:
result1 = [0xc676e86f, 0xad67e8f8, 0xca9db9ac, 0xb1ae068e]

# 导出后16字节加密后:
result2 = [0xD51B0298, 0xD827C6D3,0x31A5A335, 0x893A7A66]

if __name__ == "__main__":
decrypt(result1)
decrypt(result2)
1
2
3
PS D:\BLOG\source> python -u "d:\ctf\wmctf2024\re\ez_learn_d191751e6ada67ca536acd67e16793ca\exp.py"
b'WMCTF{CRC32andAn'
b'ti_IS_SO_EASY!!}'
CATALOG
  1. 1. PE 文件格式简析
    1. 1.1. PE文件头
      1. 1.1.1. 一个没什么用的DOS头
      2. 1.1.2. NtHeader
        1. 1.1.2.1. FileHeader
        2. 1.1.2.2. OptionalHeader
    2. 1.2. 导入表和导出表
      1. 1.2.1. INT和IAT
      2. 1.2.2. 导入表Import
      3. 1.2.3. 导出表
    3. 1.3. 重定位表
    4. 1.4. TLS表
      1. 1.4.1. 什么是TLS
      2. 1.4.2. TLS表
    5. 1.5. 延迟导入表
    6. 1.6. 资源和其他表
  2. 2. PE的装载和内存视图
  3. 3. 反调试技术
    1. 3.1. TLS反调试
  4. 4. 入口点识别
    1. 4.1. 一般的main函数
  5. 5. WMCTF2024 Ezlearn
    1. 5.1. 破坏堆栈平衡的花指令
    2. 5.2. TLS反调试函数
    3. 5.3. 加密函数
    4. 5.4. 控制流分析
  6. 6. 解密