Bootstrap

Android逆向之实战逆向APP


前言

Android逆向之指令集和CPU架构

Android逆向之必备前置知识

经过前2篇逆向前置知识的铺垫之后,我们终于要开始逆向实操了。以下操作主要是体现一下逆向的流程以及实操,为了安全考虑,并不会直接给出APP名称。
从目标出发,一步步去完成我们的目标,回头再看,大家就会发现,不过如此罢了。

逆向目标

获取APP中的某个功能,例如点击加载模型,模型会自带一些事件触发逻辑。博主的目的是拿到这部分事件逻辑的代码,参考一下。
例如下图的加载模型。
image.png

环境准备

  1. 安装有linux系统的电脑,此处以manjaro linux 23.1.0-1为例。
  2. 安装有某APP的手机,此处以iPhone 13为例。
  3. linux系统上安装以下程序。
    1. 大家可以看到,安装的程序都是我们前置知识里面提过的,是必不可少的工具。
sudo pacman -Sy binutils python jadx radare2
sudo pip install mitmproxy
  1. 确保电脑和手机处于同一个网络,方便抓包操作。

开始逆向之旅

抓包

  1. 💻 在终端执行mitmweb命令, 此时会在默认浏览器中打开一个新的标签页,如下所示:

image.png

  1. 📱 在设置->无线局域网,点击已经连接的Wifi名称右边的编辑按钮,如下所示:

IMG_3978.PNG.JPG

  1. 📱 在进入Wifi编译页之后,滚动到页面最下方,点击配置代理,选择手动,之后在出现的服务器输入框中输入电脑的IP地址,** 端口输入框中填写8080**,点击存储,如下所示:

x.jpg

  1. 📱 在默认浏览器中,此处为Safari, 打开 http://mitm.it, 滚动页面到对应平台处,此处为iOS, 按照页面提示的步骤,安装mitmproxy需要的CA证书,如下所示:

x.jpg

  1. 📱 打开 App,点击一个没有下载过的模型,触发下载,如下所示:

image.png

  1. 💻 在mitmweb打开的浏览器标签页,分析抓到的包列表,注意到有一个下载zip的请求,猜测是该模型对应的资源文件,如下所示:

image.png

  1. 💻 在终端执行以下命令,下载zip文件:
wget http://cdn3.cooolar.com/model/b/e/5/5c6a65be57f782510b26bdb4/ios_192/5c6a65be57f782510b26bdb4.zip
  1. 💻 在终端执行以下命令,解压zip文件:
unzip 5c6a65be57f782510b26bdb4.zip

提示需要输入解压密码,如下所示,这似乎进一步证实该zip包就是我们需要的模型资源文件。
image.png
接下来我们需要找到该zip包解压需要的密码。

寻找解压密码

  1. 💻 下载网址可以自己在网上找,当星网-绿色软件_最新绿色下载软件_免费软件下载网站 - 当星网这个网址可以下载到部分的APK文件。下载某APP的APK, 保存为 magicxx.apk
  2. 💻** **终端执行以下命令,解压出APK相应的资源文件和Java源码:
    1. 还记得吗,就是我们前置知识中说的jadx反编译工具
jadx -d magicxx magicxx.apk
cd magicxx
  1. 💻 终端执行以下命令,查找zip相关的Java文件:
find sources -name "*zip*.java"

得到以下结果:

sources/net/lingala/zip4j/unzip/Unzip.java
sources/net/lingala/zip4j/unzip/UnzipEngine.java
sources/net/lingala/zip4j/unzip/UnzipUtil.java
sources/net/lingala/zip4j/model/UnzipParameters.java
sources/net/lingala/zip4j/model/UnzipEngineParameters.java
sources/com/xx/magicalar/mvp/view/model/IModelUnzipView.java
sources/okio/GzipSink.java
sources/okio/GzipSource.java

应该是使用了net.lingala.zip4j库进行unzip操作。

  1. 💻 查看zip4j的API文档,发现有一个setPassword函数:

image.png

  1. 💻 终端执行以下命令,搜索Java源码中使用setPassword函数的源码:
grep -r setPassword sources

