GKCTF 2021 Reverse Writeup
前言
GKCTF 2021所以题目均以开源,下面所说的一切思路可以自行通过源码对比IDA进行验证。
Github项目地址:https://github.com/w4nd3r-0/GKCTF2021
出题及解题思路
QQQQT
Enigma Virtual Box打包的QT程序,可以解包其实也可以不解包,因为这里并没有对字符串做隐藏,按钮事件函数可以很块通过flag字符串定位,按根据钮事件的逻辑也十分好猜,base系列加密特征明显,对base系列算法稍有了解的即可识别。若还是无法识别,建议查看一些base系列算法C实现,自行积累一些识别特征的方法。
最终是用了一个base58进行加密,加密字符串也是写脸上,可以直接百度或谷歌在线网站解密即可。
flag: flag{12t4tww3r5e77}
PS:学弟出的这个题目没有对字符串做隐藏,使得选手不用了解QT任何机制就能解出来。
Crash
可根据字符串信息也可根据段gopclntab判别是golang elf程序。符号表可以在IDA7.5中通过IDAGolanHelper(该插件近一个月有更新,可以支持高版本go符号还原)还原符号。
有了符号信息后静态审代码就非常清晰明了,用了3DES CBC, SHA256, SHA512, MD5,对四部分数据进行验证,而对应的密文也都可以简单的提取出来。
3DES CBC的密钥为一个txt文件,利用golang新版特性附加到来了二进制文件中,可以方便的找到,因此直接解密即可。
hash系列函数都是4字节爆破,用python的itertools可以快速爆破。
from Crypto.Cipher import DES3
import base64
import itertools
import string
import hashlib
def des3_cbc_decrypt(secret_key, secret_value, iv):
unpad = lambda s: s[0:-ord(s[-1])]
res = DES3.new(secret_key.encode("utf-8"), DES3.MODE_CBC, iv)
base64_decrypted = base64.b64decode(secret_value.encode("utf-8"))
encrypt_text = res.decrypt(base64_decrypted)
result = unpad(encrypt_text.decode())
return result
def sha256crash(sha256enc):
code = ''
strlist = itertools.product(string.ascii_letters + string.digits, repeat=4)
for i in strlist:
code = i[0] + i[1] + i[2] + i[3]
encinfo = hashlib.sha256(code.encode()).hexdigest()
if encinfo == sha256enc:
return code
break
def sha512crash(sha256enc):
code = ''
strlist = itertools.product(string.ascii_letters + string.digits, repeat=4)
for i in strlist:
code = i[0] + i[1] + i[2] + i[3]
encinfo = hashlib.sha512(code.encode()).hexdigest()
if encinfo == sha256enc:
return code
break
def md5crash(sha256enc):
code = ''
strlist = itertools.product(string.ascii_letters + string.digits, repeat=4)
for i in strlist:
code = i[0] + i[1] + i[2] + i[3]
encinfo = hashlib.md5(code.encode()).hexdigest()
if encinfo == sha256enc:
return code
break
if __name__ == '__main__':
key = "WelcomeToTheGKCTF2021XXX"
iv = b"1Ssecret"
cipher = "o/aWPjNNxMPZDnJlNp0zK5+NLPC4Tv6kqdJqjkL0XkA="
part1 = des3_cbc_decrypt(key,cipher,iv)
part2 = sha256crash("6e2b55c78937d63490b4b26ab3ac3cb54df4c5ca7d60012c13d2d1234a732b74")
part3 = sha512crash("6500fe72abcab63d87f213d2218b0ee086a1828188439ca485a1a40968fd272865d5ca4d5ef5a651270a52ff952d955c9b757caae1ecce804582ae78f87fa3c9")
part4 = md5crash("ff6e2fd78aca4736037258f0ede4ecf0")
flag = "GKCTF{" + part1 + part2 + part3 + part4 + "}"
# GKCTF{87f645e9-b628-412f-9d7a-e402f20af940}
print (flag)
app-debug
这个题目比赛过程中出来一些情况,这里对造成不便的师傅说一声道歉。
安卓逆向题目,主要验证逻辑在native层,因此直接进入native层即可。对输入进行tea加密验证,delta是0x458BCD42,并且有利用TracerPid的反调试,当发现调试器时,会使用假的key,只要没有检测到调试器才会使用真key。
因为key是一个全局变量可以通过引用找到在哪里替换为了真key,再找到比对的密文后即可进行解密函数的编写。
#include <stdio.h>
#include <stdint.h>
void TeaDecode(uint32_t* v, uint32_t* k) {
uint32_t delta=0x458BCD42;
uint32_t v0=v[0], v1=v[1], sum=delta*32, i;
uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];
for (i=0; i<32; i++) {
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
}
v[0]=v0; v[1]=v1;
}
int main()
{
uint32_t enc[]={4121530355,2719511459, 0};
uint32_t key[] = {9, 7, 8, 6};
TeaDecode(enc, key);
puts(enc); //GKcTFg0
return 0;
}
KillerAid
程序结构分为两部分,一个C# 前端GUI,用于获取ID和Code,并将ID用Code进行循环异或处理,最后比对ID。需要获得ID的前提必须有正确的Code,因此必须先解出Code。
Code的验证逻辑在一个用C++编写的KillerAid.Core.dll中。Core.dll中主要由两部分组成,一部分是用于反调试的检测代码,一部分是基于AES-CBC的一个简单对称加密体系算法。
反调试检测代码主要有三部分,一部分是利用WIN32 API以及一些Windows下比较常规的反调试技巧;另一部分是通过便于隐藏特征的动态CRC32算法对ntdll、Core.dll、exe的代码段进行冗余码校验;最后一部分是,基于xd4d大佬一篇解析Net内核调试机制的C++代码实现方案,用于切断.net 内核调试线程(即杀死调试线程实例)与dnSpy和IDA这类托管调试器的通信。
调试启动的手段通过C++ 语言机制,用于一个全局委托类进行构造,它会在dll被加载时,十分早的调用委托类的构造函数,而所以反调试手段都是通过调用win32 API创建一个反调试线程进行检测。
这期为了实现这些功能所调用的所有Win 32 api以及一些native api都是定义成为函数指针集成到一个代理类中,调用进行动态函数地址获取,可以比较有效隐藏win32 api的调用以及一部分抗静态分析的效果。
由于反调试比较多,虽然可找到一个反调试的主调用函数,进行文件patch但最简单的方法仍然是将反调试线程挂起。但没有做好的一点是动态crc32的调用时机相对于exe的运行时机来说还是太滞后了,后面想考虑加一个隐蔽的使用文件CRC32进行检测,但由于时间原因并没有加上。
至于加密函数的设计,是一个基于AES-CBC模式设计的简单对称加密体系,具体设计如下图所示:
具体的解密部分并没有写多少,但可以参照AES的解密思路,一个道理,这里不多做赘述。
AES算法的初始key和iv向量都是定义为全局变量,并使用rand函数动态获取。由于iv向量是定位全局变量,dll一加载即有数据了,所以这里即便没过反调试,也可通过实际加载的ImageBase计算偏移获得key和iv向量的地址(PE的知识),提取数据即可。
至于解密函数,可在源码中dllmain.cpp中找到,当然这里也贴出来了。
#ifdef _DEBUG
uint8_t* AES_DecryptPro(uint8_t* encData, size_t sizeofData, uint8_t* key, uint8_t* iv, uint32_t rounds)
{
uint8_t* Ivs = nullptr;
uint8_t* Keys = nullptr;
struct AES_ctx ctx;
Ivs = new uint8_t[rounds * AES_BLOCKLEN]();
Keys = new uint8_t[rounds * AES_BLOCKLEN]();
// 迭代出所有 k 与 iv
for (size_t i = 0; i < rounds; i++)
{
memcpy(Ivs + i * AES_BLOCKLEN, iv, AES_BLOCKLEN);
memcpy(Keys + i * AES_BLOCKLEN, key, AES_BLOCKLEN);
// 对 iv 进行 sbox 替代 后 k 用 iv 异或更新
SubBytes((state_t*)iv);
XorWithIv(key, iv);
// 对 k 进行 sbox 替代 后 iv 用 k 异或更新
SubBytes((state_t*)key);
XorWithKey(iv, key);
}
// 解密流
for (size_t i = 1; i <= rounds; i++)
{
key = (uint8_t*)(Keys + (rounds - i) * AES_BLOCKLEN);
iv = (uint8_t*)(Ivs + (rounds - i) * AES_BLOCKLEN);
AES_init_ctx_iv(&ctx, key, iv);
AES_CBC_decrypt_buffer(&ctx, encData, sizeofData);
}
delete[] Ivs;
delete[] Keys;
return encData;
}
#define DecryptPro(encData, sizeofData, key, iv, rounds) AES_DecryptPro(encData, sizeofData, key, iv, rounds)
#else
#define DecryptPro(encData, sizeofData, key, iv, rounds)
#endif // _DEBUG
SoMuchCode
这个题目的混淆思路十分简单,,即再真实逻辑中插入大量的有引用的垃圾代码,用来将真实的逻辑变得更加复杂难看,其实从CFG图中可以看出,并没有任何复杂分支,基本是一条流程走到底,而具体垃圾代码的插入的实现思路是使用编译器预处理的宏展开机制进行的。
程序的原始逻辑十分简单,获取输入,进行xxtea加密,与密文比较。
具体解密函数可参考源码给出的实现。