安卓逆向新人练手项目

前言

这段时间开始接触安卓逆向,说一下我的大致的学习步骤:

  1. 学一个新知识之前需要对这个知识有一定的概念,比如js逆向,肯定要抓接口,然后看参数,搜参数,打断点等。那么拿到一个APP下一步要干些什么也需要有一些概念。如何积累概念:多看一些逆向的文章,某些地方没看懂不重要,重要的是流程你有个大概了。
  2. 找一些简单的例子练练手,比如吾爱破解论坛的:https://www.52pojie.cn/thread-408645-1-1.html。另外吾爱破解的移动安全区也有很多的文章可以看看。还有一些Crackme拿来练手,看看评论和描述,先挑简单的尝试。
  3. 看一些网上的课,免费的付费的都行。没什么推荐的,我也是刚接触不太懂哪个好。
  4. 接着你会觉得,每下载一个APP都想抓个包,然后把它拖到jeb反编译一下,这样久了就越加熟练了。

涉及软件

模拟器推荐逍遥模拟器,因为雷电4无法抓包,夜神无法安装xposed,只有逍遥啥毛病没有,我用的去广告绿色版。下载链接:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1353837&highlight=%E5%D0%D2%A3

JEB和Android Killer等:https://down.52pojie.cn/Tools/Android_Tools/

抓包工具我比较喜欢charles:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1350618&highlight=charles

新人练手项目

样本APP:https://wwx.lanzoui.com/iEatQmeg8je

这个APP属于比较简单的逆向,没有混淆没有加壳,虽然加密调用了so,so里面也是用的java加密库。

准备工作

打开模拟器,安装APP。在模拟器上安装charles证书,因为Android7.0不在信任用户安装的证书,所以https可能无法正常抓到包或者无法正常显示数据包。我的解决办法:先正常安装证书,然后将安装好的证书直接移动到系统证书目录去。

  1. 证书获取:charles->help->SSL proxying->save …(保存到一个目录改个名字就行)
  2. 复制到模拟器:
    adb devices 查看adb设备
    adb connect 127.0.0.1:21503 连接逍遥模拟器
    adb push 电脑证书目录 /sdcard 将证书复制到模拟器sd卡根目录
  3. 安装证书:打开设置->安全->从sd卡安装证书
  4. 移动证书到系统目录:下载安装es文件管理器或者re管理器将安装的证书移动到系统目录,用户证书目录:/data/misc/user/0/cacerts-added。系统证书目录:/system/etc/security/cacerts。
  5. 关闭模拟器重新打开,设置代理后,当模拟器有http请求时,charles就会弹出验证,问你是否允许charles记录来自某某ip的包,点允许就会有数据包了。代理设置电脑局域网IP即可,并不需要模拟器使用桥接模式。
  6. 如果还是没有包,可以看看proxy->SSL proxying settings里Enable SSL proxying有没有勾选,并且在include里add一个*:*(host和port都填*就行)
  7. 还是没有的话就只能百度了

温馨提示:在抓APP的时候可以关闭charles抓Windows的包,点击proxy->Windows proxy就可以停止抓Windows的包,当然你在Windows设置里取消全局代理一样的效果。

打开APP,查看数据包

只看首页一些栏目列表的数据包,随便搜索一下首页出现的文章的标题就能找到是哪条请求了,

URL:https://v6-gw.m.163.com/nc/api/v1/feed/dynamic/normal-list

参数有很多,可以多刷新几个请求看看哪些参数是变化的,测试发现就ts和sign是变化的,不过devId、devIdOD、lat和lon明显都有可能被加密,我们先处理sign,ts明显是时间戳。另外请求头也有很多加密值。

打开jeb,分析代码

将apk拖入到jeb的窗口,然后一直点是就行。接着直接在反编译的smali代码里搜索链接中的一部分比如normal-list。Ctrl+f是搜索快捷键,搜索的时候勾选环绕搜索和区分大小写。

接着你就会发现只搜索到了一个地方,你再次点击寻找的时候还是在原来的地方。点击normal-list所在的那行代码右键->解析,就会反编译出java代码。如图:

