【Pwn 笔记】Glibc 利用中那些偏门的技巧

记录 Pwn 题中的各种骚操作

泄露程序基址

修改 _dl_rtld_libname

通过调试可以看出这是一个结构体,存在于 ld.so 文件里

1
2
3
4
5
6
7
8
9
10
pwndbg> p _dl_rtld_libname
$10 = {
name = 0x55e4fceaf238 "/lib64/ld-linux-x86-64.so.2",
next = 0x7fdc9baf4fe0 <newname>,
dont_free = 0
}
pwndbg> x/6gx &_dl_rtld_libname
0x7fdc9baf5030 <_dl_rtld_libname>: 0x000055e4fceaf238 0x00007fdc9baf4fe0
0x7fdc9baf5040 <_dl_rtld_libname+16>: 0x0000000000000000 0x0000000000000000
0x7fdc9baf5050 <max_dirnamelen>: 0x000000000000001a 0x0000000000000000

读取该地址上面的内容可以泄露出程序基地址,该地址为主程序加载 ld 文件时存储 ld 文件名的地址

_dl_rtld_libname和 libc 基地址的偏移是不固定的,不同的 ld 文件有不同的偏移,需要手测或者爆破

程序基地址为0x55e4fceaf238 - 0x238 == 0x55e4fceaf000

这个地址的偏移一般离程序基地址来说始终是 0x238

通过 stdin stdout stderr

这里我只测了 stdin,就拿它来说吧

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> stack 50
00:0000│ rsp 0x7fbe027a4fb0 → 0x55f001f75020 → 0x7fbe027a5a00 (_IO_2_1_stdin_) ← 0xfbad208b
01:0008│ 0x7fbe027a4fb8 → 0x7fbe027a5408 (__check_rhosts_file) ← 0x1
02:0010│ 0x7fbe027a4fc0 → 0x7fbe027a5340 (opterr) ← 0x100000001
03:0018│ 0x7fbe027a4fc8 → 0x7fbe027a56e0 (__ctype32_toupper) → 0x7fbe025583c0 (_nl_C_LC_CTYPE_toupper+512) ← add byte ptr [rax], al
04:0020│ 0x7fbe027a4fd0 → 0x7fbe027a5c28 (__realloc_hook) ← 0x0
05:0028│ 0x7fbe027a4fd8 → 0x7fbe02c19740 (_dl_argv) → 0x7ffdf6dc47a8 → 0x7ffdf6dc628c ← 0x4c00706172632f2e /* './crap' */
06:0030│ 0x7fbe027a4fe0 → 0x7fbe027aaa20 (rpc_createerr) ← 0x0
07:0038│ 0x7fbe027a4fe8 ← 0xffffffffffffff78
08:0040│ 0x7fbe027a4ff0 ← 0x0
... ↓
14a:0a50│ rbx r14 0x7fbe027a5a00 (_IO_2_1_stdin_) ← 0xfbad208b

可以看到 0x7f8a3b926fb0 地址处存着 bss 端上对应的 stdin 的地址

这个地址的权限是可读,用 vmmap 可以查到:

1
2
3
0x7fbe023ba000     0x7fbe025a1000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
0x7fbe025a1000 0x7fbe027a1000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
0x7fbe027a1000 0x7fbe027a5000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so

_IO_2_1_stdin_的地址里这个存着 stdin 地址的地址的距离大约是 0xa50

可以从 0xa00 开始爆破,多尝试几次应该就能出来

stdout stderr 同理

执行任意地址

若程序有类似这样的功能:

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int buf[10]; // [rsp+20h] [rbp-30h]
unsigned __int64 v5; // [rsp+48h] [rbp-8h]

v5 = __readfsqword(0x28u);
write(1u, "addr:", 5uLL);
read(0, buf, 0xC8uLL);
write(1u, "data:", 5uLL);
read(0, buf[0], 0x18uLL);
return 0;
}

修改 .fini_array

可以修改.fini_array的值为任意地址,不过.fini_array只能用一次

修改 __libc_atexit

调用方法:

1
elf.sym['__elf_set___libc_atexit_element__IO_cleanup__']

程序是静态编译的时候,可以修改__libc_atexit来实现任意地址跳转

这个可以无限次使用,在 ida 里也能找到它

利用 _dl_fini 执行函数

具体的利用在 exit 函数的_dl_fini函数里:

