Android安全之某团请求加密破解

本文记录了破解某外卖App http请求参数加密过程中遇到的问题和解决思路,实现的目标是希望是能够通过一些手段实现自由调用其服务接口。
这篇文章主要讲述什么?

  1. 静态分析和动态调试的基本方法和遇到的问题
  2. 如何听过修改android源码实现全局hook

想要破解的网络请求

可以看到每次请求都会自动生成相应的请求参数,并计算出一个加密后的参数__skcy,服务端根据这些参数信息和请求内容做校验,校验通过服务端才会返回正确的结果。通过抓包和反编译后的代码大致梳理了上图请求参数的含义和生成方法,具体参数如下所示:

  • __skck:Java层面常量,6a375bce8c66a0dc293860dfa83833ef
  • **__skts:**系统时间值,通过Sytem.currentTime()获取,例如:1487054997740
  • __skua:通过UserAgent方法,获取ua,并计算出md5,通过动态调试发现该UserAgent方法返回为空,所以该值也为常量,d41d8cd98f00b204e9800998ecf8427e
  • **__skno:**通过Java randomUUID获取随机数,例如:eccb0210-c86f-43bb-b12d-04927547b9ea
  • **__skcy:**以上四个参数加上PostContent,调用native方法获取加密后得到该值

0x01 尝试静态分析

首先使用常用的静态分析工具,如apkTools、dex2jar做apk的反编译和转Jar。虽然该App安装包没有加壳,但在反编译过程中依然遇到一些问题。

1.1、遇到问题

该App针对dex2jar工具做了防范. 原理分析请看Android安全之应用防dex2jar原理及实现

1.2、解决方案

  • 将插入的Exist代码删除,再做smali–>dex–>jar处理。
  • 或者,直接阅读smali代码。

1.3、分析结果

  • 该App在java层处理Http请求的核心类是CandyPreProcessor.java
  • 该App调用加密so的JNI类是 CandyJni.java
  • 该App执行参数加密的so是libmtguard.so
1
2
3
4
5
6
7
8
9
public class CandyJni
{
static
{
System.loadLibrary("mtguard");
}

static native String getCandyDataWithKey(Object paramObject, byte[] paramArrayOfByte, String paramString);
}

0x02 尝试动态调试smali

这里先使用了源apk包在root手机上进行调试,但没有成功。只能尝试重打包调试。

2.1、如何使用AS动态调试

参考吾爱破解这篇文章,需要下载AS baksmali插件,需要注意的是:

  • AS remote debug端口号设置为8700
  • AS没有识别安卓代码情况下工具栏没有Android Device monitor的按钮,这时候通过monitor命令启动monitor
  • 如果monitor启动时提示8700端口已经被占用,可以通过 lsof -n -i4TCP:8700 | grep LISTEN 命令查看8700端口目前对应的进程名
  • attach前一定要先在monitor里选中想要调试的进程,再debug

2.2、重写Java代码,直接调用加密so

动态调试可以比较清楚的看到Java层的内部调用逻辑,通过debug watch功能可以清楚的看到寄存器值,结合反编译和静态分析可以知道调用native方法的入参格式。

现在的思路是解压出源apk包中的so文件,编写java代码直接调用so文件中的native方法获取计算__skcy参数。但测试的结果是一直返回null。这里猜测在native层中对运行环境做了监测,所以一直返回空值。

