-
-
[原创] 看雪 2022 KCTF 春季赛 第五题 危机四伏
-
2022-5-20 04:58 9121
-
(看到出题战队顿感不妙,必是一道硬题)
(p.s. 本贴算不上writeup,只是一篇零散的记录)
惯例 IDA,main 函数 F5 报错 "function frame is wrong"
不过我们还有 Ghidra,虽然反编译效果远不如 IDA,但是对于某些畸形文件有奇效(例如,IDA 要求一个函数的范围是连续的,Ghidra 就没有这个限制)。
Ghidra 看 main 函数的逻辑(可以配合 IDA,重新标记变量类型和函数签名),很普通的 fgets 获取 name 和 key 的输入,然后 name paddnig到 16 字节,key hexdecode 后是 32 字节。(p.s. 这里的 hexdecode 不限制大小写,所以题目多解)
接下来初始化了一串常量,调用疑似 zlib 算法解码。
能看到 main 函数还调用了 VirtualAlloc
,大概猜测前面的逻辑是解码 shellcode。
中间抛出了 C++ 异常。找到了几篇个人感觉不错的文章,对程序执行流程和相关的数据结构都有比较系统的介绍:
可以在 IDA 里沿着 _main_SEH
结构体一路找到所有的 catch 块与对应的 throw,不过 Ghidra 直接把所有的 catch 块都识别为了函数,且这些块都在 main 函数后面。顺着往下看,在 0x401efa
处发现了 retf
指令(这大概就是 IDA F5 失败的原因)。
(以上其实全都没用,还不如一开始就直接动态调试)
x32dbg调试,hexdecode 后的 32 字节的 key 保存在 0x8934b9
处。在 0x401efa
处下断点,按 shift+F9
无视异常执行,发现 retf 的目标地址就是之前 VirtualAlloc
出来的内存开头,先 dump 出来。
retf 指令之前有 push 0x33
,所以 retf
指令修改了 cs
寄存器把架构从 32位切换到了 64位。
按 64 位架构反编译,IDA 的效果不好(需要不停的手动 make code),Ghidra 相当舒服(能自动按照 jmp 指令生成基本块)。
shellcode 由大量基本块构成,每个块只有若干条指令,然后 jmp 到其他块,以此类推。Ghidra 识别为了函数,但是基本块太多,反编译时报了 "Decompiler process died" 错误。
(看着汇编指令,梦回去年春季赛第七题,完全一样的混淆,瞬间绝望)
这种切换架构的程序目前只有 WinDbg (x64)
能够正常调试(虽然体验比 x64dbg 差很多)。对 0x8934b9
下数据断点,确实能在 shellcode 里的访存指令断下来。
而且当 shellcode 结束后,0x8934b9
开始的长 32字节的值会变成 base64 码表的字符。
单步调试跟了一段时间后放弃,找出自己去年的 writeup,先按老方法 unicorn 跑一遍 trace,得到了 400多万 行汇编(麻了);然后再筛一遍 taint,筛完竟然还剩 47万 多行汇编。
看了下筛选结果,大部分都是死代码(所以,不做死代码消除根本达不到化简的效果,而且脚本存在大量bug,跑出来的结果应该也严重失实)
走投无路之时,不知道为啥试了下把题目给的公开输入的key输入到去年题目的exp中,没想到输出的竟然就是题目给的公开输入的name!今年的题目算法与去年的(至少主要部分)完全一样!
去年的算法,从key得到name的过程大概有4个阶段:
- 第一阶段:hexdecode + 一些可逆的变换,得到 32字节的ascii字串
- 第二阶段:换表base64 解码,得到 24字节
- 第三阶段:前8字节和后16字节分别做变换,其中后16字节变换后就是padding过的name
动态调试可发现第一处 retf (0x401efa
) 只做了第一阶段的变换,从 0x8934b9
获取到的变换后的值与去年的算法相同。
同理,第二处 retf (0x402158
) 完成了第二和第三阶段的变换,可以发现后16字节的结果写入了 0x8995f1
处,前8字节的结果写入了0x8935d9
处
继续分析 main 函数,发现程序在 0x4021a1
处验证了 0x8935d9
处的8个字节都是0,在 0x40229d
处验证了 0x8995f1
与输入的 name padding之后的值相等。两个验证都通过则输出"Succeed!"。
观察发现 0x8995f1
的16字节结果与去年的算法相同,而0x8935d9
处的8个字节结果与去年的算法不同。8字节的变换是一个循环异或再异或常量,合理猜测算法没变,只是常量变了。因为第二阶段的算法没有变化,所以稍微推导一下即可得到新的“常量”。(实际上不是常量,而是随着后16字节变化的,但是当name固定为KCTF时可以按常量处理)
exp:(仅适用于name输入为KCTF的情况;与去年的算法相比只有doencode3/dodecode3的常量不同)
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 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | def p16(n): n & = 0xffff return n.to_bytes( 2 , 'little' ) def u16(s): assert ( len (s) = = 2 ) return int .from_bytes(s, 'little' ) def p64(n): n & = 0xffffffffffffffff return n.to_bytes( 8 , 'little' ) def u64(s): assert ( len (s) = = 8 ) return int .from_bytes(s, 'little' ) def rol16(n, k): n & = 0xffff return ((n << k) | (n >> ( 16 - k))) & 0xffff def ror16(n, k): n & = 0xffff return ((n >> k) | (n << ( 16 - k))) & 0xffff def doencrypt1(buf, indextable): assert ( len (buf) = = 16 ) buf1 = bytearray(buf) v1 = u16(buf1[ 0 : 2 ]) # s1 ^ k4 v2 = u16(buf1[ 2 : 4 ]) # s2 ^ k4 v3 = u16(buf1[ 4 : 6 ]) # s3 + k3 v4 = u16(buf1[ 6 : 8 ]) # s4 - k3 v5 = u16(buf1[ 8 : 0xa ]) # s5 + k2 v6 = u16(buf1[ 0xa : 0xc ]) # s6 + k2 v7 = u16(buf1[ 0xc : 0xe ]) # s7 ^ k1 v8 = u16(buf1[ 0xe : 0x10 ]) # s8 ^ k1 t1 = v2 ^ v1 # s2 ^ s1 t2 = (v4 + v3) & 0xffff # s4 + s3 t3 = (v5 - v6) & 0xffff # s5 - s6 cl = bin (v8 ^ v7).lstrip( "0b" ).count( '1' ) # s8 ^ s7# hamming distance v7 = ror16(v7, cl) v8 = ror16(v8, cl) k1 = ( (t2 & t1) | ((~t1) & t3) ) & 0xffff k2 = ( ((k1 * t1) >> cl) + 0x18 ) & 0xffff k3 = ( k2 ^ t3 ) & 0xffff # 0x79c6 k4 = ( (k2 & k1) | (k3 & (k2 | k1)) ) & 0xffff s1 = p16(v1 ^ k4) s2 = p16(v2 ^ k4) s3 = p16(v3 - k3) s4 = p16(v4 + k3) s5 = p16(v5 - k2) s6 = p16(v6 - k2) s7 = p16(v7 ^ k1) s8 = p16(v8 ^ k1) buf1[indextable[ 0 ]] = s1[ 1 ] buf1[indextable[ 8 ]] = s1[ 0 ] buf1[indextable[ 1 ]] = s2[ 1 ] buf1[indextable[ 9 ]] = s2[ 0 ] buf1[indextable[ 2 ]] = s3[ 1 ] buf1[indextable[ 10 ]] = s3[ 0 ] buf1[indextable[ 3 ]] = s4[ 1 ] buf1[indextable[ 11 ]] = s4[ 0 ] buf1[indextable[ 4 ]] = s5[ 1 ] buf1[indextable[ 12 ]] = s5[ 0 ] buf1[indextable[ 5 ]] = s6[ 1 ] buf1[indextable[ 13 ]] = s6[ 0 ] buf1[indextable[ 6 ]] = s7[ 1 ] buf1[indextable[ 14 ]] = s7[ 0 ] buf1[indextable[ 7 ]] = s8[ 1 ] buf1[indextable[ 15 ]] = s8[ 0 ] return buf1 def dodecrypt1(serial, indextable): assert ( len (serial) = = 16 ) buf1 = bytearray(serial) s1 = (buf1[indextable[ 0 ]] << 8 ) | buf1[indextable[ 8 ]] s2 = (buf1[indextable[ 1 ]] << 8 ) | buf1[indextable[ 9 ]] s3 = (buf1[indextable[ 2 ]] << 8 ) | buf1[indextable[ 10 ]] s4 = (buf1[indextable[ 3 ]] << 8 ) | buf1[indextable[ 11 ]] s5 = (buf1[indextable[ 4 ]] << 8 ) | buf1[indextable[ 12 ]] s6 = (buf1[indextable[ 5 ]] << 8 ) | buf1[indextable[ 13 ]] s7 = (buf1[indextable[ 6 ]] << 8 ) | buf1[indextable[ 14 ]] s8 = (buf1[indextable[ 7 ]] << 8 ) | buf1[indextable[ 15 ]] ''' s1 = u16(buf1[0xb:0xd]) # 0x80c8 s2 = u16(buf1[9:0xb]) # 0x8b84 s3 = u16(buf1[7:9]) # 0xefa8 s4 = u16(buf1[5:7]) # 0xf12e s5 = u16(buf1[3:5]) # 0x0x7a s6 = u16(buf1[1:3]) # 0xba4d s7 = (buf1[0] << 8) | buf1[0xf] # 0x7d70 s8 = u16(buf1[0xd:0xf]) # 0x31b8 ''' t1 = s2 ^ s1 # 0xb4c t2 = (s4 + s3) & 0xffff # 0xe0d6 t3 = (s5 - s6) & 0xffff # 0xe22d cl = bin (s8 ^ s7).lstrip( "0b" ).count( '1' ) # 6 # hamming distance print ( hex (s1), hex (s2), hex (s3), hex (s4), hex (s5), hex (s6), hex (s7), hex (s8)) print ( hex (t1), hex (t2), hex (t3), hex (cl)) k1 = ( (t2 & t1) | ((~t1) & t3) ) & 0xffff # 0xe065 buf1[ 0xc : 0xc + 2 ] = p16(rol16(s7 ^ k1, cl)) buf1[ 0xe : 0xe + 2 ] = p16(rol16(s8 ^ k1, cl)) k2 = ( ((k1 * t1) >> cl) + 0x18 ) & 0xffff # 0x9beb buf1[ 0x8 : 0x8 + 2 ] = p16(s5 + k2) buf1[ 0xa : 0xa + 2 ] = p16(s6 + k2) k3 = ( k2 ^ t3 ) & 0xffff # 0x79c6 buf1[ 0x6 : 0x6 + 2 ] = p16(s4 - k3) buf1[ 0x4 : 0x4 + 2 ] = p16(s3 + k3) k4 = ( (k2 & k1) | (k3 & (k2 | k1)) ) & 0xffff buf1[ 0x0 : 0x0 + 2 ] = p16(s1 ^ k4) buf1[ 0x2 : 0x2 + 2 ] = p16(s2 ^ k4) print ( hex (k1), hex (k2), hex (k3), hex (k4)) return buf1 def doencode2(s): assert ( len (s) % 3 = = 0 ) chartable = [ 149 , 226 , 128 , 198 , 234 , 195 , 213 , 141 , 158 , 197 , 179 , 98 , 100 , 77 , 118 , 186 , 146 , 253 , 222 , 127 , 66 , 114 , 129 , 173 , 121 , 84 , 115 , 133 , 134 , 94 , 241 , 132 , 106 , 245 , 99 , 216 , 254 , 168 , 192 , 200 , 79 , 201 , 199 , 3 , 123 , 229 , 223 , 2 , 0 , 13 , 31 , 60 , 19 , 34 , 37 , 59 , 43 , 23 , 170 , 160 , 246 , 151 , 89 , 88 , 109 , 15 , 12 , 6 , 61 , 27 , 14 , 33 , 20 , 38 , 45 , 4 , 18 , 28 , 9 , 1 , 46 , 51 , 53 , 35 , 32 , 11 , 56 , 26 , 50 , 44 , 57 , 124 , 209 , 242 , 92 , 117 , 161 , 8 , 36 , 16 , 40 , 41 , 63 , 49 , 7 , 10 , 47 , 17 , 25 , 30 , 62 , 21 , 29 , 42 , 58 , 52 , 22 , 5 , 54 , 55 , 39 , 48 , 24 , 108 , 74 , 122 , 68 , 152 , 150 , 105 , 196 , 235 , 204 , 73 , 190 , 181 , 72 , 113 , 148 , 225 , 163 , 177 , 120 , 250 , 83 , 70 , 64 , 203 , 188 , 71 , 131 , 193 , 238 , 249 , 232 , 97 , 169 , 251 , 194 , 210 , 76 , 85 , 218 , 247 , 126 , 217 , 143 , 172 , 227 , 82 , 96 , 155 , 233 , 86 , 156 , 137 , 87 , 180 , 81 , 125 , 176 , 116 , 142 , 162 , 157 , 237 , 182 , 224 , 95 , 252 , 75 , 110 , 165 , 65 , 208 , 167 , 187 , 240 , 140 , 145 , 101 , 185 , 212 , 230 , 135 , 184 , 191 , 248 , 236 , 159 , 154 , 211 , 111 , 147 , 93 , 102 , 136 , 67 , 91 , 80 , 243 , 130 , 183 , 206 , 103 , 231 , 244 , 255 , 175 , 205 , 214 , 220 , 171 , 104 , 90 , 138 , 221 , 239 , 228 , 189 , 78 , 164 , 119 , 178 , 69 , 166 , 153 , 112 , 219 , 107 , 215 , 144 , 207 , 174 , 139 , 202 ] rchartable = [ None ] * 256 for i, c in enumerate (chartable): rchartable[c] = i r = b"" for i in range ( 0 , len (s), 3 ): x, y, z = s[i], s[i + 1 ], s[i + 2 ] a = (x & 0x3 ) | ((z & 0xf ) << 2 ) b = (y & 0x3c ) | ((x & 0xc ) >> 2 ) c = (x >> 4 ) | ((y & 0x3 ) << 4 ) d = ((y & 0xc0 ) >> 2 ) | (z >> 4 ) r + = bytes([rchartable[a], rchartable[b], rchartable[c], rchartable[d]]) return r def dodecode2(s): assert ( len (s) % 4 = = 0 ) chartable = [ 149 , 226 , 128 , 198 , 234 , 195 , 213 , 141 , 158 , 197 , 179 , 98 , 100 , 77 , 118 , 186 , 146 , 253 , 222 , 127 , 66 , 114 , 129 , 173 , 121 , 84 , 115 , 133 , 134 , 94 , 241 , 132 , 106 , 245 , 99 , 216 , 254 , 168 , 192 , 200 , 79 , 201 , 199 , 3 , 123 , 229 , 223 , 2 , 0 , 13 , 31 , 60 , 19 , 34 , 37 , 59 , 43 , 23 , 170 , 160 , 246 , 151 , 89 , 88 , 109 , 15 , 12 , 6 , 61 , 27 , 14 , 33 , 20 , 38 , 45 , 4 , 18 , 28 , 9 , 1 , 46 , 51 , 53 , 35 , 32 , 11 , 56 , 26 , 50 , 44 , 57 , 124 , 209 , 242 , 92 , 117 , 161 , 8 , 36 , 16 , 40 , 41 , 63 , 49 , 7 , 10 , 47 , 17 , 25 , 30 , 62 , 21 , 29 , 42 , 58 , 52 , 22 , 5 , 54 , 55 , 39 , 48 , 24 , 108 , 74 , 122 , 68 , 152 , 150 , 105 , 196 , 235 , 204 , 73 , 190 , 181 , 72 , 113 , 148 , 225 , 163 , 177 , 120 , 250 , 83 , 70 , 64 , 203 , 188 , 71 , 131 , 193 , 238 , 249 , 232 , 97 , 169 , 251 , 194 , 210 , 76 , 85 , 218 , 247 , 126 , 217 , 143 , 172 , 227 , 82 , 96 , 155 , 233 , 86 , 156 , 137 , 87 , 180 , 81 , 125 , 176 , 116 , 142 , 162 , 157 , 237 , 182 , 224 , 95 , 252 , 75 , 110 , 165 , 65 , 208 , 167 , 187 , 240 , 140 , 145 , 101 , 185 , 212 , 230 , 135 , 184 , 191 , 248 , 236 , 159 , 154 , 211 , 111 , 147 , 93 , 102 , 136 , 67 , 91 , 80 , 243 , 130 , 183 , 206 , 103 , 231 , 244 , 255 , 175 , 205 , 214 , 220 , 171 , 104 , 90 , 138 , 221 , 239 , 228 , 189 , 78 , 164 , 119 , 178 , 69 , 166 , 153 , 112 , 219 , 107 , 215 , 144 , 207 , 174 , 139 , 202 ] tmp = bytearray( len (s)) for i, c in enumerate (s): tmp[i] = chartable[c] for c in tmp: assert (c < 0x40 ) r = b"" for i in range ( 0 , len (tmp), 4 ): a, b, c, d = tmp[i], tmp[i + 1 ], tmp[i + 2 ], tmp[i + 3 ] x = ((c << 4 ) & 0xff ) | ((b & 0x3 ) << 2 ) | (a & 0x3 ) y = ((d & 0xf0 ) << 2 ) | (b & 0x3c ) | ((c & 0x3f ) >> 4 ) z = ((d << 4 ) & 0xff ) | ((a & 0x3f ) >> 2 ) r + = bytes([x, y, z]) return r def doencode3(r, constn = 0x8267f5d9b0ea143c ): assert ( len (r) = = 8 ) r = p64(u64(r)^constn) s = bytearray( 8 ) s[ 3 ] = r[ 0 ] s[ 2 ] = r[ 2 ] ^ s[ 3 ] s[ 1 ] = r[ 1 ] ^ s[ 2 ] s[ 0 ] = r[ 3 ] ^ s[ 1 ] s[ 7 ] = r[ 4 ] s[ 6 ] = r[ 6 ] ^ s[ 7 ] s[ 5 ] = r[ 5 ] ^ s[ 6 ] s[ 4 ] = r[ 7 ] ^ s[ 5 ] return s def dodecode3(s, constn = 0x8267f5d9b0ea143c ): assert ( len (s) = = 8 ) r = bytearray( 8 ) r[ 0 ] = s[ 3 ] r[ 1 ] = s[ 1 ] ^ s[ 2 ] r[ 2 ] = s[ 2 ] ^ s[ 3 ] r[ 3 ] = s[ 0 ] ^ s[ 1 ] r[ 4 ] = s[ 7 ] r[ 5 ] = s[ 5 ] ^ s[ 6 ] r[ 6 ] = s[ 6 ] ^ s[ 7 ] r[ 7 ] = s[ 4 ] ^ s[ 5 ] return p64(u64(r)^constn) def name_to_serial(name): assert ( len (name) < = 16 ) #buf5 = bytearray(b"\x9a\x1e\x1d\x1c\x1b\x1a\x19\x18\x17\x16\x15\x14\x13\x12\x11\x10") buf5 = bytearray( 16 - i for i in range ( 16 )) buf5[: len (name)] = name if len (name) < 16 : buf5[ len (name)] = 0 print (buf5. hex ()) # stage4 indextable3 = [ 0xb , 0xd , 0xf , 0x1 , 0x3 , 0x5 , 0x7 , 0x9 , 0xc , 0xe , 0x0 , 0x2 , 0x4 , 0x6 , 0x8 , 0xa ] buf3_2 = doencrypt1(buf5, indextable3) print (buf3_2. hex ()) # stage3 print ( "buf5" , buf5[: 8 ]) #buf3_1 = doencode3(buf5[:8]) buf3_1 = doencode3(b '\0' * 8 , u64(bytes.fromhex( "5E 87 C4 F6 8F C6 60 81" .replace( " " ,"")))) print (buf3_1. hex ()) buf3 = buf3_1 + buf3_2 # stage2 buf2 = doencode2(buf3) print (buf2. hex ()) # stage1 indextable1 = [ 0xc , 0xa , 0x8 , 0x6 , 0x4 , 0x2 , 0x0 , 0xe , 0xb , 0x9 , 0x7 , 0x5 , 0x3 , 0x1 , 0xf , 0xd ] indextable2 = [ 0xb , 0x9 , 0x7 , 0x5 , 0x3 , 0x1 , 0xf , 0xd , 0x8 , 0x6 , 0x4 , 0x2 , 0x0 , 0xe , 0xc , 0xa ] serial = doencrypt1(buf2[: 16 ], indextable1) + doencrypt1(buf2[ 16 :], indextable2) print ( "serial" ) print (serial. hex ()) return serial def serial_to_name(serial): assert ( len (serial) = = 32 ) # stage1 indextable1 = [ 0xc , 0xa , 0x8 , 0x6 , 0x4 , 0x2 , 0x0 , 0xe , 0xb , 0x9 , 0x7 , 0x5 , 0x3 , 0x1 , 0xf , 0xd ] indextable2 = [ 0xb , 0x9 , 0x7 , 0x5 , 0x3 , 0x1 , 0xf , 0xd , 0x8 , 0x6 , 0x4 , 0x2 , 0x0 , 0xe , 0xc , 0xa ] buf2 = dodecrypt1(serial[: 16 ], indextable1) + dodecrypt1(serial[ 16 :], indextable2) print ( "buf2:" , buf2. hex (), buf2) # stage2 buf3 = dodecode2(buf2) print ( "buf3" , buf3. hex ()) # stage3 print (buf3[: 8 ]. hex ()) buf4 = dodecode3(buf3[: 8 ]) # stage4 indextable3 = [ 0xb , 0xd , 0xf , 0x1 , 0x3 , 0x5 , 0x7 , 0x9 , 0xc , 0xe , 0x0 , 0x2 , 0x4 , 0x6 , 0x8 , 0xa ] buf5 = dodecrypt1(buf3[ 8 :], indextable3) print (buf5) # stage5 #assert(buf4 == buf5[:8]) return bytes(buf5) print (name_to_serial(b "KCTF" ). hex ().upper()) |
题目答案:
1 2 3 | Input name:KCTF Input key: 39ED62B341BC560217EAB3BF90265D101067856B36495264144A5B487264CB4B Succeed! |
================================================================================
破解VMP类型的混淆,感觉最优的做法应该就是充分利用编译器的优化能力去除冗余指令。
之前在公众号看到了的一篇推文 [原创]angr符号变量转LLVM IR,作者 R1mao 实现了把 angr 的符号变量转换为 LLVM IR 的Python程序。文中还提到了一个项目 VMProtect-devirtualization ,给出了完整的基于符号执行和LLVM重编译的示例。
(R1mao 到目前为止在论坛发了3篇文章,都是非常硬核的干货好文,值得学习)
除了符号执行,还可以考虑 retdec
(retdec反编译器的特点是支持重新编译)(参考此帖评论区)
(注:以上方案都未在本题尝试,希望有兴趣的人可以折腾下看看效果)