这篇文章上次修改于 958 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

原文 -> https://blog.csdn.net/qq_38851536/article/details/117418582


前言

这是SO逆向入门实战教程的第一篇,总共会有十三篇,十三个实战。有以下几个注意点:

  • 主打入门级的实战,适合有一定基础但缺少实战的朋友(了解JNI,也上过一些Native层逆向的课,但感觉实战匮乏,想要壮壮胆,入入门)。
  • 侧重新工具、新思路、新方法的使用,算法分析的常见路子是Frida Hook + IDA ,在本系列中,会淡化Frida 的作用,采用Unidbg Hook + IDA 的路线。
  • 主打入门,但并不限于入门,你会在样本里看到有浅有深的魔改加密算法、以及OLLVM、SO对抗等内容。
  • 细,非常的细,奶妈级教学。
  • 共十三篇,1-2天更新一篇。每篇的资料放在文末的百度网盘中。

准备

52480-9lc4ijqwpsf.png

s方法就是我们的分析目标,它接收两个参数。参数1是字节数组,参数二是布尔值,为false。伪代码如下:

import java.nio.charset.StandardCharsets;

public class oasis {

    public static void main(String[] args) {
        String input1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8" +
                "uyTumcCNm4e8awxyC2ANU.&cfrom=28B529501" +
                "0&cuid=5999578300&noncestr=46274W9279Hr1" +
                "X49A5X058z7ZVz024&platform=ANDROID&timestamp" +
                "=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8_" +
                "_Android__Android10&version=3.5.8&vid=10190135" +
                "94003&wm=20004_90024";

        Boolean input2 = false;

        oasis test = new oasis();
        String sign = test.s(input1.getBytes(StandardCharsets.UTF_8), input2);
        System.out.println(sign);

    }

    public String s(byte[] barr, boolean z){
        return "Sign";
    };
}

返回值是32位的Sign,比如伪代码中的输入,返回3882b522d0c62171d51094914032d5ea ,且输入不变的情况下,输出也固定不变。

Unidbg模拟执行

目标函数的实现在liboasiscore.so中,在IDA中看一下

42613-fo3h05s6t1j.png

似乎并不是静态绑定,接下来去JNI OnLoad查看一下动态绑定的位置。

55459-n1xezkvsclr.png

很不幸的是,这个样本经过了OLLVM混淆,很难直接找到动态绑定的地址。

在以Frida为主的路子里,我们可以使用hook_RegisterNatives脚本得到动态绑定的地址,但这里我们采用Unidbg的方案。

下载Unidbg最新版,在IDEA中打开,跑通如图例子,和图示一致即可。
30550-2kxvk343tv6.png

首先搭一个架子,新建包和类,并把APK和SO资源放在目录下

66161-f85vj32m808.png

首先导入通用且标准的类库,然后一步步往下,如下写了注释

package com.lession1;

// 导入通用且标准的类库
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;

import java.io.File;

// 继承AbstractJni类
public class oasis extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    oasis() {
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.oasis").build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession1\\lvzhou.apk"));
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession1\\liboasiscore.so"), true); // 加载so到虚拟内存
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        vm.setJni(this); // 设置JNI
        vm.setVerbose(true); // 打印日志

        dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
    };

    public static void main(String[] args) {
        oasis test = new oasis();
    }
}

样本的init相关函数和JNI OnLoad函数已经运行过了,接下来Run

63446-24jadx0y7ct.png

日志中可以发现,JNI OnLoad中主要做了两件事

  • 签名校验
  • 动态绑定

值得一提的是,如果创建Android虚拟机时,选择不传入APK,填入null,那么样本在JNI OnLoad中所做的签名校验,就需要我们手动补环境校验了。

47573-ntzqejze2ki.png

接下来就是如何执行我们目标函数的问题了,这并不是一个小问题。Unidbg封装了相关方法执行JNI函数以及有符号函数等,但需要区分类方法和实例方法,我觉得有些别扭,在Frida的使用过程中,通过地址直接Hook和Call是一种非常美妙的体验,所以这里我们只介绍通过地址模拟执行这个更一般和通用的法子,如果对前一种方法感兴趣,可以看Unidbg提供的相关测试代码。

字节数组需要裹上unidbg的包装类,并加到本地变量里,两件事缺一不可。

package com.lession1;

// 导入通用且标准的类库
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

// 继承AbstractJni类
public class oasis extends AbstractJni{
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    oasis() {
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.sina.oasis").build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession1\\lvzhou.apk"));
        //
