查看原文
其他

2020网鼎杯玄武组_babyvm

Craft_A 看雪学院 2021-03-07

本文为看雪论坛精华文章

看雪论坛作者ID:Craft_A



这个组,是朋友赛后让我看的这个题,个人并没有参赛,当时也没给做出来,后续断断续续地调试、工作,好久好久才做出来。





基本流程分析


IDA 分析,发现没有符号,有点函数跟的很深具体也不知道什么作用,这时结合动态调试,看看函数的输入输出,可以大概知道函数做了什么,并把函数根据功能进行重命名。

 

 

这里在正确 flag 的上面有一个函数,显然即使 check,传入的就是 str->大写 hex 的值。

 

 

下面进入check分析:


 

分析check时,这里还是通过动态调试,确定了几个函数。


1. 偏移D410 -->getmd5,传入一个 input,和一个 buffer 最终会计算根据 input 计算处 md5 存入 buffer:


2. 偏移 D290 --> gettable,这个函数还是输入的是 input,和一个 table缓冲区:


3. 偏移 CF00 --> jisuan:


4. 偏移 CD50 --> vm 这里传入就是三个 jisuan 调用而产生的 3 个值:


5. 偏移 CCB0:


结果表的值

[0x0c5d83690,0x1f7902cc,0x978162ea,0x2fae3d15,0x1932a96c,0xceebfe91,0x4222669,0xaff6af42]


一共是8*4个字节。





总结一下


下面把 chek 中的基本的函数就分析大概流程分析完了,下面总结一下 check的基本流程:


1. 根据用户输入的信息计算出 md5,记作 input_md5(后面方便引用)


2. 根据用户输入计算出来一个100 byte 的表,记作 input_table


3. 算出 input_table 的 md5,记作 table_md5(这里后面也没有比较什么的,真是绝了,爆破吧)


4. 进入一个循环,根据开始,和结束条件,可以知道总共循环4次,并且会调用三个jisuan,分别传入的是input、input_md5、input_table。


先把input_md5 的4个字节放入v9,再进入sub_140075159出入计算(记作vm),计算的结果拼接 v9,这么一算得到的 v9 格式就是(input_md5,vm,input_md5+4,vm+4,....,input_md5+12,vm+12)总共 8*4 个字节,刚和和结果的表对应。





算法还原


vm流程算法


check 的流程分析结束,下面就要还原算法了。

1. flag 为去掉格式为 32 个字符长度,最开始将 32 个字符转为 hex 值放进内存,算出 MD5 需要满足下表。

根据结果表,提出来md5正确的输入:
[0x0c5d83690,0x978162ea,0x1932a96c,0x4222669]

MD5 如何保证固定?难道要撞嘛?

2. flag 为去掉格式为 32 个字符长度,不仅要满足上述条件,还要满足 vm 运算结果的匹配。

{0x1F7902CC,0x2FAE3D15,0x0CEEBFE91,0x0AFF6AF42}

 

先分析 vm 的流程,首先要知道传入 vm 中的表的结构,这里只能动态调试,把之前的反调试 patch 掉,开始调试,再 array 的地方下断点,观察array 的内存变化。


 
array 的结构如下:

 
opcode 表如下,并不会根据输入的不同而不同,基本上固定的操作路径,vm 计算产生的中间值其实就是放到 array 中,和 array+8 的指针中的。

 
最后 9ea4eff588 保存的内容如下,第一个是用户输入,第二个是给用户输入产生的 MD5,第三个是用户输入得到的 100 byte 的 md5。

 
知道了传 入vm 的 array 的结构,下面分析 vm 的流程,人肉调试还原。
 
总体的结构是这样的,吐了。

 
根据下面的分析,每次寻址是 *(array + (array + 20)),可以知道 *array 就是 opcode,*(array+20) 则是偏移量,由此可知,opcode 的寻址方式,可以发现每个 case 中让 array 移动都是操作 *(array+20):

 

我的方法是,在每个 case 的偏移加上注释,动态调试一边关注 array 的变化,结合静态分析推断出每个 case 的作用,再在 x64debug 的对应位置做好标注,进行跟踪就很容易看出来规律了。

 

下面给一些重要的流程函数标记出来,然后跟踪找规律。

 

下面重点的按照处理流程顺序还原几个重要的 case:

1. case 17 : 有个函数,主要做的操作就是先右移在左移,每次移动产生的结果再在一起或运算得出结果,记作 r2l ( input,i ),这里的i经过跟踪发现是递减的 7~0:


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}

2. case 12 : 有个函数,主要做的操作就是上一次 ‘r2l’ 产生的结果与固定的值,0x9e3779b9 进行异或,将结构保存到 array 结构中:


3. case18 :有个函数,主要做的操作就是先左移再右移,每次移动产生的结果再在一起或运算得出结果,记作 l2r ( input,i ),这里的 i 已经是固定值了,就是 6 。



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 = inputfor(i=0;i<2;i++) { for(j=7;j>=0;j--) { result = r2l(result,j); result = xor(result); result = l2r(result,6); } }


继续往下跟


1. case 16 :执行或操作,具体的数值关注 eax , ecx 对应哪一个步骤产生的值:


2. case 15:执行或操作,具体数值管 eax , ecx,确定对应那一步的计算:


3. case 19 : 执行按位取反操作,将上述步骤产生的值按位取反:


这里跟了很多次,因为每一次位运算产生的结果会参与到不同流程的计算,这里要十分仔细,我当时就是拿着纸笔一条一条的记录数据。

最终的操作如下:

获取步骤 4 产生的值(记作re)
~((re | md5) ^ (md5_table & md5) ^ (re & md5_table) ^ (re & md5_table & md5) ^ (re | md5_table | md5))

根据标注,其实追踪也能发现规律。






总结一下


1. sub_140075159是vm流程的计算,传入的就是输入,md5,以及md5_table:


2. 进入 sub_140075159,首先初始化一个 array 结构,再将 array 传入 vm 函数进行 vm 流程的计算。

3. 还原 vm 流程,先进行 r2l,xor,l2r共2*8==16 轮。

4. 还原 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

5. 最终产生的就是 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 的产生:


1. table 的产生 gettable:


2. 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;}


 



- End -





看雪ID:Craft_A

https://bbs.pediy.com/thread-259636.htm

*本文由看雪论坛 Craft_A 原创,转载请注明来自看雪社区。

推荐文章++++

*  记一次基于Soket通信的app的分析

*  TP-Link Archer路由器LAN RCE

*  深入理解静态变量

*  KERNEL PWN状态切换原理及KPTI绕过

*  暴力爆破靶场搭建及爆破实验


好书推荐







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存