android bp文件_[Android]恋恋APK登录协议分析

★推荐关注本公众号!会有更多精彩内容等着大家!本期邀请链接藏在文章内哦!!

★(点击上面“飘云阁”即可关注)

[Android]恋恋APK登录协议分析

1.1 简介

本文分析一款恋恋的交友APK。具体是分析它登录时是如何把信息加密,然后发送到服务器的。本文使用到的工具如下:

  • JEB

  • IDA(分析so文件)

  • Fiddler(抓包)

  • 任意一款Android模拟器/手机

dfd68969f0d60d6314474d3663bc6d1e.png

1.2 抓包

1)设置Fiddler


首先使用抓包工具Fiddler,抓下此APK登录时发送的数据包,设置如下:

91c12af38cde4cee59be9185a7d88840.png

c8f18f6b68bfddce69f67f16877b69f5.png

2)设置模拟器网络

7b866b4646b4a592fd0fd806e8db0a07.png

3)开始抓包

登录后的封包(用户名为123456789 密码为!@#$%)

85c8e1055de344dc1930d27b15990b6d.png

之后的分析就从这个数据包展开。

1.3 寻找切入点

从上一节的封包中,我们看到了请求的url(POST后面跟着的那串),还有加密后的用户名和密码。下面我们就使用url字符串(http://mob.imlianai.com/call.do?cmd=mobileUser.login)搜索,定位到关键点。

打开JEB,把APK拖入,然后切换到字符串。搜索上面抓到的url(http://mob.imlianai.com/call.do?cmd=mobileUser.login)其实我们这里只需要搜索mobileUser.login 就行了。

1)双击找到的字符串,来到Smali界面。

164dd50694ed4dd5591fb3a3dec2dc59.png

2)点击任意一行,按TAB键,到java代码界面:

9fb8f099c9858062161043e9343d7532.png

这里是switch语句,主要应该是根据参数来选择操作的连接地址。我们往下找找。看看有没有什么我们需要的。

在下面很多地方,我们都发现了类似这样的代码:

JSONObject v1_1 = new JSONObject();   //创建一个构建JSON字符串的对象
v1_1.put("xxxx", xxxx); //往里面加入key/value形式的键值对
v1_1.put("xxxx", xxxx);
v0 = com.a.a.a.f.a.a(v1_1.toString()).getBytes(); //com.a.a.a.f.a.a(v1_1.toString()) 就是具体的加密逻辑了

由此可见,“com.a.a.a.f.a.a()”这个方法就是具体的加密逻辑了。

进入到“com.a.a.a.f.a.a()”方法实现位置,具体代码如下:

public class a {
private static final byte[] a;
private static final IvParameterSpec b;
private static String c;
private static Key d;

static {
a.a = new byte[]{1, 2, 3, 4, 5, 6, 7, 8};
a.b = new IvParameterSpec(a.a);
a.c = "hqi/FjjcBxA=";
a.d = null;
}

public static String a(String arg1) { //这就是加密的方法体
return Jni.getInstance().encryptString(arg1);
}
}

观察上面的代码,发现其又调用了Jni中的encryptString方法。把json字符串传递过去。我们照样双击这个方法。得到如下代码:

public String encryptString(String arg9) {
String v0_1;
if(arg9 == null || arg9.length() == 0) { // 如果什么都没输入的话,就把v0_1置为空串,返回。否则则进行加密操作
v0_1 = "";
}
else {
String v2 = this.encode(arg9); //调用本类中的encode方法。这个方法主要是把字符串的每个字符的字节码转换成16进制形式。例如 ‘a’ 的ASCII是[color=#333333]97,经过这个方法后,就变成了61[/color]
int v3 = v2.length();
StringBuffer v4 = new StringBuffer();
if(v3 < 500) { //如果v2的长度小于500.则执行下面的逻辑。否则执行else中的。我们现在就只看下面的逻辑,先不看else中的逻辑
v4.append(this.getEncryptString(v2, true)); //调用this.getEncryptString(v2, true)加密。具体细节我们后面说
}
else {
int v0;
for(v0 = 1; v0 < v3 / 500 + 1; ++v0) {
if(v3 % 500 == 0 && v0 == v3 / 500) {
v4.append(this.getEncryptString(v2.substring((v0 - 1) * 500, v0 * 500), true));
break;
}

v4.append(this.getEncryptString(v2.substring((v0 - 1) * 500, v0 * 500), false));
}

if(v3 % 500 == 0) {
goto label_15;
}

v4.append(this.getEncryptString(v2.substring((v0 - 1) * 500, v3), true));
}

label_15:
v0_1 = DESencryption.getEncString(v4.toString(), this.getEncryptString("a", true).substring(0, 8)); //使用DES对加密后的数据再进行加密。DES的key是一个固定的值。因为用到了getEncryptString()方法,所以等分析完这个方法我们再提
}
return v0_1;

}

// 本期邀请注册链接:aHR0cHM6Ly93d3cuY2hpbmFweWcuY29tL2hvbWUucGhwP21vZD1pbnZpdGUmaWQ9NDc0MCZjPWswY3Vicw==

可以清楚的看到这里由两个关键方法构成:

  • this.getEncryptString()

  • DESencryption.getEncString() 

切入点已经被找到,下一节重点分析它们。

1.4 算法分析

1.4.1 this.getEncryptString()

this.getEncryptString()代码如下:

private native String getEncryptString(String arg1, boolean arg2) {}  //native为原生态方法,一般是调用C++、C语言代码

上面代码是通过JNI调用C++或者C语言代码。既然这里是通过JNI调用,那么我们怎么知道它调用的是哪个so文件呢?在这个类上面的static静态代码块中声明出来了。如下:

static {
Jni.hexString = "0123456789ABCDEF"; //这个先不管。这个是用于上面说的encode方法把字符串的每个字符的字节码转换成16进制形式的。
System.loadLibrary("jni"); //这个就是加载so的库了。so的名字是libjni.so。字符串jni前面再加上lib。
}

在APK中的\lib\armeabi下有个libjni.so文件。我们现在把它拖入IDA中。

在IDA载入完成后,进入Exports中。如果没有Exports视图,则需要进行如下操作就行了。Exports中会显示所有的导出函数

496326f1cf1ef76119a7c984416f8b69.png

3fe42b6e2d64808b46a715afecbcc227.png

因为供Java调用的API有个明显的特征:Java_包名_方法名。我们Ctrl+F搜索下Java开头的。很幸运,这个里面只有一个。而且发现方法名也是和Java中声明的Native方法名是一样的。

14f82413f74ac6cb78f3516e02df11a4.png

双击进去吧。就进入了汇编页面。汇编代码页面我就不贴了。直接按F5变成C代码,我们再分析吧。总体分析如下。

int __fastcall Java_com_jni_Jni_getEncryptString(_JNIEnv *a1, JNIInvokeInterface *a2, int inputStr, int inputBool) //你们一点进去可能方法定义不是这样的。你只需要导入下jni.h就行。具体操作,在第7点中给出
{
_JNIEnv *v4; // r5@1
int str; // r4@1
int v6; // r2@1
const char *v7; // r7@1
size_t v8; // r4@1
_JNIEnv *v9; // r0@2
char *v10; // r1@2
jstring (__cdecl *v11)(JNIEnv *, const char *); // r3@2
int result; // r0@6
char *s; // [sp+0h] [bp-828h]@1
int v14; // [sp+4h] [bp-824h]@1
char dest; // [sp+Ch] [bp-81Ch]@3
int v16; // [sp+80Ch] [bp-1Ch]@1

v4 = a1;
str = inputStr;
v14 = inputBool;
v16 = _stack_chk_guard;
g_env = a1;
s = (char *)initAddStr(); // 初始化一个字符串。待会我们再分析
v7 = (const char *)jstringTostring((int)v4, str, v6);// 调用JNI的方法,把Java中的String变成C中的char *
v8 = strlen(s); // 求出初始化字符串的长度
if ( strlen(v7) + v8 <= 0x7FF ) // 转成C的inputStr的长度和s的长度<0x7ff(2047)如果小于则拼上s.否则不拼
{
memset(&dest, 0, 0x7FFu); // 往dest这个地址填充0x7FF个0
strcat(&dest, v7); // 这里是把v7的值,也就是inputStr转成Char的值赋给dest
if ( v14 ) // 第2个传参也就是inputBool为true的时候,就在后面跟上初始值s。否则不跟
strcat(&dest, s);
v9 = v4; // v9 = JNIEnv
v10 = &dest;
v11 = v4->functions->NewStringUTF; // v11 = NewStringUTF:把C的char* 转换成Java中的String
}
else
{
v9 = v4;
v10 = (char *)v7;
v11 = v4->functions->NewStringUTF; // v11 = NewStringUTF:把C的char* 转换成Java中的String
}
result = ((int (__fastcall *)(_JNIEnv *, char *))v11)(v9, v10);// 调用NewStringUTF方法,把v10转换成String
if ( v16 != _stack_chk_guard )
_stack_chk_fail(result);
return result;
}

此文件在java_home/jdk/lib下。当然你直接导会报错。我们需要修改一下。具体修改方法百度下吧。导入操作如下:

346088055537f5c7fb84d1496e302af9.png

现在我们逐步来分析下。

1)分析具体参数 