第一张
第二张
g.d很有可能就是链接的前一部分了,那么com.netease.newsreader.common.constant.g.l.c就是完整的链接了,点击c,c的背景色会变成黄色,说明被选中了,然后右键->交叉引用,就能知道这个变量在什么地方被调用了。
第三张
可以看到有两个地方引用了这个变量,点进去看会发现第二个其实就是上面的代码,所以第一个应该就是发送请求的位置了。点进去看看代码,
在这里插入图片描述
代码很长,就看后面那一部分,前面的应该只是判断参数是否正常。正常就执行后面那个,能发出请求说明肯定正常。

a.a(b.b(l.c, new NGRequestVar().setSize(Integer.valueOf(arg4)).setFn(Integer.valueOf(arg5)).setOffset(Integer.valueOf(arg3)).addExtraParam(new c("from", arg2))), arg6);

把代码分开来看

t1 = new NGRequestVar().setSize(Integer.valueOf(arg4)).setFn(Integer.valueOf(arg5)).setOffset(Integer.valueOf(arg3)).addExtraParam(new c("from", arg2)));
t2 = b.b(l.c, t1);
a.a(t2, arg6)

t1应该就是一个请求参数的容器对象,可以点进去看看,addExtraParam是添加额外参数的方法,其他几个方法有啥用就不分析了。

接着看b.b这个方法,双击点进去看看。
在这里插入图片描述

看到这些参数就知道这就是拼接参数的地方,而我们要找的ts和sign都在这里生成的。ts确实是10位的时间戳,虽然他还调用了一个com.netease.newsreader.common.utils.a.a.a,但是抓包的结果和System.currentTimeMillis() / 1000L;是一样的,就不用关心他里面的逻辑了,估计就是整型转字符串的方法。

String v0 = d.a();
long v1_4 = System.currentTimeMillis() / 1000L;
String v0_1 = v0 + v1_4;
sign = b.a(com.netease.newsreader.framework.e.a.c.b(v0_1))

而点进去d.a()可以看到v0,就是下面的代码生成的也就是v0_1

public static String a() {
  Object v0 = d.v.get("nrcommon_sys_1");
   if(v0 != null && ((v0 instanceof String))) {
       return (String)v0;
   }

   String v0_1 = ((IGalaxyApi)b.a(IGalaxyApi.class)).a(Core.context());
   if(!TextUtils.isEmpty(v0_1) && (TextUtils.isEmpty(a.a()))) {
       a.a(v0_1);
   }

   d.a("nrcommon_sys_1", v0_1);
   return v0_1;
}

IGalaxyApi是自定义的一个类,点进去看b.a和b.a().a这两个方法都挺麻烦的,就不看了下去了。等下可以直接调试一下看看这个值会不会变,上面的代码将他放到一个容器里去获取,如果容器没有才生成,虽然不知道具体算法,但这说明这个值在APP启动之后就是个固定值了。

先把他当成固定值来看,也就是说v0_1是一个固定的字符串+时间戳。我们在看c.b这个方法

 public static String b(String arg2) {
        if(TextUtils.isEmpty(arg2)) {
            return arg2;
        }

        try {
            return c.a(MessageDigest.getInstance("MD5").digest(c.a(arg2, Charset.forName("UTF-8"))), false);
        }
        catch(NoSuchAlgorithmException v2) {
            throw new AssertionError(v2);
        }
    }

应该只是做个简单的md5,至于到底是不是可以调试一下看看输入和输出是不是md5的结果。最后就是b.a这个方法了。

private static String a(String arg0) {
        return com.netease.newsreader.support.utils.k.b.a(Encrypt.getEncryptedParams(arg0));
    }

com.netease.newsreader.support.utils.k.b.a这个方法只是做个简单的url编码,可以不用看,关键就是Encrypt.getEncryptedParams(arg0)
点进去看,关键代码就是这个:
Encrypt.encrypt(arg0, arg1, arg2);arg0是Core.context(),不清楚是什么,arg1就是上面传入的参数,arg2是0.而你在追encrypt方法时发现:

 private static synchronized native byte[] encrypt(Context arg0, String arg1, int arg2) {
    }

