前言
最近无聊,想找点事情做,刚好又在学习安卓逆向,又偶尔在玩这个游戏,拿来练练手也挺好。写的不是很好,请各位看官看个热闹就好。
使用工具
- Fiddler 任意版本 、jadx、IDA
- 雷电模拟器
- 任意一款编程工具
教程内容
打开游戏,随便登录个账号密码抓个登录包。
可以发现返回内容是没经过加密,但是请求数据是经过加密了的
寻找加密过程
先看看APK是否加壳了
发现游戏是“易盾”的壳,这里我就直接脱壳,把拖出来的DEX直接丢进jadx里,全局搜索提交地址的部分信息 sdk/login.do 试着找出关键登录函数
继续跟进搜索 USERNAME_PWD_LOGIN_URL 被谁调用了
可以看到只有一个地方有使用
可以看到 他在这里面找就大概率能找到登录封装过程了。hashmap就是他的请求内容了,我们继续跟进看 loginRequestTask
到了这里,我们看到了,后面的 else 段,有个解密,可是我们并没有发现返回值是加密的,所以我们先不管,我们继续跟进到 asyncPostNew
继续跟进,这里面还是没有发现我们要的加密函数
到了这里,我们发现了一个处理params的函数,看到这里,我们就知道,加密函数一定到这里面,赶紧继续
发现了,原来是把map转成json 最后 进行一次Aes加密,既然是AES加密,那就会有个Key,我们现在就是要找到Key,我们继续跟进。
到了这里,发现,他的加密都在JNI层,这时候,我们就得用到IDA了,那我们在lib/armeabi-v7a里找到libSocketHelper.so 拖入 IDA里
还是很好看的,这里我们就得到了Key还有个IV值,我们用易语言解密看看参数,到底有啥数据
把参数带进入,运行,成功解密出来了数据,但是从参数里,我们知道了password是加密的,那我们还得继续,我们继续回到 userNamePwdLogin 我们看看是哪里调用了这个函数
最后,我们来到了这里,也发现了,password 参数 是使用DES加密,我们还是和上面一样,继续根据,寻找Key
DES加密又是走的JNI层,我们继续看看
这里说明了 Key 为 leiting 然后跟进去就会发现
int __fastcall DesByJ::encodeAndHexToByte(int *a1, int a2, const char *a3)
{
const char *v6; // r5
size_t v7; // r0
int v8; // r8
size_t v9; // r0
int v10; // r11
size_t v11; // r0
int v12; // r5
size_t v13; // r0
int v14; // r0
int v15; // r9
int v16; // r6
int v17; // r5
int v18; // r0
int v19; // r10
int v20; // r0
int v21; // r0
int v22; // r1
int v23; // r5
int v24; // r0
const char *v25; // r2
int v26; // r5
int v27; // r0
int v28; // r5
int v29; // r9
int v31; // r0
int v32; // r8
int v33; // [sp+4h] [bp-24h]
int v34; // [sp+8h] [bp-20h]
if ( a2 && (*(int (__fastcall **)(int *, int))(*a1 + 656))(a1, a2) >= 1 )
{
v6 = (const char *)(*(int (__fastcall **)(int *, int, _DWORD))(*a1 + 676))(a1, a2, 0);
v7 = strlen(v6);
v8 = (*(int (__fastcall **)(int *, size_t))(*a1 + 704))(a1, v7);
v9 = strlen(v6);
(*(void (__fastcall **)(int *, int, _DWORD, size_t, const char *))(*a1 + 832))(a1, v8, 0, v9, v6);
v10 = (*(int (__fastcall **)(int *, const char *))(*a1 + 24))(a1, "com/leiting/sdk/SocketHelper");
v11 = strlen(a3);
v12 = (*(int (__fastcall **)(int *, size_t))(*a1 + 704))(a1, v11);
v13 = strlen(a3);
(*(void (__fastcall **)(int *, int, _DWORD, size_t, const char *))(*a1 + 832))(a1, v12, 0, v13, a3);
v14 = (*(int (__fastcall **)(int *, int, const char *, const char *))(*a1 + 452))(
a1,
v10,
"getKey",
"([B)Ljava/security/Key;");
v33 = v12;
v15 = _JNIEnv::CallStaticObjectMethod(a1, v10, v14, v12);
v16 = (*(int (__fastcall **)(int *, const char *))(*a1 + 24))(a1, "javax/crypto/Cipher");
v17 = (*(int (__fastcall **)(int *, int, const char *, const char *))(*a1 + 452))(
a1,
v16,
"getInstance",
"(Ljava/lang/String;)Ljavax/crypto/Cipher;");
v18 = (*(int (__fastcall **)(int *, const char *))(*a1 + 668))(a1, "DES/ECB/PKCS5Padding");
v19 = _JNIEnv::CallStaticObjectMethod(a1, v16, v17, v18);
v20 = (*(int (__fastcall **)(int *, int, const char *, const char *))(*a1 + 132))(
a1,
v16,
"init",
"(ILjava/security/Key;)V");
v34 = v15;
_JNIEnv::CallVoidMethod(a1, v19, v20, 1, v15);
v21 = (*(int (__fastcall **)(int *))(*a1 + 60))(a1);
v22 = *a1;
if ( v21 )
{
(*(void (__fastcall **)(int *))(v22 + 64))(a1);
(*(void (__fastcall **)(int *))(*a1 + 68))(a1);
v23 = (*(int (__fastcall **)(int *, const char *))(*a1 + 24))(a1, "java/lang/Exception");
v24 = *a1;
v25 = "Des init fail!!";
}
else
{
v27 = (*(int (__fastcall **)(int *, int, const char *, const char *))(v22 + 132))(a1, v16, "doFinal", "([B)[B");
v28 = _JNIEnv::CallObjectMethod(a1, v19, v27, v8);
if ( !(*(int (__fastcall **)(int *))(*a1 + 60))(a1) )
{
v29 = v33;
if ( v28 )
{
v31 = (*(int (__fastcall **)(int *, int, const char *, const char *))(*a1 + 452))(
a1,
v10,
"byteArr2HexStr",
"([B)Ljava/lang/String;");
v26 = _JNIEnv::CallStaticObjectMethod(a1, v10, v31, v28);
if ( (*(int (__fastcall **)(int *))(*a1 + 60))(a1) )
{
(*(void (__fastcall **)(int *))(*a1 + 64))(a1);
(*(void (__fastcall **)(int *))(*a1 + 68))(a1);
v32 = (*(int (__fastcall **)(int *, const char *))(*a1 + 24))(a1, "java/lang/Exception");
(*(void (__fastcall **)(int *, int, const char *))(*a1 + 56))(a1, v32, "Encode byteArr2HexStr fail !!");
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v32);
}
}
else
{
v26 = 0;
}
goto LABEL_9;
}
(*(void (__fastcall **)(int *))(*a1 + 64))(a1);
(*(void (__fastcall **)(int *))(*a1 + 68))(a1);
v23 = (*(int (__fastcall **)(int *, const char *))(*a1 + 24))(a1, "java/lang/Exception");
v24 = *a1;
v25 = "please check input argument, last block incomplete in decryption";
}
(*(void (__fastcall **)(int *, int, const char *))(v24 + 56))(a1, v23, v25);
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v23);
v26 = 0;
v29 = v33;
LABEL_9:
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v16);
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v10);
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v29);
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v34);
(*(void (__fastcall **)(int *, int))(*a1 + 92))(a1, v19);
return v26;
}
return 0;
}
以上的伪代码 还是比较好分析的,既然知道了加密方法 就可以写程序跑一下了。
我们随便输入账号密码,试试
请求成功了,返回值也是正确的,我们使用一个正确的账号密码试试看
这里,我们就奇怪了,为啥是加密的,试了几次,只要账号密码是正确的,都是加密的,别急,还记得 loginRequestTask 函数中,最后请求结果会取出 data 字段,然后使用AES解密,我们试试看。
测试结果,也是和我们预想的一样
返回关键信息
表单名称 | 值 | 说明 |
---|---|---|
sid | v6dy672w | 游戏ID |
token | f2670b7b30eededf25af7c94ccb6a73e | 登录令牌 |
登录成功后,还会二次令牌登录游戏,这里我就不演示了,大家可以自己尝试一下
写在最后
本篇文章仅供学习交流,提供一个学习思路。致谢所有为逆向工作做出贡献的所有大佬。