首先看方法声明中的参数。“int __fastcall Java_com_jni_Jni_getEncryptString(_JNIEnv *a1, JNIInvokeInterface *a2, int inputStr, int inputBool)。”

参数列表函数如下: 

  • a1:这个参数是固定的。传入的是Dalvik虚拟的函数表。具体可参考 https://www.cnblogs.com/gavanwanggw/p/6907893.html

  • a2:这个参数也是固定的。传入的是正在调用这个方法的类。

  • inputStr:这个是函数的参数,也就是java传递过来的参数,因为在Java中第一个参数传的是String,而C中没有String。所以这个应该是char*指针。

  • inputBool:这个同样是Java传递过来的参数。在Java中传的是boolean。C中没有。所以用int代替。

2)分析s = (char *)initAddStr(); 

int initAddStr()
{
int v0; // r0@2
int v1; // r2@2

if ( !isInit ) //这里是如果初始化一次了,就不需要再执行了。也就是这里只会执行一次。
{
v0 = initInflect((int)jniStr); //在第10点中分析
key = jstringTostring((int)g_env, v0, v1); //调用方法,把java的String变成C语言中的char*
isInit = 1;
}
return key;
}

3)分析initInflect((int)jniStr);

参数为jniStr。那么jniStr是什么呢?我们双击下jniStr。发现如下:

