这个组,是朋友赛后让我看的这个题,个人并没有参赛,当时也没给做出来,后续断断续续的的调试,工作,好久好久才做出来,贼鸡儿菜,打吉尔比赛。
IDA分析,发现没有符号,有点函数跟的很深具体也不知到什么作用,这是结合动态调试,看看函数的输入输出,可以大概知道函数做了什么,并把函数根据功能进行重命名
这里再正确flag的上面有一个函数,显然即使check,传入的就是str->大写hex的值
下面进入check分析
分析check时,这里还是通过动态调试,确定了几个函数
结果表的值:
[0x0c5d83690,0x1f7902cc,0x978162ea,0x2fae3d15,0x1932a96c,0xceebfe91,0x4222669,0xaff6af42]
一共是8*4个字节
下面把chek中的基本的函数就分析大概流程分析完了,下面总结一下check的基本流程:
check的流程分析结束,下面就要还原算法了,
根据结果表,提出来md5正确的输入
[0x0c5d83690,0x978162ea,0x1932a96c,0x4222669]
MD5如何保证固定?难道要撞嘛?
{0x1F7902CC,0x2FAE3D15,0x0CEEBFE91,0x0AFF6AF42}
先分析vm的流程,首先要知道传入vm中的表的结构,这里只能动态调试,把之前的反调试patch掉,开始调试,再array的地方下断点,观察array的内存变化
array的结构如下
opcode表如下,并不会根据输入的不同而不同,基本上固定的操作路径,vm计算产生的中间值其实就是放到array中,和array+8的指针中的
最后9ea4eff588保存的内容如下,第一个是用户输入,第二个给是用户输入产生的MD5,第三个是用户输入得到的100byte的md5
知道了传入vm的array的结构,下面分析vm的流程,人肉调试还原
总体的结构是这样的,吐了。。。。。
根据下面的分析,每次寻址是*(array + (array + 20)),可以知道*array就是opcode,*(array+20)则是偏移量,由此可知,opcode的寻址方式,可以发现每个case中让array移动都是操作*(array+20)
我的方法是,再每个case的偏移加上注释,动态调试一边关注array的变化,结合静态分析推断处每个case的作用,再在x64debug的对应位置做好标注,进行跟踪就很容易看出来规律了。
下面给一些重要的流程函数标记出来,然后跟踪找规律
下面重点的按照处理流程顺序还原几个重要的case
unsigned int r2l(unsigned int *input,unsigned int i){//i = 7~0 共执行了八轮 。整体3个函数共执行了2论 unsigned int tmp; unsigned int tmp2; tmp = *input >> i; i=0x20-i; tmp2 = *input << i; return tmp | tmp2 }
unsigned int l2r(unsigned int *input,unsigned int i){ //i=6 unsigned int tmp; unsigned int tmp2; tmp = *input << i; i=0x20-i; tmp2 = *input >> i; return tmp | tmp2; }
4.下面再次一直运行可以看到其他的case并没有操作存在与array中的结果,而都是操作的操作表的索引,或者其他的,一直调式运行分析发现法律,一直在调用上面的3个case,并更新着结果值,分析还原整体的流程,小流程做了8轮(0~7)大流程做了两轮
result = input for(i=0;i<2;i++) { for(j=7;j>=0;j--) { result = r2l(result,j); result = xor(result); result = l2r(result,6); } }
继续往下跟
这里跟了很多次,因为每一次位运算产生的结果会参与到不同流程的计算,这里要十分仔细,我当时就是拿着纸笔一条一条的记录数据。
最终的操作如下:
获取步骤4产生的值(记作re)
~((re | md5) ^ (md5_table & md5) ^ (re & md5_table) ^ (re & md5_table & md5) ^ (re | md5_table | md5))
根据标注,其实追踪也能发现规律。
进入sub_140075159,首先初始化一个array结构,再将array传入vm函数进行vm流程的计算
还原vm流程,先进行r2l,xor,l2r共2*8==16轮
还原vm流程,获取r2l,xor,l2r共2*8==16轮的结果(记作re),j进行下面的计算
这里经过大佬的指点可以这样化简,原来想的是爆破
(re | md5) ^ (md5_table & md5) ^ (re & md5_table) ^ (re & md5_table & md5) ^ (re | md5_table | md5)
re == A
md5 == B
md5_table == C
(A|B) ^ (C&B) ^ (A&C) ^ (A & C & B) ^ (A | C | B) == A ^ B ^ C
最终产生的就是vm的运算结果
(input_md5,vm,input_md5+4,vm+4,....,input_md5+12,vm+12)
vm的结果因该是:
{0x1F7902CC,0x2FAE3D15,0x0CEEBFE91,0x0AFF6AF42}
input_md5的结果因该是
{0x0c5d83690,0x978162ea,0x1932a96c,0x4222669}
已经有了md5的正确结果,以及input的正确结果,但是md5_table还不知道
下面分析一下md5_table的产生:
table的产生gettable
100比特的表进入md5 就获取了md5_table,由于table有有限个可能,那么md5_table也就有有限个可能
那么就可以编码求出100个md5_table然后参与解密运算中
接下来用枚举出来的md5_table和vm正确的结果,md5的正确结果返推出input。但是这会产生多个input,因为不一定满足md5,这是再次比较md5筛选出真正的input
#include <windows.h> #include <stdio.h> byte table[100] = { 0 }; BOOL getMd5(BYTE *pbData, DWORD dwDataLen, BYTE *md5, DWORD &err) { HCRYPTPROV hProv; if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { err = GetLastError(); return FALSE; } HCRYPTHASH hHash; if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) { err = GetLastError(); CryptReleaseContext(hProv, 0); return FALSE; } if (!CryptHashData(hHash, pbData, dwDataLen, 0)) { err = GetLastError(); CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return FALSE; } DWORD dwSize; DWORD dwLen = sizeof(dwSize); CryptGetHashParam(hHash, HP_HASHSIZE, (BYTE*)(&dwSize), &dwLen, 0); BYTE* pHash = new BYTE[dwSize]; dwLen = dwSize; CryptGetHashParam(hHash, HP_HASHVAL, pHash, &dwLen, 0); for (DWORD i = 0; i < dwLen; i++) { md5[i] = pHash[i]; } delete[] pHash; CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return TRUE; } void getTable(byte mod) { byte v8 = mod; byte v10; for (int k = 0; k < 100; k++) { v10 = 0; v8 ^= 0xc3; *(byte*)(table + k) = v8; for (int l = 0; l < 8; l++) { v10 ^= (v8 >> l) & 1; } v8 = v10 | (2 * v8); } } int main() { unsigned int vm[] = { 0x1F7902CC ,0x2FAE3D15 ,0xCEEBFE91 ,0xAFF6AF42 }; unsigned int md5[] = { 0xC5D83690 ,0x978162EA ,0x1932A96C ,0x4222669 }; BYTE md5_byte[20] = { 0x90,0x36,0xD8,0xC5,0xEA,0x62,0x81,0x97,0x6C,0xA9,0x32,0x19,0x69,0x26,0x22,0x04 }; byte md5_table[16]; unsigned int input; byte input_src[16] = { 0 }; DWORD err; for (int mod = 0; mod < 100; mod++) {//枚举模数 getTable(mod); getMd5(table, 100,md5_table, err); for (int p = 0; p < 4; p++) { input = ~vm[p]; input = input ^ md5[p] ^ *((int*)md5_table+p); for (int k = 0; k < 2; k++) { for (int l = 0; l < 8; l++) { input = ((input >> (6)) | (input << (0x20 - 6))); input = input ^ 0x9E3779B9; input = ((input << (l)) | (input >> (0x20 - l))); } } //小端序 input_src[p * 4 + 3] = (byte)(input >> 24) & 0xff; input_src[p * 4 + 2] = (byte)(input >> 16) & 0xff; input_src[p * 4 + 1] = (byte)(input >> 8) & 0xff; input_src[p * 4 + 0] = (byte)(input >> 0) & 0xff; } byte md5_2[16]; bool flag = true; getMd5(input_src, 16, md5_2, err); for (int i = 0; i < 16; i++) { if (md5_2[i] != md5_byte[i]) { flag = false; break; } if (flag) { printf("flag{%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x}\n", input_src[0], input_src[1], input_src[2], input_src[3], input_src[4], input_src[5], input_src[6], input_src[7], input_src[8], input_src[9], input_src[10], input_src[11], input_src[12], input_src[13], input_src[14], input_src[15]); } } } return 0; }
[推荐]看雪工具下载站,全新登场!(Android、Web、漏洞分析还未更新)
最后于 1天前 被Craft_A编辑 ,原因: