这是2022年HGAME比赛的REVERSE复现,其中有思路借鉴于官方答案;
Week1:
easyasm
IDA:
可以在主要部分找到一个循环运算,而这里的 [si] 的间接地址就代表了输入flag的位置,es是保存了seg001数组的寄存器,这个循环的意思就是:循环28次,每次交换输入字符的前4位和后4位,最后异或23得到seg001的值;
按照这个思路反向运算写出的C语言:
1 |
|
运行后得到flag:hgame{welc0me_to_4sm_w0rld}
creakme
IDA:
一来可以发现一个类似于base64的码表,但再看算法,发现并不是base64;
经过这个for运算后,会让运算后的值与v11进行比较;
所以解题思路就是去逆向这个for循环,但写出解题代码后发现并不正确;
最有意思的原因是仔细看v10,它的定义是_DWORD,也就是32位,运算中出现的v3也是32位;
所以让所有运算的数据成为32位的,再写代码:
1 |
|
输出后的hex转换为ascii后发现也不对,但很明显这就是flag的值:
1 | HEX 6D616768 34487B65 5F797070 34633476 6E306974 7D21 0 0 |
之后发现原因是小端序排列的原因;
调整下顺序便可以得到flag:hgame{H4ppy_v4c4ti0n!}
Flag Checker
这是个安卓apk,用jeb分析:
点进flag checker的MainActivity里,会发现有一个encrypt方法,使用了标准RC4加密;
之后出现了主函数,使用 ‘carol’ 做密钥来RC4加密,之后可以看到Base64的字样,那么说明使用了base64加密;
最后用两次加密的结果与 mg6CI 开头的那个字符串比较相同与否;
那么就可以反着逆,先解base64,再解RC4:
1 | import base64 |
运行后得到flag:hgame{weLC0ME_To-tHE_WORLD_oF-AnDr0|D}
猫头鹰是不是猫
IDA:
最左边是main函数,右上是sub_55B565CA524E,右下是crypto;
一上来就会运行两次右上的函数,效果是用0,和1打印出猫和猫头鹰的样子(解题没什么用);
之后会让输入长度为64的字符串,将字符串经过crypto函数过后加密,加密后与cmp数组比较与否;
这里可以说明一下:cat,owl都是16384字节的数组,也就是4096 * 4;cmp是256字节的数组,也就是64 *4;
重点在于crypto函数,先后两次用sub_55B565CA5347函数与cat和owl两个数组参数将output变量加密;
进入sub_55B565CA5347函数:
首先是经过第一个嵌套循环将输入的cat或者owl数组的每个数据都除10;
之后经过第二个嵌套函数将64个输入的字符,分别每个字符与对应的cat或者owl数相乘,再把64次运算的结果加在一起;
这样的一次总和,就成为了新的output数组里的一个字符;而注意这个函数将运算结果ans换到output里的时候,output数组下标有个4*n,这说明output最后的结果是DWORD类型的,也就是单个数据32位,一共64个数据;这也和256个字节(也就是256 * 8 = 2048 = 32 * 64(单位是位))的cmp比较数组对的上号;
所以让cmp数组变为DWORD型数据,先经过owl输入的运算,再经过cat输入的运算,最后得到输入的64个字符;
这样一看,就会发现:当cmp成为已知实数的时候,在owl输入的运算中,有64个未知数(正着运算前的output),和64组方程(64个未知数分别加上指定常数的和,一共64个);这是一个线性代数,且有唯一解;同理,在cat输入的运算中也是这样;
那么思路就是解两次线性代数,写代码的时候采用z3库解方程;
代码:
1 | from z3 import * |
多次运行补充数据后得到flag:hgame{100011100000110000100000000110001010110000100010011001111}
Week2:
xD MAZE
IDA:
一进主函数就能看见关键词的拼接:hgame{ + x + } ;可以通过v3,v4的赋值以及下面的 if 判断推测出x的长度是28;
右图是28个长度的v11循环运算,正好对应了 cin 输入的v11;有四个数字,对应四个方向,不同的方向会导致 j 的不同增长;
左下图是有个maze数组的判断; 如果不是空格(32),或者 j 超出了maze范围,就会失败,否则就成功;这个数组是由4096个 空格 和 # 符号组成;可以把空格想象成可以走的路,而 # 符号是围墙,通过 j 变量来操控人物走迷宫;
那可以写一个简单的迷宫算法,模拟 j 变量走通迷宫,并记录 j 变量如何增长,对应的 v11 输入是怎么样的情况;
代码如下:
1 | maze = [] #4096个数据太多,不写出来 |
运行后得到flag:hgame{3120113031203203222231003011}
upx magic 0
这道题很怪,它的题目是和upx压缩保护有关,但用IDA并没有发现这是个包装程序;
IDA分析:
进入start函数后,可以找到一个叫 sub_400BBD 的函数,进入后,发现这就是要找的目标函数(右图);
输入32个字符,用输入的字符进行for运算,最后变成chan变量;
然后用chan变量和v14变量比较与否;那么可以用给的v14变量通过for的逆运算算回输入的字符;或者爆破;
代码如下:
1 | d = [0] * 33 #输入的data |
运行后得到flag:hgame{noW_YOukoNw-UPxmAG|C_@Nd~crC16}
fake shell
运行程序后,是一个模仿 Linux 终端的玩意儿,打开flag.txt需要sudo密码:
没办法就开IDA:
从左往右看,第一张图是main函数,找到使用sudo命令后运行的函数:sub_559D9EE5B9E9;
第二张是进入这个函数后的内容,可以看到需要输入密码v4,32个字符长度,然后进入加密函数:rc4;
第三张就是rc4的加密了,先用rc4_init函数和已知密钥:aHappyhg4me字符串创建Sbox,再用rc4_crypto加密输入数据,最后用加密后的数据与v7变量比较与否;
因为rc4加密的异或运算,导致加密后的数据再用原函数加密一遍就可以变回加密前的数据;
所以代码如下:
1 | data = [0xB6, 0x94, 0xFA, 0x8F, 0x3D, 0x5F, 0xB2, 0xE0, 0xEA, 0x0F, 0xD2, 0x66, 0x98, 0x6C, 0x9D, 0xE7, 0x1B, 0x08, 0x40, 0x71, 0xC5, 0xBE, 0x6F, 0x6D, 0x7C, 0x7B, 0x09, 0x8D, 0xA8, 0xBD, 0xF3, 0xF6] |
结果发现sudo密码就是flag:hgame{s0meth1ng_run_bef0r_m4in?}
creakme2
IDA:
这是main函数,首先定义了一个num数组,然后输入32个字符,接着经过四次crypto加密,而从定义变量可以看出,input只有8个长度,所以这四个加密的意义就是把32个字符分成了四分,每份8个字符进入crypto单独加密,之后再拼凑在一起;最后把运算出来的字符串与cmp变量比较与否;
再来看crypto函数:
输入的a1是稳定的32,用于循环控轮次;输入的a2是8个字符;输入的a3是num数组;
这个for运算,是让前4个字符用后4个字符和num以及v4运算得到;让后4个字符通过变化后的前4个字符和v4以及不变的num运算得到;这个过程持续32轮;
那么可以倒着来算,把数据算回来;用给定的比较数组cmp当已知,先算后4个字符,因为变化后的v4和前4个字符是已知的,就可以减回去;之后减一遍v4,就可以用还原的后4个字符与v4算前4个字符了,然后持续32轮;
这个算法肯定没问题,但就是算不对,因为最终的v4应该变为0,但运算结果v4却不是0;
是什么原因呢?打开汇编层代码进行查找,于是就发现了一个神奇的东西:
这一部分是翻译为了 v4 += 9E3779B1;[rsp+58h+var_38] 这个地址,代表的就是v4;
可以看出上半部分的运算是没问题的,确实翻译成伪代码就是这个意思;但之后它把eax右移31位(这个时候eax等于v4)得到符号位,然后传送给ecx,之后用ecx来做除法,如果这个数是正数,那么符号位为0,这样算肯定有问题;所以就会有异常处理;
一旦出现了异常处理,就会使得下半部分的运算进行,把v4异或上0x1234567;这就是为什么光看伪代码找不出错误原因的地方;按照它这个思路,可以写出如下代码:
1 |
|
运行后得到flag:hgame{SEH_s0und5_50_1ntere5ting}
upx magic 1
这个是用upx加壳的内容,用checksec就可以知道,但upx -d 命令对它没用;
用十六进制查看器搜索upx后发现原标准标记upx! 被改成了upx? 所以搜搜机器没有发现它是一个upx加壳;
将改过的标记改回来后脱壳:
之后用IDA:
左上是start起始位置,进入sub_400B8D函数,可以从文中知道这个就是main函数;
可以看出,输入的flag长度需要为37,然后进行for循环的运算,之后与v14进行比较与否,这系列操作就和upx0一模一样了;
解题代码:
1 | d = [0] * 38 #输入的data |
运行后得到flag:hgame{noW_YOukoNw-rea1_UPxmAG|C_@Nd~crC16}
Week3:
Answer’s Windows
这是个模拟 Windows面板,需要输入密码;
可以想到用IDA搜索显示正确或错误的句子,而IDA里搜不出中文,可以想到是用了图片;
使用IDA搜索:true,right,false,lose,win,wrong 这些特殊字样,会发现两个图片叫做right和wrong,跳转之后,会发现if的比较字符串:
而根据静态分析和动态调试可以发现它将输入的字符串进行了base64加密,又因为反调试将调试中的base64码表换成了错误的,导致怎么也得不到比较用的字符串;但能够发现比较数据有常规base64所没有的奇怪符号;于是去字符串列表里搜索123456789(赌码表含连续数字)连着的数据,可以发现所需要的码表;
于是有了下面两组数据:
1 | // ;'>B<76\=82@-8.@=T"@-7ZU:8*F=X2J<G>@=W^@-8.@9D2T:49U@1aa 比较数据 |
为什么码表以a结尾呢?因为比较数据后面跟着两个a,和原base64加密结果后面跟= 异曲同工;
还有个坑,可以看到图中的比较数据和得出的比较数据有所不同,原因在于 \ 是转义符,需要消掉解密,在没消掉之前,可以得到解除的明文是hgame开头,可后面是乱码;
此时解密得到flag:hgame{qt_1s_s0_1nteresting_so_1s_b4se64}
creakme3
这是PCC架构的文件,和以往的arm,x86有所不同,由PowerPC编译,所以IDA不能分析,linux不能运行;
此题有提示,使用Ghidra分析便可得知主体逻辑;
Ghidra下载:https://github.com/NationalSecurityAgency/ghidra
此时可以看到main的逻辑:
可以看到最中间的while()函数,他在给关于a的偏移进行排序,点击进入a后,发现是.data节中的一些数据,它们有规律:每8个字节为一个单位,初始是ascii码范围的hex值,加上4个字节后,变为一个比较大的数字;而在中间的while()函数中,排序是乘上8加了4,所以在利用较大的数字比大小,而最后putchar()进行输出,只是乘上了8,可以想到这个main函数的逻辑便是由每个单位的较大数字排序,最后输出排序后的ascii码,这应该便是flag了;
代码:
1 |
|
运行后得到flag:hgame{B0go_50rt_is_s0_stup1d}
hardened
使用jeb分析发现只有SecShell,应该是被加壳了;
使用BlackDex进行脱壳;(下载地址:https://github.com/CodingGay/BlackDex)
脱壳后会在java代码中发现加载了 libenc.so 库,调用了两个本地方法,其中加密部分就在这个库里;
查看 AES 加密 key、iv 的引用可以发现混淆加密的部分;
字符串混淆的解密可以用frida;
1 | //script.js |
之后异或回去就能解字符串;
解密得到flag:hgame{cONGraTUl4T|0N5!N0w_yoU_C4n_eN?OythEMUsIc}
fishman
原码中用了 init 和 check 函数;
使用IDA分析fishman库,字符串搜索 init 以及 check:
可以找到init和check的函数入口,进入过后,可以在init函数里发现一些运算,根据搜索运算所给的数据和格式,可以知道这是blowfish加密;
此时根据加密规则可以找到,密钥就是aLetUD:LET_U_D;
而比较数据就存在于check函数里,果不其然,会输出win或者lose:
于是使用blowfish的解密库解密:(blowfish库下载:https://github.com/xtbanban/blowfish)
1 |
|
运行后得到flag:hgame{D0_y0u_re411V_11k3_9Vthon}
Week4:
( WOW )
IDA分析:
可以看到main函数里先输入容纳40长度的内容,然后进行一次for循环里的加密,从input变成output;
之后与比较数据比较与否,输出错误或者正确,但后面还有个for循环,后面这个是把output输入,变成input,猜想一下它可能是解密;
尝试将output直接修改为cmp的数据,查看解密内容:
由此可得flag:hgame{WOWOW_h@ppy_n3w_ye4r_2022}
补充:这个题的加密是不常规的DES加密,DES加密为对称加密,即一组密钥即可完成加解密;核心思想为扩散混淆,扩散即为将明文的1个字符扩展为密文中的多个;混淆即为算法多层,让密钥和密文的关联更难找到;
server
IDA:
可以看到函数列表里有个叫 main_main 的主函数,说明这是go语言;
其中还有个函数叫做 main_encrypt ,既然是加密就应该有数据才对,打开汇编模式,可以发现伪代码中看不到的数据;
根据汇编更改 math_big函数的输入参数:
1 | __int64 __usercall math_big___ptr_Int__SetString@<rax>(char *str@<rbx>, __int64 a2@<rax>, int a3@<edi>, int a4@<ecx>) |
可以变为:
也可以用同样的原理,右键其他无参函数,点 set call type ,然后更改,最后可以修复encrypt函数看到原理:
发现在经过之前的加密后,进行了异或;且长度为153;
总之,根据标记符号函数名称和数据判断,这是RSA加密加上异或;
代码:
1 | from Crypto.Util.number import * |
运行得到flag:hgame{g0_and_g0_http_5erv3r_nb}
ezvm
IDA:
可以看出主函数输入后进入switch;
翻译出每个case对应操作,恢复程序逻辑:
1 | VM_START |
由此代码:
1 | xor_keys = [94, 70, 97, 67, 14, 83, 73, 31, 81, 94, 54, 55, 41, 65, 99, 59, 100, 59, 21, 24, 91, 62, 34, 80, 70, 94, 53, 78, 67, 35, 96, 59] |
运行可得flag:hgame{Ea$Y-Vm-t0-PrOTeCT_cOde!!}
hardasm
IDA:
可以看到伪代码的main函数显示的也是__asm,汇编指令;
根据汇编指令可以搜索出,这是AVX2 指令集;
具体思路就是给 ymm1~ymm7 寄存器赋值,然后进行运算,ymm0是输入数据;
但运算过程过于庞大,而且不会这个指令集;又因为ymm0到比较与否的区域始终没有改变,所以可以爆破;
在比较数据的地方下个断点,调试的时候输入 hgame{aaaaaaaaaaaaaaaaaaaaaaaaa} 共32个字符串(因为scanf的格式为%32s),这时候可以发现:[rsp+70h+var_50] 的地方从原来的输入数据,变为了6个0xFF;
这是因为比较后,因为前6个字符输入的都是正确的,而其他错误,所以其他的都成为了false,0;
那么就可以使用python的 subproccess 子程序模块,在运行脚本时对该程序进行操作,使其循环找到32个0xFF;
subprocess菜鸟教程:https://www.runoob.com/w3cnote/python3-subprocess.html
既然要找0xFF,那么需要从子程序中返回0xFF才行;
根据程序的输出可知:通过rcx将 error字符串或success字符串 打印出:
那可以patch程序,将[rsp+70h+var_50]处的内容传递给rcx,以此打印;
代码:
1 | import subprocess |
运行得flag:hgame{right_your_asm_is_good!!}
总结
这次hgame的逆向之旅收获了很多,应该可以说有了叫baby和easy题的题感;更多的包括JAVA的语法,z3的模型可运算,UPX的标签规则,POWERPC的PCC架构,Blowfish标准河豚算法,RSA大素数算法,DES加密的了解;有两道题并不是特别理解,一道是第三周的hardened,另一道就是第四周的ezvm了;前一道没有可用手机或是kali虚拟机,所以无从下手;另一道虚拟机因为还不太理解虚拟机的构造,所以无法翻译;
因此接下来的任务就是抽空把虚拟机的实验完成,以及ROOT手机到手;