APK渗透踩坑历程

发布于 2023-03-31  1165 次阅读


写在前面

  这是一篇总结了我目前为止在APK渗透过程中遇到的一些问题及解决方案,另外还有对其中问题的理解和相关资料,望有所帮助。

模拟器、测试机证书安装

方法一:直接安装

  将证书下载到已经root的机器中,直接进行点击安装,简单粗暴,注意需要root权限,文件格式需要.der

image-20230330145656049

image-20230330145847773

方法二:ADB安装

  ADB,Android Debug Bridge,Android调试桥,一种功能多样的命令行工具,adb命令可用于执行各种设备操作,例如安装和调试应用。adb 提供对 Unix shell(可用来在设备上运行各种命令)的访问权限。它是一种客户端-服务器程序,通常包含在Android SDK平台工具软件包中。工具本身包括以下三个组件:

  • 客户端:用于发送命令。客户端在开发机器上运行。您可以通过发出 adb 命令从命令行终端调用客户端。

  • 守护程序 (adbd):用于在设备上运行命令。守护程序在每个设备上作为后台进程运行。

  • 服务器:用于管理客户端与守护程序之间的通信。服务器在开发机器上作为后台进程运行。


  对于模拟器来说,一般是自带有adb工具,网易mumu模拟器存放在特定的文件夹下,雷电、夜神等模拟器放在安装路径下,一般来说各个模拟器厂商都会对adb工具进行重命名。
  针对真机时,adb工具需要自行下载备用,自行下载的adb也可以对模拟器进行操作,但对模拟器时需要注意,模拟器一般只支持特定版本的adb,也可以说仅支持自带的,这个要求并不是绝对的,将自己的adb工具copy到对应路径下进行覆盖即可。


处理证书文件

  在导入证书之前需要注意的是,证书格式需要从原本的格式转换为.pem格式,这一步可以使用Openssl工具进行转换,官网也可以下载

  • pem证书转.cer证书
    openssl x509 -outform der -in demo.pem -out demo.cer
  • cer证书转.pem证书
    openssl x509 -inform der -in demo.cer -out demo.pem

  转换后还需要计算证书文件hash值,并使用hash值进行重命名,将证书文件命名成hash-value.0

openssl x509 -subject_hash_old -in burp.pem

image-20230330165553032

导入证书

  以夜神模拟器为例,打开模拟器,在命令行使用adb工具

查看连接状态
# 查看连接状态
adb devices

  命令执行不报错,不出意外的状况如下

image-20230330151852936

  可能会出现下列报错,这就是adb工具的版本和模拟器的adb版本不一致所导致的,解决办法也在上述中有讲到,复制自己的adb工具到模拟器路径覆盖并重命名为模拟器本身的adb工具图片来自于网络

  另外还需要注意一点就是,多输入几次命令查看devices列表,如果说第一次出现连接,后续中没有出现,也是存在问题的。下图中是直接没能启动连接的

image-20230330161331913

  此时需要注意几个点,查看模拟器是否打开开发者模式是否允许USB调试,如果还不行的话暂时是没有什么解决办法,换个模拟器或者换个机器。一般来说,高版本的Android会出现很多奇奇怪怪的问题,建议为了稳定暂时不要使用高版本Android模拟器进行测试。这里点名MuMu_X!并且MuMu低版本Android模拟器是我用过的几个模拟器中唯一一个使用adb没有反应的模拟器,也经常出现一堆乱七八糟的无解问题。

image-20230330160912180

image-20230330161001603

  进行完开发者模式、USB调试开启动作后需要重新启动devices,就需要将之前的进程干掉,一般来说adb工具会进行这一步,为避免意外可以手动进行结束,devices会在机器的5037端口开放一个监听服务,查看网络信息,然后查看进程PID,结束进程即可

image-20230330162440081

判断是否有root权限
# 查看root权限
adb root

  下图表示是没有root权限,直接到模拟器中打开就行

image-20230330163508659

  打开root权限后,夜神模拟器对此命令无响应,保证模拟器中设置打开即可。没有打开也不会进行到这里,不root的话,devices连接都打不开

image-20230330163756827

  如果出现下图中内容说明devices连接未启动,如果已经启动可以尝试使用adb connect ip:port命令进行对devices进行连接,连接后再次尝试,如果还不行……憋犟了,换模拟器吧

image-20230330164353285

将/system进行挂载权限变为可写入
adb remount

image-20230330165652100

安装证书
adb push hash-value.0 /system/etc/security/cacerts/

image-20230330170234202

  这里要注意给相应证书文件读权限,下面的操作我直接使用adb shell生成的交互式界面进行,相关命令跟Linux命令基本相同,就不再列出,最终结果如下

