夏季赛#
HardSignin#
查到了UPX壳,发现是魔改过的,不能直接脱。在010editor中看到upx壳的节区名UPX
被改了,改回来后可以直接脱壳。
32位程序,进行反汇编,又看到很多花指令。主要在TlsCallback
的几个函数里面,而且这几个函数本身也常用来反调试。
先patch掉所以花指令。但是发现main
函数依然不能正常反汇编成指令。这里猜测可能有SMC
代码自解密。查看TlsCallback_0
,能够看到它把main函数的前170个字节异或了0x66
加密。用idaPython
脚本解密一下。
import idc
import idaapi
import idautils
def smc_xorenc(src_addr,dst_addr,key):
length = (dst_addr-src_addr) if (dst_addr>src_addr) else 0
for offset in range(length):
addr = src_addr + offset
value = idc.get_wide_byte(addr)
enc_value = value^key
ida_bytes.patch_byte(addr,enc_value)
if __name__ == "__main__":
smc_xorenc(0x401890,0x401890+170,0x66)
重建 main
函数,F5查看伪码。除了常规的输入输出,加密函数是 sub_4016B0
。
继续跟踪该函数,分析加密流程:先对输入进行一次base64,之后每8位进行一次RC4(16轮),最后进行一次TEA。后面两次加密的key在 TlsCallback_2
中用随机数生成,种子已知。在 TlsCallback_1
中发现base64的编码表被打乱了顺序。属于魔改的base64。
编写脚本进行解密。思路是先还原编码表。之后解密TEA和RC4,得到编码后的字符串。在Cyberchef上进行最后的解码。
#include<stdio.h>
#include<stdlib.h>
void TEAdecrypt(int a1, unsigned char* a2, unsigned char* a3)
{
for (int i = 0; i < 16; i += 2) {
unsigned int v7 = *(unsigned int*)(a2 + 4 * i);
unsigned int v6 = *(unsigned int*)(a2 + 4 * i + 4);
int v5 = 0x9E3779B9 * a1; // 0x9E3779B9 是 1640531527
for (int j = 0; j < a1; ++j) {
v6 -= ((v7 + ((v7 >> 5) ^ (v7 << 4))) ^ (v5 + *(unsigned int*)( a3 + 4 * ((v5 >> 11) & 3))));
v5 -= 0x9E3779B9;
v7 -= ((v6 + ((v6 >> 5) ^ (v6 << 4))) ^ (v5 + *(unsigned int*)( a3 + 4 * (v5 & 3))));
}
*(unsigned int*)(a2 + 4 * i) = v7;
*(unsigned int*)(a2 + 4 * i + 4) = v6;
}
}
void RC4decrypt(unsigned char *a1,int a2,unsigned char *key,int length)
{
unsigned char s_box[256],v6[256],temp,t;
for ( int i = 0; i < 256; ++i )
s_box[i] = i;
for ( int j = 0; j < 256; ++j )
{
v6[j] = key[j % length];
}
int v4 = 0, v1 = 0, v2 = 0;
for ( int k = 0; k < 256; ++k )
{
v4 = (v6[k] + v4 + s_box[k]) % 256;
temp = s_box[k];
s_box[k] = s_box[v4];
s_box[v4] = temp;
}
for ( int m = 0; m < a2; ++m)
{
v1 = (v1 + 1) % 256;
v2 = (v2 + s_box[v1]) % 256;
t = s_box[v1];
s_box[v1] = s_box[v2];
s_box[v2] = t;
a1[m] ^= s_box[(s_box[v1] + s_box[v2]) % 256];
}
}
char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
unsigned char data[] =
{
0x59, 0x1B, 0xFD, 0xB4, 0x6B, 0xB8, 0xBE, 0xD9, 0xB3, 0xD3,
0x77, 0xD6, 0xF0, 0x65, 0x5F, 0x18, 0xA0, 0x9D, 0x3A, 0x53,
0x6D, 0x4A, 0x7B, 0x26, 0x74, 0x3A, 0x9C, 0x4E, 0x20, 0x43,
0x19, 0xD8, 0x72, 0xED, 0x95, 0xB5, 0x9C, 0x05, 0x22, 0x56,
0xCB, 0x7A, 0x11, 0x91, 0x9F, 0x7A, 0xBC, 0x0C, 0x4A, 0x69,
0x6D, 0xCE, 0x3D, 0xB4, 0xAB, 0x29, 0x61, 0xFA, 0x62, 0x32,
0xB4, 0xEC, 0x4C, 0xB6
};
int main()
{
srand(0x114514u);
int v6,v4;
unsigned char key1[17],key2[17];
for ( int i = 0; i < 100; ++i )
{
v6 = rand() % 64;
v4 = rand() % 64;
char temp = table[v6];
table[v6] = table[v4];
table[v4] = temp;
}
printf("%s\n",table);
srand(0x1919810u);
for (int i = 0; i<0x10; ++i )
{
key1[i] = rand() % 255;
key2[i] = rand() % 255;
}
TEAdecrypt(0x64,data,key2);
RC4decrypt(data,64,key1,0x10);
printf("%s",data);
return 0;
}
//4yZRiNP8LoK/GSA5ElWkUjXtJCz7bMYcuFfpm6+hV0rxeHIdwv32QOTnqg1BDsa9
//C+vFCnHRGPghbmyQMXvFMRNd7fNCG8jcU+jcbnjRJTj2GTCOGUvgtOS0CTge7fNs02@
snack ( 复现 )#
一个贪吃蛇游戏。运行发现是用pygame开发的,说明程序经过python打包,使用pyinstxtractor
解包。
这里的解包实际上不算成功。由于游戏程序的python版本和本地版本不符,所以pyz没有解压而是直接跳过。复现到后面发现需要用到 pyz_extracted
里面的库。这里更换python版本重新解包。
之后尝试用uncompyle6
进行反编译,但是出了问题。一开始怀疑pyc文件里有花指令一类的,后面换成了pycdc
引擎结果成功了。估计问题还是在版本上
反编译后的代码不多,加密逻辑是魔改的RC4,而且程序本身就有解密代码,直接copy下来用。(反编译生成的代码语法有一点小问题,s盒的交换还有后面列表推导式不能直接运行,需要改一下)
解密密钥在自定义的库key中,这个就是上面提到的 pyz_extracted
目录下的 key.pyc
,反编译就能看到key
key_bytes = b"V3rY_v3Ry_Ez"
def initialize(key):
key_length = len(key)
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % key_length]) % 256
S[i],S[j] = S[j],S[i]
return S
def generate_key_stream(S, length):
i = 0
j = 0
key_stream = []
for _ in range(length):
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i],S[j] = S[j],S[i]
key_stream.append(S[(S[i] + S[j]) % 256])
return key_stream
def decrypt(data, key):
S = initialize(key)
key_stream = generate_key_stream(S, len(data))
decrypted_data = [ i ^ data[i] ^ key_stream[i] for i in range(len(data))]
return decrypted_data
data = [101,97,39,125,218,172,205,3,235,195,72,125,89,130,103,213,
120,227,193,67,174,71,162,248,244,12,238,92,160,203,185,155]
decrypted_data = decrypt(bytes(data), key_bytes)
print(bytes(decrypted_data))
BEDTEA ( 复现 )#
反编译main函数的加密部分,先发现了两个反调试:一个是常规的IsDebuggerPresent()
调用API,结果保存到dword_404010
,和后面加密过程有关系;另一个是用时间戳,两次调用system_clock::now
判断间隔时间。二者都可以在动调时改寄存器绕过。
跟踪输入v15
,定位到sub_401E80
。跟进分析,函数分成两个部分:先修改程序的一段数据作为密钥(参考官方wp,算法是斐波那契);后面是魔改的TEA,左移右移、delta还有轮数都改了。反编译的结果不太好分析,从汇编看比较清晰。生成密钥的初始值就是第一次反调试时得到的dword_404010
。
回到主函数,sub_401770
和sub_401560
两个函数这里踩了坑。一开始以为是没有实际功能的库函数一类的,看了官方wp才知道是二叉树的前序遍历和后序遍历。其实看到输入v17
参数传进去,就应该猜到是加密的一部分。这里是把加密数据逆序了。
后面调用了SSE指令集的xor指令,还额外多异或了一些无关数据。xor的值是固定的,直接写脚本即可。
#include<stdio.h>
void tea_decry(unsigned int *data, unsigned int *key)
{
unsigned int d1 = data[0], d2 = data[1];
unsigned int delta = 0x9e3449b8, number = 0x987e55d0;
for (int i = 0; i < 22; i++)
{
d2 -= ((d1<<5) + key[2]) ^ ((d1>>4) + key[3]) ^ (d1 + number);
d1 -= ((d2<<5) + key[0]) ^ ((d2>>4) + key[1]) ^ (d2 + number);
number -= delta;
}
data[0] = d1;
data[1] = d2;
}
int main()
{
unsigned int key[] = {3,5,8,13}, key2[] = {21,34,55,89}, key3[] = {144,233,377,610};
unsigned char datac[] ={
0x76, 0x71, 0x9D, 0xE7, 0x70, 0x77, 0x3F, 0xA3,
0x02, 0xF1, 0x8D, 0xC9, 0x02, 0xC6, 0xA2, 0x4B,
0xBA, 0x19, 0x56, 0x05, 0xF2, 0x89, 0x5E, 0xE0 };
unsigned char *fd = datac,*bd = datac+23 ,tmp;
for(int i = 0; i < 24; i ++)
{
datac[i] ^= 0x33;
}
while(fd < bd)
{
tmp = *fd;
*(fd++) = *bd;
*(bd--) = tmp;
}
unsigned int *data = (unsigned int *)datac;
tea_decry(data, key);
tea_decry(data+2, key2);
tea_decry(data+4, key3);
printf("%s\n",(char*)data);
return 0;
}
冬季赛 Day1#
ezre#
自定义的md5,利用openssl库的md5函数,对程序text段的1204个字节取hash。实际上后面将近300个字节是未定义的,这一部分取0x0即可。通过动调查看内存也可以确定。
在linux下动调发现MD5的结果有时会变,但是这一部分应该与输入无关,也就是固定的才对。赛后意识到是断点导致了内存中指令机器码发生变化。(应该考虑硬件断点,或者直接dump求MD5)
按照正常的逻辑,主函数用MD5值的前4个字节,取随机数逐字节异或。关键还是找到正确的hash值,一般来说通过动调dump内存是最优解,但是断点的因素导致这一方法比较困难。开多线程分段爆破也是有效的方法。
ezgo#
有符号的Golang逆向,快速找到main部分。考虑到和c语言的差异,直接看反汇编CFG可能会比反编译伪码更清楚一些
main_init_0
用syscall 加载内核库中的IsDebuggerPresent
反调试,容易绕过。
对base64进行换表,并且编码结果再 xor 0x0c
TSRQPONMLKJIHGFEDCBAUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
继续分析main_main
,主要逻辑是提取zip文件的二进制数据进行解密,写入原文件中。程序要求输入4长度的字符串,经过魔改base64产生8字节密钥,参与后续的异或解密。
程序中并没有关于flag的更多信息,猜测是保存在zip文件中,解题的目标应该是找到正确解密该文件的密钥。参考zip文件格式固定位置的一些magic number,实际上已知部分明文,而且加密算法只是异或这种单字节的线性运算。可以考虑转换成若干个方程,用z3约束求解
magic number:
50 4b 03 04 14 00
感觉比较通用的方法是爆破,编码前密钥仅4字节且均为可见ascii字符,复杂度并不高。
编写脚本还原程序的逻辑来爆破,效果最好但是难度很高,前提是要对题目程序逆得很透彻,而且脚本编写不能出错。爆破之前也要简单估计一下字符范围,密钥空间等,避免大规模的爆破。
ko0h#
算是签到题吧,经典inline hook并且加了花
交叉引用 VirtualProtec
t定位到 Tlscallback
,找到hook后真正的主函数,逻辑是魔改RC4
注意到有反调试,在其附近还能发现修改了RC4 key
data = [
0x18,0x9c,0x47,0x3d,0x3b,0xe1,0x29,0x27,0x9f,0x34,0x83,0xd5,0xed,0xb5,
0x6e,0x59,0x7f,0xde,0x47,0xd7,0x65,0x3f,0x7a,0x33,0x5b,0x64,0xb6,0xfa,
0x94,0x55,0x87,0x42,0x20,0x06,0x0c,0x69,0xfe,0x72,0xa9,0xe4,0xd1,0x7c
]
key = b"DDDDAAAASSSS"
length = len(key)
S = [m for m in range(256)]
T = [key[n % length] for n in range(256)]
j = 0
for i in range(256):
j = (j + S[i] + T[i]) % 256
S[i],S[j] = S[j],S[i]
i = j = t = 0
for k in range(len(data)):
i = (i + 1) % 256
j = (j + S[i]) % 256
t = (S[i] + S[j]) % 256
S[i],S[j] = S[j],S[i]
data[k] = (S[t] + data[k]) % 256
print(bytes(data))
ezvm#
加密部分勉强能看出来是变种XTEA,但是魔改得太狠了。
vm使用 switch-case
实现,比较常规,字节码的长度也不算多。纯静态手撕的话应该是问题不大的。bytecode以4字节为单位,有一些冗余。
B2 00 00 00 00 00 00 00
C4 00 00 00 04 00 00 00
B2 00 00 00 00 00 00 00
C5 00 00 00 06 00 00 00
D7 00 00 00
F4 00 00 00
E2 00 00 00
D8 00 00 00 03 00 00 00
C6 00 00 00
A1 00 00 00
A2 00 00 00
B3 00 00 00 00 00 00 00
C4 00 00 00 04 00 00 00
B3 00 00 00 00 00 00 00
C5 00 00 00 06 00 00 00
D7 00 00 00
F4 00 00 00
E1 00 00 00
D8 00 00 00 03 00 00 00
C6 00 00 00
A3 00 00 00
66 00 00 00
实际分析时发现一个 opcode
对应很多条汇编指令,导致 opcode
的含义比较模糊,而且各个 case
之间也会共享局部变量,所以各个字节码指令之间独立性不高,看起来更像是一段正常代码被拆成几部分,有一种控制流平坦化的感觉。
尝试在运行时trace,恢复出vm的执行流程,可以看出加密有多轮循环。
在trace结果中筛选出 switch
相关的部分。switch
本身通过跳转表实现,edx
控制跳转的地址,也就实现 case
的功能。
结合伪码可以还原出加密整个流程。还缺一些运算用到的操作数,需要回到bytecode中确认一下
另外 and
、xor
两个函数做了一点混淆,需要留意
~(~a2 | ~a1); // and
~(a2 & a1) & ~(~a2 & ~a1); // xor
也可以尝试条件断点或者hook switch结构,效果可能比trace要好,这道题就不深究了。
32轮,delta = 0x20252025, 其余魔改的点参考脚本
#include<stdio.h>
#include<string.h>
unsigned char box[] =
{
0xa4, 0xc4, 0x04, 0xce, 0x14, 0x95, 0xe9, 0x11,
0x31, 0x18, 0xb6, 0xb0, 0x01, 0x26, 0x24, 0x6a,
0x7b, 0x12, 0xcb, 0x67, 0xdb, 0xf8, 0xd2, 0x7e,
0x9d, 0xd0, 0x0c, 0x5f, 0x82, 0x21, 0x87, 0x83,
0x86, 0x7c, 0xc2, 0x9f, 0x29, 0xca, 0xbf, 0x49,
0xde, 0x4e, 0xcd, 0x62, 0x53, 0xbe, 0xa7, 0x03,
0x2f, 0xb5, 0xab, 0x94, 0xcc, 0x2e, 0x1d, 0xf3,
0x36, 0x10, 0xba, 0xd7, 0x13, 0x35, 0xe5, 0xb3,
0x81, 0x1a, 0xa0, 0xe7, 0x25, 0x75, 0xaf, 0x51,
0x43, 0x5c, 0x50, 0x48, 0xd8, 0xa3, 0x3f, 0x71,
0x7a, 0xc7, 0xc6, 0x90, 0xb1, 0xbb, 0xfa, 0xdd,
0xb9, 0xf6, 0xa9, 0xb7, 0x64, 0x38, 0xdf, 0xe0,
0x08, 0xb2, 0x77, 0x33, 0x5b, 0x02, 0x5e, 0x79,
0x61, 0x07, 0x69, 0x23, 0x57, 0x4a, 0xfd, 0xc0,
0x2b, 0xa1, 0xd1, 0x28, 0x09, 0x6f, 0x80, 0x55,
0xfe, 0x42, 0xe3, 0x47, 0x44, 0xe1, 0xff, 0xbc,
0x7d, 0x8b, 0x9a, 0x60, 0xad, 0x97, 0xfb, 0x8d,
0xd6, 0xac, 0x1e, 0x0f, 0x45, 0xea, 0xf5, 0x4b,
0x2d, 0x3b, 0x22, 0x1c, 0x5a, 0x72, 0x46, 0xc3,
0xe4, 0x5d, 0xda, 0x92, 0x9b, 0x0a, 0xbd, 0x99,
0x85, 0x34, 0x73, 0xa5, 0x56, 0x37, 0x4c, 0x16,
0x84, 0xa2, 0xb4, 0x6d, 0x54, 0xe6, 0xc1, 0x1f,
0x17, 0x3d, 0x88, 0xf7, 0x15, 0x58, 0xef, 0x4d,
0xee, 0x89, 0x68, 0x59, 0xb8, 0x20, 0xe8, 0xdc,
0xc9, 0x91, 0xfc, 0xd5, 0xc8, 0x41, 0x9e, 0x76,
0x78, 0x32, 0x19, 0x66, 0x65, 0x39, 0x6b, 0xc5,
0x52, 0x27, 0xa8, 0x06, 0x8e, 0xa6, 0x0d, 0x98,
0x8c, 0xf9, 0x05, 0x1b, 0x40, 0x8f, 0x4f, 0x3c,
0xeb, 0x70, 0xd9, 0x63, 0xd3, 0xe2, 0x2c, 0xf0,
0x93, 0x3a, 0xf4, 0x00, 0xf2, 0xaa, 0x7f, 0x2a,
0x30, 0xec, 0x6c, 0x74, 0x6e, 0xf1, 0x0e, 0x3e,
0xed, 0x96, 0xae, 0x8a, 0xcf, 0x0b, 0x9c, 0xd4
};
unsigned int substitute(unsigned int data)
{
unsigned char *bytes = (unsigned char *)&data;
for(int i = 0; i < 4; i++)
{
for(int j = 0; j < 256; j++)
{
if(bytes[i] == box[j])
{
bytes[i] = j;
break;
}
}
}
return data;
}
void modify_xtea_decry(unsigned int *data, unsigned int *key)
{
unsigned int round = 32;
unsigned int d1 = data[0], d2 = data[1];
unsigned int delta = 0x20252025, number = delta * round;
for (int i = 0; i < round; i++)
{
d2 -= ( ((d1<<4) ^ (d1>>6) ^ 66) + d1) ^ (number + key[(number>>7) & 3]) ^ 3;
number -= delta;
d1 -= ( ((d2<<4) ^ (d2>>6) ^ 66) + d2) ^ (number + key[number & 3]) ^ 3;
}
data[0] = d1;
data[1] = d2;
}
int main()
{
unsigned int data[] =
{
0x83845981,0x34402115,0xFB1F53D2,0x547564C9,0x3B42FCC6,
0x2B67FCDE,0x675AB09C,0x1D47F41A,0x876D3272,0x734D7D95
};
unsigned int key[] =
{
2,0,2,5
};
for(int k=0;k<10;k+=2)
{
data[k] = substitute(data[k]);
}
for(int i=0;i<5;i++)
{
modify_xtea_decry(data+2*i,key);
}
printf("%s",(char*)data);
return 0;
}
冬季赛 Day2#
skip#
apk反编译,发现在native层检验用户名,java层检查password,二者合起来是flag
对于java层的加密,结合函数名和反编译代码可以分析出是 skip32
( skipjack
) 算法。github能找到对应解密脚本
之后分析native的 libskip.so
,只有一个加密函数。问一下DeepSeek分析出是DES算法。但是S盒经过修改,需要手动提取出来再解密。
另外还要注意 JNI_onlode
反射修改了java层加密的ftable
。这部分通过c++异常处理调用,看反编译伪码不容易发现,隐蔽性比较强,但是 其中std::runtime_error
也是有所提示。可以用frida hook出修改的结果。
Nu1tka#
小众而高端的python打包工具。通过DIE可以查出经过nuitka打包
先尝试github上找到的解包工具 nuitka-extractor
成功解包后有一个同名的exe文件以及几个动态库
参考官方wp,该程序会创建新进程并执行。主函数中可以找到 CreateProcessW
这个API,在其参数 ApplicationName
地址处下硬件断点,找到进程对应的程序路径。不难发现该路径下临时文件夹中的文件和上述工具解包的结果相同,执行的是解包后的同名exe。(也就是说目标程序及其依赖库作为原程序的资源,在运行时才解包执行。继续动调发现主函数执行过后这些资源被删除)
冬季赛 Day3#
easyasm#
入口点0x4c刚好是 start
函数。加密逻辑是逐字节异或再检验,一开始调用的 sub_84C0
修改了异或用的key
起初静态分析修改key的算法,看起来很像冒泡排序,但汇编功力不够,不好直接确定
用DOSBOX进行动态调试。没有折腾明白DOSBOX-X的debugger怎么用,于是就拿低配一点的debug进行分析。
单步跳过直到 call sub_84C0
后面(貌似不能打断点?)dump下来 key
修改后对应的内存
结合脚本进行解密
check_data = [0x44, 0x7C, 0x43, 0x72, 0x1D, 0x72, 0x74, 0x41,
0x05, 0x14, 0x19, 0x1A, 0x19, 0x0F, 0xF5, 0x10, 0xAE, 0x18,
0x6D, 0x01, 0x10, 0x56, 0x00, 0x1E, 0x26, 0x71, 0x65, 0x73,
0x78, 0x72, 0xEB, 0x72, 0x52, 0x06, 0xAA, 0xBB, 0xA3, 0xA4,
0x1B, 0xFC, 0xC7, 0x82]
encrypted_key = [0x22, 0x10, 0x22, 0x15, 0x66, 0x16, 0x11, 0x20,
0x30, 0x20, 0x21, 0x22, 0x2c, 0x22, 0xcc, 0x22, 0xcc, 0x2c,
0x40, 0x30, 0x21, 0x33, 0x66, 0x33, 0x44, 0x40, 0x50, 0x40,
0x55, 0x41, 0x88, 0x42, 0x33, 0x60, 0x99, 0x88, 0xc2, 0xc2,
0x22, 0xcc, 0xff, 0xff]
for i in range(42):
check_data[i] ^= encrypted_key[i]
print(bytes(check_data))
ooooore#
脚本去花
# ida-7.5 idapython
import idc
import idautils
def nop(addr, endaddr):
while addr <= endaddr:
ida_bytes.patch_byte(addr, 0x90)
addr += 1
def patch_junkcode(cur_addr,end_addr,pattern,nop_startoff,nop_endoff):
while cur_addr<end_addr:
cur_addr = idc.find_binary(cur_addr,SEARCH_DOWN,pattern)
if cur_addr == idc.BADADDR:
break
nop(cur_addr + nop_startoff,cur_addr + nop_endoff)
print("patch address: " + hex(cur_addr))
cur_addr = idc.next_head(cur_addr)
start_address = 0x1080
end_address = 0x7390
pattern1 = "0F 85 06 00 00 00 0F 84 01 00 00 00 21" # jz jnz
pattern2 = "E8 00 00 00 00 48 83 04 24 08 C3 E8" # call ret
pattern3 = "E9 00 00 00 00" # jmp $+5
patch_junkcode(start_address,end_address,pattern1,0,12)
patch_junkcode(start_address,end_address,pattern2,0,11)
patch_junkcode(start_address,end_address,pattern3,0,5)
jnz jz
call ret
jmp $+5
貌似也会干扰反编译,一并patch掉即可
修复后重新反编译(发现疑似控制流平坦化,d810处理效果一般)
逻辑是比较简单的单字节异或,可以动调验证
和谐#
hap鸿蒙逆向,分析思路和apk差不多。
先改后缀为zip解压,找到其中的方舟字节码,即etc\modules.abc
。使用 abc-decompiler反编译。
核心代码在Pages部分,只需要分析其中Index
这个页面对应的类
按照一般逆向题目的逻辑,加密之前先输入flag,所以找字符串输入函数或者控件。发现一个匿名函数中的 TextArea
符合要求
向前回溯,发现 initialRender
这个方法调用该匿名函数来渲染控件。再次交叉引用来到主函数func_main
,其中调用了构造函数 Index
进行初始化(并且储存了(key?密文?)
一些API和库函数可以在鸿蒙官网查询:HarmonyOS NEXT开发文档
接下来分析输入flag后的加密逻辑。先调用方法 d
检查flag长度和格式,之后调用方法 g
进行加密
(官方wp:魔改SM4)
easyvm#
Cython实现的stack vm,通过py脚本交互。好在保留符号,逆起来应该轻松点
由于是so库,在Linux python 3.8环境下尝试运行,用 help
和 dir
输出一些信息
看出有一个 StackVM
类,大部分类方法是vm用到的指令handler。
继续动态分析,尝试函数级hook,直接修改类属性发现没有写权限。
from challenge import StackVM
func_add = StackVM.__dict__["vm_add"]
def hook_add():
print("Call add")
func_add()
StackVM.__dict__["vm_add"] = hook_add
#TypeError: 'mappingproxy' object does not support item assignment
不过可以考虑在子类中重写方法,之后运行虚拟机,实现类似trace的功能
from challenge import StackVM
null = ""
func_map = StackVM.__dict__
class hookedVM(StackVM):
def vm_push(self,val):
print("[Call Method] {} {}".format("vm_push",val))
func_map["vm_push"](self,val)
print(self.stack)
def vm_pop(self):
print("[Call Method] {} {}".format("vm_pop",null))
func_map["vm_pop"](self)
print(self.stack)
def vm_add(self):
print("[Call Method] {} {}".format("vm_add",null))
func_map["vm_add"](self)
print(self.stack)
def vm_and(self):
print("[Call Method] {} {}".format("vm_and",null))
func_map["vm_and"](self)
print(self.stack)
def vm_lsh(self,val):
print("[Call Method] {} {}".format("vm_lsh",val))
func_map["vm_lsh"](self,val)
print(self.stack)
def vm_rsh(self,val):
print("[Call Method] {} {}".format("vm_rsh",val))
func_map["vm_rsh"](self,val)
print(self.stack)
def vm_read(self):
print("[Call Method] {} {}".format("vm_read",null))
func_map["vm_read"](self)
print(self.stack)
def vm_print(self):
print("[Call Method] {} {}".format("vm_print",null))
func_map["vm_print"](self)
print(self.stack)
def vm_jmp(self):
print("[Call Method] {} {}".format("vm_jmp",null))
func_map["vm_jmp"](self)
print(self.stack)
def vm_be(self):
print("[Call Method] {} {}".format("vm_be",null))
func_map["vm_be"](self)
print(self.stack)
def vm_bl(self):
print("[Call Method] {} {}".format("vm_bl",null))
func_map["vm_bl"](self)
print(self.stack)
vm = hookedVM()
flag = input("Enter your flag: ")
vm.input_buffer = list(flag)
vm.vm_execute()
运行几次观察规律,大概推测出 vm_read
从 input_buffer
中读取字符,之后通过 vm_be
和压栈的数字进行比较,不相等直接 vm_jmp
跳转到错误分支,用 vm_print
逐字节输出信息。
整个过程都是单字节加密,而且每个字符加密后立即比较。可以考虑多次执行,逐个爆破字符。
测试爆破脚本的时候踩了两个坑:
- 对于同一个vm对象只能调用一次
vm_execute
方法,如果重复调用则从第二次开始,只提示"program stop"
而不运行bytecode。(猜测是有一个类似rip
的全局变量指示字节码的地址,运行过一次vm之后这个变量没有重置) - 栈上数据应当从后向前读取。vm执行到后面的时候
pop
没有严格和push
对应,导致堆栈不平衡。栈前面遗留了一些垃圾数据。
from challenge import StackVM
class solveVM(StackVM):
buffer = []
def vm_print(self):
pass
def vm_be(self):
current_char = self.stack[-3]
target_char = self.stack[-2]
if current_char == target_char:
self.buffer = self.input_buffer
else:
self.buffer = self.input_buffer[:-1]
StackVM.__dict__["vm_be"](self)
flag = []
digist_table = "0123456789"
lower_table = "abcdefghijklmnopqrstuvwxyz"
supper_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
brute_table = digist_table + lower_table + supper_table + "_-{}"
while True:
for char in brute_table:
vm = solveVM()
vm.input_buffer = flag
vm.input_buffer.append(char)
vm.vm_execute()
flag = vm.buffer
if len(flag) > 0:
if flag[-1] == "}":
break
print("".join(flag))
#flag{222cccf2-71b6-477b-bcea0f230e0e}