buuoj 逆向刷题之旅(二)
以下是本文涉及到的关键词:
- 架构:x86, JVM
- 文件类型:exe, elf, apk
- 考点:vm虚拟机, angr, 图形界面, 花指令, 二叉树, java层, .NET, Unity游戏, hook, Windows异常机制, sm4, ollvm平坦化, 虚假控制流, 数独, apk加固, 栈溢出, 变种base64, 反调试, SMC, AES
PS. 用到的 python 脚本运行环境除特殊标注外均为 python2,如果内容有误或者图片缺失,欢迎联系我修正。
好耶!φ(>ω<*)
[GWCTF 2019]babyvm | x86,elf,vm虚拟机,angr
出题人上来就写了一个虚拟机,很复杂啊:
我不讲武德直接上 angr
1 | import angr |
跑出来个假的 flag:
放到 ubuntu 上运行:
没毛病啊,那这个小伙子才是真的不讲武德,敢用假 flag 和假 code,这好么,这不好。从 Vm_Run 函数可以得知,运行结束的条件是碰到 0xF4:
那就翻翻 unk_202060:
原来真正的 code 藏在 0x202180 了,那就把 Vm_Run 中的 0x202060 patch 成 0x202180:
同时 Check_Flag 函数也是假的,与之相似的函数还有一个 sub_F00,把 main 函数检查 flag 的函数也 patch 成 sub_F00:
再用 angr 跑一遍(脚本不用改),得到真正的 flag
[HDCTF2019]MFC | x86,exe,图形界面
ExeInfo PE 查一下,发现被 VMProtect 加壳了:
不急着脱壳,毕竟 VM 的壳不好脱,运行起来:
好家伙,谜语人。xspy(提取码 f93p)探一下最外层的窗口,发现一个奇怪的类名和奇怪的消息回调:
C++ 写个程序,给窗口 0x210ADE(窗口句柄,每次运行都会改变)发个 0x464 消息:
1 |
|
得到一个 key:
所以刚才的奇怪类名就是 DES 密文咯,找个在线网站解密:
findKey | x86,exe,图形界面,花指令
找到 WinMain,注册了一个类:
点进去,找到窗口函数 loc_401640:
该函数存在花指令,按 P 无法直接创建函数,下方 Output window 提示 0x40191F 处解析失败,检查一下:
把 jmp 给 nop 掉,在 0x401640 处按 P 创建函数并 F5,首先观察他的 else 分支:
如果到来的消息是 WM_COMMAND,且 wParam 是 104 的话,就会用 Dialog 模板(编号 103)创建一个对话框,对应的窗口函数是 DialogFunc。Resource Hacker 打开,找到 104 对应的菜单为 About:
其 Dialog 模板也说明弹出的对话框是 关于:
窗口函数 DialogFunc:
也就是说,如果在关于对话框中按左/中/右键,input 数组就会被逐字节赋值为字符 1/2/3。再回到 0x401640 函数的 if 分支:
XorDecrypt 函数逻辑很简单,这里直接给脚本,将 cipher 还原一下(也可以动调):
1 | def XorDecrypt(key, cipher): |
将得到的结果拿去 网站 查询,得到 md5 前是 123321。至于为什么那个函数是 md5,你可以点进去,来到函数 sub_4013A0,里面有这么一行:
CryptCreateHash 第二个参数是 0x8003,该宏可以在 MSDN 找到,为 CALG_MD5
剩下的你可以接着用脚本,把 unk_423030 拿出来,用 123321 作为 key,XorDecrypt 一下得到 flag:
1 | key2 = "123321" |
也可以让程序自己弹出 flag。input 数组是 123321 的话,在 Help -> About 对话框中空白处应该依次单击鼠标 左、中、右、右、中、左 键,关闭 About 对话框(OK 或者 × 都行),再在主界面空白处单击鼠标右键:
[ACTF新生赛2020]SoulLike | x86,elf,angr
最简单的方式是让你的 5800X 起床干活
1 | import angr |
大概 20s 出结果。当然,因为程序本身也会告诉你输入的 flag 错在了哪一位,也可以写个 python 脚本来爆破,用 os.popen + 输入重定向
[FlareOn5]Ultimate Minesweeper | x86,exe,.NET
ExeInfo PE 打开,得知是个 .NET 框架的程序,于是用 dnspy 打开,在所有的类中找到了一个叫 SuccessPopup 的玩意儿:
Ctrl+Shift+R 分析一下,发现他被实例化在 MainForm 类中:
跟过去:
那就看看这个 TotalUnrevealedEmptySquares 属性,就一个 get 方法,在获取的时候会去检查是不是非雷的都点了:
同时注意到有一个可疑的数组 MinesVisible,看这名字应该和控制所有方块是否显示(里面是啥)有关。同样 Ctrl+Shift+R 对这个数组进行分析,看看啥时候被赋的值:
就只有这里进行了初始化,并全初始化为了 false(全不可见):
Ctrl+Shift+E 尝试把这里 patch 一下,每个元素都赋值为 true:
点击右下角编译,然后 Ctrl+Shift+S 保存 -> 确定,再运行:
不管运行多少次都是固定的三个方块没雷,直接点击发现没有效果,应该是点击的回调函数里判断了方块是否可见,可见就表明已经被点开,不会进行其他操作。那就记下这三个方块的坐标,重新解压一下附件,在没有patch 过的程序里点击这三个方块,得到 flag。
[UTCTF2020]basic-re | x86,elf
shift + F12
[WUSTCTF2020]level4 | x86,elf,二叉树
总体分析程序逻辑,大致可以猜测与二叉树的遍历有关。由 main 开头的提示,得知用到的结构体有三个字段,在 IDA 的结构体视窗中创建 node 结构体:
第一个字段实际上只用一个字节(char),由于内存对齐让其占用了 8 个字节,该字段存放结点的数据(一个 ascii 字符),其余两个字段分别是左、右后继结点的指针。现在再看 type1 函数,将函数参数类型修改为定义的 node 结构体指针,可以发现是一个很显然的 左-中-右 中序遍历:
进一步还原 main 函数符号:
直接运行得到中序和后序遍历的结果:
众所周知,已知中序和其他任意序可以唯一确定一个二叉树,具体方法详见 百度经验,将树构建出来:
前序遍历得到 flag
PS. 如果不会通过中序、后序序列构建树,也可以动态调试。在 init 函数调用结束后(0x40086B)下断点,通过根节点(0x601290)找到所有的后继节点:
[SCTF2019]Strange apk | JVM,apk,java层
jadx 打开,找到 sctf.hello.c 类,发现其重写了 attachBaseContext 方法,该方法同 onCreate 一样,会在 Application 实例化过程中被自动调用,并且调用时间早于 onCreate 方法:
该函数可疑在后面使用 DexClassLoader 从别的 dex 中加载了类,分析一下可疑部分创建的新文件干了什么
简单来说就是将 assets 目录下的 data 文件内容全部读出,拿来与 syclover 异或。那就把 data 从 apk 中取出,python 处理一下:
1 | with open("data", 'rb') as f: |
得到一个新的 apk,再用 jadx 打开,找到类 sctf.demo.myapplication.s,按钮对应的函数:
后 18 个字符在 sctf.demo.myapplication.t 类中重写的 onActivityResult 方法里验证:
encode 函数很简单,这里就不贴出来了,直接贴脚本:
1 | import base64 |
[MRCTF2020]PixelShooter | JVM,apk,.NET,Unity游戏
jadx 打开可以发现很多 Unity 的类,那就找一下 Assembly-CSharp.dll 被打包在哪里了,解压出来直接搜索,发现在路径 assets/bin/Data/Managed 下。dnSpy 打开该 dll,找到 GameController 类的 GameOver 方法:
发现有更新 UI 的语句,在 UIController 类的 GameOver 方法中找到 flag:
[安洵杯 2019]crackMe | x86,exe,hook,Windows异常机制,sm4
程序中使用 IAT hook 技术,将导入表中的 MessageBoxW 函数替换为 sub_411023,具体怎么实现 hook 的后面再说,现在只需知道 main 函数中调用 MessageBoxW 时实际是调用了 sub_411023:
sub_411023 会跳转到 sub_412AB0,该函数干的事就是将 base64 编码表的大小写交换,并且注册一个 VEH:
之后返回到 main 函数,安装了一个 SEH,然后故意引发了一个异常:
众所周知,Windows 用户态异常发生先找调试器,没有再找 VEH,VEH 处理不了再找 SEH, SEH 还处理不了找 UEF(UnhandledExceptionFilter,用户设置的 TopLevelExceptionFilter 在该阶段被调用)。前面已经注册了 VEH 和 SEH,所以先走 VEH:
VEH 干了两件事
- 用长度为 16 个字节的 key 对 sm4 加密进行初始化
- 设置 UEF 为 TopLevelExceptionFilter
注意,返回值为 0(EXCEPTION_CONTINUE_SEARCH),表示此 VEH 没能处理这个异常,接着异常交给 SEH:
SEH 中对输入进行了 sm4 加密,结果存在 output 中,返回值为 1(EXCEPTION_DISPOSITION::ExceptionContinueSearch),表示 SEH 仍没能处理这个异常,故异常递交给 TopLevelExceptionFilter:
变种 base64 编码函数 sub_41126C 跳转 sub_413090,该函数有两点要注意:
最后异常返回,去到 sub_411136 执行比较
整理一下程序流程:IAT hook -> main -> printf -> scanf_s -> MessageBoxW(被替换成 sub_411023,注册了 VEH) -> 安装 SEH -> 异常发生 -> VEH(sub_412F40,设置了 UEF) -> SEH(sub_412EA0) -> UEF(sub_412C30) -> sub_411136
解题脚本(sm4 库安装:pip install sm4):
1 | import base64 |
PS. 本题的 IAT hook 是怎样完成的?
与 elf 类似,exe 的 main 函数并非程序入口点,main 函数是由 start 函数调用 mainCRTStartup,完成一系列初始化后才调用的。初始化过程中会调用 initterm/initterm_e 函数来初始化所有全局和静态 C++ 类对象的构造函数,可以在 rdata 段的函数指针表中找到 dd offset sub_411235:
该函数是某个类的构造函数,并且出题人将该类的一个对象声明在了全局变量区域或声明为静态对象,导致 sub_411235 在 mainCRTStartup 的初始化中被调用
知道了 hook 函数被调用的时机,现在来理理函数的逻辑。sub_411235 跳转到 sub_411E40:
获取了 exe 基址并调用 sub_41114A,传递了三个参数:
- exe 基址
- 要 hook 的函数所在的 dll 名 - User32.dll
- 要 hook 的函数名 - MessageBoxW
sub_41114A 跳转 sub_412DF0:
调用 LoadLibrary 加载 user32.dll,返回 user32.dll 的基址存到 hModule 中;调用 GetProcAddress,传入 hModule 与 MessageBoxW 函数名,得到 MessageBoxW 函数的内存地址;调用 sub_41118B,传入三个参数:
- exe 基址
- 要 hook 的函数所在的 dll 名 - User32.dll
- MessageBoxW 的内存地址
sub_41118B 跳转 sub_4127B0(对符号进行还原):
内容有些多,一点点来看:
1 | optional_header = (IMAGE_OPTIONAL_HEADER32 *)(baseAddr + *(_DWORD *)(baseAddr + 60) + 24); |
先将 baseAddr(exe基址)类型转化为 IMAGE_DOS_HEADER 指针,*(_DWORD *)(baseAddr + 60) 就是 baseAddr->e_lfanew,该值为 IMAGE_NT_HEADERS 的偏移,加上 baseAddr 就是 IMAGE_NT_HEADERS 结构体的起始地址,再加上 24,就是 IMAGE_NT_HEADERS.OptionalHeader 字段的地址,这样就得到了 optional_header
1 | import_descriptor = (IMAGE_IMPORT_DESCRIPTOR *)(optional_header->DataDirectory[1].VirtualAddress + baseAddr); |
optional_header->DataDirectory[1] 就是 IAT(导入表)对应的 IMAGE_DATA_DIRECTORY 结构体,访问字段 VirtualAddress 获得 IAT 起始地址的偏移,加上 baseAddr 得到 IAT 表的地址。IAT 表是由一个个 IMAGE_IMPORT_DESCRIPTOR 结构体组成的数组,一个 dll 对应这么一个 IMAGE_IMPORT_DESCRIPTOR 结构体。重新审视这个 for 循环:
很清楚,目的就是找到 user32.dll 对应的 IMAGE_IMPORT_DESCRIPTOR 结构体地址
1 | thunk_data = (IMAGE_THUNK_DATA32 *)(import_descriptor->FirstThunk + baseAddr); |
找到以后,访问字段 FirstThunk,加上 baseAddr,获得由多个 IMAGE_THUNK_DATA 结构体组成的 thunk_data 数组,从该 dll 中导入了多少个函数,就有多少个 IMAGE_THUNK_DATA 结构体。那么下面这个 while 循环也不难理解了:
该循环遍历 thunk_data 数组,找到 MessageBoxW 对应的 IMAGE_THUNK_DATA 结构体地址,现在只需要将 IMAGE_THUNK_DATA 的该字段覆写为 sub_411023 函数的地址,再调用 MessageBoxW 时,就会跳到 sub_411023 去了!
要想往一个内存地址里写东西,一般需要三个步骤:
- 修改该地址所在内存页的保护属性,至少改成可读可写,记下原页面属性
- 写入数据
- 还原原页面属性
覆写的部分:
thunk_data 现在是找到的 MessageBoxW 的 IMAGE_THUNK_DATA 结构体地址,右移再左移是为了使低 12 位清零,计算出该地址所在内存页的起始地址(内存页 4K 对齐);VirtualQuery 获取该内存区域的属性到 Buffer 中(主要是为了通过 Buffer.RegionSize 获取内存区域的大小);VirtualProtect 修改这片内存区域的保护属性为 0x40(可读可写可执行);WriteProcessMemory 在 thunk_data 处写入 4 个字节(sub_411023 函数的地址);最后的 VirtualProtect 还原原来的页面保护属性,大功告成!
之后 mainCRTStartup 完成剩余初始化任务,调用 main 函数。main 中再调用 MessageBoxW 时,就会跳到 sub_411023 去了
[网鼎杯 2020 青龙组]jocker | x86,exe
找到 main 函数,F5 发现栈不平衡,Options -> General -> Disassembly 界面勾选上 Stack pointer,找到 IDA 分析错误的 0x401833 行,Alt + k 将 sp 的变化值改为 0:
同理,将 0x401847 行的 sp 变化值改为 0,再 F5 可以看见伪代码:
python 写个 idc 脚本在 ida 中还原 encrypt 函数:
1 | import idc |
在 ida 中选择 File -> Script File…,选择该脚本运行,来到 encypt 函数,先将函数 Undefine,并从上到下,在其解释为 db xx 指令处按 c,强转为汇编代码,保证整个函数内没有 db xx 这样的指令。再在 0x401500 处右键创建函数,F5:
逻辑比较简单,直接上脚本:
1 | origin = "" |
获得 flag 前 19 位,接着同理处理 finally 函数,得到:
莫名其妙的一个函数,没看到哪里能搞到最后 5 位,好像就函数开头的 5 个赋值有点关系了,flag 最后肯定是 },那就拿 58 和 ord(‘}’) 来异或,得到的值再与这 5 个值逐一异或,得到最后 5 位:
1 | tail = '' |
想不到这里也可以写个脚本来爆破,就 5 位,空间很小
[2019红帽杯]childRE | x86,exe,二叉树
shift + F12,查找字符串 “flag{MD5(your input)}” 的交叉引用,来到函数 sub_140001610,该函数依照逻辑可以分为两个部分。第一个部分:
BuildTree 函数通过输入建立一个 5 层的满二叉树(正好 31 个结点),并返回根节点。这块儿可以通过动调得知树的构建顺序(层序从上到下,层内从左到右),输入 base64 编码表前 31 位(ABCDEFG…),建出来的树长这样:
后面的那个 if 判断里干的事其实就是整个树的后序遍历,将遍历结果存到 name 数组。不是很明白为什么他不直接写 Postorder(rootNode),而是后序遍历左右子树,最后再填入根节点,这效果不一样吗(地铁老人看手机
第二个部分:
可以先通过下面的 do…while 循环求出这个长度为 62 的字符串 outputString:
1 | table = "1234567890-=!@#$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;'ASDFGHJKL:\"ZXCVBNM<>?zxcvbnm,./" |
得到一个函数签名 private: char * __thiscall R0Pxx::My_Aut0_PWN(unsigned char *)
,UnDecorateSymbolName 函数将被修饰过的函数签名还原,得到上面这个未被修饰的字符串(关于符号修饰和函数签名详见 博客园)。那现在需要将这个函数签名进行符号修饰,有两种方法 —— 按照编译器的符号修饰规则自己修饰、VS 写个程序通过 __FUNCDNAME__
宏定义查看
第一种学习成本太高直接 pass。第二种方法的 C++ 程序:
1 |
|
请务必在 x86 而非 x64 平台下编译运行,否则会导致结果不对。要问为什么的话,x64 寄存器多,所以不按 x86 的那套函数调用约定来做。注意到这里函数签名里的调用约定是 __thiscall,这是 x86 下的类方法调用约定,如果在 x64 下编译会被编译器强转为 cdecl,结果自然错误。
运行得到符号修饰结果 ?My_Aut0_PWN@R0Pxx@@AAEPADPAE@Z
,长度 31,符合前面的推理,且该字符串应该是输入后序遍历的结果。写个程序得到输入(这里顺带 md5 了):
1 | import hashlib |
[FlareOn1]Bob Doge | x86,exe,.NET
双击 C1.exe,将题目解压到任意文件夹,得到 Challenge1.exe,exeinfo pe 查一下,得知是 .NET 框架的程序。dnSpy 加载,在 XXXXXXXXXXXXXXX.Form1 类中找到 btnDecode_Click 方法,该方法就是 DECODE 按钮的回调:
右键 -> 编辑方法,将函数 patch 成如下形式(只留第一个 foreach,同时 lbl_title 设置为 text):
点击编译、保存全部(ctrl + shift + s)。再运行一遍,点击 DECODE! 得到 flag:
当然也可以 dnSpy 动态调试得到 flag
[安洵杯 2019]game | x86,elf,ollvm平坦化,数独
新去平坦化脚本 github,原来腾讯安全应急响应中心的那个 deflat.py 对环境依赖太高,不推荐使用。github 的这个只需要 python3 和 angr 就能用
把 main、general_inspection、blank_num、trace、check1、check2、check3 函数都去平坦化了(trace 函数去平坦化失败,但影响不大)。下面给出去掉 main 平坦化的命令行:
python deflat.py -f attachment --addr 0x4006F0
再用 ida 打开 attachment_recovered,可以发现 main 的逻辑变正常了:
总的来说,程序中有两个数独数组(9 * 9),一个是 sudoku,另一个是 D0g3,它们的初始元素完全相同,都有 40 个没有填的空。不过 trace 函数将 sudoku 填写好了,check1 函数对输入做变换得到要填写的数字序列:
check3 中调用 check2,填写 D0g3 数组,然后与 trace 函数填写好的 sudoku 数组进行逐一比较:
那正常输入 check1 过后得到的数字序列可以通过动调直接从 sudoku 中提取出来,得到 4693641762894685722843556137219876255986
然后再做 check1 的逆向操作得到输入:
1 | num_seq = "4693641762894685722843556137219876255986" |
[RoarCTF2019]polyre | x86,elf,ollvm平坦化,虚假控制流
跟 [安洵杯 2019]game 一样,先用脚本去掉 main 函数的平坦化 python deflat.py -f polyre --addr 0x400620
。再用 ida 打开,发现还有一些有规律的永真判断:
因此可以在观察时忽略或删除 while 语句和 do…while 语句,我选择写 ida 脚本来删掉虚假控制流方便观察。首先观察一下汇编,发现有一段代码总是出现:
把 jnz pacth 成 jmp,while 循环就会消失掉。需要注意,jnz 的 opcode 占两个字节(0x0F 0x85),而 jmp 的 opcode 只占用 1 个字节(0xE9),所以 jnz 指令长度为 6,jmp 的指令长度为 5,在 patch 时还应该把 jnz 指令的最后一个字节 patch 成 0x90(nop)。下面是脚本:
1 | #coding=utf-8 |
在 ida 中执行后重新 F5,得到逻辑清晰的 main 函数:
1 | __int64 __fastcall main(__int64 a1, char **a2, char **a3) |
程序的逻辑是输入 48 个字符,分 6 次处理。每次取来 8 个字节(存在 v4 中),进行 64 轮计算,每轮判断 v4 是否小于 0,小于 0 就左移一位后异或 0xB0004B7679FA26B3,否则就只是左移一位,最后与一个数组 unk_402170 进行比较。解题脚本:
1 | import struct |
这里我取了巧,如果上一轮异或了 0xB0004B7679FA26B3,那 v4 的最低位应该是 1,说明 v4 是个奇数,所以有 if tmp % 2 == 1
这个判断。tmp += 1 << 64
是为了防止负数因为 tmp //= 2
变成正数
[网鼎杯 2020 青龙组]bang | JVM,apk,apk加固,java层
看这个题目名字就知道是梆梆加固了,脱壳需要用到
- 安装了 Xposed 的模拟器,教程见 Xposed与EdXposed框架搭建 的第一部分
- 反射大师
- MT 文件管理器
确保模拟器 Xposed 框架已被激活后,点击上方链接下载最新版反射大师,在模拟器中安装。打开反射大师,会弹出如下提示:
点击模块管理会跳转到 Xposed Installer,在模块中勾选反射大师,并重启模拟器:
打开反射大师,点击 how_debug 这个应用,选择 “选择这个软件”:
在弹出的提示中选择 “打开”:
点击中央的六芒星:
选择 “当前ACTIVITY”:
选择 “写出DEX”:
打开 “修复 Magic”,点击确定:
在弹出的提示中,点击复制,获得路径:
打开 MT 文件管理器,点击上方路径,弹出跳转对话框:
粘贴路径并跳转,将 classes.dex 复制到 PC(MuMu 模拟器是复制到 /storage/emulated/0/MuMu共享文件夹),用 jadx 打开,在 MainActivity 的 onCreate 方法中找到 flag:
Dig the way | x86,exe,栈溢出
main 函数:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
现在我有三个函数:
- func0:ret[arg2]与ret[arg3]交换位置
- func1:abs(ret[arg2] + ret[arg3]) - abs(ret[arg2]) - abs(ret[arg3]) + 2
- func2:abs(ret[arg3]) - abs(ret[arg3] + ret[arg2]) + abs(ret[arg2]) + 2
目标是让 ret[3] = 0,注意到 ret[3] 最后是被 func[2] 的返回值赋值了,如果按正常流程,func[2] 即 func2,这个函数的返回值是恒大于 0 的,自然是拿不到 flag。而 func0 只返回 1,故只有在最后调用 func1 才有希望。那就需要将 func1 与 func2 的位置调换,自然就需要用到 func0 了。所以期望的逻辑是:func0 交换 func1 和 func2的位置 -> 先调用 func2 -> 再调用 func1
程序正好存在一个溢出点(for 循环溢出 v7),可以修改到 ret 数组、arg2、arg3(注意到 arg2、arg3 只有第一次调用 func[0] 时是可控的,后面就会被赋值为 ++i 和 i + 1)。func[1]、func[2] 相对于 ret[0] 的偏移是 7、8,那得把 arg2、arg3 修改为 7、8 才能保证调用 func0 时将 func[1]、func[2] 交换。调用完 func0,会使得 ret[1] = 1,假设 ret[2] 我修改为 0,那么在调用 func[1] 即 func2 时,会使得 ret[2] = 2。在调用 func[2] 即 func1 时,传递的三个参数依次是 ret,2,3,此时 ret[2] = 2,只有让 ret[3] = -1 才能保证 func1 返回 0。所以构造的 data 数据如下:
1 | AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF 07 00 00 00 08 00 00 00 |
各部分解释:
1 | v7[]: AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA |
在题目的同级目录下创建这样一个 data 文件,运行题目 exe 得到 flag
PS. 当然这题还有非预期,至少我找到了一个点。动态调试的时候我发现题目没开基址重定向,那么在溢出 v7 的时候还能控制 func 数组,反正只要 func[2] 调用完返回 0 就行了,那找找题目中有没有什么能用的函数。上看下看最后锁定了 strlen,因为调用的时候传的第一个参数是 &ret[0],只要 ret[0] = 0,那么返回值就是 0 了。构造的 data 如下:
1 | v7[]: AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA |
这样显示出来的 flag 是全 0。调整 func[0]、func[1]、arg2、arg3 使之满足这个思路且程序不会异常终止,你就可以获得一堆 flag,哈哈哈我好无聊
[SWPU2019]ReverseMe | x86,exe
shift + F12,查找字符串 “Try again!” 的交叉引用,来到函数 sub_402810,反编译的伪代码比较乱,实际上只干了三件事:
- 使用字符串 SWPU_2019_CTF 对输入(长度为 32)进行变换(do…while 循环)
- 调用 sub_4025C0 函数加密变换后的输入
- 加密后的 32 个字符应该为:
1 | B3 37 0F F8 BC BC AE 5D BA 5A 4D 86 44 97 62 D3 4F BA 24 16 0B 9F 72 1A 65 68 6D 26 BA 6B C8 67 |
有难度的地方只有 sub_4025C0 函数,该函数存在钓鱼行为,前面用 ZUC 算法产生了一个固定的数组(32 字节),对输入进行加密的部分其实只有最底下的 do..while 循环:
1 | do |
该加密仅仅是每次取来变换后输入的 4 个字节,与固定的数组元素异或。动态调试取出固定的数组,在 0x4027D7 处下断点,第一次断下时,esi + eax 的值就是程序产生的数组地址:
解题脚本:
1 | import struct |
[GKCTF2020]Chelly’s identity | x86,exe
shift + F12,查找字符串 “hi.Are you know of chelly?” 的交叉引用,来到函数 sub_41C290。由于符号去的很彻底,考虑通过动态调试来推测大多数函数的作用
sub_41C290 函数中的 for 循环将输入的每个字节变成一个 4 字节,依次存入数组 a1 中,sub_4111BD 函数检查数组 a1 的长度是否为 16(说明输入字符串长度为 16)
sub_411721 函数对数组 a1 进行计算、修改,该函数干的事用 python 翻译如下:
1 | def isPrime(num): |
最后在 sub_411852 函数中将 a1 与目标数组进行逐一比较,解题脚本:
1 | def isPrime(num): |
[CFI-CTF 2018]IntroToPE | x86,exe,.NET
dnSpy 打开,检验密码的函数在 IntroToPe 命名空间 ValidatePasswd 类的 verifyPasswd 方法。对 Q0ZJey5OZXRDI18xc19AdzNzMG0zfQ==
进行 base64 解码得到 flag
[SCTF2019]creakme | x86,exe,反调试,SMC,Windows异常机制,AES
程序执行流程是这样的:
- main 函数首先调用 sub_402320,函数序言部分注册 SEH stru_407B58,之后找到 image 中的 SCTF 段并制造断点异常(DebugBreak)
- 异常经过 FilterFunc sub_4023DC 交由 HandlerFunc sub_0x4023EF 处理,该函数调用 sub_402450 修改 SCTF 段(0x404000)的代码(SMC)
- 异常处理完成后返回 main,再调用 sub_4024A0,不存在调试器的话才调用 0x404000(反调试)
- sub_404000 将字符串
>pvfqYc,4tTc2UxRmlJ,sB{Fh4Ck2:CFOb4ErhtIcoLo
修改为nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo=
- 等待输入
- 调用 sub_4020D0 对输入进行 AES_CBC 加密,key 是
sycloversyclover
,iv 是sctfsctfsctfsctf
- 加密结果的 base64 形式与
nKnbHsgqD3aNEB91jB3gEzAr+IklQwT1bSs3+bXpeuo=
进行比较
这里需要注意,ida 识别 sub_402320 的范围出现了错误(至少我这里是这样的),将 sub_402320 的范围调整为 0x402320 ~ 0x4023DC。0x4023DC ~ 0x4023EF 是一个 SEH(对应结构体 stru_407B58)的 FilterFunc,0x4023EF ~ 0x402439 则是其 HandlerFunc
shift + F12 查找字符串 “please input your ticket:” 的交叉引用,来到 main 函数 sub_402540,首先调用 sub_402320:
引发异常后,SEH stru_407B58 的 FilterFunc 判断该异常由自己处理:
调用 HandlerFunc sub_4023EF,其中再调用 sub_402450 进行 SMC:
sub_402450 逻辑简单就不贴出来了,可以写个脚本来还原 0x404000:
1 | import idc |
函数 0x404000 修改了目标字符串。当然为了做题的话,上面的步骤都可以不用搞懂,直接把程序运行起来,用调试器附加就能取到修改后的目标字符串
同时,Findcrypt 插件可以发现 AES 的盒子,推测 sub_4020D0 与 AES 加密有关,之后就是动调/静态分析加密模式为 CBC,找到 key 和 iv,写脚本:
1 | from Crypto.Cipher import AES |
[WUSTCTF2020]funnyre | x86,elf,angr
存在 4 处花指令,导致 main 函数无法反汇编,将它们 nop 掉:
分析后有两种做法,第一种是用 angr:
1 | # python3 |
第二种做法是取巧,通过观察可以发现,每个 do…while 循环都会对 flag 的每一位进行相同的变换,所以我只要把所有可见字符都试一遍,通过动调,就能知道这些字符最后都被替换成了什么。再通过目标数组还原 flag 即可,脚本如下:
1 | a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" |
[NPUCTF2020]你好sao啊 |x86,elf,变种base64
只需要分析 RxEncode 这个编码函数,其实现的功能与 base64 相反,该函数将 4 个可见字符变成 3 个字节:
1 | void *__fastcall RxEncode(const char *input, int len) |
比较简单就直接贴脚本了:
1 | from struct import * |