这里该app的参数签名详细过程是,通过__skck、__skts、__skno、__skua(详细规则见上),调用CandyPreprocessor.javagetParametersSignature()方法获取__skcy参数。Java层核心方法getParametersSignature()如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @param paramBuilder https://eapi.*****/api/poi/logon/acctdevice?__skck=6a375bce8c66a0dc293860dfa83833ef&__skts=1486970558719&__skua=d41d8cd98f00b204e9800998ecf8427e&__skno=5ed4629c-098c-489c-b598-81841e0479e0
*/
String getParametersSignature(Uri.Builder paramBuilder, Context paramContext) {
if (paramBuilder == null) {
throw new RuntimeException("CandyPreprocessor getParametersSignature builder is null");
}
// 获取网络接口名称 https://eapi.waimai.meituan.com/api/poi/logon/acctdevice
Object localObject = baseString();
if (TextUtils.isEmpty((CharSequence) localObject)) {
throw new RuntimeException("CandyPreprocessor getParametersSignature normalizedURI is null");
}
ArrayList localArrayList = new ArrayList();
/**
* 0 = {CandyPreprocessor$MyEntry@5016} "__skck" -> "6a375bce8c66a0dc293860dfa83833ef"
1 = {CandyPreprocessor$MyEntry@5017} "__skts" -> "1486970558719"
2 = {CandyPreprocessor$MyEntry@5018} "__skua" -> "d41d8cd98f00b204e9800998ecf8427e"
3 = {CandyPreprocessor$MyEntry@5019} "__skno" -> "5ed4629c-098c-489c-b598-81841e0479e0"
*/
// 获取参数key和参数value放到数组中
appendList(localArrayList, paramBuilder, false);
// add "__sksc" -> "https"
// 获取scheme放入数组中
localArrayList.add(new CandyPreprocessor.MyEntry("__sksc", this.candyOriginalMaterial.getScheme()));
// 将post content放入数组中
if (formURLEncoded()) {
// /?dVersion=23_6.0&utm_medium=android&password=2222&utm_content=867689027084732&appCode=388&acctId=&wmPoiId=&token=&uuid=03EB35F2C93419DC64AFFBC9EF6BB6BADE4DD72DC1E2F4733198E91C91AEAF93&logType=C&appName=%E7%BE%8E%E5%9B%A2%E5%A4%96%E5%8D%96%E5%95%86%E5%AE%B6%E7%89%88&appType=4&dType=PLK-TL01H&userName=1111&utm_term=4.2.0.388&utm_source=&utm_campaign=&wm_appversion=4.2.0.388&
// post 的内容入参
/**
* 5 = {CandyPreprocessor$MyEntry@5071} "dVersion" -> "23_6.0"
6 = {CandyPreprocessor$MyEntry@5072} "utm_medium" -> "android"
7 = {CandyPreprocessor$MyEntry@5073} "password" -> "123"
8 = {CandyPreprocessor$MyEntry@5074} "utm_content" -> "867689027084732"
9 = {CandyPreprocessor$MyEntry@5075} "appCode" -> "388"
10 = {CandyPreprocessor$MyEntry@5076} "acctId" ->
11 = {CandyPreprocessor$MyEntry@5077} "wmPoiId" ->
12 = {CandyPreprocessor$MyEntry@5078} "token" ->
13 = {CandyPreprocessor$MyEntry@5079} "uuid" -> "03EB35F2C93419DC64AFFBC9EF6BB6BADE4DD72DC1E2F4733198E91C91AEAF93"
14 = {CandyPreprocessor$MyEntry@5080} "logType" -> "C"
15 = {CandyPreprocessor$MyEntry@5081} "appName" -> "****"
16 = {CandyPreprocessor$MyEntry@5082} "appType" -> "4"
17 = {CandyPreprocessor$MyEntry@5083} "dType" -> "PLK-TL01H"
18 = {CandyPreprocessor$MyEntry@5084} "userName" -> "***"
19 = {CandyPreprocessor$MyEntry@5085} "utm_term" -> "4.2.0.388"
20 = {CandyPreprocessor$MyEntry@5086} "utm_source" ->
21 = {CandyPreprocessor$MyEntry@5087} "utm_campaign" ->
22 = {CandyPreprocessor$MyEntry@5088} "wm_appversion" -> "4.2.0.388"
*/
appendList(localArrayList, Uri.parse("/?" + new String(this.candyOriginalMaterial.getPostContent())).buildUpon(), true);
}
// 特殊字符转码
List percentParamList = getPercentList(localArrayList);
// 分别对Value和Key做升序排列
dictionarySort(percentParamList);
// 将排序后的数组,拼接成字符串__skck=6a375bce8c66a0dc293860dfa83833ef&__skno=5ed4629c-098c-489c-b598-81841e0479e0&__sksc=https&__skts=1486970558719&__skua=d41d8cd98f00b204e9800998ecf8427e&acctId=&appCode=388&appName=%E7%BE%8E%E5%9B%A2%E5%A4%96%E5%8D%96%E5%95%86%E5%AE%B6%E7%89%88&appType=4&dType=PLK-TL01H&dVersion=23_6.0&logType=C&password=2222&token=&userName=1111&utm_campaign=&utm_content=867689027084732&utm_medium=android&utm_source=&utm_term=4.2.0.388&uuid=03EB35F2C93419DC64AFFBC9EF6BB6BADE4DD72DC1E2F4733198E91C91AEAF93&wmPoiId=&wm_appversion=4.2.0.388
String paramBuilderString = getNormalizedParameters(percentParamList);
// POST+空格+"接口名称"+空格+"参数内容"
// POST http://eapi.waimai.meituan.com/api/poi/logon/acctdevice __skck=6a375bce8c66a0dc293860dfa83833ef&__skno=57749130-26d4-4239-8abd-b99c16584908&__sksc=http&__skts=1486995346041&__skua=d41d8cd98f00b204e9800998ecf8427e&acctId=&appCode=388&appName=%E7%BE%8E%E5%9B%A2%E5%A4%96%E5%8D%96%E5%95%86%E5%AE%B6%E7%89%88&appType=4&dType=PLK-TL01H&dVersion=23_6.0&logType=C&password=2222&token=&userName=1111&utm_campaign=&utm_content=867689027084732&utm_medium=android&utm_source=&utm_term=4.2.0.388&uuid=03EB35F2C93419DC64AFFBC9EF6BB6BADE4DD72DC1E2F4733198E91C91AEAF93&wmPoiId=&wm_appversion=4.2.0.388
paramBuilderString = this.candyOriginalMaterial.getHttpMethod() + " " + (String) localObject + " " + paramBuilderString;
// JV1EOT1VZGN8USskB3jZYnVGyyQ=
// 调用jni获取加密后的参数值__skcy
localObject = CandyJni.getCandyDataWithKey(paramContext, paramBuilderString.getBytes(), "candyKey");

// TODO for test
return (String) localObject;
}

