湖南省网络攻防邀请赛 RE 题解
ez_apkk
解题过程:
-
将apk拖入jadx,查看MainActivity,发现是简单RC4加密,密钥是“55667788”,最后再将加密结果+1
public String Encrypt(String plainText, String key) { int[] S = new int[256]; byte[] K = new byte[256]; char[] cArr = {'\n', '+', 181, '*', 225, ':', 244, 147, '\'', 182, 'J', 250, '-', 25, 135, 4, 188, '-', 230, '[', 'Q', '5', 'c', 22, 220, 25}; char[] cArr2 = new char[26]; Character[] keySchedul = new Character[plainText.length()]; KSA(S, K, key); PRGA(S, keySchedul, plainText.length()); //前面是RC4算法初始化 for (int i = 0; i < plainText.length(); i++) { cArr2[i] = (char) ((plainText.charAt(i) ^ keySchedul[i].charValue()) + 1);//这行代码是RC4加密当前字节后 +1 } for (int i2 = 0; i2 < 26; i2++) { if (cArr[i2] != cArr2[i2]) { return "wrong!!!"; } } return "right"; }
解密脚本
#include <stdio.h>
void rc4_init(unsigned char* s, unsigned char* key, unsigned long Len_k)
{
int i = 0, j = 0;
char k[256] = { 0 };
unsigned char tmp = 0;
for (i = 0; i < 256; i++) {
s[i] = i;
k[i] = key[i % Len_k];
}
for (i = 0; i < 256; i++) {
j = (j + s[i] + k[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
}
void rc4_crypt(unsigned char* Data, unsigned long Len_D, unsigned char* key, unsigned long Len_k) //加解密
{
unsigned char s[256];
rc4_init(s, key, Len_k);
int i = 0, j = 0, t = 0;
unsigned long k = 0;
unsigned char tmp;
for (k = 0; k < Len_D; k++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
tmp = s[i];
s[i] = s[j];
s[j] = tmp;
t = (s[i] + s[j]) % 256;
Data[k] = Data[k] ^ s[t];
}
}
void main()
{
unsigned char key[] = "55667788";
unsigned long key_len = sizeof(key) - 1;
unsigned char data[] = {'\n', '+', 181, '*', 225, ':', 244, 147, '\'', 182, 'J', 250, '-', 25, 135, 4, 188, '-', 230, '[', 'Q', '5', 'c', 22, 220, 25};
for (int i = 0; i < sizeof(data); i++)
{
data[i]-=1;
}
rc4_crypt(data, sizeof(data), key, key_len);
for (int i = 0; i < sizeof(data); i++)
{
printf("%c", data[i]);
}
printf("\n");
return;
}
运行得到flag:flag{a9k_1s_enjoy_ha6py!!}
ezdriver
解题过程:
-
IDA打开 R3-CTF.exe,发现程序调用了驱动文件 Driver_ctf.sys,用
deviceIoControl
函数来调用驱动程序里的功能。发送的控制代码为 0x222000 -
逆向分析Driver_ctf.sys,发现加密代码在函数
sub_1400010A0
,其中里面调用的函数sub_140001590
为对称加密算法,对加密数据byte_140003000
再执行一次此对称加密算法即可得到原数据。 -
但是还有一段加密算法对数据进行了加密,这里使用了求模运算,只能单字节逐个爆破
for ( j = 1; j < 32; ++j ) v12[j - 1] ^= ((unsigned __int8)v12[j - 1] % 0x12u + v12[j] + 5) ^ 0x41;
-
我这里是使用递归算法来遍历每个可能的路径
解密算法:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
unsigned char enc[] =
{
0xA2, 0x23, 0x62, 0x6B, 0xDD, 0x75, 0x72, 0xD1, 0x7A, 0x88,
0x34, 0xD0, 0x6C, 0x23, 0xCB, 0x39, 0xAC, 0xDA, 0x76, 0x6C,
0x2E, 0x9B, 0x95, 0xC5, 0x79, 0x89, 0x39, 0x3C, 0x83, 0xC3,
0xDA, 0x1D, 0
};
void sub_140001590(unsigned int *a1, int a2)
{
int j; // [rsp+4h] [rbp-44h]
int i; // [rsp+8h] [rbp-40h]
int v4; // [rsp+Ch] [rbp-3Ch] BYREF
int *v5; // [rsp+10h] [rbp-38h]
int v6; // [rsp+18h] [rbp-30h]
char v7[16]; // [rsp+20h] [rbp-28h]
v7[1] = a2 ^ 0x24;
v7[0] = a2 ^ 0x6A;
v7[8] = a2 ^ 0x1A;
v7[2] = a2 ^ 0x35;
v7[9] = a2 ^ 0x8D;
v7[3] = a2 ^ 0x23;
v7[11] = a2 ^ 0x9A;
v7[4] = a2 ^ 0xCA;
v7[5] = a2 ^ 0x4B;
v7[6] = a2 ^ 0x21;
v7[7] = a2 ^ 0x35;
v7[10] = a2 ^ 0x91;
v7[12] = a2 ^ 0x2C;
v7[13] = a2 ^ 0xC2;
v7[14] = a2 ^ 0x92;
v7[15] = a2 ^ 7;
for ( i = 0; i < 4; ++i )
{
v5 = &v4;
v4 = *a1 ^ a2;
for ( j = 0; j < 4; ++j )
{
v5 = (int *)((char *)v5 + 1);
*((char *)v5 - 1) ^= v7[15 - (((unsigned __int8)i + (unsigned __int8)j) & 0xF)] | ((unsigned __int8)j << j) | j | 4;
}
v6 = ~a2 ^ v4;
*a1++ = v6;
}
}
void solver(int index)
{
for(int i = index;i >= 0;i--)
{
unsigned char backup = enc[index - 1];
unsigned char t = enc[i] + 5;
for(int j = 18;j < 144;j+=0x12)
{
for(int k = 0;k < 0x12;k++)
{
//此处存在剪枝策略,若不符合限制条件则不会遍历该节点及其所有子节点
if(((k + t) ^ 0x41 ^ (k + j)) == enc[i - 1])
{
enc[i - 1] = k+j;
solver(index-1);
enc[i-1] = backup; //状态回溯
}
}
}
if(index == 0)
printf("%s\n",enc); //输出符合条件的路径
}
}
int main()
{
unsigned int *p = (unsigned int*)enc;
int v6 = 0;
do{
sub_140001590(p, v6++); //对称算法解密
p += 4;
}while(v6 < 2);
//递归算法遍历所有可能的路径
solver(31);
}
运行后发现有四个结果,很明显第二个是正确的结果
blag{zcswelsvyioostfzjskygmojew}
flag{zcswelsvyioostfzjskygmojew}
olag{zcswelsvyioostfzjskygmojew}
墏g{zcswelsvyioostfzjskygmojew}
崅g{zcswelsvyioostfzjskygmojew}
分割线
有朋友说ezdriver讲的不够细,特意在此处补充一些内容
-
DeviceIoControl
函数在哪?在
R3-CTF.exe
的main_0
函数里,有个函数sub_14003EE10
,这个函数就是对DeviceIOControl
的调用。程序存在LLVM混淆,所有反编译出来的伪代码有些奇怪,可以用工具去混淆。(实际上程序的逻辑并不复杂,即使存在混淆,分析的难度也不大) -
在这里就借着上一个问题,给大家讲解一下
R3-CTF.exe
的主函数逻辑程序首先执行
CreatFileA
来创建IO设备,如果创建IO设备成功则获取用户输入的flag,然后执行DeviceIOControl
函数来调用sys驱动的功能,输出驱动程序返回的信息。伪代码如下(涉及Windows API,遇到看不懂的函数可以自己去微软官方文档中查询)int main() { char outBuffer[60]; char inBuffer[60]; HANDLE hDevice = CreateFileA("\\\\.\\DeviceNONO", 0xC0000000, 0, 0i64, 3u, 0x80u, 0i64); if(hDevice) { memset(outBuffer, 0, 30); memset(inBuffer, 0, 32); printf("请输入flag:"); char ch = 0; int index = 0; while(ch != '\n') { ch = getchar(); inBuffer[index++] = ch; } int returnedBytes = 0; DeviceIOControl(hDevice, 0x222000, inBuffer, 32, outBuffer, 32, &returnedBytes, 0); CloseHandle(hDevice); printf("%s\n", outBuffer); system("pause"); return 0; }else { printf("打开设备失败了\n"); return 0; } }
当然这不是程序真实的代码,只是伪代码。
-
为什么我运行
R3-CTF.exe
只有个黑框框一闪而过?黑框框一闪而过是因为如果创建IO设备失败就退出程序,中间没有暂停。为什么创建IO设备失败了呢?
要搞清楚这一点,首先需要知道Windows中驱动是如何加载的。大部分驱动是在系统启动时加载的,相信很多人在cmd中运行
R3-CTF.exe
时看到了 **打开设备失败了 ** 这句话。这是因为与之配套的Driver_ctf.sys
驱动文件并没有被系统加载。如果想知道驱动如何安装可以自己去搜索引擎搜索,这里就不展开讲了。 -
驱动程序是如何执行加密过程的?
了解过Windows GUI编程的应该知道,Windows窗体有个叫消息机制的东西,驱动设备控制和它有些类似。调用方通过
DeviceIoControl
函数和参数中的控制代码dwIoControlCode
来让驱动设备执行相应的操作并返回结果。而驱动会用到回调函数来处理调用方发送的消息。在
DriverEntry
函数中,我们看到里面的一个子函数指定了消息处理回调函数,里面还有驱动被关闭/链接时的回调函数。if ( v3 >= 0 ) { DeviceObject->Flags |= 4u; a1->MajorFunction[0] = (PDRIVER_DISPATCH)sub_140001050; a1->MajorFunction[2] = (PDRIVER_DISPATCH)sub_140001000; a1->MajorFunction[14] = (PDRIVER_DISPATCH)sub_1400010A0; //sub_1400010A0 就是消息处理回调函数 return 0i64; }
我们跟进
sub_1400010A0
函数就能知道他的消息处理机制了。这是他的主要逻辑,当控制代码为 0x222000 时就会执行加密,而R3-CTF.exe
程序中发送的控制代码就是0x222000if ( *(_DWORD *)(v10 + 24) == 0x222000 ) { DbgPrint("已经输入\n"); memset(v12, 0, sizeof(v12)); for ( i = 0; i < v8; ++i ) v12[i] = Format[i]; for ( j = 1; j < 32; ++j ) v12[j - 1] ^= ((unsigned __int8)v12[j - 1] % 0x12u + v12[j] + 5) ^ 0x41; v6 = 0; a1a = v12; do { sub_140001590(a1a, v6++); a1a += 16; } while ( v6 < 2 ); for ( k = 0; k < 32; ++k ) { if ( v12[k] == byte_140003000[k] ) ++v7; } DbgPrint((PCSTR)"加密完毕\n"); if ( v7 == 32 ) { strcpy(Format, "flag is you input"); a2->IoStatus.Information = 18i64; DbgPrint("成功\n"); } else { strcpy(Format, "wrong"); a2->IoStatus.Information = 6i64; DbgPrint("失败\n"); } }
-
为什么
sub_140001590
函数是对称加密算法?你是如何写出解密算法的?-
问题一
一开始不确定是不是对称加密算法,用IDA把函数的代码拷下来,然后自定义了几个16字节大小的数据,经过调试发现对数据执行两次
sub_140001590
函数的话,数据的数值是不变的,所以推断出sub_140001590
函数是对称加密算法。 -
问题二
看程序的加密流程,对称加密很好解,但中间那段加密有些复杂:既使用了异或运算又使用了取模运算,并且数据中第i个字节的加密结果与第i+1个字节有关联。程序是从第1个字节加密到第31个字节,最后一个字节未加密。逆向算法则是从倒数第二个字节逆解密到第一个字节。
v12[j - 1] = ((v12[j - 1] % 0x12u + v12[j] + 5) ^ 0x41) ^ v12[j-1];
这种情况下无法直接用一个固定的运算表达式来得到v12[j-1]的初始值,也就是加密前的结果,只能用枚举的方法来爆破。但因为存在取模运算
v12[j - 1] % 0x12u
,结果很可能是多解。比如倒数第二个字节的解就可以是字符 'r', 's', 'v', 'w';它们都满足方程
v12[31 - 1] = ((v12[31 - 1] % 0x12u + v12[31] + 5) ^ 0x41) ^ v12[31-1];
又因为倒数第三个字节的值和倒数第二个字节的值存在关联,若是正解则必满足方程
v12[30 - 1] = ((v12[30 - 1] % 0x12u + v12[30] + 5) ^ 0x41) ^ v12[30-1];
,以此类推,若能一直解密到最后一个字节,则输出该路径。若其中某个解不符合条件,则抛弃此解。用上面的例子,倒数第二个字符可以是 r,s,v,w;但是只有s和w符合条件,所以r和v被剔除,递归算法会继续遍历s和w两个解下的路径。
-