1
2
3
4
5
6
0x7f22d9fdca02 <_dl_fini+98>     lea    rdi, [rip + 0x217f5f] <0x7f22da1f4968>
0x7f22d9fdca09 <_dl_fini+105> call qword ptr [rip + 0x218551] <0x7f22d9c2a440>
rdi: 0x7f22da1f4968 (_rtld_global+2312) ← 0x68732f6e69622f /* '/bin/sh' */
rsi: 0x0
rdx: 0x7f22d9fdc9a0 (_dl_fini) ← push rbp
rcx: 0x1

call qword ptr [rip + 0x218551]_rtld_global上面的地址,rdi 也是用的_rtld_global上面的地址

不同的 ld 对应的_rtld_global不一样,需要手调爆破

修改 _stack_chk_fail 内部函数的 .got.plt 表

__libc_message 函数

该函数内部函数按调用顺序排列如下:

1
2
3
strchrnul 函数
mempcpy 函数
strlen_ifunc 函数

got 表内有可改函数就可以改为 main 函数的地址来进行无限循环

制造栈迁移

修改 .fini_array 为 leave ; ret

这个方法可以使程序跳转到可写地址,达到栈迁移的作用

具体实现如下:

1
2
3
4
5
6
7
8
addr_fini_array + 0x18 --> addr_fini_array + 0x18
...
在 addr_fini_array + 0x20 及之后填充完整的 rop 链
...
addr_fini_array - 0x10 --> addr_fini_array + 0x10
addr_fini_array - 0x08 --> addr_fini_array + 0x18
addr_fini_array - 0x00 --> rop_leave_ret
返回地址 --> addr___libc_start_main_1072[不定,这就是正常程序在主函数 ret 结束时所到达的 __libc_start_main 地址]

之后栈就会被转移到addr_fini_array + 0x20

利用 setcontext

这个函数载 libc 里,函数内部汇编利用点如下:

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
0000000000052070 <setcontext@@GLIBC_2.2.5>:
52070: 57 push rdi
52071: 48 8d b7 28 01 00 00 lea rsi,[rdi+0x128]
52078: 31 d2 xor edx,edx
5207a: bf 02 00 00 00 mov edi,0x2
5207f: 41 ba 08 00 00 00 mov r10d,0x8
52085: b8 0e 00 00 00 mov eax,0xe
5208a: 0f 05 syscall
5208c: 5f pop rdi
5208d: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001
52093: 73 5b jae 520f0 <setcontext@@GLIBC_2.2.5+0x80>
52095: 48 8b 8f e0 00 00 00 mov rcx,QWORD PTR [rdi+0xe0]
5209c: d9 21 fldenv [rcx]
5209e: 0f ae 97 c0 01 00 00 ldmxcsr DWORD PTR [rdi+0x1c0]
520a5: 48 8b a7 a0 00 00 00 mov rsp,QWORD PTR [rdi+0xa0]
520ac: 48 8b 9f 80 00 00 00 mov rbx,QWORD PTR [rdi+0x80]
520b3: 48 8b 6f 78 mov rbp,QWORD PTR [rdi+0x78]
520b7: 4c 8b 67 48 mov r12,QWORD PTR [rdi+0x48]
520bb: 4c 8b 6f 50 mov r13,QWORD PTR [rdi+0x50]
520bf: 4c 8b 77 58 mov r14,QWORD PTR [rdi+0x58]
520c3: 4c 8b 7f 60 mov r15,QWORD PTR [rdi+0x60]
520c7: 48 8b 8f a8 00 00 00 mov rcx,QWORD PTR [rdi+0xa8]
520ce: 51 push rcx
520cf: 48 8b 77 70 mov rsi,QWORD PTR [rdi+0x70]
520d3: 48 8b 97 88 00 00 00 mov rdx,QWORD PTR [rdi+0x88]
520da: 48 8b 8f 98 00 00 00 mov rcx,QWORD PTR [rdi+0x98]
520e1: 4c 8b 47 28 mov r8,QWORD PTR [rdi+0x28]
520e5: 4c 8b 4f 30 mov r9,QWORD PTR [rdi+0x30]
520e9: 48 8b 7f 68 mov rdi,QWORD PTR [rdi+0x68]
520ed: 31 c0 xor eax,eax
520ef: c3 ret
520f0: 48 8b 0d 71 8d 39 00 mov rcx,QWORD PTR [rip+0x398d71] # 3eae68 <h_errlist@@GLIBC_2.2.5+0xdc8>
520f7: f7 d8 neg eax
520f9: 64 89 01 mov DWORD PTR fs:[rcx],eax
520fc: 48 83 c8 ff or rax,0xffffffffffffffff
52100: c3 ret
52101: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
52108: 00 00 00
5210b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]

