题目信息
题目链接
https://github.com/ctfs/write-ups-2014/tree/master/codegate-preliminary-2014/weirdsnus
准备环境
1 | $ echo HEHE_I_DONT_KNOW_YOU > admin_passwd |
基本信息
静态分析
x86二进制文件分析
使用IDA
- 重命名变量
- stack上的变量,全局变量等,考虑他们在哪里被使用
- 确定变量的size
- 特别是char[]类型的数组
- 确定结构体的结构
- IDA中可以定义结构体成员
- 猜测函数功能
- 根据变量名,变量size,结构体结构等,可能看出函数功能
- 重命名函数
- 根据函数的功能重命名函数
- 发现处理不当(错误)的地方
- 找出可能利用点
重复以上流程,找出漏洞点,分析编写exploit
main
整体大概是这样
- 从函数定义,函数实现,参数传递,canary相关等进行分析
- 复杂的处理可以从library call为起点确定变量
- 因为library call有固定的参数个数和参数类型
第一块
- 保存旧的ebp,在stack frame上更新当前的ebp,准备好函数使用的各种寄存器的值,准备好stack空间,做好各种准备,以便函数启动后能立即就绪
- 将函数参数(这里是argv)保存到stack上。x86中通常将参数重新保存在栈上
- 从TLS区域(gs+0x14)将stack canary复制到栈上
以上三个是常见模式
- buffer是0x110-0x10=0x100=256字节
- memset的内部展开,”rep stosd”四个字节是memset高速实现
- stosd: EAX存储在[EDI]中
- rep: ecx!=0时[重复ecx-=N(这里是DWORD,N=4)]
- strlen是library call,需要注意signed和unsigned的区别
第2到5快
- 图上已经做了函数重命名(分析后的结果)
- 函数结束时,释放使用的stack,还原用到的寄存器,还原ebp的值。如果函数有返回值,则返回值给eax
转换成C语言代码
大概是这样:
password_check
第一块
第二块~
转换成C语言代码
- memcmp的第三个参数len,是用户可控的
- 输入\x00的话,len(“\x00”)=0
- 这样就跳过了memcmp
doit
第一块
第三~块
- EAX范围检查按照unsigned,如果不在0x28~0x7a则nop
第七~块
- switch 保存在.rodata区域的jump_table
- 分支分析大概是这样
- 如果argv[1]有’(‘, ‘)’,则可以调用更深的函数,但需要先bypass前面的认证
转换成C语言代码
- 实际上signed比较函数也有bug,一般size比较都是signed,如果输入一个字节的负数(0x80~0xff),能够通过比较。但是没法进行后续利用
- case ‘z’那里的free(ptr)之后没有清空ptr,后续进行写入会造成Use After Free,但是这里也无法进行exploit
do_command_loop
- 可以看到一个malloc(16)的结构体
- 结构体大概这样,定义后应用
- 根据需要应用结构体偏移
分析之后大概是这样
转换成C语言代码
do_command_loop的bug
- 存在两个bug
- 第一个bug,read_key(new_buffer, buffer_len),两个参数完全没关系,但是read_key内部将buffer_len作为new_buffer的size,buffer_len用户可控
- 第二个bug,free(ptr)之后,没有将ptr设置为NULL,也就能够进行Use After Free
- ptr != NULL的检查不能确定UAF。也就是说,如果UAF之后ptr用作其他用途,则可能修改func_ptr,之后可能调用func_ptr。
漏洞思考
- 如何控制EIP
- 重写函数指针
我们已经有一个16字节的结构体:
1 | struct struc_1 { |
首先构造Use After Free
- do_A - malloc
- do_M - free
- free时,因为是16字节,会连接到fastbins区域
- LIFO,下次申请16字节内存时会重新使用这部分内存
- 寻找一个16字节分配以及任意写入的点
- 这里会根据我们指定的size进行malloc,之后read_key用到new_buffer
- getcwd()通过new_buffer读取当前工作目录,如果当亲工作目录path超过16字节,就能够覆盖函数指针
利用流程
大概是这样:
动态调试
- 测试运行
- 已经控制了EIP
getshell
因为有NX,shellcode比较困难,尝试system(“/bin/sh”)
- 常规方式需要将参数加载到stack上
- 第一个参数必定是0x0a,不能指定文件名
Use After Free
这是可能的利用方式
func_ptr overwrite + ROP
- 先在stack上准备好ROP
- 通过Use After Free,在func_ptr之前,pop×N; ret
- 这样当调用该函数时,popxN除去了多余的参数,执行ROP
- command使用fgets()读取输入到buffer
- 因此可以考虑在换行前输入字符串
- 但是这种方法只能读取10个字节
- 而这种ROP需要至少12个字节
- 注意到do_D的buffer可以控制
- 考虑使用command D
- 尝试
- 计算偏移
- 需要一个add esp, 0x144+α的gadget
- 找到一个add 0x140 + pop3回 = 0x14c的gadget
exploit
大概流程是这样:
替换为system, /bin/sh的地址
可以看到测试能够getshell
实际运行
- 发生了SIGSEGV
- 实际上成功了
- system(“/bin/sh”),command从标准输入读取
- 但是,标准输入绑定input,因此无法读取到
- 因此,不执行任何操作,/bin/sh结束
- 之后因为stack已经被破坏,发生SIGSEGV
- 我们需要做一点处理
- (cat input; cat -)
- 这样能够向input继续输入
- 成功getshell