2.3、修改app代码,重打包调用so

直接调用so的方法失败了,现在尝试反编译APK后,加入包含自己逻辑的smali代码,调用CandyJni的目的。具体的步骤是:

  • 利用上2.2的项目,拷贝和该App CandyJni完全相同的类名,方法名。
  • 编写CandyHackActivity.Java和CandyHack.java,去调用新项目中的CandyJni,并打包。
  • apktools反编译新项目包,获取CandyHack.smali 和CandyHackActivity.smali。
  • 修改反编译后项目的AndroidManifest.xml文件,将CandyHackActivity注册到manifest中进去。
  • 重打包,签名,安装。
  • 通过adb命令启动CandyHackActivity。

结果一点不意外,能够成功调用CandyJni中的方法并且有返回值,但是经过测试在相同入参的情况下,和源APK包的加密结果不一致。问题应该出在签名。猜测应该是so中的方法将应用签名作为了一个加密参数。虽然能正常调用,但结果是错的~~

2.4、尝试IDA pro反编译so

一句话,IDA反编译后发现,在function窗口中找不到getCandyDataWithKey或者getCandyData这些在Java层声明的native方法。应该是使用了动态注册的方式注册了native方法,又一次把代码阅读门槛从C升级到了汇编,泪目~~~

native方法混淆

