GHCTF 2025 Reverse WP 这次也是第一次给CTF比赛出题,经验不足,完全是凭借之前打比赛做的题的经验来出的,难度尽可能把控住梯度上升,但是可能还是没做的那么好,各位师傅见谅了,如果有什么建议也可以联系我。
ASN?Signin! 分析 这题估计大伙都AI一把梭了(),不过还是讲讲asm代码分析流程。
asm代码开头是数据段,存放着DATA1和DATA2,这两段就是关键数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 .DATA WELCOME_MSG db 'Welcome to GHCTF!', 0DH, 0AH, '$' INPUT_MSG db 'Input your flag:', '$' WRONG_MSG db 0DH, 0AH, 'Wrong!', 0DH, 0AH, '$' RIGHT_MSG db 0DH, 0AH, 'Right!', 0DH, 0AH, '$' DATA1 DB 26H,27H,24H,25H,2AH,2BH,28H,00H DB 2EH,2FH,2CH,2DH,32H,33H,30H,00H DB 36H,37H,34H,35H,3AH,3BH,38H,39H DB 3EH,3FH,3CH,3DH,3FH,27H,34H,11H DATA2 DB 69H,77H,77H,66H,73H,72H,4FH,46H DB 03H,47H,6FH,79H,07H,41H,13H,47H DB 5EH,67H,5FH,09H,0FH,58H,63H,7DH DB 5FH,77H,68H,35H,62H,0DH,0DH,50H BUFFER1 db 33 dup(0) BUFFER2 db 33 dup(0) .CODE
下面这部分就是程序启动时执行代码,读入33字节到BUFFER1中,然后CALL DO1函数,再CALL ENC函数,最后LOOP1进行比对跳转到结果输出。所以可知DATA2就是加密后的Flag数据。
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 START: MOV AX,@DATA MOV DS,AX MOV AH,09H MOV DX,OFFSET WELCOME_MSG INT 21H MOV DX,OFFSET INPUT_MSG INT 21H MOV AH,0AH MOV DX,OFFSET BUFFER1 MOV BYTE PTR[BUFFER1],33 ; 读入33字节到BUFFER1 INT 21H CALL DO1 ; Call DO1 CALL ENC ; Call ENC MOV SI,OFFSET BUFFER1 + 2 MOV DI,OFFSET DATA2 MOV CX,32 LOOP1: ; 比对DATA2和加密后的数据 MOV AL,[SI] CMP AL,[DI] JNE P2 INC SI INC DI LOOP LOOP1 P1: MOV AH,09H LEA DX,RIGHT_MSG ; 输出正确信息 INT 21H JMP EXIT_PROGRAM P2: MOV AH,09H LEA DX,WRONG_MSG ; 输出错误信息 INT 21H
以下是DO1函数和DO2函数代码,可以看到是针对DATA1进行的一些操作。
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 DO1 PROC PUSH SI PUSH DI PUSH CX XOR SI,SI MOV CX,8 SWAP_LOOP: PUSH CX MOV DI,SI ADD DI,4 CMP DI,28 JL NOWRAP SUB DI,28 NOWRAP: MOV BX,SI CALL DO2 ADD SI,4 POP CX LOOP SWAP_LOOP POP CX POP DI POP SI RET DO1 ENDP DO2 PROC PUSH CX MOV CX,4 LOOP3: MOV AL,DATA1[BX] MOV AH,DATA1[DI] MOV DATA1[BX],AH MOV DATA1[DI],AL INC BX INC DI LOOP LOOP3 POP CX RET DO2 ENDP
下面这部分是ENC函数代码,是利用DO1后的DATA1数据与输入的字符进行XOR操作,注意到xor操作都是在WORD,也就是二字节的基础上进行操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ENC PROC PUSH CX MOV SI,OFFSET BUFFER1 + 2 MOV DI,OFFSET DATA1 MOV CX,8 LOOP2: MOV AX,WORD PTR[DI + 1] XOR WORD PTR[SI],AX ; *(SHORT*)(Input + i) ^= *(SHORT*)(DATA1 + i + 1) MOV AX,WORD PTR[DI + 2] XOR WORD PTR[SI + 2],AX ; *(SHORT*)(Input + i + 2) ^= *(SHORT*)(DATA1 + i + 2) ADD SI,4 ADD DI,4 LOOP LOOP2 POP CX RET ENC ENDP
由于DO1和DO2只是对DATA1这个静态数据进行操作,所以可以直接动调拦截到执行完DO1的DATA1数据进行对DATA2的解密。这题ASM代码故意给全的,就是为了让选手可以直接编译到EXE并且使用DosBox动调,(大伙入门8086汇编应该都用过的工具)。
直接g命令执行到DO1函数,并且p命令步过,
再t命令步入ENC函数,再u命令反汇编就可以看到DATA1的地址。
直接d命令就可以看到DATA1的数据,提取出来与DATA2进行解密计算即可得到flag。
解密代码:
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 #include <iostream> int main () { uint8_t DATA1[] = { 0x26 , 0x27 , 0x24 , 0x25 , 0x3e , 0x27 , 0x34 , 0x11 , 0x32 , 0x33 , 0x30 , 0x00 , 0x36 , 0x37 , 0x34 , 0x35 , 0x3a , 0x3b , 0x38 , 0x39 , 0x3e , 0x3f , 0x3c , 0x3d , 0x2a , 0x2b , 0x28 , 0x00 , 0x2e , 0x2f , 0x2c , 0x2d }; uint8_t DATA2[] = { 0x69 , 0x77 , 0x77 , 0x66 , 0x73 , 0x72 , 0x4F , 0x46 , 0x03 , 0x47 , 0x6F , 0x79 , 0x07 , 0x41 , 0x13 , 0x47 , 0x5E , 0x67 , 0x5F , 0x09 , 0x0F , 0x58 , 0x63 , 0x7D , 0x5F , 0x77 , 0x68 , 0x35 , 0x62 , 0x0D , 0x0D , 0x50 }; for (int i = 0 ; i < 32 ; i += 4 ) { *(uint16_t *)(DATA2 + i) ^= *(uint16_t *)(DATA1 + i + 1 ); *(uint16_t *)(DATA2 + i + 2 ) ^= *(uint16_t *)(DATA1 + i + 2 ); } printf ("%.32s\n" , DATA2); return 0 ; }
Flag值 NSSCTF{W0w_y0u're_g00d_@t_@5M!!}
LockedSecret 分析 这题本来是当作第一批第二难度的题,实际题解远低于FishingKit(),没想到展开Tea卡住了不少人了。
前面常规UPX变异脱壳就跳过讲解了,不会的新生可以去看文章再学习学习。 将脱完壳的程序拖入IDA分析,可以看到主函数内共有两个关键函数调用,byte_3F4060就是加密后的Flag。
第一个函数的利用伪随机对一个全局数组进行初始化赋值。
第二个函数就是加密函数,将一串明文key与上一个函数初始化的值进行异或计算得到用于加密的Key。底下加密部分代码由于IDA的反反编译器问题,导致有点丑陋,但是还是可以看出Tea加密的特征,只不过和常规Tea相比似乎少了循环。
换到Ghidra分析得到的伪代码会更简洁,可以很清楚的看出就是Tea加密,不过用重复计算和不同的sum值来代替了循环加密。
将第二次计算的bc46effe减去5e2377ff会发现结果还是5e2377ff,所以可以知道5e2377ff就是delta值,那么这就是一个完整的从delta值开始的Tea加密。
并且两两计算为一组,可以看出是8轮加密的Tea,并且最后将加密完的值再异或上了0xf。
直接断点该处获得Key,进行8轮的Tea解密即可得到Flag。
解密代码:
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 #include <iostream> #include <Windows.h> void tea_decrypt (uint32_t v[2 ], const uint32_t k[4 ]) { uint32_t v0 = v[0 ], v1 = v[1 ]; uint32_t sum = 0x5E2377FF * 8 ; uint32_t delta = 0x5E2377FF ; for (uint32_t i = 0 ; i < 8 ; i++) { v1 -= ((v0 << 4 ) + k[2 ]) ^ (v0 + sum) ^ ((v0 >> 5 ) + k[3 ]); v0 -= ((v1 << 4 ) + k[0 ]) ^ (v1 + sum) ^ ((v1 >> 5 ) + k[1 ]); sum -= delta; } v[0 ] = v0; v[1 ] = v1; } int main () { unsigned char Key[] = { 0x2D ,0xF7 ,0x3D ,0x42 ,0x01 ,0x9A ,0xF5 ,0x05 ,0x1D ,0xCF ,0x3F ,0x63 ,0x22 ,0x91 ,0xD1 ,0x77 }; unsigned char EncFlag[] = { 0xDC ,0x45 ,0x1E ,0x03 ,0x89 , 0xE9 ,0x76 ,0x27 ,0x47 ,0x48 , 0x23 ,0x01 ,0x70 ,0xD2 ,0xCE , 0x64 ,0xDA ,0x7F ,0x46 ,0x33 , 0xB1 ,0x03 ,0x49 ,0xA3 ,0x27 , 0x00 ,0xD1 ,0x2C ,0x37 ,0xB3 , 0xBD ,0x75 }; for (int i = 0 ; i < 4 ; i++) { *(uint32_t *)(EncFlag + i * 8 ) ^= 0xf ; *(uint32_t *)(EncFlag + i * 8 + 4 ) ^= 0xf ; tea_decrypt ((uint32_t *)(EncFlag + i * 8 ), (uint32_t *)Key); } printf ("%.32s\n" ,EncFlag); return 0 ; }
Flag值 NSSCTF{!!!Y0u_g3t_th3_s3cr3t!!!}
FishingKit 分析 结合题目描述可以知道这题的考点是Hook。
首先程序要求输入bait数据,进行一个计算检验,这部分直接用z3就可以解出目标数据。
z3代码:
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 from z3 import *s = Solver() a0,a1,a2,a3,a4,a5,a6,a7,a8,a9 = BitVecs("a0 a1 a2 a3 a4 a5 a6 a7 a8 a9" ,12 ) s.add(202 * a8 + 216 * a5 - 4 * a4 - 330 * a9 - 13 * a4 - 268 * a6 == -14982 ) s.add(325 * a8 + 195 * a0 + 229 * a1 - 121 * a6 - 409 * a6 - (a1 << 7 ) == 22606 ) s.add(489 * a1 + 480 * a6 + 105 * a2 + 367 * a3 - 135 * a4 - 482 * a9 == 63236 ) s.add(493 * a1 - 80 * a4 - 253 * a8 - 121 * a2 - 177 * a0 - 243 * a9 == -39664 ) s.add(275 * a4 + 271 * a6 + 473 * a7 - 72 * a5 - 260 * a4 - 367 * a4 == 14255 ) s.add(286 * a0 + 196 * a7 + 483 * a2 + 442 * a1 - 495 * a8 - 351 * a4 == 41171 ) s.add(212 * a2 + 283 * a7 - 329 * a8 - 429 * a9 - 362 * a2 - 261 * a6 == -90284 ) s.add(456 * a5 + 244 * a7 + 92 * a4 + 348 * a7 - 225 * a1 - 31 * a2 == 88447 ) s.add(238 * a9 + 278 * a7 + 216 * a6 + 237 * a0 + 8 * a2 - 17 * a9 == 83838 ) s.add(323 * a9 + 121 * a1 + 370 * a7 - (a4 << 6 ) - 196 * a9 - 422 * a0 == 26467 ) s.add(166 * a9 + 90 * a1 + 499 * a2 + 301 * a8 - 31 * a2 - 206 * a2 == 88247 ) s.add(355 * a0 + 282 * a4 + 44 * a9 + 359 * a8 - 167 * a5 - 62 * a3 == 76658 ) s.add(488 * a6 + 379 * a9 + 318 * a2 - 85 * a1 - 357 * a2 - 277 * a5 == 35398 ) s.add(40 * a0 + 281 * a4 + 217 * a5 - 241 * a1 - 407 * a7 - 309 * a7 == -35436 ) s.add(429 * a3 + 441 * a3 + 115 * a1 + 96 * a8 + 464 * a1 - 133 * a7 == 157448 ) if s.check() == sat: ans = s.model() for i in range (10 ): t = int (f"{ans[eval (f'a{i} ' )]} " ) print (chr (t),end='' )
得到DeluxeBait 字串
然后下面部分有个加密函数,可以看出是魔改RC4,加密完与一个数组进行比对,这边解密后会得到假Flag,有兴趣的可以自己解密试试。
由于知道是Hook考点,直接字符串搜索VirtualProtect,定位,查找交叉引用,发现有一个函数有调用。
可以看出这部分就是Hook代码,上面的0xff,0x25就是far jmp的汇编,参数一传进来要Hook的函数,参数二是自己的函数。
在他的调用上层就可以看到获取了模块和函数地址,不过做了异或加密处理,sub_140001CE0就是Hook函数要跳转执行的函数。
可以解密出来是strcmp,也就是对strcmp做了Hook,之前在主函数有看到strcmp,就是那里程序发生了跳转,没有执行真实的strcmp。
所以接下来重点分析sub_140001CE0函数。可以看到这边是对数据进行了24轮Tea加密,最后与byte_1400063C8数组进行比对,那么byte_1400063C8就是真实被加密的Flag。
这边重点就是传入的buf和buf_1数据实际是什么,可以在赋这两个数组赋值完后处进行断点,查看实际数据。
可以发现实际上就是DeluxeBait字串以及输入的Flag值。
由于buf数组在开头已经memset 0了,赋值的字串”DeluxeBait”长度仅有10,而Tea加密要的Key必须是16字节,所以最终进行加密的Key是这个字串加上6个0x00。
解密代码:
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 #include <iostream> void decipher (uint32_t v[2 ], const uint32_t key[4 ]) { unsigned int i; uint32_t v0 = v[0 ], v1 = v[1 ], delta = 0x66778899 , sum = delta * 24 ; for (i = 0 ; i < 24 ; i++) { v1 -= (((v0 << 4 ) ^ (v0 >> 5 )) + v0) ^ (sum + key[(sum >> 11 ) & 3 ]); sum -= delta; v0 -= (((v1 << 4 ) ^ (v1 >> 5 )) + v1) ^ (sum + key[sum & 3 ]); } v[0 ] = v0; v[1 ] = v1; } int main () { uint8_t EncFlag[24 ]{}; memcpy (EncFlag, "!V" , 2 ); EncFlag[2 ] = -105 ; EncFlag[3 ] = -90 ; EncFlag[4 ] = 26 ; EncFlag[5 ] = -43 ; EncFlag[6 ] = -60 ; EncFlag[7 ] = -34 ; EncFlag[8 ] = -92 ; EncFlag[9 ] = -100 ; EncFlag[10 ] = -126 ; EncFlag[11 ] = 77 ; EncFlag[12 ] = -47 ; EncFlag[13 ] = 69 ; EncFlag[14 ] = -56 ; EncFlag[15 ] = 86 ; EncFlag[16 ] = -89 ; EncFlag[17 ] = -76 ; EncFlag[18 ] = -106 ; EncFlag[19 ] = 92 ; EncFlag[20 ] = 77 ; EncFlag[21 ] = 73 ; EncFlag[22 ] = -121 ; EncFlag[23 ] = 32 ; uint8_t Key[] = "DeluxeBait\0\0\0\0\0\0" ; decipher ((uint32_t *)(EncFlag), (uint32_t *)Key); decipher ((uint32_t *)(EncFlag + 8 ), (uint32_t *)Key); decipher ((uint32_t *)(EncFlag + 16 ), (uint32_t *)Key); printf ("%.23s\n" , EncFlag); return 0 ; }
Flag值 NSSCTF{Wh@t_@_b1g_F1sh}
Mio?Ryo?Soyo? 分析 第二批题目的签到题,常规的Python程序解包。
使用pyinstxtractor进行解包,需要对应程序python版本,使用python3.8运行,否则无法解压pyz文件得到Secret.pyc。
使用uncompyle6进行pyc反编译program.pyc可以看到源码。发现引入了Secret文件,可以在PYZ-00.pyz_extracted找到对应pyc文件,同样使用uncompyle6得到源码。
发现引入了SecretEncrypt文件,并且这边有program.py里看到被调用比对的数据,大概率就是被加密的Flag。
反编译得到SecretEncrypt代码。
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 import mathclass MMMMiiiiiio : MMiiiiiiooo = "" .join([chr (Miiooooooooo) for Miiooooooooo in range (33 , 118 )]) @staticmethod def MMMMiiooooooo (MMMMMMMMMiiiooo: bytes ) -> str : MMMMiiiiioooooooooo = "" MMMMMMMiiiiioo = (4 - len (MMMMMMMMMiiiooo) % 4 ) % 4 MMMMMMMMMiiiooo += b'\x00' * MMMMMMMiiiiioo for MMMMMMiiiiiio in range (0 , len (MMMMMMMMMiiiooo), 4 ): MMMMiiiiiiooooo = MMMMMMMMMiiiooo[MMMMMMiiiiiio[:MMMMMMiiiiiio + 4 ]] MMMMMMiiioooooo = int .from_bytes(MMMMiiiiiiooooo, "big" ) MMMMMMMiiooooooooo = "" for _ in range (5 ): MMMMMMMiiooooooooo = MMMMiiiiiio.MMiiiiiiooo[MMMMMMiiioooooo % 85 ] + MMMMMMMiiooooooooo MMMMMMiiioooooo //= 85 else : MMMMiiiiioooooooooo += MMMMMMMiiooooooooo else : if MMMMMMMiiiiioo: MMMMiiiiioooooooooo = MMMMiiiiioooooooooo[None [:-MMMMMMMiiiiioo]] return MMMMiiiiioooooooooo class RRRRyyooo : RRRRyooooooo = "" .join([chr (RRRRRRRyyyyyoooo) for RRRRRRRyyyyyoooo in range (48 , 93 )]) @staticmethod def RRRRRRRyyyyooooo (RRRRRRyyyoooooo: bytes ) -> str : RRRRyyyyyooo = [] RRyyyyyyyyyoooooo = 0 while RRyyyyyyyyyoooooo < len (RRRRRRyyyoooooo): if RRyyyyyyyyyoooooo + 1 < len (RRRRRRyyyoooooo): RRRRRRRRRyyo = RRRRRRyyyoooooo[RRyyyyyyyyyoooooo] << 8 | RRRRRRyyyoooooo[RRyyyyyyyyyoooooo + 1 ] RRRRyyyyyooo.append(RRRRyyooo.RRRRyooooooo[RRRRRRRRRyyo % 45 ]) RRRRRRRRRyyo //= 45 RRRRyyyyyooo.append(RRRRyyooo.RRRRyooooooo[RRRRRRRRRyyo % 45 ]) RRRRRRRRRyyo //= 45 RRRRyyyyyooo.append(RRRRyyooo.RRRRyooooooo[RRRRRRRRRyyo]) RRyyyyyyyyyoooooo += 2 else : RRRRRRRRRyyo = RRRRRRyyyoooooo[RRyyyyyyyyyoooooo] RRRRyyyyyooo.append(RRRRyyooo.RRRRyooooooo[RRRRRRRRRyyo % 45 ]) RRRRRRRRRyyo //= 45 RRRRyyyyyooo.append(RRRRyyooo.RRRRyooooooo[RRRRRRRRRyyo]) RRyyyyyyyyyoooooo += 1 return "" .join(RRRRyyyyyooo) def SSSooooyyooo (SSSSooyoooooo, SSSSSoyyooooo ): SSoooooyyyyyyoo = [] for SSSSSSSSSoyooo in SSSSooyoooooo: if "a" <= SSSSSSSSSoyooo <= "z" : SSSSoooyooooooo = (ord (SSSSSSSSSoyooo) - ord ("a" ) + SSSSSoyyooooo) % 26 SSoooooyyyyyyoo.append(chr (ord ("a" ) + SSSSoooyooooooo)) elif "0" <= SSSSSSSSSoyooo <= "9" : SSSSoooyooooooo = (ord (SSSSSSSSSoyooo) - ord ("0" ) - SSSSSoyyooooo) % 10 SSoooooyyyyyyoo.append(chr (ord ("0" ) + SSSSoooyooooooo)) else : SSoooooyyyyyyoo.append(SSSSSSSSSoyooo) else : return "" .join(SSoooooyyyyyyoo)
可以看出第一个是标准Base85加密,第二个是Base45加密,但是和标准的表不一样,第三个是经典的凯撒加密。
这部分就是Base45实际用到的表。
TABLE = "".join([chr(i) for i in range(48, 93)])
编写三个加密的解密函数,按Secret文件的代码对Enc数据进行按顺序调用解密即可得到Flag。
解密代码:
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 import base64def caesar_encrypt (text, shift ): result = [] for char in text: if 'a' <= char <= 'z' : result.append(chr ((ord (char) - ord ('a' ) + shift) % 26 + ord ('a' ))) elif '0' <= char <= '9' : result.append(chr ((ord (char) - ord ('0' ) - shift) % 10 + ord ('0' ))) else : result.append(char) return '' .join(result) def base85_decode (encoded_text ): byte_data = encoded_text.encode('utf-8' ) decoded = base64.a85decode(byte_data) return decoded.decode('utf-8' ) def base45_decode_custom (data: str ): decoded = bytearray () i = 0 chars = '' .join([chr (i) for i in range (48 , 93 )]) char_to_val = {ch: i for i, ch in enumerate (chars)} while i < len (data): if i + 2 < len (data): num = (char_to_val[data[i + 2 ]] * 45 * 45 + char_to_val[data[i + 1 ]] * 45 + char_to_val[data[i]]) decoded.append(num >> 8 ) decoded.append(num & 0xFF ) i += 3 else : num = (char_to_val[data[i + 1 ]] * 45 + char_to_val[data[i]]) decoded.append(num) i += 2 return decoded.decode() if __name__ == "__main__" : enc = bytes ([57 , 118 , 33 , 114 , 68 , 56 , 117 , 115 , 34 , 52 , 52 , 95 , 78 , 40 , 49 , 59 , 95 , 85 , 63 , 122 , 54 , 33 , 77 , 110 , 49 , 54 , 34 , 109 , 106 , 122 , 60 , 92 , 108 , 91 , 61 , 51 , 42 , 62 , 35 , 38 , 52 , 67 , 62 , 122 , 116 , 48 , 76 , 50 , 67 , 51 , 59 , 41 , 122 , 45 , 45 , 51 , 90 ]) p1 = caesar_encrypt(enc.decode(),-9 ) p2 = base85_decode(p1) p3 = caesar_encrypt(p2,-7 ) flag = base45_decode_custom(p3) print (flag)
Flag值 NSSCTF{Th3y'r3_a11_p1aY_Ba5e!}
TimeSpaceRescue 分析 这题考点是反调试、花指令、时间爆破、AES魔改。
AES魔改的地方很明显,对比标准算法代码就可以马上找出,就多调用了两个函数对Key和加密数据进行了变换。
IDA反编译main函数,发现使用一个函数生成了一个数据,然后将生成数据与输入内容传入到加密函数内加密,最后比对。
进入生成数据函数中,可以看到反调试代码,核心逻辑是获取当前时间。
时间结构体如下,可以看到使用memcpy将tm_mday开始往后三个整数数值复制到Src数组中,也就是复制了day、month、year数据。
然后调用sub_4021A0利用复制的时间数据生成16字节数据,可以观察得知sub_4021A0是MD5函数,如果不知道的话也可以直接把代码抠出来进行调用。
最后将MD5得到的字节都异或上0x14。
1 2 3 4 5 6 7 8 9 10 11 12 struct tm { int tm_sec; int tm_min; int tm_hour; int tm_mday; int tm_mon; int tm_year; int tm_wday; int tm_yday; int tm_isdst; };
这边看到函数结尾return 39有点奇怪,转到这个函数汇编界面,会发现函数底部有个花指令。
这边花指令的原理是二次计算,call完第一次计算结果不等于目标值,函数返回后还是从计算地址指令开始执行,进行了第二次计算,此时计算结果等于目标值进行jz跳转,用二次计算误导IDA认为该部分会直接ret而不会jz跳转执行下面部分命令。
将红框内汇编nop掉,重新反编译会发现函数尾部又多了一次异或加密。
所以这边的密钥生成就是利用当前时间的年月日数据进行MD5,然后进行两次的异或计算得到最终的16字节密钥,结合题目描述,应该是需要爆破时间来得到有效密钥。
接下来分析AES加密函数,发现函数尾部return 39,返回汇编解密,发现同样函数尾部有花指令。
nop后就会发现尾部有个异或0x11的操作,跳到那个全部byte的交叉调用函数,发现是反调试,如果检测到调试,AES加密尾部就会把加密的数据再进行异或0x11处理,进行数据混淆。
通过比对标准算法队函数进行命名,发现多了这三个非AES标准流程的函数,
第一个函数是传入了密钥数据,将相邻两个字节两两互换并异或上5。第三个函数也是和第一个相同的,不过处理的是加密完的密文。
第二个函数是将要进行加密的16字节明文字节数据前后翻转,并都异或上0xF。
至此所有流程分析完毕,可以复制一份标准算法直接进行解密,不过需要手动调用那两个函数进行再次逆向处理,然后配合月日爆破(年份题目描述已给,2024年)。
注:该程序在初始化会进行三个反调试函数的调用,一个是调用IsDebuggerPresent和CheckRemoteDebuggerPresent,一个是刚刚上文提到的给全局一个Byte赋值,最后一个是对当前进程遍历查找恶意进程,对反调试感兴趣的可以根据我提供的这些函数去反向查找自行处理试试看。
解密代码:
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 #include <iostream> #include <Windows.h> #include "aes.h" #include "md5.h" unsigned int __cdecl sub_401030 (int a1) { unsigned int result; unsigned int i; unsigned int v3; char v4; v3 = 0 ; for (i = 15 ; ; --i) { result = v3; if (v3 >= i) break ; v4 = *(BYTE*)(v3 + a1) ^ 0xF ; *(BYTE*)(v3 + a1) = *(BYTE*)(i + a1) ^ 0xF ; *(BYTE*)(i + a1) = v4; ++v3; } return result; } unsigned int __cdecl sub_4010A0 (int a1) { unsigned int result; unsigned int i; char v3; for (i = 0 ; i < 0x10 ; i += 2 ) { v3 = *(BYTE*)(i + a1) ^ 5 ; *(BYTE*)(i + a1) = *(BYTE*)(i + a1 + 1 ) ^ 5 ; *(BYTE*)(i + a1 + 1 ) = v3; result = i + 2 ; } return result; } int main () { for (int month = 0 ; month < 12 ; month++) { for (int day = 1 ; day <= 31 ; day++) { uint8_t EncFlag[] = { 0xCD ,0x16 ,0xDB ,0xB5 ,0xD1 ,0x02 ,0xA4 ,0x82 ,0x8E ,0x59 , 0x73 ,0x9E ,0x96 ,0x26 ,0x56 ,0xF2 ,0x16 ,0x8E ,0x46 ,0xF2 , 0x55 ,0x7B ,0x92 ,0x31 ,0x30 ,0xDC ,0xAA ,0x8A ,0xF3 ,0x1C , 0xA0 ,0xAA }; uint8_t Key[16 ]{}; int TimeData[]{ day,month,2024 - 1900 }; md5 ((uint8_t *)TimeData, 12 , Key); for (int i = 0 ; i < 16 ; i++) *(Key + i) = (Key[i] ^ 0x114 ) % 256 ; for (int i = 0 ; i < 16 ; i++) *(Key + i) = (Key[i] ^ 0x11 ) % 256 ; sub_4010A0 ((int )Key); sub_4010A0 ((int )EncFlag); sub_4010A0 ((int )(EncFlag + 16 )); aesDecrypt (Key, 16 , EncFlag, 32 ); sub_401030 ((int )EncFlag); sub_401030 ((int )(EncFlag + 16 )); if (EncFlag[0 ] == 'N' && EncFlag[1 ] == 'S' && EncFlag[2 ] == 'S' ) { printf ("%.32s\n" , EncFlag); break ; } } } return 0 ; }
Flag值 NSSCTF{W0w_Y0u're_@n_AE5_M@5t3r}
Room 0 分析 这题出的时候也是突发奇想,想把SMC和异常两个考点结合起来,于是出了一个这样强技巧性的题目,如果选手没有意识到考点就没办法解出。后续放出了两个Hint指出了是除0异常和SMC特性,题解就猛涨了(,技巧性确实会强一点。
IDA分析main函数,发现有个CPPEH_RECORD的异常结构体。
回到汇编流程图会发现存在try和catch的捕获异常代码。
将此处的jmp和ret都nop掉就可以在反编译代码段看到catch执行的代码部分。
发现多了三个函数调用,就是catch部分执行的函数。
第一个函数是经典的unhex函数(”1f2f”->0x1f2f)。
第二个函数存在三处花指令,第一处和第三处直接全部nop,第二处对call按u,在跳过第一个字节按c重新分析下面部分代码,跳过0xE8字节。
选中函数按P重新识别为函数进行反编译,发现这部分实际上就是SMC函数处,利用传进的密钥数据对.enc代码进行异或解密。
只不过传进的四字节整数密钥再函数开头做了字节倒序的处理,将四个字节顺序翻转,那么接下来任务就是找到密钥。
回到main函数,发现程序要求输入key,然后调用一个call返回值要是0x11451419,然后利用0x11451419进行了一个RC4加密,最后对比一个字节数组,如果选手尝试解密这部分字节数组,会发现解出来的是FakeFlag。
NSSCTF{FAKE_FAKE_FAKE_FAKE_FAKE}
进入sub_402000函数,可以看到是将输入的密钥调用unhex得到四字节数值,然后进行一个32轮的计算,在箭头处可以看到有一个除法计算,以及除数部分的一个变量与常数相减。
结合前面发现的异常捕获,可以知道这部分会有可能出现除数等于0,也就是触发除0异常。那么可以通过爆破的方式找到一个可以在计算过程中触发除0异常的值,那就是目标密钥了,因为SMC传入的密钥就是我们输入的密钥字串经过unhex的数据。
但是经过实际检验,如果直接从0x0-0xffffffff爆破会出现非常多的符合值,无法确定哪个是真实密钥,就必须得缩小范围,这时会想到这个SMC加密函数存在特性,因为通过的是异或加密,所以可以根据原始字节与当前字节异或得到中间密钥。
跳到enc段开头,复制开头三个字节,然后随便找一个函数也复制开头三个字节,将两对数据进行异或,就会得到密钥的前三个字节。
这下范围缩小至0x755ff000-0x755ff0ff之间,使用代码爆破即可得到密钥。
爆破代码:
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 int __cdecl sub_4011C0 (int v2) { int i{}; int v4{}; int v5{}; int v6{}; int v7{}; unsigned int v8{}; int v9{}; v6 = 0 ; v9 = v2; v8 = (v2 >> 24 ) & 0xff ; v5 = (v2 >> 16 ) & 0xff ; v4 = (v2 >> 8 ) & 0xff ; for (i = 0 ; i < 32 ; ++i) { v7 = v6 * (v8 + 1415881080 ) * (v9 - 1467486175 ) * ((v8 - v9) ^ (v8 >> 4 )); v5 = (v9 + v5) ^ (8 * v4); v4 = (v9 + v8) ^ (8 * v5); v8 = (v9 + v4) ^ (8 * v5); v9 -= v4 + v5 + v8; if (v9 == 1415881080 ) { printf ("Key:%X\n" , v2); return 0 ; } v6 = v7 + (v8 + 1467486175 ) * (((v8 - v9) ^ (unsigned __int64)(v8 >> 4 )) / (unsigned int )(v9 - 1415881080 )); } return v9 ^ v6; } int main () { for (int i = 0x755ff000 ; i < 0x755ff0ff ; i++) { sub_4011C0 (i); } return 0 ; }
得到密钥755FF0D3
还原main函数nop的三行汇编,重新ida载入程序分析也可以,在异常except处断点,然后运行输入密钥,发现确实途中会触发div 0异常,然后断点会触发,
单步执行完SMC函数解密函数,然后跳到enc段的函数进行按c还原代码。
还原途中会发现几个花指令,和之前遇到的一样类型的,同样去除就好,最后选中函数按p识别为函数进行反编译即可。
会发现是一个变种RC4,发现v18是RC4密钥,且最后加密完的值又与v18字节数组进行异或。
v18不知道是什么数据,直接让代码执行到第一个for执行完处,就可以看到v18值,这8个字节就是密钥,编写代码将byte_405020字节数组进行解密即可得到密钥。
附:v18是从a2数据转换得来,a2数据实际是触发异常的时候通过栈进行获取异常前数据,实际数据是触发异常时候的v6值,传过来到enc段函数当作密钥进行加密。
解密代码:
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 #include <iostream> #include <string.h> unsigned char sbox[256 ] = {0 };void swap (unsigned char *a, unsigned char *b) { unsigned char tmp = *a; *a = *b; *b = tmp; } void init_sbox (unsigned char key[]) { for (unsigned int i = 0 ; i < 256 ; i++) sbox[i] = i; unsigned int keyLen = strlen ((char *)key); unsigned char Ttable[256 ] = {0 }; for (int i = 0 ; i < 256 ; i++) Ttable[i] = key[i % keyLen]; for (int j = 0 , i = 0 ; i < 256 ; i++) { j = (j + sbox[i] + Ttable[i]) % 256 ; swap (sbox + i, sbox + j); } } void RC4 (unsigned char data[], unsigned char key[]) { unsigned char k, i = 0 , j = 0 , t; init_sbox (key); unsigned int dataLen = 32 ; for (unsigned h = 0 ; h < dataLen; h++) { i = (i + 1 ) % 256 ; j = (j + sbox[i]) % 256 ; swap (sbox + i, sbox + j); t = (sbox[i] + sbox[j]) % 256 ; k = sbox[t]; data[h] ^= k ^ key[i % 8 ]; } } int main () { uint8_t enc[]{0x22 , 0xC4 , 0xA0 , 0x5A , 0xDE , 0xED , 0x62 , 0x5E , 0x25 , 0xE2 , 0x6D , 0xA6 , 0x05 , 0xA7 , 0x20 , 0x8D , 0x7D , 0x99 , 0x52 , 0x3E , 0x8C , 0xA7 , 0x7F , 0xFA , 0x09 , 0xD8 , 0x62 , 0xDB , 0x00 , 0x80 , 0xC2 , 0xA9 , 0x00 }; uint8_t key[]{0xD4 , 0x35 , 0x6D , 0xF8 , 0xF8 , 0x6D , 0x35 , 0xD4 , 0x00 }; RC4 (enc, key); printf ("%.32s\n" , enc); return 0 ; }
Flag值 NSSCTF{Int3r3st1ng_5MC_Pr0gr@m?}
Canon 分析 IDA分析main函数,发现是将输入的字符串分成3份,在循环里面互为密钥进行循环加密,并且利用a3数组数据传入调用不同的加密。
可以看到加密函数里面有多个case,但由于a3数组里面数字是有限的,只出现了13456,2和7加密没有用到,所以不用分析那两个case。
case1:凯撒加密
case3:栅栏加密
case4:字符串位移
case5:异或加密 + Base64
case6:变种RC4 + Base64
注意,Base64的表是被换过的,可以查找交叉调用发现,可以直接动调获取到换后的表。
main函数执行加密的双循环中用到的加密方式数值和加密顺序都可以通过模拟循环来得到,然后逆向就是解密调用的函数顺序和密文顺序。
解密代码:
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 import base64import mathnew_table = "stuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr" old_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" def rc4_decrypt (ciphertext, key ): key = key.encode() S = list (range (256 )) j = 0 for i in range (256 ): j = (j + S[i] + key[i % len (key)]) % 256 S[i], S[j] = S[j], S[i] i = j = 0 plaintext = [] for byte in ciphertext: i = (i + 1 ) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] k = S[(S[i] + S[j]) % 256 ] plaintext.append(((byte - 0x39 ) ^ k) % 256 ) return bytes (plaintext) def replay (violin, bass, mode ): if mode == 1 : res = '' for i, char in enumerate (violin): offset = ord (bass[i % len (bass)]) if 'a' <= char <= 'z' : res += chr ((ord (violin[i]) - ord ('a' ) - offset) % 26 + ord ('a' )) elif 'A' <= char <= 'Z' : res += chr ((ord (violin[i]) - ord ('A' ) - offset) % 26 + ord ('A' )) elif '0' <= char <= '9' : res += chr ((ord (violin[i]) - ord ('0' ) - offset) % 10 + ord ('0' )) else : res += violin[i] return res elif mode == 3 : Ek = ord (bass[0 ]) % 10 + 2 Dk = int (len (violin) / Ek) res = '' yushu = len (violin) % Ek steps = [] if len (violin) % Ek == 0 : step = Dk for i in range (Ek): steps.append(step) else : big_step = math.ceil(len (violin) / Ek) small_step = int (len (violin) / Ek) for p in range (yushu): steps.append(big_step) for q in range (Ek - yushu): steps.append(small_step) n_column = 0 while n_column < math.ceil(len (violin) / Ek): count_steps = 0 for one_step in steps: if len (res) == len (violin): break else : res += violin[n_column + count_steps] count_steps += one_step n_column += 1 return res elif mode == 4 : step = ord (bass[0 ]) % 10 + 2 res = '' res += violin[step:] res += violin[:step] return res elif mode == 5 : violin_decode = base64.b64decode(violin.translate(str .maketrans(new_table, old_table))) res = '' for i, char in enumerate (violin_decode): res += chr (char ^ ord (bass[i % len (bass)]) + 0x39 ) return res elif mode == 6 : violin_byte = base64.b64decode(violin.translate(str .maketrans(new_table, old_table))) res = rc4_decrypt(violin_byte, bass) return res.decode() def main (): violin = ["WgvDmssEvcY326bHo3nNro3vXvvfmgrz" , "gX+Ri9PG=bt5=00B6hscPQOL" , "T6bHgUPL2gXUd=xT=FNHtPzV" ] v = [3 , 2 , 1 , 3 , 2 , 1 , 3 , 2 , 1 , 3 , 2 , 1 , 3 , 2 , 1 , 3 , 2 , 1 , 2 , 1 , 1 ] chord = [1 , 4 , 5 , 4 , 1 , 4 , 3 , 4 , 1 , 6 , 3 , 4 , 5 , 6 , 3 , 1 , 5 , 6 , 1 , 5 , 1 ] for i in range (len (v)): tmp = replay(violin[v[i] - 1 ], violin[v[i] % 3 ], chord[i]) violin[v[i] - 1 ] = tmp print ('' .join(violin)) if __name__ == '__main__' : main()
Flag值 NSSCTF{P4ch3Lbel's_C@n0n_1n_D_mAjOr}
腐蚀 分析 IDA分析main函数,发现开头程序要读入一个Input.png文件。
并且在程序尾部将加密后的数据写到enc文件,输出完成语句。
进入这个函数。
发现这个函数是一个RC4加密流程,通过0x100特征和一些异或计算可以看出来的。
并且是变种RC4,最后异或加密的时候多异或上了0x1f。
我们创建一个Input.png文件,动调程序运行到这个函数断住,a2可以在第二个while循环中看出来,是充当RC4密钥。
跳到a2地址,发现是一个这样的结构,数据长度+数据指针,数据指针跳过去,这十六个字节就是密钥,发现一些特征,看起来像是PNG文件的开头部分字节和结尾部分字节乱序混在一起,不过不重要,只要动调拿到16字节的key即可。 密钥:60 82 AE 42 4E 44 49 45 1A 0A 0D 0A 4E 47 89 50
而a3是即将被加密的明文数据,跳过去发现也是同样结构,发现大小是0x30B,和我给的Input.png文件字节数一样,跳过去就会发现就是读入的png文件字节。
从函数尾部的赋值可以看出a1就是加密后数据储存的地方,将该函数执行完返回到main函数,然后跳到a1地址将数据复制出来尝试RC4+Xor解密,发现确实可以解密出原字节,
直接执行到write_all即将写出到enc文件处,然后查看v18,v18是通过上面RC4加密返回的v44得到的。
发现对比原RC4加密返回的数据,写出的数据做了字节顺序翻转处理。
直接让程序跑起来,写出enc文件,拖入到010Editor中,发现确实是将RC4加密后的数据翻转后再写到的文件。
所以解密流程为:读入题目enc附件->翻转字节->RC4解密
Cyberchef:
导出图片得到Flag
Flag值 NSSCTF{Y0u_ar3_ru5t_m@st3r}
ezObfus 分析 IDA分析程序,发现存在花指令,大部分花指令就以下几种情况,第一种截图区域所有字节都nop,后面几种就跳转地址汇编按u进行undefine,然后再跳过对应字节按c还原汇编代码。
去掉main函数几个花指令后按p还原函数进行反编译,发现存在代码混淆,且开头有反调试。
直接条件断点在IsDebuggerPresent,将返回值eax设置为0即可跳过反调试。
发现代码中有用到一些常数,跳转过来发现是0-9,手动将名字都命名为对应数字,方便分析代码。
上图发现for循环最后i是+=了一个函数传进去1的返回值,去掉该函数花指令后,可以发现是以下代码,返回的永远是传参的值,也就是1,所以其实这个函数没什么用,直接看参数值就是对应值。
然后配合动调,省略去混淆部分代码,抽离出主要核心代码,且所有常量字符串都被加密过的,只能动调获取。
第一部分key校验代码如下。
同理第二部分的Flag校验如下,byte_F1A004就是被加密的Flag。
去掉加密函数的花指令,然后同样进行动调抽离关键代码。
还原出程序完整原始代码:
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 #include <iostream> #include <windows.h> void Encrypt (unsigned char *Input, uint32_t Key) { int InputLen = strlen ((char *)Input); for (int i = 0 ; i < InputLen; i++) { uint8_t v26 = i ^ (Key >> ((3 - i % 4 ) * 8 )); uint8_t v27 = v26 ^ Input[i]; uint8_t v28 = (v27 >> 5 ) | (v27 * 8 ); v28 += i; Input[i] = v28; } } int main () { uint32_t Key = 0 ; char Input[256 ]{}; uint32_t v66 = 0x811C9DC5 ; for (int i = 0 ; i < 4 ; i++) { uint8_t v71 = Key >> (i * 8 ); uint32_t v67{}; if (v71 % 2 ) v67 = v66 ^ v71; else v67 = 16777619 * v66; v67 = (v67 >> 25 ) | (v67) << 7 ; v66 = v67 - v71; } if (v66 != 1172912374 ) { } for (int j = 0 ; j < strlen (Input); j++) { Input[j] ^= 8 * (v66 >> 16 ); } Encrypt (Input, Key); return 0 ; }
可以通过爆破得到目标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 int main () { uint32_t Key = 0 ; for (; Key < 0xffffffff ; Key++) { uint32_t v66 = 0x811C9DC5 ; for (int i = 0 ; i < 4 ; i++) { uint8_t v71 = Key >> (i * 8 ); uint32_t v67{}; if (v71 % 2 ) v67 = v66 ^ v71; else v67 = 16777619 * v66; v67 = (v67 >> 25 ) | (v67) << 7 ; v66 = v67 - v71; } if (v66 == 1172912374 ) { printf ("%X\n" , Key); break ; } } return 0 ; }
爆破得到Key:8C90F77B
然后通过加密函数编写解密函数解密byte_F1A004数组即可得到Flag。
解密代码:
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 #include <iostream> #include <windows.h> unsigned char EncFlag[]{ 0x54 , 0x55 , 0x79 , 0x9E , 0xA8 , 0xE1 , 0x1C , 0xDA , 0x04 , 0x1D , 0xC1 , 0x6E , 0x80 , 0x82 , 0x0D , 0x8A , 0x4C , 0x65 , 0xE1 , 0x46 , 0x71 , 0x31 , 0xED , 0xD2 , 0x14 , 0xC5 , 0x39 , 0xB5 , 0x49 , 0xE2 , 0x04 , 0xA9 }; void Decrypt (unsigned char *Input, uint32_t Key) { for (int i = 0 ; i < 32 ; i++) { uint8_t v26 = Key >> 8 * (3 - i % 4 ); v26 ^= (uint8_t )(i & 0xff ); Input[i] = (Input[i] - (uint8_t )i) & 0xFF ; Input[i] = ((Input[i] >> 3 ) | (Input[i] << 5 )) & 0xFF ; Input[i] = Input[i] ^ v26; } } int main () { uint32_t Key = 0x8C90F77B ; Decrypt (EncFlag, Key); uint32_t v66 = 0x811C9DC5 ; for (int i = 0 ; i < 4 ; i++) { uint8_t v71 = Key >> (i * 8 ); uint32_t v67{}; if (v71 % 2 ) v67 = v66 ^ v71; else v67 = 16777619 * v66; v67 = (v67 >> 25 ) | (v67) << 7 ; v66 = v67 - v71; } for (int j = 0 ; j < 32 ; j++) { EncFlag[j] ^= 8 * (v66 >> 16 ); } printf ("%.32s\n" , EncFlag); return 0 ; }
Flag值 NSSCTF{NSSCTF{NSSCTF{NSSCTF{}}}}