Fanxs's Blog

记一次APP加密通信后的分析过程

字数统计: 3.7k阅读时长: 14 min
2019/07/28 Share

前言:
最近有一次驻场测试,需要对甲方开发的支付SDK进行测试。到了甲方公司,对方负责人只给了我们一个SDK demo APP和接口文档,其他啥都没有给。让人崩溃的是,Demo里出来的通信过程是加密的,所有接口也是进行加密通信,而负责人并没有给我们报文的加密过程。这篇文章就记录了这一次测试的坎坷分析经历,以供自己以后再做参考。

0x00.Demo报文

甲方爸爸给我们的接口文档,包含了十几个不同的接口。接口共用几个URL,由请求包中的TransId来注明不同的接口服务。
而文档里只给了参数的定义,没有发包的加密过程:

Demo APP打开后,有一个输入框和“支付”按钮:

输入框里的内容为接口明文报文的base64:

1
{"MerchantId":"1000000000000000001","TransAmt":"39.09","MerchantSeqNo":"1551944989458","MerchantDate":"20190307","MerchantTime":"203718","Currency":"CNY","FrontUrl":"http://www.baidu.com","BackUrl":"http://10.1.193.48:7001/gwback/weixin","PortFlag":"0","OrderInfo":"[{\"OrderTime\":\"201115\",\"OrderAmt\":\"39.09\",\"OrderBody\":\"",\"OrderId\":\"15519449894580001\",\"OrderSubject\":\"",\"OrderDate\":\"20190307\"}]","Body":"","Subject":"Payment","sign":"YPRhN2pn+sZKEwbepdzoy+M15YG60lEFmClVlYrVFT1TZ/c+qH0vUVHmfbI9JGGFeTVvUtBeD5IIYyJgvna4T5ekwM/8uv2N7cuUp/h9SmzDLOjqCu05kz5T5bsKGZT7s2DXgeiScqEn5YlpVAL7oXjH3/Sf+r7vuQlwAx5NSPc="}

发出的请求是这样的:

尝试base64解密后,都是不知其意的字节码,可知整个报文是经过加密的。和甲方负责人确认后,可知支付按钮发出的请求是下单的接口,此接口定义为:

接口定义和base64解码后的原文根本不对应啊,而且用这个Demo只能测试下单一个接口,其他十几个接口只能通过burp造请求包来测试。这时测试的首要问题,就是要知道该如何加密请求包。

要搞清楚这一点,我就要知道:

  1. 加密前的原报文是什么样(内容、格式)
  2. 加密的方法是什么
  3. 实现加密

0x01. 逆向Demo

为了知道加密方法,先逆向demo app。打开dex后,发现demo被混淆了:

心情是崩溃的,连给我们驻场测试的demo都是混淆的,图啥呢。。搜索请求包里的参数“data”,找到了这一个类:


非常明显,这个类负责构造请求包参数并发出请求,此处调用了telecomAesDecrypt做加密。这个方法在类PEJniLib中:

这个类中用System.loadLibrary加载了一个so文件,并声明了相关的方法。telecomAesDecrypt函数调用了so文件中的native method进行具体的加密。

0x02.逆向So

可见telecomAesDecrypt具体调用的native方法是getNativeValue(s, s2)。在java代码中调用具体的native方法时,需要在so文件中以固定命名规则定义相应的函数。根据官方文档,命名规则为:

1
2
3
4
5
6
7
Dynamic linkers resolve entries based on their names. A native method name is concatenated from the following components:

* the prefix `Java_`
* a mangled fully-qualified class name
* an underscore (`_`) separator
* a mangled method name
* for overloaded native methods, two underscores (`__`) followed by the mangled argument signature

所以这里的getNativeValue在so文件中应该会命名为:

1
Java_package_PEJniLib_getNativeValue(JNIEnv *env, jobject obj, jstring s ,jstring s2)

打开ida,对so文件进行逆向。在Export窗口搜索“java”:

竟然没有以Java开头的函数。这时又懵逼了,不以固定的命名规则来定义函数,Java VM怎么能调用到正确的函数?后来查了很多的资料才发现,当时太年轻了,JNI接口的Native方法不仅仅能以固定的命名规则来进行定位(静态注册),还能通过加载so文件时的JNI_Onload函数进行 动态注册 ,当然这是后话了。

在Export里搜JNI:

发现一个名称和目标 getNativeValue(JNIEnv *env, jobject obj, jstring s ,jstring s2) 匹配度非常高的函数getValues(_JNIEnv ,_jobject ,_jstring ,_jstring )。它调用了其他的函数来进行加密,最后追述到进行具体加密的函数getValue(char a1, char a2):

因为这个so文件保留了符号表,可以很清晰地看出这里的函数做了什么。分析这个函数,可发现报文加密后会用“|”分成3个部分。第一部分是RSA加密的AES密钥,第二部分是AES加密报文串,第三部分是报文串MD5摘要值,每次加密都会随机生成AES密钥。

再对比Demo发出的请求:

因为base64字符表中并不包含“|”,所以加密后的报文的确由“|”分为了三部分。在so文件中看了其他函数后,并没有发现更合适的函数了,因此可断定了报文的加密方式。

