前言
看雪 CTF 的一道题目,感觉做完之后能对 Android 逆向有更进一步的理解,特地码一下。
题目链接:第二题 变形金钢
Android
既然本题是 apk,那直接拖到 jadx 查看源码,很明显就能看到以下逻辑:
private void login(final String str, final String str2, final Handler handler) {
Toast.makeText(this, "登录中。。。", 1).show();
runnable = new Runnable() {
public void run() {
Message obtain = Message.obtain();
StringBuilder stringBuilder = new StringBuilder(str2);
if (str.equals(stringBuilder.reverse().toString())) {
obtain.obj = stringBuilder.toString();
} else {
obtain.what = 1;
}
handler.sendMessage(obtain);
}
};
cachedThreadPool.execute(runnable);
}
看样子很简单,貌似只需要将账号的反序作为密码输入即可通过,但在实际操作过程中发现这是错误的。再仔细观察就能发现 public class MainActivity extends AppCompiatActivity {
这行代码非常奇怪,正常情况下继承的应该是 AppCompatActivity
这个类,二者存在细微的差异,于是继续去看 AppCompiatActivity
的代码:
可以发现谁是李逵,谁是李鬼了。由此我们可以推断真正的校验逻辑在 AppCompaitActivity
这个类的 public native boolean eq(String str);
函数内,可以看到这个函数是一个 native 函数,那么具体的校验逻辑必然在 oo000oo
对应的 so 文件中
So
看到 Android 的 so 文件第一步就是看 JNI_ONLoad
函数:
signed int __fastcall JNI_OnLoad(int a1)
{
int v1; // r8
signed int result; // r0
JNIEnv *v3; // r5
jclass v4; // r6
JNIEnv *v5; // [sp+0h] [bp-18h]
int v6; // [sp+4h] [bp-14h]
int v7; // [sp+8h] [bp-10h]
v7 = v1;
v5 = 0;
if ( !(*(int (**)(void))(*(_DWORD *)a1 + 24))() )
goto LABEL_4;
LABEL_2:
result = -1;
while ( _stack_chk_guard != v6 )
{
LABEL_4:
v3 = v5;
v4 = (*v5)->FindClass(v5, (const char *)off_4010);
dword_4110 = (int)(*v3)->NewGlobalRef(v3, v4);
if ( !v4 || (*v3)->RegisterNatives(v3, v4, (const JNINativeMethod *)off_4014, 1) <= -1 )
goto LABEL_2;
result = 65542;
}
return result;
}
可以发现本题使用的是利用 jni
所提供的 RegisterNatives
方法来动态注册 Native 函数,很明显 off_4014
上存储的就是需要注册的函数的地址,使用 ida 查看发现地址对应是 sub_784()
函数。
算法分析
本题使用的是魔改的 RC4 和替换码表的 Base64 编码,本函数主要实现的功能如下:
- 对输入的字符串先调用一次 RC4 算法
- 再用魔改的 Base64 函数对加密后的字符串进行编码
RC4
先简单介绍 RC4 加密,该加密主要分为以下几个步骤:
- 根据密钥 key 生成 s-box
- 根据 s-box 生成伪随机的密钥流
- 异或按位加密明文
更详细的链接可以参考 经典对称加密RC4分析
下面我们来看程序内部的 RC4 加密,和经典的 RC4 算法一样,程序内部的 RC4 算法也分为这样几个步骤:
- 密钥生成
这一段的代码最为冗长,但其实非常简单,即通过原始的密钥生成 RC4 算法所使用的密钥,大致是去掉 -
后翻转再加上 -
- sbox 生成
下一步是 RC4 算法的 sbox:
- 伪随机密钥流 / 加密明文
加密明文分为两部分,在和 sbox 生产的密钥流进行异或之后,密文会通过一个魔改的 base64 进行输出
- 魔改 Base64
对 Base64 的魔改有两个部分,一是替换码表,二是在每四个字符中,对第一个、第三个字符进行额外的异或处理,具体可以看上图的标注。
- 比较
最后程序会将加密后的输出对内置的输出进行比较,只有每一个字符相同才会认为是通过判断。
Solve
最后编写脚本如下:
import base64
std_base64table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
base64table = "!:#$%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz0123456789\\';"
sbox = [0xf0,0x37,0xe1,0x9b,0x2a,0x15,0x17,0x9f,0xd7,0x58,0x4d,0x6e,0x33,0xa0,0x39,0xae,0x4,0xd0,0xbe,0xed,0xf8,0x66,0x5e,0x0,0xd6,0x91,0x2f,0xc3,0x10,0x4c,0xf7,0xa6,0xc1,0xec,0x6d,0xb,0x50,0x65,0xbb,0x34,0xfa,0xa4,0x2d,0x3b,0x23,0xa1,0x96,0xd5,0x1d,0x38,0x56,0xa,0x5d,0x4f,0xe4,0xcc,0x24,0xd,0x12,0x87,0x35,0x85,0x8e,0x6f,0xc6,0x13,0x9a,0xd3,0xfc,0xe7,0x8,0xac,0xb7,0xe9,0xb0,0xe8,0x41,0xaa,0x55,0x53,0xc2,0x42,0xbc,0xe6,0xf,0x8a,0x86,0xa8,0xcf,0x84,0xc5,0x48,0x74,0x36,0x7,0xeb,0x88,0x51,0xf6,0x7f,0x57,0x5,0x63,0x3e,0xfe,0xb8,0xc9,0xf5,0xaf,0xdf,0xea,0x82,0x44,0xf9,0xcd,0x6,0xba,0x30,0x47,0x40,0xde,0xfd,0x1c,0x7c,0x11,0x5c,0x2,0x31,0x2c,0x9c,0x5f,0x46,0x27,0xc4,0x83,0x73,0x16,0x90,0x20,0x76,0x7b,0xf2,0xe3,0xf3,0x77,0x52,0x80,0x25,0x9,0x26,0x3f,0xc7,0x18,0x1b,0xa3,0xff,0xfb,0xcb,0xa9,0x8c,0x54,0x7a,0x68,0xb4,0x70,0x4b,0xe2,0x49,0x22,0x7e,0xa5,0xb6,0x81,0x9d,0x4e,0x67,0xf1,0xa7,0x3c,0xd9,0x94,0xef,0x32,0x6b,0x1f,0xb1,0x60,0xb9,0x64,0x59,0x1,0xb3,0x7d,0xe0,0x6c,0xad,0x97,0x19,0xb5,0x3a,0xf4,0xd8,0x8d,0x98,0x3,0x93,0x1a,0xdc,0x1e,0x4a,0xc0,0x5a,0xe5,0xd1,0x3d,0x14,0xc8,0x79,0xbd,0x43,0xdb,0x69,0xd2,0x61,0x95,0x9e,0x21,0x45,0x89,0x2b,0xab,0x29,0xa2,0x8b,0x2e,0xd4,0xe,0x62,0xca,0x28,0xda,0x5b,0x72,0x8f,0x99,0x75,0xee,0x78,0xc,0x71,0xbf,0xdd,0xce,0x92,0x6a,0xb2]
encode_flag = " {9*8ga*l!Tn?@#fj'j$\g;;"
def base64decode():
encode_flag1 = []
for i in range(len(encode_flag)):
if i % 4 == 0:
bit = 7
elif i % 4 == 2 and encode_flag[i] != ';':
bit = 0xf
else:
bit = 0
encode_flag1.append(chr(ord(encode_flag[i])^bit))
encode_flag2 = ''.join(encode_flag1).translate(''.maketrans(base64table, std_base64table))
return base64.b64decode(encode_flag2)
def rc4decrypt(s):
i = 0
j = 0
ss = []
for k in range(len(s)):
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
index = (sbox[i] + sbox[j]) % 256
ss.append(chr(s[k] ^ sbox[index]))
print(''.join(ss))
print(rc4decrypt(base64decode()))
# fu0kzHp2aqtZAuY6
补充
.init_array 函数执行了 datadiv_decode5009363700628197108
函数对字符串进行解密,之前在 ida 上直接显示的字符串是加密后的,因此可能部分是不可见的,只有在程序执行后执行该解密函数才会变成可见字符。