2.5、总结问题

  • 想要在root机器上动态调试dex,但不进行重打包,就需要绕过他的反调试手段。反调试手段是什么?怎么绕过?我不知道
  • 想要通过重打包动态调试dex,重打包必须重新签名,重新签名会导致加密算法计算结果出错。
  • 想要反编译so,需要阅读加密后的C语言和汇编语言。汗!我是菜鸟汇编阅读不来

0x03 万金流破解思路

根据以上分析的过程,发现不管是重打包、借用so、静态分析、动态调试等基础的破解方式,该app都做了相应的防护措施,总的破解思路都是破坏了app原有环境。在一次逛安全论坛的过程中,看到通过修改Android原生代码加载特定so的思路,其实这种破解思路也适用于app的破解。如果在app启动时,让app进程的DexClassLoader能够加载你自己的代码,那么不管在任何的app运行环境中,诸如修改内存变量、调用方法或者是改变原有app系统的处理流程等目的都能够轻松达到。当然前提是运行在自己编译的Android源码或者是已经root的机器上。下面就以这个思路针对该外卖app做具体的破解实操,目标是在pc上能够自由调用该app服务端的http接口。

3.1. 下载并修改源码

如何下载编译源码这里不废话,没有尝试过的可以参考这篇《Mac OS上编译Android源码》
ActivityThread.javahandleBindApplication()方法中增加dex注入的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void handleBindApplication(AppBindData data) {
// 省略源码...

//=================全局注入之修改java层===================================================
checkFrameworkInject(data.processName);
//=============================================================================
}

private void checkFrameworkInject(String processName) {
//=================全局注入之修改java层===================================================
Log.e("frameworkInjector", "--processName=" + processName);
invokeJarLoader(processName);
//=============================================================================
}

从指定路径加载配置文件,配置信息包括要dex注入的进程名,dex路径,dex中main函数信息。
当相应的进程启动时,加载指定路径的dex,并运用其中的main函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 根据配置加载jar,执行指定类的main方法
*/
public void invokeJarLoader(String processName) {
List<String> config = readHookConfig("/data/local/jarHookConfig.txt");

if (config == null || config.size() < 3) {
return;
}

String configProcessName = config.get(0);
String configJarPath = config.get(1);
String configClassName = config.get(2);

if (processName.equals(configProcessName) && !TextUtils.isEmpty(configJarPath)
&& !TextUtils.isEmpty(configClassName)) {
Log.e("frameworkInjector", "targetPackage:" + configProcessName +
",start-load-jarPath" + configJarPath + ", class name:" + configClassName);
jarInvokeTest(configJarPath, configClassName);
}
}

public void jarInvokeTest(String jarPath, String className) {
final File optimizedDexOutputPath = new File(jarPath);
Context context = getApplication();
File dexOutputDir = context.getDir("dex", 0);
DexClassLoader cl = new DexClassLoader(
optimizedDexOutputPath.getAbsolutePath(),
dexOutputDir.getAbsolutePath(), null, context.getClassLoader());
Class libProviderClazz = null;

try {
libProviderClazz = cl.loadClass(className);
Method method = libProviderClazz.getMethod("main");
method.invoke(null, (Object[]) null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

3.2 生成注入的dex

在这里具体的注入代码就不啰嗦了,总的来说你可以针对任何app中的任何方法变量任意操作。如果你想在pc上实现请求的自由收发要稍微复杂些,你需要实现一个简单的Hook socket server(简称HSS),将HSS打包成dex做注入,将参与参数加密的数据发送给HSS,HSS调用app中的加密方法获取加密结果,并将最终的加密结果发送出来就可以啦。具体的实现如下图所示:

hook结构图

0x04 严重声明

本文的意图只有一个,就是通过分析app学习更多的逆向技术,如果有人利用本文知识和技术进行非法操作进行牟利,带来的任何法律责任都将由操作者本人承担,和本文作者无任何关系,最终还是希望大家能够秉着学习的心态阅读此文。

0x05 参考文档

0x06 转载请标明出处病已blog https://ivonhoe.github.io/