2022HWS硬件安全冬令营 X DASCTF Jan 做完了re,简单总结下本次的比赛,以为是个小比赛,也没奖,冬令营很多人估计也不会去,没想到这么多人来打,且人均全栈。。。
基本上考的最多的就是去花指令和vm动调了,算法部分都不是太难,毕竟有两道是vm题型,还有道是加了控制流平坦化。之前也总结过vm的专题文章,主要是调试和明白变量所带的含义。
快过春节了,新年快乐。
BabyVM 先去花,只有jnz,jz,E8一种花,patch掉E8就行,但是比较多,可以考虑写个idapython脚本。
之后可以在这个位置找到vm的初始化。 可以看到对于vm的初始化可以看出,这个vm还是比较复杂,甚至有EIP和跳转之类的东西,而不是简单的一条流水线。
然后到了主函数。 可以看到opcode是3个8字节的数为一组,一共24,相当于第一个数是case语句的选择,第二个数通过调试可以知道,代表的是30字节table的index/2,第三个数就是操作的数据。
我们打印出所有的opcode。
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 #include <stdio.h> int main () { unsigned char opcode1[]; unsigned char opcode2[]; unsigned char opcode3[]; unsigned char opcode4[]; int i,k=0 ; for (i=0 ;i<1583 ;i+=24 ) { printf ("%x : " ,k++); printf ("%x, " ,*(__int64*)(&opcode1[i])); printf ("%llx, " ,*(__int64*)(&opcode1[i+8 ])); printf ("%llx, " ,*(__int64*)(&opcode1[i+16 ])); printf ("\n" ); } printf ("\n\n\n" ); k=0 ; for (i=0 ;i<191 ;i+=24 ) { printf ("%x : " ,k++); printf ("%x, " ,*(__int64*)(&opcode2[i])); printf ("%llx, " ,*(__int64*)(&opcode2[i+8 ])); printf ("%llx, " ,*(__int64*)(&opcode2[i+16 ])); printf ("\n" ); } printf ("\n\n\n" ); k=0 ; for (i=0 ;i<887 ;i+=24 ) { printf ("%x : " ,k++); printf ("%x, " ,*(__int64*)(&opcode4[i])); printf ("%llx, " ,*(__int64*)(&opcode4[i+8 ])); printf ("%llx, " ,*(__int64*)(&opcode4[i+16 ])); printf ("\n" ); } printf ("\n\n\n" ); k=0 ; for (i=0 ;i<1836 ;i+=24 ) { printf ("%x : " ,k++); printf ("%x, " ,*(__int64*)(&opcode3[i])); printf ("%llx, " ,*(__int64*)(&opcode3[i+8 ])); printf ("%llx, " ,*(__int64*)(&opcode3[i+16 ])); printf ("\n" ); } printf ("\n\n\n" ); }
然后来看看vm内部如何处理的。
opcode3,注释已经标里面了。
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 #打印input flag: 0 : 12 , 0 , 0 ,1 : 12 , 1 , 1 ,2 : 12 , 2 , 2 ,3 : 12 , 3 , 3 ,4 : 12 , 6 , 6 ,5 : 12 , 7 , 7 ,6 : 1 , 0 , 69 ,7 : 1 , 1 , 6 e,8 : 1 , 2 , 70 ,9 : 1 , 3 , 75 ,a : 1 , 6 , 74 , b : 1 , 7 , 20 , c : 18 , 0 , ffffffffffffffff, d : 18 , 1 , ffffffffffffffff, e : 18 , 2 , ffffffffffffffff, f : 18 , 3 , ffffffffffffffff, 10 : 18 , 6 , ffffffffffffffff,11 : 18 , 7 , ffffffffffffffff,12 : 1 , 0 , 66 ,13 : 1 , 1 , 6 c,14 : 1 , 2 , 61 ,15 : 1 , 3 , 67 ,16 : 1 , 6 , 3 a,17 : 1 , 7 , 20 ,18 : 18 , 0 , ffffffffffffffff,19 : 18 , 1 , ffffffffffffffff,1 a : 18 , 2 , ffffffffffffffff,1b : 18 , 3 , ffffffffffffffff,1 c : 18 , 6 , ffffffffffffffff,1 d : 18 , 7 , ffffffffffffffff,1 e : 12 , 1 , 1 ,#循环getchar(),长度为0x26 1f : 17 , 0 , ffffffffffffffff, 20 : 5 , 0 , ffffffffffffffff, 21 : 7 , 1 , 1 , 22 : 1 a, 1 , 26 , 23 : 1 e, 1f , ffffffffffffffff, 24 : 19 , ffffffffffffffff, ffffffffffffffff,
这段opcode设计到一些重要的case语句。
case 0x17: 读取一个字符。
case 5: push flag[i] 到addr+偏移的地址处。
case 7: 类似于i++之类的操作,i存放在table的某个位置,忘了。
case 0x1A: 比较i是否等于0x26,从而改变伪标志寄存器,也就是那4个byte。
case 0x1E: 根据伪标志寄存器决定是否跳转,第一个参数就是代表要跳转的地方。
调试可知,前面很大一部分都是在读数据,然后打印,然后后面部分就是在循环getchar()。
sub_C22AB0函数断下来,opcode4,计算部分了。
0 : 6, 0, ffffffffffffffff, //pop form add1+偏移 到T[0],注意相当于是从最下面开始pop的,也就是先pop的 '}'
1 : 1a, 0, 7d,
2 : 1c, 12, ffffffffffffffff, //等于7d则跳到12
#failed。
3 : 1, 0, 77,
4 : 1, 1, 72,
5 : 1, 2, 6f,
6 : 1, 3, 6e,
7 : 1, 6, 67,
8 : 1, 7, 21,
9 : 18, 0, ffffffffffffffff,
a : 18, 1, ffffffffffffffff,
b : 18, 2, ffffffffffffffff,
c : 18, 3, ffffffffffffffff,
d : 18, 6, ffffffffffffffff,
e : 18, 7, ffffffffffffffff,
f : 1, 0, a,
10 : 18, 0, ffffffffffffffff,
11 : 19, ffffffffffffffff, ffffffffffffffff,
#开始循环,不断pop出0x19个数据,也就是flag{},花括号里面的部分,并且push到add2+偏移。
12 : 1, 8, 100,
13 : 1a, 8, e1,
14 : 1e, 19, ffffffffffffffff,
15 : 6, 0, ffffffffffffffff, //pop form add1+偏移
16 : 4, 8, 0, //push to add2+偏移
17 : 9, 8, 1,
18 : 1d, 13, ffffffffffffffff, //是否跳转到13
#比较前5个字符是否是flag{
19 : 6, 0, ffffffffffffffff,
1a : 1a, 0, 7b,
1b : 1f, 3, ffffffffffffffff, //是否跳转到3,也就是failed
1c : 6, 0, ffffffffffffffff,
1d : 1a, 0, 67,
1e : 1f, 3, ffffffffffffffff,
1f : 6, 0, ffffffffffffffff,
20 : 1a, 0, 61,
21 : 1f, 3, ffffffffffffffff,
22 : 6, 0, ffffffffffffffff,
23 : 1a, 0, 6c,
24 : 1f, 3, ffffffffffffffff,
25 : 6, 0, ffffffffffffffff,
26 : 1a, 0, 66,
27 : 1f, 3, ffffffffffffffff,
#开始循环加密和比较。
28 : 12, 9, 9, //异或清零T[2*9]
29 : 1, a, e1,
2a : 3, 7, 9, //pop form add2+偏移,获取enc[i]
2b : 3, 6, a, //pop form add2+偏移,获取flag[i]
2c : 11, 6, 42, //flag[i]^42
2d : d, 6, 2, //flag[i]<<2,存放在T[6*2]
2e : 1b, 6, 7, //比较flag[i]和enc[i]从而改变标志寄存器
2f : 1f, 3, ffffffffffffffff,
30 : 7, 9, 1,
31 : 7, a, 1,
32 : 1a, 9, 20, //判断循环是否结束
33 : 1e, 2a, ffffffffffffffff, //是否跳转到2a
#success
34 : 1, 0, 63,
35 : 1, 1, 6f,
36 : 1, 2, 72,
37 : 1, 3, 72,
38 : 1, 6, 65,
39 : 1, 7, 63,
3a : 18, 0, ffffffffffffffff,
3b : 18, 1, ffffffffffffffff,
3c : 18, 2, ffffffffffffffff,
3d : 18, 3, ffffffffffffffff,
3e : 18, 6, ffffffffffffffff,
3f : 18, 7, ffffffffffffffff,
40 : 1, 0, 74,
41 : 1, 1, 6c,
42 : 1, 2, 79,
43 : 1, 3, 21,
44 : 1, 6, a,
45 : 18, 0, ffffffffffffffff,
46 : 18, 1, ffffffffffffffff,
47 : 18, 2, ffffffffffffffff,
48 : 18, 3, ffffffffffffffff,
49 : 18, 6, ffffffffffffffff,
4a : 19, ffffffffffffffff, ffffffffffffffff,
4b : 0, 0, 0,
4c : 0, 4d0000000000, 402a100000072000,
一些重要的case语句。 case 6:pop 语句,从add1+偏移地址处,取数据提到T[0],是反着提的。
case 0x1C: 和1E差不多,根据伪标志寄存器决定是否跳转,第一个参数就是代表要跳转的地方。
case 0x11:^42
case 0xd: <<2
case 3:pop form add2+偏移
case 4:push to add2+偏移
所以加密就是(flag[i]^0x42)<<2
1 2 3 4 5 6 7 8 9 10 11 12 enc=[0x000000000000009C , 0x00000000000001C0 , 0x00000000000001D8 , 0x00000000000001D4 , 0x00000000000001D4 , 0x00000000000001E8 , 0x00000000000001C8 , 0x0000000000000098 , 0x00000000000001C8 , 0x00000000000001C0 , 0x00000000000001EC , 0x000000000000008C , 0x00000000000001D4 , 0x000000000000008C , 0x00000000000001EC , 0x00000000000001EC , 0x00000000000001C0 , 0x00000000000001C0 , 0x00000000000001D8 , 0x00000000000001D4 , 0x000000000000009C , 0x00000000000001D0 , 0x00000000000001D0 , 0x00000000000001D0 , 0x00000000000001D4 , 0x00000000000001E8 , 0x00000000000001D0 , 0x00000000000001EC , 0x00000000000001C8 , 0x00000000000001C8 , 0x00000000000001E8 , 0x000000000000008C ] for i in enc: print(chr ((i>>2 ) ^0x42 ),end='' )
虽然这道题加密部分并不是很难,但是这种vm的格式却让我眼前一亮,不仅有EIP,更是模拟出来了C语言中的for循环,不再是简简单单的流水线式opcode。
EasyVM 做题时的笔记没了,g。
开头一个NtCurrentPeb()->BeingDebugged,简单绕过下就行。
然后会引发一个异常。 断下来后我们就可以看到大概的逻辑了。 sub_401000函数,vm结构体,vm指令处理函数。
struct vm {
_DWORD* functions;
_DWORD* EIP;
_DWORD eax;
_DWORD ebx;
_DWORD ecx;
_DWORD edx;
_DWORD* enc;
_DWORD 忘了;
_DWORD* flag;
}
c++类实现的,还是动调慢慢分析一轮吧,实际上需要分析两轮,函数对EIP的修改都不一样,opcode很少,加密部分在循环,基本上只用到eax,ebx,ecx三个个寄存器。ecx作为index。
加密逻辑就是
flag[0]^0xee^0
flag[1]^0xee^flag[0]
flag[2]^0xee^flag[1]
···
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import base64enc=[ 0xBE , 0x36 , 0xAC , 0x27 , 0x99 , 0x4F , 0xDE , 0x44 , 0xEE , 0x5F , 0xDA , 0x0B , 0xB5 , 0x17 , 0xB8 , 0x68 , 0xC2 , 0x4E , 0x9C , 0x4A , 0xE1 , 0x43 , 0xF0 , 0x22 , 0x8A , 0x3B , 0x88 , 0x5B , 0xE5 , 0x54 , 0xFF , 0x68 , 0xD5 , 0x67 , 0xD4 , 0x06 , 0xAD , 0x0B , 0xD8 , 0x50 , 0xF9 , 0x58 , 0xE0 , 0x6F , 0xC5 , 0x4A , 0xFD , 0x2F , 0x84 , 0x36 , 0x85 , 0x52 , 0xFB , 0x73 , 0xD7 , 0x0D , 0xE3 ] print(hex (0xBE ^0xee ),end=',' ) for i in range (1 ,len (enc)): print(hex (enc[i-1 ]^enc[i]^0xee ),end=',' ) table=[0x50 ,0x66 ,0x74 ,0x65 ,0x50 ,0x38 ,0x7f ,0x74 ,0x44 ,0x5f ,0x6b ,0x3f ,0x50 ,0x4c ,0x41 ,0x3e ,0x44 ,0x62 ,0x3c ,0x38 ,0x45 ,0x4c ,0x5d ,0x3c ,0x46 ,0x5f ,0x5d ,0x3d ,0x50 ,0x5f ,0x45 ,0x79 ,0x53 ,0x5c ,0x5d ,0x3c ,0x45 ,0x48 ,0x3d ,0x66 ,0x47 ,0x4f ,0x56 ,0x61 ,0x44 ,0x61 ,0x59 ,0x3c ,0x45 ,0x5c ,0x5d ,0x39 ,0x47 ,0x66 ,0x4a ,0x34 ,0x0 ] key=[0xa ,0xb ,0xc ,0xd ] for i in range (len (table)): table[i]=table[i]^key[i%4 ] print("\n" ) flag=base64.b64decode(bytes (table)) print(flag)
babyre 先去花,这里有两种花,一种和前面的差不多,还有种如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .text:00401513 push eax .text:00401514 push ecx .text:00401515 call sub_4010A0 .text:0040151 A add eax, 15 h .text:0040151 D push eax .text:0040151 E pop eax .text:0040151F mov ecx, 0F FFFFFFFh .text:00401524 xor eax, ecx .text:00401526 push eax .text:00401527 pop ecx .text:00401528 not ecx .text:0040152 A push ecx .text:0040152B retn .text:0040152B ; --------------------------------------------------------------------------- .text:0040152 C db 7 Eh ; ~ .text:0040152 D db 0F Fh .text:0040152 E db 15 h .text:0040152F ; --------------------------------------------------------------------------- .text:0040152F pop ecx .text:00401530 pop eax
解决方法,全部patch即可。
之后就可以看到流程了,加了平坦化控制流,尝试去了下没去掉,今天日常访问大佬博客Mason,看到了个github链接https://github.com/moliam/llvm-fla-cracker 。
然后在有数据处理的地方打上断点。
加密一
1 2 3 4 5 6 7 8 9 case 399047545 :v35 = ~flag[i] & v35 | ~v35 & flag[i]; v36 = ~flag[i]; v23 = -1395711487 ; break ;flag[v33] = ~flag[v33] & v35 | ~v35 & flag[v33];
就是将32byte转为8DWORD,然后全部异或产生一个值,然后和flag[i]挨着异或,翻译下就是
1 2 3 4 5 6 7 8 9 10 11 flag=[0x6463626b ,0x6867666b ,0x6c6b6a6e ,0x706f6e39 ,0x74737271 ,0x78777675 ,0x42417a79 ,0x46454443 ] temp=0 for i in range (8 ): temp=flag[i]^temp print(hex (temp)) for i in range (8 ): flag[i]=flag[i]^temp print(hex (flag[i]),end=',' )
解的话爆破就行,异或的特性,可以看做单字节单字节的,得到异或的4byte数据。
1 2 3 4 5 6 7 8 9 10 11 enc=[0x6660656b ,0x35613568 ,0x3161306e ,0x633d6039 ,0x32666535 ,0x3063306c ,0x3060606b ,0x603d336b ] xor=[] for k in range (4 ): for i in range (0 ,127 ): a=0 for j in range (8 ): a^=((enc[j]>>8 *k)&0xff )^i if (a==i): xor.append(hex (i)) print(xor)
然后第二部分就是变表base加密,3个为一组,共11轮,除了第一组,每次都进行base64加密的时候,都会将后面还没base64加密的数据先异或一个长度为三的key,key由上一轮base64加密中table[index]中的index组成。翻译如下。
1 2 3 4 5 6 7 8 key=[0 ]*3 for r in range (10 ): key[0 ] = flag[r * 3 ] >> 2 key[1 ] = (flag[r * 3 ] & 0x3 ) << 4 | (flag[r * 3 + 1 ] >> 4 ) key[2 ]=(flag[r * 3 + 1 ] & 0xf ) << 2 | (flag[r * 3 + 2 ] >> 6 ) for i in range ((r+1 )*3 ,33 ): flag[i]=flag[i]^key[i%3 ]
解的话可以采用z3,如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from z3 import *enc=[0x6b ,0x65 ,0x60 ,0x7c ,0x5e ,0x20 ,0x64 ,0x6 ,0x43 ,0x2c ,0x52 ,0x5 ,0x2e ,0x56 ,0x1 ,0x7f ,0x26 ,0x41 ,0x65 ,0x13 ,0x51 ,0x2a ,0x53 ,0x0 ,0x7b ,0x75 ,0x5c ,0x3e ,0x49 ,0x1a ,0x3c ,0x66 ,0x50 ] s = Solver() flag = [BitVec('flag[%d]' % i, 8 ) for i in range (33 )] key=[0 ]*3 for r in range (10 ): key[0 ] = flag[r * 3 ] >> 2 key[1 ] = (flag[r * 3 ] & 0x3 ) << 4 | (flag[r * 3 + 1 ] >> 4 ) key[2 ]=(flag[r * 3 + 1 ] & 0xf ) << 2 | (flag[r * 3 + 2 ] >> 6 ) for i in range ((r+1 )*3 ,33 ): flag[i]=flag[i]^key[i%3 ] for i in range (33 ): s.add(flag[i]==enc[i]) print(s.check()) m=s.model() print(m)
或者采用下面的方式,先求出所有的key,然后以同样的方式去异或。
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 import base64str1 = 'Fi9X/fxX6Q6JBfUfBM1V/y6V6PcPjMaQLl9IuttFuH68' string1 = 'QVEJAfHmUYjBac+u8Ph5n9Od16FrICL/X0GvtM4qk7T2z3wNSsyoebilxWKgZpRD' string2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" for i in range (len (str1)): print(hex (string1.index(str1[i])), end=',' ) print("\n" ) for i in range (len (str1)): if ((i + 1 ) % 4 == 0 ): continue print(hex (string1.index(str1[i])), end=',' ) enc = base64.b64decode(str1.translate(str .maketrans(string1, string2))) for i in enc: print(hex (i), end=',' ) enc = [0x6b , 0x65 , 0x60 , 0x7c , 0x5e , 0x20 , 0x64 , 0x6 , 0x43 , 0x2c , 0x52 , 0x5 , 0x2e , 0x56 , 0x1 , 0x7f , 0x26 , 0x41 , 0x65 , 0x13 , 0x51 , 0x2a , 0x53 , 0x0 , 0x7b , 0x75 , 0x5c , 0x3e , 0x49 , 0x1a , 0x3c , 0x66 , 0x50 ] key1 = [0x1a , 0x36 , 0x15 , 0x1f , 0x5 , 0x38 , 0x19 , 0x0 , 0x19 , 0xb , 0x5 , 0x8 , 0xb , 0x25 , 0x18 , 0x1f , 0x32 , 0x19 , 0x19 , 0x11 , 0xd , 0xa , 0x25 , 0xc , 0x1e , 0x37 , 0x15 , 0xf , 0x24 , 0x24 , 0xf , 0x6 , 0x19 ] key = [0 ] * 3 for r in range (10 ): key[0 ] = key1[r * 3 ] key[1 ] = key1[r * 3 + 1 ] key[2 ] = key1[r * 3 + 2 ] print(key) for i in range ((r + 1 ) * 3 , 33 ): enc[i] = enc[i] ^ key[i % 3 ] flag = '' xor = [0xd , 0x6 , 5 , 0x53 ] for i in range (32 ): flag += chr (enc[i] ^ xor[i % 4 ]) print(flag)