1.1Creakeme参考链接https://www.yunzh1jun.com/2022/05/27/WindowsSEH/https://github.com/0xE4s0n/creakme_sctf2019/blob/master/creakme/creakme.cpp 题目源码对照参考链接提到过的增强版的SEH结构体我们可以发现入栈的参数是一一对应的:入栈的参数是ebp、0xFFFFFFFE、stru_407B58、__except_handler4、large fs:0分别对应_EXCEPTION_REGISTRATION中的_ebp、trylevel、scopetable、handler和prev前面介绍TEB时已经说过FS:[0]指向SEH起始地址。那么按照scopetable的定义这个结构体中存储了lpfnFilter当前try块的过滤函数和lpfnHandler当前try块的Handler。双击stru_407B58查看这个结构体loc_4023DC是FilterFuncloc_4023EF是HandlerFunc。这俩一个是过滤函数一个是处理函数.rdata:00407B58 stru_407B58 dd 0FFFFFFE4h ; GSCookieOffset .rdata:00407B58 ; DATA XREF: sub_4023205↑o .rdata:00407B58 dd 0 ; GSCookieXOROffset .rdata:00407B58 dd 0FFFFFFC4h ; EHCookieOffset .rdata:00407B58 dd 0 ; EHCookieXOROffset .rdata:00407B58 dd 0FFFFFFFEh ; ScopeRecord.EnclosingLevel .rdata:00407B58 dd offset loc_4023DC ; ScopeRecord.FilterFunc .rdata:00407B58 dd offset loc_4023EF ; ScopeRecord.HandlerFunc .rdata:00407B74 align 8过滤函数通过触发断点异常80000003h来转到处理函数处理函数最终调用一个函数sub_402450并且有两个参数ecxesi。ecx又是ebpvar_28eacC。ebpvar_28又是程序初始时的ecx值。下面这些指令是标准的PE 文件结构解析动作。由于所有的偏移如3Ch,0F8h都是基于ecx计算的这证明ecx存储的是 PE 文件的起始内存地址。mov eax, [ecx3Ch] ; 读取 PE 头偏移 (e_lfanew) movzx ebx, word ptr [eaxecx6] ; 读取 NumberOfSections (节数量) lea esi, [ecx0F8h] ; 定位到节表 (Section Table)再结合4024A0函数其实ecx就是找.SCTF节的位置然后进行函数402450处理。也就是自解密SMC。知道加密逻辑和加密数据后在 IDA 菜单栏点击File - Script command...选择Python然后输入以下代码import idc start_addr 0x00404000 length 0x200 key bsycloversyclover key_len len(key) print(f[*] Starting decryption at {hex(start_addr)}) for i in range(length): current_byte idc.get_wide_byte(start_addr i) s key[i % key_len] decrypted_byte (~(current_byte ^ s)) 0xFF idc.patch_byte(start_addr i, decrypted_byte) print([] Decryption finished!)然后在404000这就会获得一段逻辑。按p后f5反编译对密文做一个这样的处理先--然后字符反转。最终的解密脚本spvfqYc,4tTc2UxRmlJ,sB{Fh4Ck2:CFOb4ErhtIcoLo print(len(s)) slist(s) for i in range(len(s)): s[i]chr(ord(s[i])-1) S.join(x for x in s) SS[::-1] print(S) from Crypto.Cipher import AES import base64 ciphertext_base64 nKnbHsgqD3aNEB91jB3gEzArIklQwT1bSs3bXpeuo key bsycloversyclover iv bsctfsctfsctfsctf raw_cipher base64.b64decode(ciphertext_base64) cipher AES.new(key, AES.MODE_CBC, iv) decrypted cipher.decrypt(raw_cipher) print(fFlag: {decrypted.decode(utf-8).strip()})2.1hardcpp逻辑比较简单使用z3即可求解from z3 import * data[0xF3, 0x2E, 0x18, 0x36, 0xE1, 0x4C, 0x22, 0xD1, 0xF9, 0x8C, 0x40, 0x76, 0xF4, 0x0E, 0x00, 0x05, 0xA3, 0x90, 0x0E, 0xA5] v18 [BitVec(fv18_{i}, 8) for i in range(21)] s Solver() for char in v18: s.add(char 32, char 126) for i in range(20): s.add((((v18[i]^18)*3)2)^((v18[i]%7)(0 ^ v18[i1])) data[i]) # 求解并输出 if s.check() sat: m s.model() print(解出 v18 数组结果 (Hex):) for i in range(20): val m[v18[i]].as_long() print(fv18[{i}] {hex(val)}) # 将结果转换为字符串CTF 中常见的 Flag 格式 import struct try: flag b for i in range(20): flag struct.pack(Q, m[v18[i]].as_long()) print(f\n可能的 Flag 字符串: {flag.decode(errorsignore)}) except: pass else: print(无法求解请检查约束公式是否输入有误特别是括号匹配。)3.1powerPackedupx解壳即可。4.1Repyc5.1Shit改名为put和cin按Y分别定义put这里不指定ecx和edx识别不出来cin不用指定就能识别。主要是汇编那。void __usercall put(void *thisecx, const char *stredx); void* __thiscall cin(void *this, char *buffer);看见genkey那有花指令写一个idc脚本把花指令去除掉手动nop也许就是比较麻烦。然后把整个函数U掉再按P还原函数后F5得到如下结果import idc def patch_junk(start_addr, end_addr): curr start_addr while curr end_addr: if (idc.get_wide_byte(curr) 0xE8 and idc.get_wide_byte(curr 1) 0x03 and idc.get_wide_byte(curr 2) 0x00 and idc.get_wide_byte(curr 3) 0x00 and idc.get_wide_byte(curr 4) 0x00): for i in range(13): idc.patch_byte(curr i, 0x90) curr 13 else: curr 1 patch_junk(0x401460,0x401630)然后把encode按相同的脚本和方法先去花全部U掉然后PF5查看encode函数。里面有内联函数ROR4也就是循环右移4字节32位。循环左移Rotate Left的本质是把移出去的高位重新补到低位。循环右移Rotate Right的本质是把移出去的低位重新补到高位。width取32是ROR4width取8是ROR1def ROL(val, n, width32): return ((val (n % width)) (2**width - 1)) | (val (width - (n % width))) def ROR(val, n, width32): return (val (n % width)) | ((val (width - (n % width))) (2**width - 1))反调试:尝试修改运行逻辑也并不能调试尝试使用attach方法获取key。选择本地下个断点再附加程序直接search找到shit即可按F9跑起来再输入一个长度为24的字符串跑到这按F7进去在这通过修改标志位过去import struct def ROL(val, n, width32): return ((val (n % width)) (2**width - 1)) | (val (width - (n % width))) def ROR(val, n, width32): return (val (n % width)) | ((val (width - (n % width))) (2**width - 1)) data[0x8C2C133A, 0xF74CB3F6, 0xFEDFA6F2, 0xAB293E3B, 0x26CF8A2A, 0x88A1F279] key[0x00000003, 0x00000010, 0x0000000D, 0x00000004, 0x00000013, 0x0000000B] for i in range(5,0,-1): data[i] ^ data[i-1] for i in range(6): temp data[i] ^ (1 key[i]) temp (temp16)0xffff | (~(temp16)0xffff0000) data[i] ROL(temp, key[i], 32) flag for i in range(6): flagstruct.pack(I, data[i]).decode(utf-8, errorsignore) print(flag)6.1Crash可以先使用GoReSym恢复一下符号GoReSym.exe -t -d -p xxx.exe xxx.json生成json文件后 选择脚本文件选择idapython下的python脚本让后选择生成的json即可。随后便会恢复文件符号。在ida里找到了string和interface的定义分别定义v1到v6按T后搜索可以直接选择输出函数的原型是void fmt_Fprintln(runtime_itab *a1, void *os_Stdout_ptr, BUILTIN_INTERFACE *a3, __int64 a4, __int64 a5);输入函数的原型是void fmt_Fscanln(runtime_itab *a1, void *os_Stdin, BUILTIN_INTERFACE *a3, __int64 a4, __int64 a5);大概就能还原成这个样子按照逻辑开始逆向main_check在main_check里先调用main_encryptovoid *runtime_newobject(type *t)输入参数t这是一个指向runtime._type结构体的指针。它描述了要分配的对象是什么类型大小、对齐方式、是否包含指针等。返回值返回分配好的内存空间的起始地址指针。在结构体里找到了main_secretdata类型看来是分配这个类型的指针在 Go 语言中string是不可变的而[]byte是可变的。为了保证安全当你把字符串转成字节切片时Go 必须在堆上开辟一块新内存并将字符串的内容拷贝过去。这个分配内存并拷贝的过程就是由runtime.stringtoslicebyte完成的。第一个参数 (0): 这是一个临时缓冲区buf。如果编译器通过逃逸分析发现这个转换后的切片不会逃逸出当前函数它可能会在栈上分配一个小缓冲区通常是 32 字节传进来以避免堆分配。如果传入0NULL则表示该切片会逃逸必须在堆上分配内存。第二个参数 (ptr): 字符串的地址。第三个参数 (n): 字符串的长度Go 编译器在处理切片Slice传参时会将每个切片拆分为(指针, 长度, 容量)三个参数传给函数。ptr_1 Encrypt_DesEncrypt( ptr_cast, a5, v5, // 参数 1: 待加密数据 (你的输入) 的 Slice (ptr, len, cap) key_ptr_cast, a5, v5, // 参数 2: 密钥 Key 的 Slice (来自 JSON 的 WelcomeToTheGKCTF2021XXX) iv_ptr_cast_1, a5, v5 // 参数 3: 偏移量 IV 的 Slice (来自 JSON 的 1Ssecret) );Encrypt_DesEncrypt的返回类型struct desret { byte *data; int64 len; int64 cap; void *err_itab; void *err_data; };大概能还原成这样最复杂的加密就逆向完了check显示得并不完整jz改成jmp即可。那整个逻辑就一目了然了