Previously
大概是去年年底,想着到春节应该还有一段比赛真空期,至少把FlareOn3的逆全刷一遍吧,结果一看除了签到题全是活
Challenge1
刚进die乱翻就看到了一个形似base64换表的东西——
那我们直接把另一边看起来很像数据的东西放进赛博厨子试试——
啊?
不过这种暴力猜测还是不可取,我们正儿八经地看看算法——
位运算,三份对四份,这特征b64没跑了,问题就出在密码表上。
很符合我对签到题的想象.jpg
DukeLocker
上来给了咱一个内容物意义不明的doc文档,怀疑是什么被加密的文件,或者是RE里面塞misc啥的。但隔壁还有个exe,既然都逆向题了那肯定先看可执行文件——
虽然它运行时似乎会产生中间文件,但是不动调单步的话程序一下子就过去了,我们干脆拿x64dbg直接一步到胃——
但是无事发生。那就考虑换个思路,既然它不是自己产文件,那就直接创一个Briefcase文件夹,然后把doc放进去,为了防止扫的是绝对路径,干脆直接在桌面上也放一个——
还是无事发生。
这时候应该就是我们的理解问题了。我们再来看一遍程序——
byd我知道火绒为什么老是把你关小黑屋了,扫我硬件是吧?我直接进函数先把你硬件码掀了(比较在取硬件码之后,汇编也很好找,动调改完进1A94地址就是成了)——
然后快进到异或之后,函数会输出一组数据,想来就是之前那一串加密数据的明文——
接着步过到生成图片的位置,如果你亲手动调步过你会发现这玩意是存在你桌面上的Briefcase,那第一次取地址应该就是取的桌面的地址。不过这些都不重要,关键是这个图片写了什么——
虽然那个莫名其妙的doc是被加密的结果是真,然而RSA-4096就是纯纯误导,正儿八经的加密逻辑还得看回程序,然而,这也是最折磨的部分,因为你还得疯狂翻阅Windows相关加密函数的文档一个个对:
首先是sub_401180对传入的lpMem进行加密,经查表得为SHA1,结果存入hProv,顺带生成一个AES256的hKey——
然后加密结果在第二次扫文件的时候经过两轮加密,第一轮是sub_401770的MD5——
第二轮就显得比较迷惑,前面生成的又是AES的key,到这里表现出来的特征又像DES的分块加密,每次最多取到0x4000,再加上还有KP_BLOCKLEN的参数,这玩意又突然谜起来了。
虽然后来又突然发现前面MD5的时候其实把它的KP_MODE定义为了CRYPT_MODE_CBC(1):
if ( CryptGetHashParam(phHash, 2u, (BYTE *)*a3, &v10, 0) )
{
if ( CryptSetKeyParam(*a1, 1u, *a3, 0) )
{
CryptDestroyHash(phHash);
return 1;
}
但不管这个key最后是用在AES还是DES上,不明了的地方就一个,大不了挨个试一遍。于是,整个解密逻辑大致就有了,还是跟之前一样用明文sha1完MD5当后续的key,然后解密AES和DES二选一(最后实际上确实是DES),代码参考Papaya的博客,对着逆出来的代码就是一顿照猫画虎——
#include <stdio.h>
#include <windef.h>
#include <windows.h>
#include <wincrypt.h>
int main()
{
char* filename = "businesspapers.doc";
char* str = "thosefilesreallytiedthefoldertogether";
BYTE md5_data[100] = {0};
DWORD md5_len = 16;
DWORD dwDataLen = 37;
BYTE pbData[4];
*(DWORD *)pbData = 1;
HCRYPTKEY phKey = 0;
HCRYPTPROV phProv = 0;
HCRYPTHASH phHash = 0;
DWORD NumberOfBytesRead=0;
HANDLE hFile = 0;
HANDLE hObject = 0;
BYTE lpBuffer[0x5000]={0};
int v18 = 0;
if (!CryptAcquireContextW(&phProv, 0, 0, 0x18u, 0)){
printf("ERR_ON_SHA1\n");
return 0;
}
// sub_401180
CryptCreateHash(phProv, 0x8004u, 0, 0, &phHash);
CryptHashData(phHash, (BYTE*)str, dwDataLen, 0);
CryptDeriveKey(phProv, 0x6610u, phHash, 1u, &phKey);
CryptDestroyHash(phHash);
// sub_401080
CryptSetKeyParam(phKey, 4u, pbData, 0);
// sub_401770
phHash = 0;
CryptCreateHash(phProv, 0x8003u, 0, 0, &phHash);
CryptHashData(phHash, (BYTE*)filename, strlen(filename), 0);
CryptGetHashParam(phHash, 2u, md5_data, &md5_len, 0);
if(!CryptSetKeyParam(phKey, 1u, md5_data, 0)){
printf("ERR_ON_MD5\n");
return 0;
}
CryptDestroyHash(phHash);
// sub_401500
hFile = CreateFile("Briefcase\\BusinessPapers.doc", 0x80000000, 3u, 0, 3u, 0x80u, 0);
if(hFile == (HANDLE)-1){
printf("ERR_ON_SCANNING_DOC\n");
return 0;
}
hObject = CreateFile("Briefcase\\output.txt", 0x40000004u, 3u, 0, 3u, 0x80u, 0);
if(hObject == (HANDLE)-1){
printf("ERR_ON_CONDUCTING_OUTPUT\n");
return 0;
}
while (ReadFile(hFile, lpBuffer,0x4000, &NumberOfBytesRead, 0)){
if ( NumberOfBytesRead < 0x4000) { v18 = 1;}
CryptDecrypt(phKey, 0, v18, 0, (BYTE *)lpBuffer, &NumberOfBytesRead);
//CryptEncrypt(phKey, 0, v18, 0, (BYTE *)lpBuffer, &NumberOfBytesRead, 0x4010);
WriteFile(hObject, lpBuffer, NumberOfBytesRead, &NumberOfBytesRead, 0);
if ( v18 )
break;
}
return 0;
}
最后随便拿个什么十六进制查看器一看,是个图片,改个后缀——
好家伙,拿个逆向的flag怎么跟做密码一样。
hashes
这玩意咋看咋像go:
但是无所谓,IDAGolangHelper会出手……
找不到模块数据?啥情况?kali打开看了才发现是golang7,太过古老。算了直接硬看汇编吧,这里还有一个小寄巧,库函数不多的时候可以通过查看出题人到底调用了什么,来推测出题人的意图。比方说,我们发现出题人调用了crypto_sha1_New
和go_string_slice
,跟过去看一眼:
sub_8049CFB
调用了crypto_sha1_New
,并且循环了三遍,想来是要对参数进行三次sha1了(话说回来怎么这B出题人这么喜欢sha1):
mov dword ptr [ebp-0Ch], 0
cmp dword ptr [ebp-0Ch], 2
jle loc_8049D5E
……
call _crypto_sha1_New
……
add dword ptr [ebp-0Ch], 1
而这玩意唯一的调用点就是主函数,我们可以发现string_slice
刚好就在前面:
与此同时,随便乱翻字符串的时候还发现了更前面调用的sub_8049F6D
有一个诡异的字符集,对应着一个strings_ContainsAny
,应该是用来检查密文或者原文的的字符是否在字符集里面。
mov ebx, offset aAbcdefghijklmn ;
mov esi, 22h ; '"'
……
push esi
push ebx
call _strings_ContainsAny
……
xor eax, 1
test al, al
jz short loc_8049FFD
并且前面在___go_int_to_string
调用时调用的参数有[ebp-28h],这玩意在main里面也出现过,那就权当是输入数据的长度为30吧——
sub_8049F6D:
lea eax, [ebp-28h]
sub esp, 8
push edi
push eax
call ___go_int_to_string
main_main:
cmp dword ptr [ebp-28h], 1Eh
setz al
test al, al
jz short loc_804A10D
在调用sub_8049F6D
之后,切分字符串之前,传入的ecx还对这个长度除以过一次,之后作为切分的参数输了进去。
mov ecx, 6
cmp ecx, 0FFFFFFFFh
jnz short loc_804A1EE
……
mov eax, [ebp-28h]
cdq
idiv ecx
……
push ecx
所以合理推测,这玩意实际上是把一个长30的串拿去等分,每段长度为6(golang slices一般没有数量参数来着),然后拿去连着三次sha1,最后把得到的哈希值和程序里头的数据比对。
可问题又来了,我数据呢?
一种可能的思路是,你既然都打FlareOn了,它的flag格式肯定是邮箱,尾巴肯定是@flare-on.com,最后两个slice也解决了,数据也能找了,多好。可问题就在这里,静态环境下根本没法从IDA里面找到这些数据,那这些数据八成还要过一遍解密或者生成才能拿到手。
另一种办法肯定就是动调到指定位置拿到对应数据了。但是鉴于之前几道题目都动调的我头快飞了,故拿数据的方式肯定没这么粗暴。再加上golang7,这么古老的玩意,现在linux下载源都找不到库了,我也配不上来啊QAQ
还有一种方式是生啃汇编,当然效率肯定高不到哪去,因为解密逻辑在这里显得及其的迷幻:
sub_804A53B:
push ebp
……
push ecx
push 1CDh
push 1000h
push edx
push eax
call sub_8049EE7
sub_8049EE7:
mov eax, [ebp-10h]
cmp eax, [ebp+18h]
jl short loc_8049F10
loc_8049F10:
mov edx, [ebp-0Ch]
mov eax, [ebp+14h]
add eax, edx
cmp dword ptr [ebp+10h], 0FFFFFFFFh
jnz short loc_8049F26
loc_8049F26:
cmp dword ptr [ebp+10h], 0
jnz short loc_8049F3B
loc_8049F3B:
cdq
idiv dword ptr [ebp+10h]
mov ebx, edx
mov eax, ebx
mov [ebp-0Ch], eax
mov eax, [ebp-0Ch]
cdq
push edx
push eax
push dword ptr [ebp+8]
push offset unk_804CE40
call ___go_send_small
add esp, 10h
add dword ptr [ebp-10h], 1
byd,我在做这到这里的时候刚好在听脑浆炸裂少女,结果我自己脑子真的快炸了(笑
不过我突然想到一件事情,刚才入栈的第四个元素,也就是ebp-0xC
,就是刚才push进来的0x1CD。这玩意在idiv取余之后每次还要加回来,再加上其他传进去的参数也可能是数据集的地址,以及push了一个意义不明的0x1000进来,这玩意可能就是在一个大小为0x1000的数据集里面找一个下标,给他驳到某个东西上组成哈希值。
哪里可能存在一个长度刚好为0x1000的数据集呢?在乱翻了一阵常量之后,我找到了unk_804BB80
,这玩意完美满足条件。
算法上如果要胡诌一通也好说,那就可能是以一定基础下标为准,每次取出一个数然后加上0x1CD,然后越界了mod一个0x1000,直到组成一个sha1的哈希值,然后接着弄下一个哈希值。
只是没有动态调试的环境,我暂时没法亲自验证它的正确性(而且按理来说如果动调顺利,那么几个未知变量的问题也解决了)。而且,这么猜想的话,初始下标也是个问题,毕竟都爆破了,6个字符,就算情况固定,也得要O(m^6)的复杂度,大概有15亿种情况,你再多爆一个1000的变量,那电脑得先跟你爆了。
不对,好像不需要动态调试,也还有办法。记得刚才提到过尾巴是”@flare-on.com”,那我们只需要用上面一样的办法算出sha1,然后再用sha1值从尾到头逐字比对,寻找所有可能值然后记下偏移,就可以找出终位置和变化规律,然后从尾到头生成哈希值。
Mindaugas佬的题解至少从代码上看是这么干的,下面是我根据他的题解改的,py3以上也能用的爆破代码(毕竟题目还是py2时代的文物)——
import binascii
import hashlib
def trip_sha1(s):
return hashlib.sha1(hashlib.sha1((hashlib.sha1(s).digest())).digest()).hexdigest()
const = 0x1cd
current = 0x450 # 这个下标估计得人工确定
blob = [你提出来的0x1000大小的数据集]
hashes = [[], [], [], [], []]
for h in reversed(range(5)): # 从最后一个hash值及其终位置反过来推前面的
for b in range(20):
byte = blob[current]
current -= 0x1cd
if current < 0:
current += 0x1000
hashes[h] = [byte] + hashes[h] # 包括新加入的数据也是从前面插入
h_str = [binascii.hexlify(b''.join([chr(b).encode() for b in h])) for h in hashes]
alphabet = 'abcdefghijklmnopqrstuvwxyz@-._1234'
for c1 in alphabet: # 剩下的就是硬爆所有可能的六位字符,去找哈希值存在的那几个
for c2 in alphabet:
for c3 in alphabet:
for c4 in alphabet:
for c5 in alphabet:
for c6 in alphabet:
six = ''.join([c1, c2, c3, c4, c5, c6])
if trip_sha1(six.encode()).encode() in h_str:
print(six, trip_sha1(six))
(佬在题解里表示用这玩意他也爆了整整20min,何其恐怖的计算量,反正我跑了好一会没跑通,各位谁有超算的可以整一整,我是不想再折磨我的电脑了)
我就纳闷了,flareon你们怎么都喜欢玩这么抽象的底力题
Unknown
一上来,pe32,想着应该不会有啥花活吧,但看FlareOn那尿性还真说不准。这一看果不其然,符号表里面的信息显示这道题表面的程序名和实际运行时符号表里的程序名好像是两个东西——
从die里看内含字符串还能看到debug信息,看来这玩意真的只是个被重命名的,从原程序extraspecial
的pdb里扒出来的内存映射之类的东西吧。
但是这玩意有什么用呢?我们先分析一下程序:
也就是说,这鬼玩意的本体实际上是个哈希比对。我们理论上直接把这个简单哈希搬过来用就完事了:
import numpy
flare = 'FLARE On!'
between_c = ord('_')
hexlist = []
def cal(Str):
Sum = numpy.uint32(0)
for i in Str:
Sum = numpy.uint32(ord(i) + 37 * Sum)
return Sum
flag = ''
for i in range(26):
between_c += 1
for j in range(ord(' '), 127):
Str = chr(j) + chr(between_c) + flare
Sum = cal(Str)
if Sum == hexlist[i]:
flag += chr(j)
break
print(flag)
但是用上述代码中原装的数据是解不出东西的。那真正的数据呢?
我们回到刚才的代码,发现中间那段未知代码实际上有点像把v23的处理结果跟我们的数据串掉了个。而且,v23就是代码的十六进制中RSDS最后出现位置的后缀(因为没有break中间夹断),于是我们拉进Winhex看一眼:
好消息,我们有pdb文件的原始地址了。坏消息,我数据呢?
结果外网的题解简直是一个模子里刻出来的,都说地址有了,数据就有了。可是这道题就没给出啥额外信息,直到我乱翻github时看到了:
对啊,好像可以借由GUID去找对应PDB,然后再从对应PDB里面找我们要的数据。但是很遗憾,寻找了数个解决方案之后,它们在我的电脑环境下都行不通。
直到我在数次动调之后突然发现,这玩意好像没有正确的文件名是过不去start的检测的。那么,之前拿到的文件名应该能用得上了,重命名成extraspecial.exe试试——
哟西,能跑通,但是就是卡在了某个判定。那我们直接在x64dbg上大跨步动调到0x4027a0,也就是主函数的地址上(别问为什么不用ida,问就是寄,以及,很多地方需要动调的时候手动过,因为有反patch),接着看哪里是那个意义不明的形似加密和换表的东西,一遍给它过过去——
然而悲伤的事情发生了,这玩意似乎加了亿些异常分发和反调试,导致你反调试不是一个萝卜一个坑,而是你一个萝卜拔出来,地下还埋着几个土豆。然后从网上下了SharpOD,结果SharpOD也不好使,碰到反patch进程直接炸了。甚至我现在想截个图都得担心哪里会不会突然给我整炸了,哪里出个参数判断哪里出个Dispatch,boom,你进程没了。
出题人我XX你个X