//        vm = emulator.createDalvikVM(null);

        // 加载目标SO
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession1\\liboasiscore.so"), true); // 加载so到虚拟内存
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        vm.setJni(this); // 设置JNI
        vm.setVerbose(true); // 打印日志

        dm.callJNI_OnLoad(emulator); // 调用JNI OnLoad
    };

    public static void main(String[] args) {
        oasis test = new oasis();
        System.out.println(test.getS());
    }

    public String getS(){
        // args list
        List<Object> list = new ArrayList<>(10);
        // arg1 env
        list.add(vm.getJNIEnv());
        // arg2 jobject/jclazz 一般用不到,直接填0
        list.add(0);
        // arg3 bytes
        String input = "aid=01A-khBWIm48A079Pz_DMW6PyZR8" +
                "uyTumcCNm4e8awxyC2ANU.&cfrom=28B529501" +
                "0&cuid=5999578300&noncestr=46274W9279Hr1" +
                "X49A5X058z7ZVz024&platform=ANDROID&timestamp" +
                "=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8_" +
                "_Android__Android10&version=3.5.8&vid=10190135" +
                "94003&wm=20004_90024";
        byte[] inputByte = input.getBytes(StandardCharsets.UTF_8);
        ByteArray inputByteArray = new ByteArray(vm,inputByte);
        list.add(vm.addLocalObject(inputByteArray));
        // arg4 ,boolean false 填入0
        list.add(0);
        // 参数准备完成
        // call function
        Number number = module.callFunction(emulator, 0xC365, list.toArray())[0];
        String result = vm.getObject(number.intValue()).getValue().toString();
        return result;
    }
}

可以发现,通过地址方式调用,似乎有一点点麻烦,但这个麻烦其实并不大,我们常常需要对未导出函数进行分析,与其一会儿用符号名调用,一会儿用地址,不如统一用地址嘛。

运行测试

05721-3u2pwvq1xpn.png

结果与Hook得到的一致,即模拟执行顺利完成。

ExAndroidNativeEmu 模拟执行

这个样本非常简单,我们用ExAndroidNativeEmu来梅开二度。如果说Unidbg是小号游轮,那么ExAndroidNativeEmu就是皮划艇。在此处皮划艇可有可无,我们只是做个演示,但有时非皮划艇不可,遇到了我们再说。

import posixpath
from androidemu.emulator import Emulator, logger
from androidemu.java.classes.string import String

# Initialize emulator
emulator = Emulator(
    vfp_inst_set=True,
    vfs_root=posixpath.join(posixpath.dirname(__file__), "vfs")
)

# 加载SO
lib_module = emulator.load_library("tests/bin/liboasiscore.so")

# find My module
for module in emulator.modules:
    if "liboasiscore" in module.filename:
        base_address = module.base
        logger.info("base_address=> 0x%08x - %s" % (module.base, module.filename))
        break

# run jni onload
emulator.call_symbol(lib_module, 'JNI_OnLoad', emulator.java_vm.address_ptr, 0x00)
# 准备参数
a1 = "aid=01A-khBWIm48A079Pz_DMW6PyZR8uyTumcCNm4e8awxyC2ANU.&cfrom=28B5295010&cuid=5999578300&noncestr=46274W9279Hr1X49A5X058z7ZVz024&platform=ANDROID&timestamp=1621437643609&ua=Xiaomi-MIX2S__oasis__3.5.8__Android__Android10&version=3.5.8&vid=1019013594003&wm=20004_90024"
# 通过地址直接调用
result = emulator.call_native(module.base + 0xC364 + 1, emulator.java_vm.jni_env.address_ptr, 0x00, String(a1).getBytes(emulator, String("utf-8")), 0)
# 打印结果
print("result:"+ result._String__str)

ExAndroidNativeEmu同样提供了对JNI函数的调用封装,但我们这边依然用地址方式调用,就是不用,就是玩儿。

2021-06-01T13:23:27.png

可以发现结果也很顺利,在这个样本上,Unidbg和ExAndroidNativeEmu 都能很轻松的处理,其中ExAndroidNativeEmu的代码量甚至更少一些,这得益于样本中和JAVA层的交互极少,一旦涉及到JNI交互,皮划艇就让人难受了,在Python中补JAVA的逻辑,简直不是人该受的委屈。

但ExAndroidNativeEmu 也有它的用武之地

  • 代码量较少,适合学习和分析,可以方便的结合自己的知识和业务增删功能。
  • 在样本比较简单的情况下(即与JAVA交互少,系统调用少,一切都少,只是纯粹的Native运算)甚至比Unidbg更好用。
  • ExAndroidNativeemu的code trace做的比Unidbg好很多,在指令的trace上做了非常多的优化。