怎么什么都没有,百度了一下发现这是个native方法,方法逻辑在so里面,在前面有段System.loadLibrary("random");就是加载so文件。文件名叫librandom.so。可以在jeb左侧的工程管理器看到
在这里插入图片描述
Libraries目录下的就有(arm64-v8a和armeabi这两个里面的so基本逻辑是一样的,选择哪个都行),可以选中这个so右键导出,然后用IDA查看,左侧红框对应的就是java的函数。
在这里插入图片描述
这样反编译的c代码基本看不懂,不过可以看到一些关键的字符串。当然也可以导入jni.h头文件,可以让代码更像代码。具体参考:https://www.52pojie.cn/thread-732955-1-1.html。

AES/ECB/PKCS7Padding,javax/crypto/Cipher,doFinal这些字符足以说明他是调用了java的加密库,用的AES的加密。既然是调用的java加密库,我们可以用frida hook一下java的库,来获取key,然后验证一下结果对不对。后面测试发现确实就是这样一个加密。

jeb调试代码

准备工作
上面只是静态分析了代码,虽然知道他用的什么算法,但不确定他有没有魔改算法,或者做些手脚。所以我们需要获取参数的中间值来验证一下。这里介绍一下jeb的调试。

要想APP能被调试,需要APP AndroidManifest文件中包含 android:debuggable=“true”,而一般的APP肯定不会有这个,需要修改后重打包,很麻烦一般不会去这么做。这篇文章说明了所有开启调试的方法,写的很好:https://blog.csdn.net/qq_38851536/article/details/100026480。

我采用的是mprop来修改ro.debuggable这个值,不过模拟器关闭之后需要重新修改。
mprop x86版本:https://github.com/jedy/mprop
adb push mprop /data/local/tmp将mprop复制到模拟器的这个目录
adb shell
su
cd /data/local/tmp
chmod 755 mprop
./mprop ro.debuggable 1就可以了

为了方便我直接使用es文件浏览器mprop移动到了/system/bin下了,这样就可以全局使用了,不用cd到目录。

下断点

String v0_1 = v0 + v1_4;
if(!TextUtils.isEmpty(v0_1)) {
     arg6.add(new com.netease.newsreader.framework.d.a.c("sign", b.a(com.netease.newsreader.framework.e.a.c.b(v0_1))));
 }

我们主要是需要v0_1的值,和c.b计算之后的值,来验证一下c.b是不是普通的md5,还有v0这个值是不是固定的。右键String v0_1 = v0 + v1_4;这行代码点击解析就会跳到smali代码,
在这里插入图片描述
对照一下java代码很容易知道v0_1和c.b所在的位置。所以我们在图上框中的两行下面一行下两个断点,断点快捷键为Ctrl+b。左侧有红点表示断点下成功了。
在这里插入图片描述
这个浅蓝色背景是程序断在这一行的时候才会有的。图中我还没附加进程,应该是上次打断点留下的bug,不用管。

确认一下adb devices下有模拟器设备,没有的话adb connect 127.0.0.1:21503重新连接一下。然后打开APP,接着点击下图红框的那个按钮。

在这里插入图片描述
会出现这样一个界面
在这里插入图片描述
双击红框的那行就是附加了这个进程,这个是APP的包名,其他三个应该是这个APP的一些服务。

附加之后在模拟器里操作APP向下滑动加载新的文章就会触发断点。可能会多次出现下图这种情况,点击等待即可。
在这里插入图片描述
程序断下之后,我们关注的是v0的值,又知道它的数据类型是string,可以直接把int改成string,复制他的值备用,复制完记得改回int
在这里插入图片描述
点击第二个按钮运行,跳到下一个断点。同样改成string复制值之后改回int。
在这里插入图片描述
这样我们就得到两个值,计算一下上面的值md5确实就是下面的值,这也就验证了c.b只是做了md5的计算:
string@13512:“CQlkYTE0MDdjZmM5NTYyZjUzCTYwNTkxOTMw1614743514”
string@13513:“683c1311ef69f993fe29df30d7508fcf”

