题目信息

题目链接

https://github.com/ctfs/write-ups-2014/tree/master/codegate-preliminary-2014/weirdsnus

准备环境

1
2
$ echo HEHE_I_DONT_KNOW_YOU > admin_passwd 
$ echo N0_MoR3_SMOKING_SNUS > flag

基本信息

静态分析

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
2
3
4
struct struc_1 {
char string[12]
void* func_ptr # 想要重写这个
}

首先构造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的地址

img
img

可以看到测试能够getshell

实际运行

img
img
img
img
  • 发生了SIGSEGV
  • 实际上成功了
  • system(“/bin/sh”),command从标准输入读取
  • 但是,标准输入绑定input,因此无法读取到
  • 因此,不执行任何操作,/bin/sh结束
  • 之后因为stack已经被破坏,发生SIGSEGV
  • 我们需要做一点处理
img
img
  • (cat input; cat -)
  • 这样能够向input继续输入
  • 成功getshell