image-20230330170510045

完成

  到这里就可以正常抓包了

image-20230330170908422

  这种办法安装会出现仍提示证书安全告警,进入相关设置关掉即可

image-20230330171051158

双向认证突破

  双向认证,顾名思义,客户端和服务器端都需要验证对方的身份,在建立HTTPS连接的过程中,握手的流程比单向认证多了几步。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。

原理

单向认证流程

单向认证流程中,服务器端保存着公钥证书和私钥两个文件,整个握手过程如下:

img

  • 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务器端;

  • 服务器端将本机的公钥证书(server.crt)发送给客户端;

  • 客户端读取公钥证书(server.crt),取出了服务端公钥;

  • 客户端生成一个随机数(密钥R),用刚才得到的服务器公钥去加密这个随机数形成密文,发送给服务端;

  • 服务端用自己的私钥(server.key)去解密这个密文,得到了密钥R

  • 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。

双向认证流程

img

  • 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
  • 服务器端将本机的公钥证书(server.crt)发送给客户端;
  • 客户端读取公钥证书(server.crt),取出了服务端公钥;
  • 客户端将客户端公钥证书(client.crt)发送给服务器端;
  • 服务器端使用根证书(root.crt)解密客户端公钥证书,拿到客户端公钥;
  • 客户端发送自己支持的加密方案给服务器端;
  • 服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
  • 客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
  • 服务端用自己的私钥去解密这个密文,得到了密钥R
  • 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。

面临的问题

  简单说明就是代理工具,例如BurpSuite、fiddler等没有被允许的client端的CA证书,结合BurpSuite等工具的代理原理可以很简单的理解这句话,这就需要我们去寻找相应的证书文件,并导入到抓包代理工具中。

突破认证

  APK中的突破还是相当有难度的,首先你要先考虑能不能砸壳,如果第一步就不行的话,双向认证是无法突破的。其次代码中的证书认证流程分析及key的寻找,再者证书文件是什么样格式的,是常用格式还是非常用的,非常用的怎么去处理。下面是我所遇到的一个场景。

一次失败的经历

反编译

  因为我拿到的是无壳版本的apk,就贴心的省去了砸壳的流程。我还真不会砸壳拿到apk后肯定就是反编译

java -jar apktools.jar d <apk_name> -o <dir_path>

image-20230330173636025

  然后用apk代码工具打开阅读代码即可进行下一步,所幸给的apk并没有进行代码混淆,混淆代码那是读一个丧心病狂。

image-20230330180916747

读代码

  也不要真的一行一行去读代码,这样搞,项目工期绝对不够。所以结合以往经验和一些关键词.cer、.crt、.pfx、PKCS12、keyStore对代码进行搜索,最终确定了如下代码

image-20230330175525906

  从代码中可以看出加载了文件R.raw.truststore,并且加载了一个密钥字符,可以去文件里找找看

image-20230330175720637

  没有后缀名,没有其他的信息,和开发确认后,确定这两个无后缀文件为该apk的证书相关文件,但文件并不是常见的、很直接的东西,当然,BurpSuite是肯定用不了这俩文件的。所以我到这里也卡住了,

突破失败经历后续

  后面翻阅了一些资料后,了解到了一些关于truststore的相关内容,严格意义上来说trutstore并不算是双向认证。这种认证,在我看来追根究底算是单向的一种。为什么这么说继续往下看。

  • keystore和truststore

  KeyStoreTrustStore是JSSE中使用的两种文件。这两种文件都使用java的keytool来管理,他们的不同主要在于用途和相应用途决定的内容的不同。
  这两种文件在一个SSL认证场景中,KeyStore用于服务器认证服务端,而TrustStore用于客户端认证服务器。
  KeyStoreTrustStore的不同,也主要是通过上面所描述的使用目的的不同来区分的,在Java中这两种文件都可以通过keytool来完成。不过因为其保存的信息的敏感度不同,KeyStore文件通常需要密码保护。

  • KeyStore

  内容 一个KeyStore文件可以包含私钥(private key)和关联的证书(certificate)或者一个证书链。证书链由客户端证书和一个或者多个CA证书。
  KeyStore文件有以下类型,一般可以通过文件扩展名部分来提示相应KeyStore文件的类型:
JCEKS,JKS,DKS,PKCS11,PKCS12,Windows-MY,BKS 以上KeyStore的类型并不要求在文件名上体现,但是使用者要明确所使用的KeyStore的格式。

  • TrustStore

  内容 一个TrustStore仅仅用来包含客户端信任的证书,所以,这是一个客户端所信任的来自其他人或者组织的信息的存储文件,而不能用于存储任何安全敏感信息,比如私钥(private key)或者密码。
  客户端通常会包含一些大的CA机构的证书,这样当遇到新的证书时,客户端就可以使用这些CA机构的证书来验证这些新来的证书是否是合法的。