0x03. 动态调试so

知道了加密方式,解决了第2个问题。但还需要知道加密前的明文报文是什么样子的。因为APP的dex代码被混淆了,不高兴从dex里解决这个问题,所以我想到动态调试so文件。

参考使用IDA Pro动态调试SO文件一文用真机进行动态调试:

  1. 将手机插上电脑,启动android_server
  2. adb forward tcp:23946 tcp:23946
  3. am start -D -n com.example.test/com.example.test.MainActivity命令,启动所要调试的Activity
  4. 启动ida pro,点击”Debugger - > Attach -> Remote ArmLinux/Android debugger“,port 23946,在弹出的”Choose process to attach to”窗口中找到应用进程
  5. 打开DDMS,找到要调试应用的端口,通常为8700或8600
  6. jdb -connect com.sun.jdi.SocketAttach:port=8700,hostname=localhost
  7. IDA点击执行,函数打断点,开始动态调试。

在调试过程中,遇到一些问题:

  1. IDA里的调试器不能选错,必须是Remote ArmLinux/Android debugger
  2. 在第5步中,打开DDMS看不到任何应用的端口。

要进行第6步,就必须要知道应用端口,这个只能通过DDMS来获取。但DDMS打开来看不到任何应用的端口,这个问题愁了我好久,后来才发现答案:

1
2
3
DDMS中看不到进程,是因为它只能显示可调试debuggable=true的进程信息。要调试一个apk里面的dex代码,必须满足以下两个条件中的任何一个:
· APP的AndroidManifest.xml文件必须设置属性android:debuggable="true",设置为可调试。
· 系统/default.prop中ro.debuggable的值为1

而这个demo APP并没有设置为可调试。解决方法有:

  1. 用apktools进行APP重打包,修改Manifest文件,加上可调试属性。
  2. Hook system debug
    (用xposed的插件Xinstaller/BuildProp,都可以做到, Xposed插件Xinstaller,打开“专家模式”和“调试应用,或BuildProp设置ro.debuggable=1)
  3. 修改底层系统的boot.img,设置/default.prop中ro.debuggable=1,再刷机
  4. 用mprop进行hook,设置应用为可调试

由于我的安卓测试机已经root了,所以我使用了最简单的第二个方法,在Xposed框架支持下安装了BuildProp,设置为ro.debuggable=1即可。附上链接:BuildProp

整好BuildProp,拿到端口后,终于可以进行动态调试。但在IDA中点击运行后,并没有停在设置好的断点上,出现了不明的原因,所以动态调试就算失败了。

0x04. TraceView和Frida

动态调试so文件失败了,还是没有解决构造加密前报文的问题。所以想到了另一条路,用Frida进行java层的hook。

之前我涛哥和震哥在破解一个加壳APP的通信加密算法时,用Frida来进行了加密类初始化函数的hook,找到了密钥,用TraceView来猜测到加密类初始化的函数。本着学习的目的,虽然前面已通过逆向APP来找到hook的函数,但我也用TraceView来进行了尝试。

TraceView配合Frida,具体可参考:

TraceView

Traceview 工具是Android SDK中自带的工具,用于分析APP的计算性能,可以对安卓进程进行trace分析,通过图形化的方式展示要跟踪的程序的性能,并且能具体到方法。_

  1. 打开DDMS,在窗口中选中要监控的进程后,点击Start Method Profiling,开始对进程函数调用的监控:

  2. 在APP中进行操作。在Demo APP中点击按钮,发送请求。此时会经过一系列的函数调用,包含了报文初始化、报文加密、请求发送等过程。这些函数调用都已被Traceview所记录。

  3. 点击Stop Method Profiling,停止记录。这时TraceView显示了监控期间的函数调用情况:

  4. 分析监控期间的函数调用。在方法调用最密集的时候,应该就是一连串的方法调用来发出了请求。这时需要根据调用函数的 参数类型,参数个数,返回值类型 来进行猜测哪一个函数可能参与了加密过程。

当通信加密的APP被加壳或严重混淆时,TraceView可协助猜测加密类和加密函数,以便下一步Frida进行Hook。

此前我涛哥和震哥,在面对通信加密的加壳APP时,搜到了测试APP未加壳时的老版本,找到了里面的加密函数和加密类,再结合TraceView成功猜测到了新版本APP里的加密方法的类名和函数名,最后用Frida Hook出了被修改的密钥。这是个非常有趣的思路。

Frida

Frida 是一款基于Python和Javascript 的hook框架,可运行在Android,iOS,Linux,Win等各平台。Frida不仅可以用于hook java层,也可以进行native层的hook。利用Frida进行hook时,要求需要使用已ROOT的手机。

根据逆向Demo APP和Traceview的结果,可知类PEJniLib的telecomAesEncrypt函数返回了明文报文的加密结果。所以用Frida Hook这个函数,就可以找到明文报文是啥格式。

telecomAesEncrypt:

  • 2个参数
  • 参数类型都为 字符串
  • 返回值类型 字符串

