前言 上个周末也是参加了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}
本来想讲下调试的过程的,但其实想了想调试就是为了方便能看懂代码,这个过程文章写起来又太麻烦,所以就不写了,笔者复现最好多调试便于理解。