得到以下结果:

sources/net/lingala/zip4j/core/HeaderReader.java:            localFileHeader.setPassword(fileHeader.getPassword());
sources/net/lingala/zip4j/core/ZipFile.java:    public void setPassword(String str) throws ZipException {
sources/net/lingala/zip4j/core/ZipFile.java:        setPassword(str.toCharArray());
sources/net/lingala/zip4j/core/ZipFile.java:    public void setPassword(char[] cArr) throws ZipException {
sources/net/lingala/zip4j/core/ZipFile.java:                ((FileHeader) this.zipModel.getCentralDirectory().getFileHeaders().get(i)).setPassword(cArr);
sources/net/lingala/zip4j/model/ZipParameters.java:    public void setPassword(String str) {
sources/net/lingala/zip4j/model/ZipParameters.java:        setPassword(str.toCharArray());
sources/net/lingala/zip4j/model/ZipParameters.java:    public void setPassword(char[] cArr) {
sources/net/lingala/zip4j/model/FileHeader.java:    public void setPassword(char[] cArr) {
sources/net/lingala/zip4j/model/LocalFileHeader.java:    public void setPassword(char[] cArr) {
sources/com/xx/magicalar/retrofitUtils/downmodel/DownLoadUtil.java:                            zipFile.setPassword(MD5_1.substring(MD5_1.length() - 10).toCharArray());
sources/com/xx/magicalar/retrofitUtils/scandown/ScanDownUtils.java:                        zipFile.setPassword(MD5_1.substring(MD5_1.length() - 10).toCharArray());
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleDrawable(int i) {
sources/android/support/design/widget/TextInputLayout.java:        setPasswordVisibilityToggleDrawable(i != 0 ? AppCompatResources.getDrawable(getContext(), i) : null);
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleDrawable(Drawable drawable) {
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleContentDescription(int i) {
sources/android/support/design/widget/TextInputLayout.java:        setPasswordVisibilityToggleContentDescription(i != 0 ? getResources().getText(i) : null);
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleContentDescription(CharSequence charSequence) {
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleEnabled(boolean z) {
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleTintList(ColorStateList colorStateList) {
sources/android/support/design/widget/TextInputLayout.java:    public void setPasswordVisibilityToggleTintMode(PorterDuff.Mode mode) {
sources/android/support/v4/widget/ExploreByTouchHelper.java:        obtain.setPassword(obtainAccessibilityNodeInfo.isPassword());
sources/android/support/v4/view/accessibility/AccessibilityRecordCompat.java:    public void setPassword(boolean z) {
sources/android/support/v4/view/accessibility/AccessibilityRecordCompat.java:        this.mRecord.setPassword(z);
sources/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java:    public void setPassword(boolean z) {
sources/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java:        this.mInfo.setPassword(z);

其中sources/com/xx/magicalar/retrofitUtils/downmodel/DownLoadUtil.java中的setPassword调用应该是和模型下载之后解压相关的。

  1. 💻 打开sources/com/xx/magicalar/retrofitUtils/downmodel/DownLoadUtil.java,找到setPassword调用的地方,只有436行一处调用,如下所示:
if (zipFile.isEncrypted()) {
    String substring = str.substring(0, str.indexOf(".zip"));
    String MD5_1 = Signature.MD5_1(new Object[]{substring.substring(substring.length() - 10), Constant.getZipSecret()});
    zipFile.setPassword(MD5_1.substring(MD5_1.length() - 10).toCharArray());
}
zipFile.extractAll(str2);

看到密码是通过zip文件名以及Constant.getZipSecret()函数的返回值计算MD5得到的。其中zip文件名是5c6a65be57f782510b26bdb4.zip,还需要得到**Constant.getZipSecret()**返回值。

  1. 💻 终端执行以下命令,搜索Java源码中getZipSecret函数的定义:
    1. 这里涉及到了Android的native层,前置知识中提到过
grep -r getZipSecret sources

得到以下结果:

sources/com/xx/magicalar/retrofitUtils/downmodel/DownLoadUtil.java:                            String MD5_1 = Signature.MD5_1(new Object[]{substring.substring(substring.length() - 10), Constant.getZipSecret()});
sources/com/xx/magicalar/retrofitUtils/scandown/ScanDownUtils.java:                        String MD5_1 = Signature.MD5_1(new Object[]{substring.substring(substring.length() - 10), Constant.getZipSecret()});
sources/com/xx/magicalar/common/Constant.java:    public static native String getZipSecret();

看到该函数是以native方式定义在Constant.java中的。需要去APK中的so库中寻找getZipSecret()函数的实现

so库逆向

  1. 💻 在终端执行以下命令,搜索包含getZipSecret相关信息的so库:
ls resources/lib/armeabi-v7a/*.so | xargs nm -A  2>&1 | grep -v "no symbols" | c++filt | grep getZipSecret

得到以下结果:

resources/lib/armeabi-v7a/libnative-lib.so:000116c9 T Java_com_qjtc_magicalar_common_Constant_getZipSecret

libnative-lib.so中有Java_com_xx_magicalar_common_Constant_getZipSecret函数的定义。

  1. 💻 在终端执行以下命令,使用radare2分析libnative-lib.so:
    1. 重头戏来了,二进制文件分析
r2 resources/lib/armeabi-v7a/libnative-lib.so
  1. 💻 在radare2程序中,依次执行以下命令
    1. aaaa:分析这个二进制文件
    2. s sym.Java_com_qjtc_magicalar_common_Constant_getZipSecret指令 :跳转到指定函数地址

然后我们跳转到Java_com_qjtc_magicalar_common_Constant_getZipSecret函数起始地址处, 如下所示:
image.png

  1. 💻 在radare2程序中,执行pdf指令,查看Java_com_xx_magicalar_common_Constant_getZipSecret函数的实现, 如下所示:
    1. 没错,开始需要汇编知识了

image.png

  1. 💻 分析函数代码,看到在libnative-lib.so文件的0xf8d5处有一串字符串d(EcaK#9cBQL,就是需要的secret:

image.png

解压密码生成函数

💻 根据得到的secret数据d(EcaK#9cBQL, 以及之前的相关的Java源码:

if (zipFile.isEncrypted()) {
    String substring = str.substring(0, str.indexOf(".zip"));
    String MD5_1 = Signature.MD5_1(new Object[]{substring.substring(substring.length() - 10), Constant.getZipSecret()});
    zipFile.setPassword(MD5_1.substring(MD5_1.length() - 10).toCharArray());
}
zipFile.extractAll(str2);

重新使用Python实现如下代码,保存成unzip.py:

import hashlib
import sys

ZipSecret = 'd(EcaK#9cBQL'


def genPassword(zipFile: str) -> str:
    substring = zipFile[0:zipFile.index('.zip')]
    md5 = hashlib.md5((substring[-10:] + ZipSecret).encode()).hexdigest()
    return md5[-10:]

if __name__ == '__main__':
    print(genPassword(sys.argv[1]))

在终端执行以下代码,

python unzip.py 5c6a65be57f782510b26bdb4.zip

得到解压密码为 1cfa866766, 执行 unzip 5c6a65be57f782510b26bdb4.zip, 输入加压密码,正常解压。最终得到如下内容:

5c6a65be57f782510b26bdb4
├── 5c6a65be57f782510b26bdb4.bytes
├── 5c6a65be57f782510b26bdb4.png
└── LuaProject
    └── main.lua

查看5c6a65be57f782510b26bdb4.png文件,的确是我们下载的模型的缩略图。而这个main.lua就是我们想要参考的事件逻辑代码了。

总结

是的,整体流程也许没有大家想象中的那么神秘,但我们确实是实现了最初的逆向目标。在整个流程中也用到了前面提到的必备前置知识。大家可以举一反三,比如逆向目标变成下载所有的模型?比如逆向目标变成更改源代码重新打包?比如逆向目标变成检测自己公司内APP的安全性? 等等。

逆向不是银弹,有逆向自然也有防御。逆向的目标并不是只有破坏,很多逆向操作反而是为了完善自己的安全策略。所以呢,技术没有好坏,有好坏的是使用技术的人。

再会了各位!

end

;