一道IOS逆向,之前忘了哪个比赛遇到过一次,当时没有复现,只是草草看看Writeup,知道这个其实并不一定要依赖IOS环境动态调试才能解题,IDA好像可以硬做。
复现一下,以下是题目描述
#小麦饮料。
这里的描述应该说的就是题目名字,首字母合起来就是IPA
不知道IOS的IPA是个什么概念,这里AI一下
IPA包是iOS应用程序的文件格式,代表“iOS App Store Package”。它是苹果公司用于分发和安装iOS应用的压缩文件,类似于Android的APK文件。IPA文件包含了应用的二进制代码、资源文件(如图像和音频)、以及应用的元数据。
把附件的ipa解压获取其中的EasyiOS文件,然后扔到IDA里面开始分析。
找字符串可以找到这里
很好就找到关键函数了,因为要根据sub_100005C24函数的返回结果来选择输出的字符串。
sub_100005C24内有两个关键函数,sub_1000059A8是一个很明显的Base64算法,但是sub_1000057E0的分析有点困难,但是比赛快结束才看的这一题,并且看不出是什么加密算法,就直接放弃了。
我们先从简单的开始分析,这题有个更坑,在程序执行前,有三个初始化函数会执行
double InitFunc_0()
{
__int64 v0; // x8
signed int v1; // w9
int v2; // w13
int v3; // w9
int v4; // w14
__int64 i; // x9
double result; // d0
__int128 v7[2]; // [xsp+0h] [xbp-90h]
__int128 v8; // [xsp+20h] [xbp-70h]
__int128 v9; // [xsp+30h] [xbp-60h]
__int128 v10[2]; // [xsp+40h] [xbp-50h]
__int128 v11[2]; // [xsp+60h] [xbp-30h] BYREF
v0 = 0LL;
v1 = 0;
v10[0] = xmmword_100007EC0;
v10[1] = xmmword_100007ED0;
qmemcpy(v11, " !\"#$%&'()*+,-./0123456789:;<=>?", sizeof(v11));
do
{
v2 = *(v10 + v0);
v3 = v1 + v2 + byte_100007F00[v0 % 5u];
v4 = v3 + 63;
if ( v3 >= 0 )
v4 = v3;
v1 = v3 - (v4 & 0xFFFFFFC0);
*(v10 + v0) = *(v10 + v1);
*(v10 + v1) = v2;
++v0;
}
while ( v0 != 64 );
for ( i = 0LL; i != 64; ++i )
*(v7 + i) = aAbcdefghijklmn[*(v10 + i)];
*aAbcdefghijklmn = v7[0];
*&aAbcdefghijklmn[16] = v7[1];
result = *&v8;
*&aAbcdefghijklmn[32] = v8;
*&aAbcdefghijklmn[48] = v9;
return result;
}
这个函数估计是对Base64的table的有一个变换
void InitFunc_1()
{
__int64 i; // x8
for ( i = 0LL; i != 13; ++i )
aSimplekeyhere[i] ^= 0xA5u;
}
对密钥的修改
void InitFunc_2()
{
__int64 i; // x8
for ( i = 0LL; i != 44; ++i )
byte_10000D897[i] ^= 0x90u;
}
对密文的异或修改。
这个在主函数前的Init机制和Golang有点像,这种在主函数执行前的初始化,如果我们够细心,能够养成在做题时对关键变量进行一个交叉引用的查看的话,应该是可以发现的。
我们先写脚本来对三者进行一个恢复,脚本如下
byte_10000D897 = [0xF2, 0xD5, 0xE8, 0xF1, 0xA9, 0xE9, 0xBB, 0xC8, 0xFC, 0xD1,
0xF2, 0xFC, 0xF5, 0xDA, 0xC0, 0xFC, 0xD2, 0xDA, 0xE9, 0xA5,
0xE2, 0xA0, 0xD1, 0xD6, 0xC0, 0xF5, 0xDA, 0xC1, 0xDB, 0xD5,
0xDF, 0xD4, 0xD3, 0xC1, 0xA6, 0xD4, 0xA2, 0xA3, 0xFA, 0xDF,
0xE0, 0xC2, 0xBB, 0xC8]
for i in range(len(byte_10000D897)):
byte_10000D897[i] ^= 0x90
print(byte_10000D897)
# 输出[69, 120, 97, 57, 121, 43, 88, 108, 65, 98, 108, 101, 74, 80, 108, 66, 74, 121, 53, 114, 48, 65, 70, 80, 101, 74, 81, 75, 69, 79, 68, 67, 81, 54, 68, 50, 51, 106, 79, 112, 82, 43, 88]
aSimplekeyhere = list("SimpleKeyHere")
for j in range(len(aSimplekeyhere)):
aSimplekeyhere[j] = 0xA5 ^ ord(aSimplekeyhere[j])
print(aSimplekeyhere)
# 输出[246, 204, 200, 213, 201, 192, 238, 192, 220, 237, 192, 215, 192]
Base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
v10 = list(range(64))
byte_100007F00 = [0x0D, 0x11, 0x13, 0x17, 0x1D]
v1 = 0
v7 = [0] * 64
for k in range(64):
v2 = v10[k]
v3 = v1 + v2 + byte_100007F00[k % 5]
v4 = v3 + 63
if v3 >= 0: v4 = v3
v1 = v3 - (v4 & 0xFFFFFFC0)
v10[k] = v10[v1]
v10[v1] = v2
v7 = [Base64_table[v10[i]] for i in range(64)]
Base64_table = ''.join(v7)
print(Base64_table)
# 输出NF01ihUKST9q3lnjEBs47k2w5ad+AVHfPezg/CDyxrMLR6GvomIQJOXcpW8ZbutY
其实就是照着伪代码写一遍。
Base64加密的函数代码如下,看不出哪里有魔改
__int64 __usercall sub_1000059A8@<X0>(__int64 result@<X0>, _QWORD *a2@<X8>)
{
__int64 v2; // x20
__int64 v4; // x22
unsigned __int8 v5; // w8
__int64 v6; // x9
unsigned __int64 v7; // x8
__int64 v8; // x8
__int64 v9; // x8
__int64 v10; // x8
int v11; // w10
__int64 v12; // x8
__int64 v13; // x9
unsigned __int64 v14; // x8
__int64 v15; // x8
__int64 v16; // x8
__int64 v17; // x8
v2 = result;
v4 = 0LL;
*a2 = 0LL;
a2[1] = 0LL;
a2[2] = 0LL;
while ( (*(v2 + 23) & 0x80000000) == 0 )
{
v5 = *(v2 + 23) % 3u;
v6 = v2;
if ( v4 == *(v2 + 23) - v5 )
{
v11 = v5 & 3;
if ( ((*(v2 + 23) % 3u) & 3) == 0 )
return result;
v12 = v2;
v13 = v2;
if ( v11 == 1 )
goto LABEL_23;
LABEL_28:
std::string::push_back(a2, aAbcdefghijklmn[*(v13 + v4) >> 2]);
if ( *(v2 + 23) >= 0 )
v16 = v2;
else
v16 = *v2;
std::string::push_back(a2, aAbcdefghijklmn[(16 * *(v16 + v4)) & 0x30LL | (*(v16 + v4 + 1) >> 4)]);
if ( *(v2 + 23) >= 0 )
v17 = v2;
else
v17 = *v2;
std::string::push_back(a2, aAbcdefghijklmn[(4 * *(v17 + v4 + 1)) & 0x7CLL]);
return std::string::push_back(a2, 61LL);
}
LABEL_7:
std::string::push_back(a2, aAbcdefghijklmn[*(v6 + v4) >> 2]);
if ( *(v2 + 23) >= 0 )
v8 = v2;
else
v8 = *v2;
std::string::push_back(a2, aAbcdefghijklmn[(16 * *(v8 + v4)) & 0x30LL | (*(v8 + v4 + 1) >> 4)]);
if ( *(v2 + 23) >= 0 )
v9 = v2;
else
v9 = *v2;
std::string::push_back(a2, aAbcdefghijklmn[(4 * *(v9 + v4 + 1)) & 0x3CLL | (*(v9 + v4 + 2) >> 6)]);
if ( *(v2 + 23) >= 0 )
v10 = v2;
else
v10 = *v2;
result = std::string::push_back(a2, aAbcdefghijklmn[*(v10 + v4 + 2) & 0x3F]);
v4 += 3LL;
}
v7 = *(v2 + 8);
if ( v4 != 3 * (v7 / 3) )
{
v6 = *v2;
goto LABEL_7;
}
v14 = v7 % 3;
if ( !v14 )
return result;
if ( v14 != 1 )
{
v13 = *v2;
goto LABEL_28;
}
v12 = *v2;
LABEL_23:
std::string::push_back(a2, aAbcdefghijklmn[*(v12 + v4) >> 2]);
if ( *(v2 + 23) >= 0 )
v15 = v2;
else
v15 = *v2;
std::string::push_back(a2, aAbcdefghijklmn[(16 * *(v15 + v4)) & 0x30LL]);
std::string::push_back(a2, 61LL);
return std::string::push_back(a2, 61LL);
}
有难度的地方主要在sub_1000057E0函数,代码如下
void __usercall sub_1000057E0(__int64 *a1@<X0>, __int64 *a2@<X1>, _QWORD *a3@<X8>)
{
_BYTE *v6; // x0
unsigned __int64 v7; // x10
_BYTE *v8; // x8
unsigned __int64 v9; // x9
unsigned __int64 v10; // x10
unsigned __int64 v11; // x11
__int64 v12; // x8
__int64 v13; // x11
unsigned __int64 v14; // x12
int v15; // w13
__int64 *v16; // x13
__int64 v17; // x9
__int64 v18; // x12
unsigned __int64 v19; // x23
unsigned __int64 v20; // x22
__int64 i; // x21
unsigned __int64 v22; // x8
__int64 v23; // x9
char v24; // [xsp+7h] [xbp-49h] BYREF
void *v25; // [xsp+8h] [xbp-48h] BYREF
_BYTE *v26; // [xsp+10h] [xbp-40h]
v24 = 0;
sub_10000601C(&v25, 250LL, &v24);
*a3 = 0LL;
a3[1] = 0LL;
a3[2] = 0LL;
v6 = v26;
if ( v26 == v25 )
{
v8 = v26;
}
else
{
v7 = 0LL;
v6 = v25;
do
{
v6[v7] = v7;
++v7;
v6 = v25;
v8 = v26;
v9 = v26 - v25;
}
while ( v7 < v26 - v25 );
if ( v9 )
{
v10 = 0LL;
v11 = 0LL;
do
{
v12 = v6[v10];
v13 = v11 + v12;
v14 = *(a2 + 23);
v15 = v14;
if ( (v14 & 0x80u) != 0LL )
v14 = a2[1];
if ( v15 >= 0 )
v16 = a2;
else
v16 = *a2;
v11 = (v13 + *(v16 + v10 % v14)) % v9;
v6[v10] = v6[v11];
v6[v11] = v12;
++v10;
v6 = v25;
v8 = v26;
v9 = v26 - v25;
}
while ( v10 < v26 - v25 );
}
}
v17 = *(a1 + 23);
v18 = a1[1];
if ( (v17 & 0x80u) != 0LL )
a1 = *a1;
if ( (v17 & 0x80u) != 0LL )
v17 = v18;
if ( v17 )
{
v19 = 0LL;
v20 = 0LL;
for ( i = v17 - 1; ; --i )
{
v22 = v8 - v6;
v19 = (v19 + 1) % v22;
v23 = v6[v19];
v20 = (v20 + v23) % v22;
v6[v19] = v6[v20];
v6[v20] = v23;
std::string::push_back(a3, (*a1 ^ *(v25 + (*(v25 + v20) + *(v25 + v19)) % (v26 - v25))));
if ( !i )
break;
a1 = (a1 + 1);
v6 = v25;
v8 = v26;
}
v6 = v25;
}
if ( v6 )
{
v26 = v6;
operator delete(v6);
}
}
该函数的变量比较多,没什么常量,不仔细看根本看不出来是什么加密算法,如果静态看的话难度应该是有一点的
当时比赛还以为要逆向分析,感觉来不及了。赛后看别人的Writeup说是魔改RC4,看做出来的大部分还是动态调试做出来的,要么就是dump出S盒,要么就是dump出密钥流,参考以下文章
但是其实静态仔细看的话是完全能看的,根据这里频繁出现取模运算,并且还大多是两个变量相加后再取模某个值,其实是完全可以猜测出是RC4加密算法的。我将函数中的变量重命名一下,就更加豁然开朗了
void __usercall sub_1000057E0(__int64 *input@<X0>, __int64 *key@<X1>, _QWORD *a3@<X8>)
{
_BYTE *sbox_copy; // x0
unsigned __int64 i_0; // x10
_BYTE *v8; // x8
unsigned __int64 v9; // x9
unsigned __int64 i_1; // x10
unsigned __int64 j_1; // x11
__int64 tmp_0; // x8
__int64 v13; // x11
unsigned __int64 keylen; // x12
int v15; // w13
__int64 *keycopy; // x13
__int64 v17; // x9
__int64 v18; // x12
unsigned __int64 i_2; // x23
unsigned __int64 j_2; // x22
__int64 i; // x21
unsigned __int64 v22; // x8
__int64 tmp; // x9
char v24; // [xsp+7h] [xbp-49h] BYREF
void *sbox; // [xsp+8h] [xbp-48h] BYREF
_BYTE *v26; // [xsp+10h] [xbp-40h]
v24 = 0;
sub_10000601C(&sbox, 250LL, &v24);
//猜测函数作用是为S盒开辟大小为250的空间,这里也就是RC4的魔改
*a3 = 0LL;
a3[1] = 0LL;
a3[2] = 0LL;
sbox_copy = v26;
if ( v26 == sbox )
{
v8 = v26;
}
else
{
i_0 = 0LL;
sbox_copy = sbox;
do
{
sbox_copy[i_0] = i_0;
++i_0;
//S盒的初始化
sbox_copy = sbox;
v8 = v26;
v9 = v26 - sbox;
}
while ( i_0 < v26 - sbox );
if ( v9 )
{
i_1 = 0LL;
j_1 = 0LL;
do
{
tmp_0 = sbox_copy[i_1];
v13 = j_1 + tmp_0;
keylen = *(key + 23);
v15 = keylen;
if ( (keylen & 0x80u) != 0LL )
keylen = key[1];
if ( v15 >= 0 )
keycopy = key;
else
keycopy = *key;
j_1 = (v13 + *(keycopy + i_1 % keylen)) % v9;
sbox_copy[i_1] = sbox_copy[j_1];
sbox_copy[j_1] = tmp_0;
++i_1;
sbox_copy = sbox;
//根据密钥打乱S盒
v8 = v26;
v9 = v26 - sbox;
}
while ( i_1 < v26 - sbox );
}
}
v17 = *(input + 23);
v18 = input[1];
if ( (v17 & 0x80u) != 0LL )
input = *input;
if ( (v17 & 0x80u) != 0LL )
v17 = v18;
if ( v17 )
{
i_2 = 0LL;
j_2 = 0LL;
for ( i = v17 - 1; ; --i )
{
v22 = v8 - sbox_copy;
i_2 = (i_2 + 1) % v22;
tmp = sbox_copy[i_2];
j_2 = (j_2 + tmp) % v22;
sbox_copy[i_2] = sbox_copy[j_2];
sbox_copy[j_2] = tmp;
std::string::push_back(a3, (*input ^ *(sbox + (*(sbox + j_2) + *(sbox + i_2)) % (v26 - sbox))));
//生成密钥流,并与我们的输入进行异或
if ( !i )
break;
input = (input + 1);
sbox_copy = sbox;
v8 = v26;
}
sbox_copy = sbox;
}
if ( sbox_copy )
{
v26 = sbox_copy;
operator delete(sbox_copy);
}
}
可见算法将S盒的大小从256魔改成250,那么后续的轮数和取模的值,我们猜测也是修改成250,因为它这里没有什么常量,连取模的值都是用变量来表示,不动态调试的情况下,只能猜了。
那么我们的想法当然就是按正常流程修改RC4解密脚本,都将常量256改成250就可以解出答案了,但是实际并不能得到flag,通过运行代码,和别人正确的S盒比较,我发现也是不一样的。
这里感谢A1natas战队的carbofish师傅,这里的问题我也是询问了他,他还帮我用静态分析的方法复现了一下,并告诉了我原因,他这里应该是动态调试看了看哪里出现了问题,出问题的原因还真是之前群友提过的。😭
他后面也写了博客
2024 古剑山网络安全大赛 Reverse 方向 | Carbo
就是说RC4的key数组是有符号的,而在打乱S盒的时候,在取key的值的时候,会用到指令LDRSB
而LDRSB指令用于从内存中将一个8位的字节数据读取到指令中的目标寄存器中。并将寄存器的高位设置成该字节数据的符号位的值(即将该8位字节数据进行符号位扩展,生成32位或者64位字数据)。
题目这里是arm64,X开头是arm中64位的寄存器,所有应该将数据字节扩展到64位数据。说实话,如果没学过arm汇编,不动态调试,真的很难发现问题。
也就是说倘若取出key的值是负数,就会在高位进行字节扩展,填充0xff,这里就会填充7个0xff,如果是取模256,那显然是不用在乎这个的,可以用下列脚本验证
data=0xffffff66%256
print(data)
#输出102
data=0x66%256
print(data)
#输出102
data=0xfffffff5%250
print(data)
#输出35
data=0xf5%250
print(data)
#输出245
因为在对256取模的时候,前面扩展无论多少个0xff,最后高位0xff的部分都是256的倍数,我们只用关注低位部分,比如0xffffff00对256取模是0。而我们正常写RC4的解密代码,直接将对256取模改成250,没有字节扩展这一步,就会导致数据处理有错误。
这里直接照搬carbofish师傅的脚本
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include "defs.h"
uint8 sbox[250] = {0};
//初始化s表
void init_sbox(char *key, int klen) {
uint64 i, k;
int64 tmp;
for (i = 0; i < 250; i++) {
sbox[i] = i;
}
uint64 j = 0;
for (i = 0; i < 250; i++) {
tmp = (uint8) sbox[i];
int64 v13 = j + tmp;
char keyVal = key[i % klen];
int bitFlag = keyVal & 0x80;
uint64 covertVal = 0;
if (bitFlag) {
// 负数
covertVal = 0xFFFFFFFFFFFFFF00 | keyVal;
} else {
covertVal = keyVal;
}
//处理负数的部分主要在这里,如果是负数就进行字节扩展
j = (v13 + covertVal) % 250;
sbox[i] = sbox[j];
sbox[j] = tmp;
}
}
//加解密函数
void enc_dec(char *key, unsigned char *data, int slen, int klen) {
int i, j, k, R, tmp;
init_sbox(key, klen);
// for (int l = 0; l < 250; ++l) {
// printf("0x%x, ", (uint8) sbox[l]);
// }
j = k = 0;
for (i = 0; i < slen; i++) {
j = (j + 1) % 250;
k = (k + sbox[j]) % 250;
tmp = sbox[j];
sbox[j] = sbox[k];
sbox[k] = tmp;
R = sbox[(sbox[j] + sbox[k]) % 250];
data[i] ^= R;
}
}
int main() {
unsigned char key[13] = {0xF6, 0xCC, 0xC8, 0xD5, 0xC9, 0xC0, 0xEE, 0xC0, 0xDC, 0xED, 0xC0, 0xD7, 0xC0};
unsigned char data[33] = {0xf1,0x0a,0x19,0x2a,0x76,0xf6,0x35,0xcf,0x0d,0x87,0x48,0x0d,0x47,0x49,0xd8,0xa4,0x27,0x01,0x82,0x1d,0x33,0x1d,0x0d,0x66,0x97,0x3b,0x66,0x58,0xc3,0xf5,0xe2,0xc6,0xf6};
enc_dec((char *)key, data, 33, 13);
for (int i = 0; i < 33; ++i) {
printf("%c", data[i]);
}
return 0;
}
// flag{45_4_105_r3v3r51n6_b361nn3r}
所有flag就是flag{45_4_105_r3v3r51n6_b361nn3r}
当然这里我一开始怀有疑问,首先按解题脚本的意思,key数组从unsigned char转到char*,这里超出char数据类型的范围,发生了溢出,所以才导致有符号的key数组中的数据全部会变成负数。
但是在正常程序中是从哪里开始溢出的呢
查看key的字符串,数据类型是char,那么在初始化时进行异或运算超出char数据类型的范围,猜测就是在初始化函数当中发生了溢出,然后进入RC4加密的函数将key数组转化成了__int64的类型,依旧是有符号的。