和师傅们打了一天半的 HCTF,题目质量很高,感谢杭电的师傅们带来的精彩比赛,膜一波各位大佬,我还是太菜了。。。
the_end
这题大概一看感觉很简单,IDA的伪代码如下:
1 | void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) |
把 sleep 函数的地址给出,libc 的基地址也就知道了,紧接着就是 5 次的任意地址写,检查开启的保护发现除了 canary 全部开启,考虑一下 write-what-where,现在只有 where 不知道,而且程序在 read 之后就调用 exit 退出了,貌似没有留下任何的利用空间,那一个自然的思路就是能否利用 exit 函数呢?
赛中我考虑过这一点,但是在 google 上没有找到相关的利用姿势,赛后看了大佬的 wp 发现这道题和 0x00 ctf 2017 的 left 题目相似(为什么我比赛的时候找不到 ORZ),而且这道题更简单一些。
这道题的基本思路就是从 exit 函数入手,由于只知道 libc 的地址,而且修改的时候目的地址必须是可写的,于是我们要寻找类似 call eax 这类动态调用的函数,它们极有可能是从 libc 的可写地址取出的函数。
我们可以通过调试来看,步入 exit 函数(每次都用 si 步入每个函数):
这里有一句 call edx,但是跟过去看一下内存:
地址是乱掉的,而且观察到 mov 给 edx 之后还有对 edx 的解密操作,这是 libc 为了防止针对 _dl_fini 攻击而采取的措施,加密用到了一个 token,无法泄露。
于是继续跟进,在这里找到了另一处似乎可以利用的代码:
call 了 libc 中数据段上的一个位置,这个位置是可写的,我们可以将这个地址覆盖为 one_gadget ,获取 shell。
1 | from pwn import * |
趁着环境还没关,可以拿到 flag:
注意最后发送命令的时候需要重定向输入,因为程序在一开始就 close(1) ,关闭了 stdout,但是不知道为什么,我在本地测试的时候就不能输出 flag,远程就可以了…
需要注意 one gadget 限制的很死,找到的四个只有这一个能用。
这个应该是预期解,但是看到师傅们还有一种做法是改了 vtable,有时间仔细研究一下。
LuckyStar
基本流程
踩了很多坑,踩坑的过程就不写了。。。
首先调用了 TLSCallBack 其中对一个函数(主函数)进行了解密,包括大概两种反调试手段.
之后会执行 start 函数,通常这是编译器自动插入的启动代码,但是在本题中,这个代码被出题人修改过,其中包含一些和flag运算相关的代码。
直接反编译 start 函数,发现伪代码中有许多函数没有有意义的名字,还夹杂着一些虚函数,单从静态代码看很难,所以推荐使用 OD 配合分析。
在 start 函数中会调用之前解密的函数对另一个函数解密,计算输入并且和加密的 flag 进行对比。
分析过程
OD 调试的时候要注意绕过反调试,否则不能正常进入 start 函数。
在 TLSCallBack 函数中存在两处反调试,第一个是遍历所有进程,检查是否存在 IDA OD 或者 x32DBG等工具,将程序中硬编码的字符串 patch 掉即可绕过。
上图是我 patch 之后的效果,再次运行就能绕过这个反调试。
第二处采用系统调用 zwsetinformationthread,并设置函数参数为
1 | ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0); |
将当前进程隐藏(注意第二个参数是 0x11),调试器无法继续跟踪,通过修改此函数的参数即可绕过。
单步到对应的位置处(push 0x11),我将 push 到栈上的 0x11 改成 -2,绕过这个反调试。
patch 掉反调试之后,可以发现解密第一个函数的算法:
先 srand 设置随机数种子,然后对 0x401780 开始的 440 个字节进行解密,算法在伪代码中很清晰,我编写了一个 C 程序获取随机数:
1 | #include <stdio.h> |
还有一个 IDA-Python 脚本将函数解密:
1 | a = 0x417000 |
经过清洗的函数可以正常反编译成伪代码(注意可能需要先对代码按 U 键全部取消解析,然后再 C 键转换成代码):
1 | int __usercall sub_401780@<eax>(int a1@<ebp>, int a2@<edi>, int a3@<esi>) |
其他位置还比较好看,但是中间有一个函数不正常,这就是第二个要解密的函数,观察解密算法,和第一个函数的算法一样,但是如果你尝试使用第一个脚本解密的话,是不能正常解出来的。
猜想原因就是随机数种子不再是 0x61616161 了,在程序运行中 srand 进行了更换。
下一步就是找到哪里更换了随机数种子,继续调试,单步到 start 函数(或者参考 IDA 的地址直接下断点),分析 start 函数,抛开前面的初始化流程不看,在第 44 行发现了一个异常函数:
1 | void *__usercall sub_402510@<eax>(signed int a1@<eax>) |
“hctf” 四个大字摆在这里,这个函数应该比较重要。
不巧的是函数里调用了 3 个虚函数,静态没办法确定它们是什么,于是使用 OD 进行动态调试,在这个函数下断点,直接运行就可以断在这里:
单步就可以确定三个函数是什么了,第一个是反调试,调用了 checkremotedebuggerpresent 进行检测,这个可以不管他,直接步过就好,下面两个函数分别是 srand 和 rand,很明显了,就是在这里将随机数种子换掉了,那换成什么了呢?动态调试发现被换成了 0x10001165 ,但是如果拿这个种子生成随机数去解密第二个函数的话,解出来的都是错误代码,说明随机数种子不应该是它,那么猜想应该是 0x68637466 ,即 “hctf”,经验证是正确的。
再次编写两个小程序去解密函数:
1 |
|
1 | a = 0x417000 |
这样就可以把第二个函数解密出来了!
现在整个程序的主要逻辑已经被完全解密。
1 | char __cdecl sub_4015E0(const char *a1, const char *a2) |
对输入的 flag 做一次 BASE64 编码,注意这里的表盘被换掉了,然后取随机数对每个字节异或 4 次,循环进行,直到所有字符处理完毕。
算法很简单,由于异或可逆,那么现在只缺少密文了,观察第一个解密出来的函数发现,比较 flag 是否正确的代码就在其中,那么通过动态调试就能拿到密文!
将密文抽取出来,通过逆向算法可以拿到 BASE64 编码后的 flag(注意随机数的起始位置):
1 | v14 = 0 |
利用 BASE64 解码(注意更换表盘),即可拿到flag。
flag: hctf{1zumi_K0nat4_Mo3}
小总结
这道题目涉及到一些反调试手段和动态代码解密、以及 TLS 等知识,做题时还是要有耐心,这道题我调了很久才做出来(恶心到了。。。)中间放弃了一段时间,做出来之后发现其实没有那么难。
PS: 吐槽一下配的音乐,真的太魔性了 ORZ。。。
seven
双机调试
这是一道 Windows 64位的驱动逆向题目,之前没有做过关于驱动的逆向,做完这道题感觉驱动其实和一般的可执行程序类似,最主要的区别就是驱动和系统内核是有关系的,它需要先被加载到内核中,才能够随着内核一起运行。
在静态分析方面,驱动涉及到一些特殊的函数,主要的算法逻辑没有什么太大的变化,需要注意的一点是函数参数,因为在运行驱动的时候调用的函数是 iocalldriver 等,参数在静态分析时不好发现,于是需要结合动态调试手段。
之前说驱动和一般的可执行程序不一样就体现在调试的方法上,由于驱动是和内核绑在一块的,所以我们得调试系统内核,把操作系统看成一个软件,驱动只是这个软件中的一小部分。
调试 windows 内核微软官方给出的推荐就是 双机调试,即拿来两台电脑,一台作为调试者,另一台最为目标机器,在调试机上面使用 windbg 通过某些连接方式对另一台机器的操作系统进行调试,两台电脑的开销太高了,现在最常用的调试内核的手段就是 主机 + 虚拟机,虚拟机推荐使用 VMware。
首先搭建内核的调试环境,参考博客。
博客给出的步骤已经很详细了,大致分为:
- 配置虚拟机的管道,通过新增串口为虚拟机添加管道。
- 配置主机的 windbg 启动选项,可以通过快捷方式参数来实现。
- 配置虚拟机的开机引导项,新增调试模式。
- 以调试模式启动虚拟机,并在启动过程中使用 windbg 附加上去。
环境搭建并不困难,重点是接下来的调试过程,由于调试的是驱动,那么首先要把它安装好,可以使用 cmd 命令直接安装,也可以使用工具 OSRLoader,这个工具图形化操作比较方便。
在加载驱动之前,先要取消系统对驱动数字签名的验证,并设置一些其他的东西,可以参考以下命令:
1 | bcdedit /copy {current} /d "Windwos7" 建立一个新的启动项。 |
如果使用命令行,可以用 sc :
1 | sc query type= driver 可以查看驱动列表 |
开始调试
首先将 windbg 附加到虚拟机上,如果不出意外的话,控制台应该会显示以下内容:
现在已经在内核断下,按 F5 键运行,直到虚拟机可以正常操作。
接着在 windbg 断下系统(上方工具栏中的按钮),在控制台输入命令
1 | sxe ld seven.sys |
意思就是当 seven 这个驱动加载起来的时候就自动断下。
设置好后继续运行,按照上面的命令把驱动加载起来,如果设置没什么问题,在执行 sc start 之后就会断下来。
如上图,使用 OSR 软件加载驱动,点击 start 之后就会断下来。
但是断点所在的位置不是驱动的入口点,而是 ntdll.dll 中的某一处,我们需要使用命令 lm 来找到 seven 的入口位置:
蓝色方框就是模块的起始地址,结合 IDA 中的偏移量,可以在相应的函数下断点。
通过 IDA 分析驱动,发现主要逻辑函数在于 sub_140012f0 :
1 | __int64 __fastcall sub_1400012F0(__int64 a1, __int64 a2) |
实际上如果对驱动比较熟悉的话,逻辑并不难,由于驱动加载了 kdbclass.sys:
1 | v8 = L"\\Driver\\kbdclass"; |
所以代码中的一些判断都是基于键盘驱动号的,比如 0x11 对应字符 ‘w’ 等等,通过分析得出结论,程序对
w a s d 这四个按键进行判断,其他输入不会产生影响。
1 | 附:键盘的硬件扫描码 |
确定了断点位置,使用命令 bp fffff880`032a7000+12f0 就可以在关键函数下断点,紧接着 f5 继续运行,当在虚拟机中输入一个字符(在键盘上按下任意一个键),windbg就会再次断下,这回位置就在关键函数的入口。
接着就可以着手调试了,F10 和 F11 对应着 OD 的 F7 和 F8,通过调试可以理解这个函数的目的,它多次对内置的一个字符数组进行操作,在 IDA 中这个数组是:
看上去没什么意义,但是根据经验推断,这个应该是一种迷宫,最后对是否到达字符 ‘7’ 进行验证,猜测是要把 o 移动到 7 的位置,在 windbg 中随便调整了一下宽度,得出了迷宫:
一个 7 型迷宫,使用 wasd 操控 o 走到 7 就胜利了!
走一波记录下来每一步的操作,到最后一步的时候单步调试,查看 Dbgprint 函数的参数就是 The input is the flag!,表示输入正确。
flag: hctf{ddddddddddddddssaasasasasasasasasas}
小总结
调试驱动这是第一次,把环境搭好之后就可以像调试正常可执行程序一样调试内核和驱动了。
这道题目的数组应该是一维的,导致静态分析难一些,动态调试就很简单。
- 本文作者: Catalpa
- 本文链接: https://wzt.ac.cn/2018/11/09/HCTF2018/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。