3c9f6338d773e264abc2260aee2c6d10.png

那么,jniStr = “/key=i im lianai” 。接下来我们看看initInflect方法的内部吧:

int __fastcall initInflect(int a1)
{
int *v1; // r5@1
int v2; // r0@1
bool v3; // zf@1
int v4; // r7@1
int v5; // r0@1
int (__fastcall *v6)(int *, const char *); // r3@3
const char *v7; // r1@3
int v8; // r0@2
int v9; // r3@4
int v10; // r4@4
int v12; // [sp+Ch] [bp-1Ch]@1

v12 = a1;
v1 = g_env;
v2 = (*(int (__fastcall **)(_DWORD *, const char *))(*g_env + 24))(g_env, "com/Reflect");
v4 = v2;
v3 = v2 == 0;
v5 = *v1;
if ( v3 )
{
v6 = *(int (__fastcall **)(int *, const char *))(v5 + 668);
v7 = "jclass";
return v6(v1, v7);
}
v8 = (*(int (__fastcall **)(int *, int, const char *, const char *))(v5 + 452))(
v1,
v4,
"func",
"(ILjava/lang/String;)Ljava/lang/String;");
v9 = *v1;
v10 = v8;
if ( !v8 )
{
v6 = *(int (__fastcall **)(int *, const char *))(v9 + 668);
v7 = "method";
return v6(v1, v7);
}
(*(void (__fastcall **)(int *, int))(v9 + 668))(v1, v12);
return _JNIEnv::CallStaticObjectMethod(v1, v4, v10, 10);
}

