MRCTF2021逆向题解
本帖最后由 BXb 于 2022-1-27 10:24 编辑个人解题赛,AK了逆向题。
## Reverse
### Real_CHECKIN
换表base64加密,找到表:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/
密文:tvjdvez7D0vSyZbnzv90mf9nuKnurL8YBZiXiseHFq==
简单写一下换表脚本:
```python
import base64
s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/"
table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
enc = "tvjdvez7D0vSyZbnzv90mf9nuKnurL8YBZiXiseHFq=="
ans = enc.translate(str.maketrans(table, s))
print(base64.b64decode(ans))
#MRCTF{wElc0Me_t0_MRCTF_2o21!!!}
```
### Dynamic Debug
来到main函数,首先对输入的字符的长度进行了一个判断:
到关键函数sub_402500(),不能直接反编译,那就简单的分析一下汇编,开始是花指令之类的东西,后面也就是简单比较输入是否为:MRCTF{IS_THIS_REAL?ASK_YOURSELF},很明显这是假的。
调试一波找到对代码修改的地方:其实不关心细节,直接动调跟进我们输入字符后的函数,直接就是解密后的代码,修复一下即可。这里我做题时ida和犯病了一样,动调得到那个解密后的代码怎么都不对,,后面单步步入找原因,再次到达解密后的代码又对了,现在想模拟一下当时出错的情况,看原因,又一直不错(。。
这里手动patch不太方便,还要dump出异或流。就直接动调得到,一个清晰明了的tea结构加密:
解密:
```c
#include <stdio.h>
unsigned char ENC[] = {153, 161, 133,85, 104,93, 130, 126,57, 0,
77, 148,67, 105, 114, 113, 6,67,81, 106,
0, 173,20,75,63,13, 210, 100,21, 219,
55, 159
};
unsigned int v5 = 0x6B696C69,
v4 = 0x79645F65,
v3 = 0x696D616E,
v2 = 0x67626463;
unsigned int get_delat()
{
int i = 0;
unsigned int ans = 0, delat = 0x9E3779B9;
for(i = 0; i < 32; i++)
ans += delat;
return ans;
}
void tea_decode()
{
int i = 0, j = 0;
for(i = 0; i < 4; i++)
{
unsigned int *enc = (unsigned int *)(ENC+8*i);
unsigned int delat = get_delat();
for(j = 31; j >= 0; j--)
{
enc -= (enc + delat) ^ (enc*16+v3) ^ ((enc >> 5)+v2);
enc -= (enc + delat) ^ (enc*16+v5) ^ ((enc >> 5)+v4);
delat -= 0x9E3779B9;
}
}
}
int main(void)
{
int i = 0;
tea_decode();
for(i = 0; i < 32; i++)
{
printf("%c", ENC);
}
return 0;
}
//MRCTF{Dyn4m1c_d3buG_1s_a_ki11eR}
```
### MR_Register
考点:Debug Blocker技术。
程序的关键在子进程。分析方法很多,但绝大多数,只要找到这个技术关键点,静态分析和动调父进程看子进程反馈就好了,如果数据复杂了,可以附加调试看内存加以辅助。
首先根据函数的特征结构找到main函数,
来看main函数,刚刚开始看的时候疑惑了一会儿,为什么这个在调试状态下才执行程序的关键逻辑,经过后面的创建进程函数发现:其实这里的if else语句就区别了父进程与子进程执行不同的语句,因为创建出的子进程是调试模式运行的。这里关注一下创建进程的**dwCreationFlags**参数,分析程序是将其转换一下枚举的含义好看一些。
> dwCreationFlags标识了影响新进程创建方式的标志:
>
> **DEBUG_PROCESS:如果这个标志被设置,调用进程将被当作一个调试程序,并且新进程会被当作被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。**
>
> **DEBUG_ONLY_THIS_PROCESS:如果这个标志被设置,调用进程将被当作一个调试程序,并且新进程会被当作被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。**
>
> **上面2个不同点在于DEBUG_PROCESS会调试被调试进程以及它的所有子进程,而DEBUG_ONLY_THIS_PROCESS只调试被调试进程,不调试它的子进程。**
继续就是分析后面父进程处理子进程异常的部分sub_40188D():
关键是看DebugEvent.dwDebugEventCode == 1的活动:接受处理来自子进程的异常,进而修改子进程代码。
所以现在关键就是要去寻找子进程要执行函数中出现触发异常地方,进而对着父进程处理模块分析。这里我直接调试了下父进程,记录它处理子进程异常的位置和处理方式,随后自己用idapython去patch一下。
```c
第一次交互:通过除0异常触发
for ( i = 374; i >= 0; --i )
Buffer ^= Buffer ^ i; // 对表进行了一个简单异或运算。
Buffer = 120;
rip += 2;
第二次交互:通过int3交互,解密代码。
地址:0x0000401E1C
for ( i = 0; i <= 0x57D; ++i )
*((_BYTE *)v5 + i) ^= i;
rip += 2;
第三次,在patch后的代码第一次遇到int3,触发异常交互:
rip += 2
```
patch文件,并nop无用的代码:
随后分析程序的关键流程,也就是子进程执行的,先创建一个文件,将输入进行加密后的数据写入该文件中,最后取出文件中的数据与指定编码数据对比。
而关键加密函数就是之前patch后的:就是用我们的输入可见字符,因为最高为0,所以只用了7位,分成3 3 1三部分作为index,置换表的过程。这里的email其实并没有作为比较,看到最后用了有一个异或操作,而在最后的比对过程中也有一个同样的操作,就还原了,所以密文就是不经过异或加密后的结果。
最后先爆破,再进行一个bytes.fromhex(),最后一个减法。比赛时赶时间写的,比较水,用python处理起来方便一些。
```c
#include <stdio.h>
char enc[] = {71, 90, 53, 121, 69, 120, 71, 105, 71, 88, 69, 120, 53, 122, 71, 88, 69, 120, 71, 87, 72, 108, 72, 108, 69, 120, 71, 88, 53, 119, 71, 86, 69, 120, 71, 90, 53, 119, 71, 89, 69, 120, 71, 107, 53, 118, 71, 106, 69, 120, 53, 120, 72, 108, 53, 121, 69, 120, 71, 87, 71, 90, 53, 118, 71, 87, 69, 120, 71, 88, 71, 87, 72, 108, 53, 121, 69, 120, 71, 89, 71, 106, 53, 118, 71, 107, 69, 120, 71, 105, 72, 108, 71, 106, 71, 90, 69, 120, 72, 108, 53, 84, 72, 108, 71, 86, 69, 120, 53, 122, 72, 108, 71, 90, 71, 89, 69, 120, 71, 87, 71, 107, 72, 108, 71, 88, 71, 107, 69, 120, 71, 88, 71, 106, 71, 86, 72, 109, 53, 121, 69, 120, 71, 89, 53, 121, 72, 109, 71, 88, 71, 106, 69, 120, 71, 106, 71, 89, 53, 118, 71, 89, 72, 108, 69, 120, 53, 118, 71, 87, 71, 89, 53, 120, 71, 105, 69, 120, 71, 87, 71, 86, 71, 90, 53, 122, 71, 105, 53, 120, 69, 120, 71, 87, 53, 118, 71, 106, 71, 88, 71, 105, 71, 88, 69, 120, 71, 88, 53, 118, 53, 119, 71, 87, 71, 88, 71, 88, 69, 120, 71, 90, 71, 105, 71, 87, 71, 89, 53, 119, 71, 89, 69, 120, 71, 106, 53, 84, 53, 120, 71, 105, 71, 89, 71, 90, 69, 120, 53, 119, 71, 90, 53, 121, 72, 109, 71, 105, 71, 105, 69, 120, 71, 87, 71, 88, 71, 90, 72, 109, 53, 122, 53, 119, 72, 109, 69, 120, 71, 87, 53, 121, 72, 109, 71, 107, 72, 108, 71, 106, 53, 121, 69, 120, 71, 88, 53, 84, 53, 122, 71, 87, 71, 107, 72, 109, 53, 121, 69, 120, 71, 90, 53, 121, 71, 107, 72, 109, 71, 86, 71, 106, 53, 119, 69, 120, 71, 107, 53, 121, 71, 105, 53, 118, 72, 108, 71, 90, 71, 87, 69, 120, 53, 120, 53, 118, 53, 121, 71, 89, 72, 108, 53, 120, 53, 121, 69, 120, 71, 87, 71, 90, 72, 108, 71, 88, 53, 122, 71, 87, 72, 108, 53, 119, 69, 120};
char table = {"ABCDEFGH", "12345678", "0IJKLMNO", "+OPQRStu",
"\\vwxyzTU", "abcdefgh", "VWXYZijk", "lmnopqrs"};
int main(void)
{
int i, j, v25, v24, v23, ans1, ans2;
for(i = 12; i < sizeof(enc)-1; i += 2)
{
for(j = 32; j < 127; j++)
{
char ch = j;
v25 = (ch >> 6) & 1;
v24 = (ch >> 3) & 7;
v23 =ch & 7;
ans1 = table;
ans2 = table;
if(ans1 == enc && ans2 == enc)
{
putchar(j);
break;
}
}
}
//printf("%d %d", ans1, ans2);
}
```
```c
#include <stdio.h>
unsigned int enc =
{
0x4d, 0x52, 0xe2, 0x188, 0x2b0, 0x4b3, 0x7a6, 0xc8d, 0x14a1, 0x218d, 0x36a7, 0x5864, 0x8f80, 0xe843, 0x17827, 0x2609d, 0x3d926, 0x63a38, 0xa13c5, 0x104e5c, 0x1a6252, 0x2ab122, 0x4513b3, 0x6fc534, 0xb4d955, 0x1249eb9, 0x1d9786d, 0x2fe179d, 0x4d7906b, 0x7d5a841, 0xcad38cd, 0x1482e18b
};
char flag;
int main(void)
{
int i = 0;
int init = enc+enc;
flag = enc, flag = enc;
for(i = 2; i < 100; i++)
{
flag = enc-enc-enc;
}
for(i = 0; i < 100; i++)
putchar(flag);
}
//MRCTF{C4n_y0u_d3bug_1t?_n0_wa9!}
```
### EzGame
游戏真好玩。
之前嘶吼CTF做过一个魔塔的游戏,也是有几个通关条件但更苛刻,但是那个并不是unity3d写的,直接逆向程序看游戏的逻辑找每个关键点还是能分析。这个游戏本身用unity3d写难度就大了很多,还对一个dll进行Themida / Winlicense v3.0.0.0 - 3.0.4.0加壳。
搞了些时间,没有把程序正常调试起来,开始打用CE的主意。。
常规的通过星星数的增加,不断再次扫描缩小范围找到存放其值的内存所在的位置。
因为最后获得flag的还有一个条件就是死亡次数不能太多,那就还有一个记录死亡次数的变量,同样的方法找到内存位置。但其它三个条件是否达成标志的内存位置呢。
这时候我观察我记录下的星星数和死亡次数的内存地址:可以发现这2个内存地址隔的很近,这就让我想到其余判断条件的内存地址也是在这块区域,就在附近。
开始试探寻找另外三个条件的内存位置。
因为Eat Cookie是可实现的,所以浏览相关内存区域后我去Eat Cookie,然后看内存变化。
发现下图中的内存位置变了,然后我以为这个标志是四字节数据,其后面跟着的是6是死亡次数,那再后面的或许就是其余2个条件的标志内存区域吧。
开始把后面所有的数据作为4字节数据,然后都改为1,回到游戏,发现其余2个条件并没有变为True。。。
但还有一个条件GoHome也是可以打游戏到达的,到达后再次观察内存变化,发现了端倪,其实3个条件的标志变量是一个字节的数据,也就是存Eat Cookie标志内存地址跟着的2个字节。
现在把所有条件通过修改内存达成,GetFlag:
其实猜也是,不会这样就把flag得到了,,之前嘶吼的魔塔游戏就有很多判断,步数啊,血量,走的路径等各种参数去计算出flag。这个题应该也是这样的。。
在增加星星数时我之前观察到了后面有8个字节的数据在不断发生变化,而要得到flag就要星星数正确,而星星数又影响那8字节数据,自然想到那8字节数据影响着最后flag。。
这时候我有2个想法:
- 一是找方法计算出星星数为105时的8字节数据;
- 二是找到记录所能跳高度的内存位置或记录当前位置的参数,修改后达到外挂一样,将星星数一个一个吃掉。
第二个想法尝试了一下,不断变化位置,并没有在附近的内存中发现变化,倒是发现了记录当前已经跳动步数的内存位置,然后就没有后续了(。。
然后尝试找出计算8字节数据的方法,,先dump出几组数据观察了一下:发现奇数组和偶数组对应2个不同lfsr结构,关键在于最后一个字节移位(也是一个lfsr结构)后填入新一组的第一个位置。但是不知道每次循环填充到最高的位0或1是怎么计算的决定的。。
其实这里都知道内存的地址了,要是把程序调试起来,通过ce的是什么访问这个地址,得到操作目标数据的指令地址后在调试器中去对应找到关键代码就很简单了,然而这个游戏我没能调试起来。
但还有一个办法:dump出当时的内存,拖进ida中分析,一样和调试一样。。
有了这个思路,先是直接定位到最后GetFlag时要进行比较的代码,发现有取出那8字节数据作为key去去进行rc4解密,以为有直接比较key是否正确的地方,但并没有发现。然后又老老实实定位到计算生成key的地方:
上面的算法中有一个未知量,v0,就是要运行的次数,,我是把1-64都打印了出来,在v0 == 8时,得到正确的结果。
最后就是打印出星星数为105时的目标值:
```c
#include <stdio.h>
unsigned char init[] = {0x4E, 0x51, 0x14, 0xA1, 0xFA, 0xEE, 0xDB, 0xEA};
int fun()
{
__int64 v2, v3, v4;
char v5;
unsigned __int64 v6;
__int64 result;
int v0 = 8, i;
do{
v2 = 0;
v3 = 0;
v4 = 1;
do
{
v5 = v3++ & 0x3f;
v6 = v4 & (*((__int64 *)init));
v4 = (v4 << 1) | (v4 >> 63);
v2 ^= v6 >> v5;
}while(v3 < 64);
result = v2 | 2* (*((__int64 *)init));
(*((__int64 *)init)) = result;
--v0;
}while(v0);
}
int main(void)
{
int i, j;
for(i = 3; i <= 105; i += 2)
{
fun();
for(j = 0; j < 8; j++)
{
printf("%x ", init);
}
putchar(10);
}
}
//dd b7 d5 3b 45 51 84 ea
```
修改游戏内存,得到flag。
### MR_CheckIN
安卓题,jeb反编译后,在MainActivity的onCreate方法找到调用了一个监听文本框输入的类。也就是当我们输入password长度为39时才将SIGNIN按钮启动。。
后面就是处理从文本框输入的内容,username要为MRCTF,然后检查了password的格式MRCTF{},接着passwd的6-13进行md5加密后与编码的数据比较,这个在线解密一下就好了:**Andr01d**,剩下的数据传入check2函数进行运算检查。
check2函数就是生成一个密钥序列然后与输入异或后与编码数据比较:
我开始直接把上面生成密钥序列的next方法复制到java中运行,但得到密钥序列一直不对。。
然后用了最笨的方法,,单步调试一步一步得到密钥序列,其实把最后那个判断条件改一下,得到这个序列就方便了。。
密钥序列:****
最后异或一下:
### 古神的低语
魔改过的平坦化混淆,,用脚本没去掉,然后就是硬看了。。
开始对输入的一些操作,判断长度和对username进行bytes.fromhex()的操作。
之后就是那个很大的函数了,记为mixFuction,也是整个题的关键,,什么操作都调用了它。
首先第一次调试的时候,在内存中找到了二张表,搜索到这是祖冲之序列密码,有密钥与iv。再仔细分析第一次调用那个mixFuction函数,发现传入username的hex.decode的形式和ezivforefolwenc,猜测这是将username作为key,ezivforefolwenc作为iv,进行祖冲序列密码的初始化。
然后第二次调用mixFunction,传入参数8和一个指针,单步步过这个函数发现生成32字节数据,从传入的参数为8,和这个序列密码的生成密钥流的性质:32位4字节密钥为一个生成单元。我又自己用这个相同的密钥和iv模拟了生成的密钥,然后对比,,果然一样。。
接着调用的一个mixFunction进行了赋值操作,直接单步步过后看变化。
继续又调用的两次mixFunction传入一个key和输入的passwd,分2次加密,每次加密16字节,分组密码的特征了。单步步入调试mixFunction很卡,,直接对passwd下内存访问断点,发现开始用传入的key和passwd进行一个异或操作,继续F9,又断下来,,一个字节替换操作,而从盒子看,明显aes的s盒,哈哈被发现了,那之前就是初始轮了。
后面是用之前生成的密钥流加密:
其中又调用了mixFunction函数对密钥流进行一个序列操作,调试到关键位置,得到加密算法。
最后还有一个mixFunction:循环移位操作。
最后整体上,整个加密就类似tea结构,解密:
```c
#include <stdio.h>
unsigned int key;
unsigned char ENC[] =
{
208, 161, 129, 188, 124, 155, 2, 228,47,51,
54, 232, 187,18, 146, 244,18, 178, 177, 115,
243, 219, 248, 195, 252, 223, 212,80, 220, 140,
91, 233
};
unsigned int enc_stream[] = {3691317505, 783378847, 3109790973, 2450366240,
1882631107, 1581884682, 2916488768, 1817239551};
unsigned int mul_2(unsigned int a)
{
int i = 0, cnt = 0;
for(i = 0; i < 32; i++)
{
cnt += ((a>>i)&1);
}
//printf("%d", cnt);
if(cnt%2 == 0)
return a*2;
else
return ((a*2) | 1);
}
void gen_key(int index)
{
unsigned int i, j, v43, v44;
v44 = enc_stream, v43 = enc_stream;
for(i = 0; i < 256; i++)
{
v44 = mul_2(v43);
v43 = mul_2(v44);
key = v44 >> 7;
key = v43 << 18;
key = v43 >> 7;
key = v44 << 18;
}
}
unsigned int ror(unsigned int a, unsigned int i)
{
unsigned int ans = (a >> i) | (a << (0x20-i));
return ans;
}
int main(void)
{
int i, j;
for(i = 0; i < 4; i++)
{
gen_key(i);
unsigned int tmp;
unsigned int *enc = (unsigned int *)(ENC+8*i);
tmp = enc;
enc = enc;
enc ^= tmp;
for(j = 255; j >= 0; j--)
{
enc += ror(enc, j&0x1f) ^ key | key;
enc += ror(enc, j&0x1f) ^ key | key;
}
}
//printf("%x ", ror(0x00C68E92E3, 2));
for(i = 0; i < 32; i++)
{
printf("%d, ", ENC);
}
return 0;
}
```
aes解密:
```python
>>> cry = AES.new(b"ezkeyforenc"+bytes(5), AES.MODE_ECB)
>>> flag = cry.decrypt(bytes())
>>> flag
b'really_ez_flatten_obfu_can_u_fix'
```
最后解密还原得到的一个音频文件,这不杂项。。
找到这个:
然后,,,倒放音频听到flag。 新手12138 发表于 2021-4-16 15:02
那个魔改的混淆硬看没看明白 师傅tql
另外那个游戏 这个位置锁1连跳也可以过关哦
知道吃星星数会修改状态后,我也有这类想法,是修改跳的高度之类的,但是没有去想连跳。谢师傅。 mihacker 发表于 2021-4-15 09:21
想要游戏那个。
链接:https://pan.baidu.com/s/1dR0joxd6cR7RfUy-oCbJqg
提取码:9al8
复制这段内容后打开百度网盘手机App,操作更方便哦 这么牛逼的文章,必须顶你 我是第一个看到并回帖的人吧。很详细的讲解。辛苦了。 有题目吗?
{:1_904:} 我就是看天书来的,这我都敢点进来{:1_907:} ollvm硬看也太顶了 师傅tql Qfrost 发表于 2021-4-14 18:37
ollvm硬看也太顶了 师傅tql
哈哈,师傅见笑了。 mihacker 发表于 2021-4-14 14:53
有题目吗?
师傅要那个题文件。 不错!!不错!! BXb 发表于 2021-4-14 23:37
师傅要那个题文件。
想要游戏那个。