根据以上已知内容,写Frida Hook代码:

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
import frida, sys

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

# Javascript代码中,在Java层对PEJniLib类的telecomAesEncrypt(String, String)方法进行了hook
# 打印出了此函数调用时的参数值
jscode = """
Java.perform(function(){
var Md5Util = Java.use("com.package.PEJniLib");
Md5Util.telecomAesEncrypt.overload('java.lang.String','java.lang.String').implementation=function(p1,p2){
console.log(p1);
console.log(p2);
return this.telecomAesEncrypt(p1,p2);
};
});
"""

# 获取frida-server目标设备,和目标APP的进程pid
device = frida.get_device_manager().enumerate_devices()[-1]
pid = device.spawn(["com.package"])

# 附加到目标APP上,创建一个Session
# Session可以理解为一次会话,比如附加到某个进程则算是启动了一次会话,也可以理解为进程的抽象
session = device.attach(pid)
print("[*] Attach Application id:",pid)
# 让应用恢复运行
device.resume(pid)
print("[*] Application onResume")

# 根据参数字符串创建一个Scritp对象,参数里的js脚本会被加载到目标进程中
script = session.create_script(jscode)

# 在监听到一个message时,执行on_message函数
# 此处的message为一个Javascript对象,为Javascript和Python之间沟通的基本消息类,有type和payload属性
# 比如,Javascript代码报错时,会传出type为error的message,其中的payload就为报错信息
# 打印出message: {'type': 'error', 'description': 'ReferenceError: a is not defined', u'lineNumber': 1}
script.on('message', on_message)

# 加载代码
script.load()
sys.stdin.read()

启动frida:

  1. adb shell 后启动frida-server
  2. 端口转发:

    1
    2
    adb forward tcp:27042 tcp:27042
    adb forward tcp:27043 tcp:27043
  3. 如果PC端frida-pc打出了进程,则说明连接正常:

将代码执行,成功地输出了明文报文和密钥:

0x05. 实现加密

已知明文报文加密方式,接下来就只剩下实现加密算法,来进行之后的测试。一开始的想法是,既然我都有了具体的加密算法,那就直接写代码实现就好了。由于只能在甲方的内网机器上进行测试,机器上没有python环境和依赖库,所以只能编写java代码。

在写了2天的Java代码后,遇到了2个大坑:

  1. 由于不熟悉Java开发,所以写代码的效率很低
  2. Java库不同导致加密错误。内网机器上没有常用的库,如apache。我用一些基本库写完了加密实现后,发现加密结果永远都不对。后来和甲方的开发求助,对接了一下代码,才发现同样的明文,我们用了不一样的库,base64的结果就已经不一样了。

以上的坑导致我心态崩了,本来就不会java,驻场时间紧,得硬着头上还撞到了墙。这时我就不想搞了,换了个思路。既然我都知道明文报文的格式了,那何不直接写一个APP调用so文件来进行加密

编写APP调用已有的so文件,需要:

  1. 打开Android Studio新建一个Project,并第一步勾选Include C++ support:
  2. 在项目文件夹下,建立libs文件,并将so文件的不同系统的实现都放进去libs中。
  3. 在项目的gradle中添加这一项:
    1
    2
    3
    4
    5
    sourceSets{
    main{
    jniLibs.srcDirs = ['libs']
    }
    }

然后就可以开始编写APP的功能了。APP的layout简单地放了输入框和加解密按钮:

定义一个PEJniLib类,声明了Native方法,此处需要package名和class名严格和so文件中对应。由于动态注册,在编译时会提醒Native方法无法解析,但依然能成功build app:

这里需要注意的是,类名必须是”PEJniLib“,否则会提醒找不到类。估摸着应该是动态注册时绑定了这一个类名。

MainActivity中则简单地将输入框的内容,传到PEJniLib的响应Native类中,进行加解密:

至此,相关接口测试时的通信加解密问题,就解决了。这一篇文章,也旨在做一次记录,以供以后遇到相关问题时再做参考。

以下是在完成了这次驻场测试后,对动态注册Native方法的思考和研究。

0x06.JNI调用探索

在完成了测试后,我在考虑2个问题:

  1. Native方法的动态注册,究竟是怎么样的一个过程
  2. 在上文中,so文件保留了符号表,所以IDA逆向后能看到原始的函数名,因此我才猜测到了目标getValues方法。若如果so文件加壳了,仅仅知道Java VM调用的Native方法是getNativeValue,怎么在so文件中找到相应的函数呢?或有没有更方便的方法定位so库里的方法。

简单地说,动态注册是怎么实现的?若Native方法是动态注册的,要怎么来定位呢?基于自己不懂Android开发,就自己探索了一下JNI,记录在了下一篇文章里。

CATALOG
  1. 1. 0x00.Demo报文
  2. 2. 0x01. 逆向Demo
  3. 3. 0x02.逆向So
  4. 4. 0x03. 动态调试so
  5. 5. 0x04. TraceView和Frida
    1. 5.1. TraceView
    2. 5.2. Frida
  6. 6. 0x05. 实现加密
  7. 7. 0x06.JNI调用探索