这里大致意思是调用com.Reflect类中的func方法。参数就是“/key=i im lianai”。我们接下来回到JEB看吧。

package com;

public class Reflect {
private static String hexString;
public static String tmp;

static {
Reflect.tmp = " alien";
Reflect.hexString = "0123456789ABCDEF";
}

public Reflect() {
super();
}

private static String encode(String arg5) {//arg5 = “/key=i im lianai alien” 这里就是上面的把字符串中的字母变为16进制的代码了。我就不一行一行读了
byte[] v1 = arg5.getBytes();
StringBuilder v2 = new StringBuilder(v1.length * 2);
int v0;
for(v0 = 0; v0 < v1.length; ++v0) {
v2.append(Reflect.hexString.charAt((v1[v0] & 240) >> 4));
v2.append(Reflect.hexString.charAt((v1[v0] & 15) >> 0));
}

return v2.toString();
}

public static String func(int arg2, String arg3) {//这个就是IDA中调用的方法
return Reflect.encode(String.valueOf(arg3) + Reflect.tmp); //这里调用了此类中的encode方法,传入的是我们传递过来的参数“/key=i im lianai” 加上此类提供的一个静态字符串" alien",综合起来也就是“/key=i im lianai alien”
}
}

这样看来,initInflect((int)jniStr)的作用是将传入的“/key=i im lianai alien”字符中的每个字母转成16进制拼接起来。最后的结果是2F6B65793D6920696D206C69616E616920616C69656E这个是固定的。

4)总结

getEncryptString()这个方法处理传入的字符串,如果长度小于0x7FF的就在后面拼上“2F6B65793D6920696D206C69616E616920616C69656E”。

so部分到此结束,这个so挺容易,做的事也容易,就是拼下字符串。

1.4.2 DESencryption.getEncString()

然后我们回到JEB中的java代码中。看类Jni的encryptString的最后。

v0_1 = DESencryption.getEncString(v4.toString(), this.getEncryptString("a", true).substring0, 8));  

从名字上也能看出这个是DES算法,这个就是最终的结果了。v4就是经过so处理过的字符串,第二个参数是“a”去让so处理,在截取他的0~8位作为DES加密的key。加密后就是结果了。

我们可以直接把这些代码拷出来到Eclipse中。然后使用Java代码验证我们的分析是否正确。Java代码如下:

public class Test01 {
public static void main(String[] args) throws Exception {
String jsonHex = getHexString("{\"pwd\":\"!@#$%^\",\"number\":\"1234567890\"}"); //登录时的JSON字符串
String initStringHex = getHexString("/key=i im lianai alien");//固定的字符串
String soString = jsonHex+initStringHex;
String ret = DESencryption.getEncString(soString, "a2F6B657");
System.out.println(ret);
}

/**
* 这里是复制JEB中的把字符串中每个字符转换成16进制字符串
* @param str
* @return
* @throws Exception
*/
public static String getHexString(String str) throws Exception {
byte[] v1 = str.getBytes("UTF-8");
String hexString = "0123456789ABCDEF";
StringBuilder v2 = new StringBuilder(v1.length * 2);
int v0;
for(v0 = 0; v0 < v1.length; ++v0) {
v2.append(hexString.charAt((v1[v0] & 240) >> 4));
v2.append(hexString.charAt((v1[v0] & 15) >> 0));
}
return v2.toString();
}
}

最后的结果如下

ea3bfd2a6982db53d0df9e0166faf7be.png

1.5 总结

  • 用户输入用户名和密码。把他么封装成JSON字符串

  • 把JSON字符串的每个字符,变成16进制形式

  • 调用so库。拼接固定字符串“2F6B65793D6920696D206C69616E616920616C69656E”

  • 调用DES加密。密钥是固定的“a2F6B6579”,待加密的数据就是上一步拼接出来的。

  • 我也是小白,各位亲喷。有地方说错的,欢迎批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值