该样例来自于libc6_2.27-3ubuntu1_amd64.so,可以看到在setcontext+0x35处可以修改 rsp 的值

底下也可以修改基本所有可以用到的寄存器,不过这个传参方式是 rdi,不同的 libc 库的传参寄存器可能不同

下面的例子来自于libc6_2.31-0ubuntu6_amd64.so,传参方式是 rdx

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
0000000000045b70 <setcontext>:
45b70: 57 push rdi
45b71: 48 8d b7 28 01 00 00 lea rsi,[rdi+0x128]
45b78: 31 d2 xor edx,edx
45b7a: bf 02 00 00 00 mov edi,0x2
45b7f: 41 ba 08 00 00 00 mov r10d,0x8
45b85: b8 0e 00 00 00 mov eax,0xe
45b8a: 0f 05 syscall
45b8c: 5a pop rdx
45b8d: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001
45b93: 73 5b jae 45bf0 <setcontext+0x80>
45b95: 48 8b 8a e0 00 00 00 mov rcx,QWORD PTR [rdx+0xe0]
45b9c: d9 21 fldenv [rcx]
45b9e: 0f ae 92 c0 01 00 00 ldmxcsr DWORD PTR [rdx+0x1c0]
45ba5: 48 8b a2 a0 00 00 00 mov rsp,QWORD PTR [rdx+0xa0]
45bac: 48 8b 9a 80 00 00 00 mov rbx,QWORD PTR [rdx+0x80]
45bb3: 48 8b 6a 78 mov rbp,QWORD PTR [rdx+0x78]
45bb7: 4c 8b 62 48 mov r12,QWORD PTR [rdx+0x48]
45bbb: 4c 8b 6a 50 mov r13,QWORD PTR [rdx+0x50]
45bbf: 4c 8b 72 58 mov r14,QWORD PTR [rdx+0x58]
45bc3: 4c 8b 7a 60 mov r15,QWORD PTR [rdx+0x60]
45bc7: 48 8b 8a a8 00 00 00 mov rcx,QWORD PTR [rdx+0xa8]
45bce: 51 push rcx
45bcf: 48 8b 72 70 mov rsi,QWORD PTR [rdx+0x70]
45bd3: 48 8b 7a 68 mov rdi,QWORD PTR [rdx+0x68]
45bd7: 48 8b 8a 98 00 00 00 mov rcx,QWORD PTR [rdx+0x98]
45bde: 4c 8b 42 28 mov r8,QWORD PTR [rdx+0x28]
45be2: 4c 8b 4a 30 mov r9,QWORD PTR [rdx+0x30]
45be6: 48 8b 92 88 00 00 00 mov rdx,QWORD PTR [rdx+0x88]
45bed: 31 c0 xor eax,eax
45bef: c3 ret
45bf0: 48 8b 0d 79 f2 36 00 mov rcx,QWORD PTR [rip+0x36f279] # 3b4e70 <.got+0x110>
45bf7: f7 d8 neg eax
45bf9: 64 89 01 mov DWORD PTR fs:[rcx],eax
45bfc: 48 83 c8 ff or rax,0xffffffffffffffff
45c00: c3 ret
45c01: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0]
45c08: 00 00 00
45c0b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]

主要利用的地址如下:

1
2
3
4
45ba5:	48 8b a2 a0 00 00 00 	mov    rsp,QWORD PTR [rdx+0xa0]
45bc7: 48 8b 8a a8 00 00 00 mov rcx,QWORD PTR [rdx+0xa8]
45bce: 51 push rcx
45c00: c3 ret

利用方法,以上面libc6-amd64_2.31-0ubuntu6_i386.so为例:

按如下方式构造即可完成栈迁移:

1
2
3
<地址 A:      可读写>    <地址 B:     目标地址>
QWORD PTR [rdx+0xa0] <地址 A: 可读写>
QWORD PTR [rdx+0xa8] <pop rsp ; ret 的地址>

这样程序最终就会跳到地址 B执行代码

文章目录
  1. 1. 泄露程序基址
    1. 1.1. 修改 _dl_rtld_libname
    2. 1.2. 通过 stdin stdout stderr
  2. 2. 执行任意地址
    1. 2.1. 修改 .fini_array
    2. 2.2. 修改 __libc_atexit
    3. 2.3. 利用 _dl_fini 执行函数
    4. 2.4. 修改 _stack_chk_fail 内部函数的 .got.plt 表
      1. 2.4.1. __libc_message 函数
  3. 3. 制造栈迁移
    1. 3.1. 修改 .fini_array 为 leave ; ret
    2. 3.2. 利用 setcontext
|