一些代码

// 取到证书的输入流
InputStream caInput = context.getResources().openRawResource(R.raw.ca_cert);
Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(caInput);

// 创建Keystore包含我们的证书
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// 创建一个TrustManager仅把Keystore中的证书 作为信任的锚点
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);

// 用TrustManager初始化一个SSLContext
ssl_ctx = SSLContext.getInstance("TLS");  //定义:public static SSLContext ssl_ctx = null;
ssl_ctx.init(null, tmf.getTrustManagers(), new SecureRandom());

  上述这是一段为了避免出现CA不可信情况而编写的指定信任的CA锚点的代码,下面的就是通过SSLSocketFactory与服务器交互的内容:

// SSLSocketFactory 或 SSLSocket 都行
//1.创建监听指定服务器地址以及指定服务器监听的端口号
SSLSocketFactory socketFactory = (SSLSocketFactory)ssl_ctx.getSocketFactory();
ssl_socket = (SSLSocket) socketFactory.createSocket(serverUrl, Integer.parseInt(serverPort)); //定义:private final String serverUrl = "42.98.106.44";
                                                       //   private final String serverPort = "8086";
//2.拿到客户端的socket对象的输出/输入流,通过read/write方法和服务器交互数据
ssl_input = new BufferedInputStream(ssl_socket.getInputStream());
ssl_output = new BufferedOutputStream(ssl_socket.getOutputStream());

  以上做法只有我们的 ca_cert.crt 才会作为信任的锚点,只有 ca_cert.crt 以及他签发的证书才会被信任。换言之只有被信任的服务端证书才可以建立SSL通信。
  这里可以延伸出很多所谓有趣的玩法。
  考虑到证书会过期、升级,我们既不想只信任我们服务器的证书,又不想信任 Android 所有的 CA 证书。有个不错的的信任方式是把签发我们服务器的证书的根证书导出打包到 APK 中,然后用上述的方式做信任处理。仔细思考一下,这未尝不是一种好的方式。只要日后换证书还用这家 CA 签发,既不用担心失效,安全性又有了一定的提高。因为比起信任100多个根证书,只信任一个风险会小很多。正如最开始所说,信任锚点未必需要根证书。因此同样上面的代码也可以用于自签名证书的信任。
  这样的处理我想大家可以想到一个关键词——证书固定。没错上述的认证突破遇到的问题就是类似的相关操作。

证书固定

  HTTPS支持证书固定技术CertificatePinning,通俗的说就是对证书公钥做校验,看服务端证书是否符合期望。HttpsUrlConnection并没有对外暴露相关的API,而在Android大放光彩的OkHttp是支持证书固定的,虽然在Android中,OkHttp默认的 SSL 的实现也是调用了Conscrypt,但是重新用TrustManager对下发的证书构建了证书链,并允许用户做证书固定。具体API的用法可见CertificatePinner这个类。

另一些代码

KeyStore keyStore = KeyStore.getInstance("BKS"); // 访问keytool创建的Java密钥库
InputStream keyStream = context.getResources().openRawResource(R.raw.alitrust);

char keyStorePass[]="123456".toCharArray();  //证书密码
keyStore.load(keyStream,keyStorePass);

TrustManagerFactory trustManagerFactory =   TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);//保存服务端的授权证书

ssl_ctx = SSLContext.getInstance("SSL");
ssl_ctx.init(null, trustManagerFactory.getTrustManagers(), null);

  回到这篇文章中的apk问题中,相信根据上面证书固定以及keystoretruststore的叙述,大家应该就明白了个中问题。上述的这些代码就是apk中相关代码的简化,也比较便于理解。
  说到底,在这种模式下,客户端会制作一个truststore来存储受信任的服务器证书列表,使用BurpSuite或fiddler等中间人代理时,中间人证书并不在truststore信任范围内,因此并不能建立起SSL通信也就导致APP程序出现服务器证书异常通信异常等报错。

个人的一些想法

  根据文章中所叙述的内容以及目前了解到的资料,我大胆提出一些自己的看法:目前的思路就是如何制作出一个包含中间人代理证书的truststore文件,让中间人代理变成client端所信任的"服务端"。
  目前来说我暂时还没有什么办法,后续如果有所成果会更新进来。

参考链接

Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法

[原创]安卓APP抓包之双向认证突破

KeyStore 和 TrustStore的区别及联系

JAVA Keytool工具生成Keystore和Truststore文件

安卓应用层抓包通杀脚本