在这里插入图片描述
去抓包工具看一下sign,不知道哪条请求可以对比一下后面的时间戳。也可以直接在smali里面下断点获取。如何知道683c1311ef69f993fe29df30d7508fcf怎么得到sign(s1DyoPiJ5EPZK8gVtknxrQbP0ITFkr7O102aFhxwfIN48ErR02zJ6/KXOnxX046I)呢?

首先我们已经知道了他是AES/ECB/PKCS7Padding的加密模式,可以百度到这个加密模式只需要秘钥key,不需要iv。所以只要获取一下key,验证结果对不对了。

frida hook AES获取key

frida的环境就不说了,百度一下很简单的,pip装个包,在下载个x86版本的frida-server复制到模拟器改个权限运行就行。

直接拷贝官方文章中的一段代码,做些修改就可以了:https://frida.re/docs/examples/android/
在这里插入图片描述
修改之后,因为key是个java的对象,需要转成js可以输出的形式,一般是16进制或者base64:

import frida, sys

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

jscode = """
function bytesToHex(arr) {
    var str = '';
    var k, j;
    for (var i = 0; i < arr.length; i++) {
        k = arr[i];
        j = k;
        if (k < 0) {
            j = k + 256;
        }
        if (j < 16) {
            str += "0";
        }
        str += j.toString(16);
    }
    return str;
}
Java.perform(function () {
  var Cipher = Java.use('javax.crypto.Cipher');
  var Exception = Java.use('java.lang.Exception');
  var Log = Java.use('android.util.Log');

  var init = Cipher.init.overload('int', 'java.security.Key');
  init.implementation = function (opmode, key) {
    var result = init.call(this, opmode, key);
    var bytes_key = key.getEncoded();
    console.log('Cipher.init() opmode:', opmode, 'key:', bytesToHex(bytes_key));//
    //console.log(stackTraceHere());

    return result;
  };

  function stackTraceHere() {
    return Log.getStackTraceString(Exception.$new());
  }
});
"""

process = frida.get_usb_device().attach('com.netease.newsreader.activity')
script = process.create_script(jscode)
script.on('message', on_message)

script.load()
sys.stdin.read()

将上面的代码命名为hookaes.py,然后命令行运行python hookaes.py,在APP里面翻页查看输出。
在这里插入图片描述
有多个key,但是只有最后两个是我翻页的时候才出现的,其他的应该是另外的接口使用的。
我们找个在线AES加密的网站验证一下这个key是不是对的。注意: key是16进制(hex)格式的,不是字符串

https://the-x.cn/cryptography/Aes.aspx
在这里插入图片描述
结果和上面的是一样的,也就是说,到现在sign的加密已经分析完了。

frida hook获取中间值

上面获取v0_1是使用jeb进行调试来获取的,其实用frida获取更简单。
arg6.add(new com.netease.newsreader.framework.d.a.c("sign", b.a(com.netease.newsreader.framework.e.a.c.b(v0_1))));

想要获取v0_1和c.b的执行结果,直接hook c.b。代码如下:

import frida, sys

def on_message(message, data):
    if message['type'] == 'send':
        print(message['payload'])
    else:
        print(message)

jscode = """
Java.perform(function () {
  send("注入成功!");
  var c = Java.use('com.netease.newsreader.framework.e.a.c');
  var b = c.b;
  b.overload('java.lang.String').implementation = function (v) {
    send(v);
    var result = b.call(this, v);
    send(result);
    return result;
  };

});
"""

process = frida.get_usb_device().attach('com.netease.newsreader.activity')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()

打开APP之后,运行这个Python脚本,然后在APP滑动加载即可显示参数和返回值,也就是我们要的。

按道理来说我也可以hook b.a来获取参数和返回值,但是试了一下没有效果,也不知道什么原因。如果有知道的,希望能指教下。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值