算法分析

因为这是个简单的样本,输出又是32位,很容易就让人联想到哈希算法,掏出FindHash跑一下。

41379-31el9s4w9hk.png

运行FindHash提示的脚本,根据输出找到对应的函数并分析,很快就定位到0x8AB2这个函数,并且它是MD5_Update函数。如果对各类算法的原理缺少了解,可以看一下R0ysue的SO基础课哟,手算MD5/SHA1/DES/AES +工程实践+逆向分析,就等你来。

function hook_md5_update(){
    var targetSo = Module.findBaseAddress("liboasiscore.so");
    let relativePtr = 0x8AB2 + 1;

    console.log("Enter");
    let funcPtr = targetSo.add(relativePtr);
    Interceptor.attach(funcPtr,{
        onEnter:function (args) {
            console.log(args[2]);
            console.log(hexdump(args[1],{length:args[2].toInt32()}));

        },onLeave:function (retval){
        }
    })
}

Hook结果

 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
b2fa5800  59 50 31 56 74 79 26 24 58 6d 2a 6b 4a 6b 6f 52  YP1Vty&$Xm*kJkoR
b2fa5810  2c 4f 70 6b 26 61 69 64 3d 30 31 41 2d 6b 68 42  ,Opk&aid=01A-khB
b2fa5820  57 49 6d 34 38 41 30 37 39 50 7a 5f 44 4d 57 36  WIm48A079Pz_DMW6
b2fa5830  50 79 5a 52 38 75 79 54 75 6d 63 43 4e 6d 34 65  PyZR8uyTumcCNm4e
b2fa5840  38 61 77 78 79 43 32 41 4e 55 2e 26 63 66 72 6f  8awxyC2ANU.&cfro
b2fa5850  6d 3d 32 38 42 35 32 39 35 30 31 30 26 63 75 69  m=28B5295010&cui
b2fa5860  64 3d 35 39 39 39 35 37 38 33 30 30 26 6e 6f 6e  d=5999578300&non
b2fa5870  63 65 73 74 72 3d 4a 32 33 33 39 67 41 43 79 30  cestr=J2339gACy0
b2fa5880  44 35 6b 33 32 39 35 33 71 30 31 67 74 66 36 78  D5k32953q01gtf6x
b2fa5890  30 38 31 39 26 70 6c 61 74 66 6f 72 6d 3d 41 4e  0819&platform=AN
b2fa58a0  44 52 4f 49 44 26 74 69 6d 65 73 74 61 6d 70 3d  DROID&timestamp=
b2fa58b0  31 36 32 31 35 32 36 32 39 38 31 32 37 26 75 61  1621526298127&ua
b2fa58c0  3d 58 69 61 6f 6d 69 2d 4d 49 58 32 53 5f 5f 6f  =Xiaomi-MIX2S__o
b2fa58d0  61 73 69 73 5f 5f 33 2e 35 2e 38 5f 5f 41 6e 64  asis__3.5.8__And
b2fa58e0  72 6f 69 64 5f 5f 41 6e 64 72 6f 69 64 31 30 26  roid__Android10&
b2fa58f0  76 65 72 73 69 6f 6e 3d 33 2e 35 2e 38 26 76 69  version=3.5.8&vi
b2fa5900  64 3d 31 30 31 39 30 31 33 35 39 34 30 30 33 26  d=1019013594003&
b2fa5910  77 6d 3d 32 30 30 30 34 5f 39 30 30 32 34        wm=20004_90024
0x1a
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
bbe3c322  80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
bbe3c332  00 00 00 00 00 00 00 00 00 00                    ..........
0x8
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
bae15ed8  f0 08 00 00 00 00 00 00

MD5 Update一共被调用了三次,需要注意的是,MD5的Update的后两次调用,都是数据的填充,属于算法内部细节,所以我们只用关注第一次的输出。

我们的明文是从aid开始的,前面多了一块,这一块每次运行都不变,所以猜测它是盐,使用逆向之友Cyberchef测试一下:

55376-23e5sqf6283.png

大功告成!

尾声

这是一个非常简单的样本,用于熟悉Unidbg的简单操作。下一讲会复杂一点点的,熟悉Unidbg的更多基础操作。

样本百度网盘:https://pan.baidu.com/s/1eg7FRtbKkD2ZEh6nARBK7w 提取码:yh5h

2021-06-01T13:29:44.png