博客地址:http://blog.androidcrack.com/index.php/archives/38/
去除干扰项
在上篇文章中说到, 我们主动调用了几次,返回结果都是不同的
相同参数, 我们主动多次call. 可以看到结果是不同的. 只有一个Key不同.
接下来, 引用龙哥的文章
引用自龙哥文章, 我仅仅是对关键信息做加粗
1.1 引言
在使用 Unidbg 模拟执行以及辅助算法还原时,有这样一种需求——入参不变,返回值也不变。本文讨论这个需求的使用场景以及如何实现它。
1.2 问题分析
在代码开发过程中,因为各种各样的原因,常常会引入随机数、时间戳等信息参与运算。这些信息随着时间、系统状态等因素不停变化,它们导致函数入参不变的情况下,计算出的结果不固定。在模拟执行以及算法还原时,我们有控制这些干扰项,使得结果固定的需求。
对于模拟执行而言,如果能固定结果,就能确定 Unidbg 补环境是否符合预期,是否和真实环境完全一样。下面详细展开讨论。
在固定干扰项后,使用 Unidbg 与 Frida 调用目标函数且入参一致时,理论上返回结果也一致,这意味着我们得到了绝对意义上的,完全正确的模拟执行。
具体操作流程如下
- 在 Unidbg 中找到和固定干扰项,使得 Unidbg Call 在入参不变的情况下结果固定。
- 参考 Unidbg,用 Frida 同等固定干扰项(使用 inline hook / patch / replace 等等),使得 Frida Call 在入参不变的情况下结果固定。
- 对比两个固定值是否一致。
在顺序上,选择先在 Unidbg 里分析,然后迁移到真机上,这是因为在 Unidbg 找干扰项更容易,所有的系统调用、文件访问、JNI 都由 Unidbg 处理和监控,固定它们并进行测试十分方便。
对于算法还原而言,如果能固定干扰项,使得参数不变时结果固定,这会对辅助算法还原帮助巨大。这意味着,只要不改变入参,程序的控制流和数据流就完全固化。逆向分析好比闯关卡,分析人员有无数条命,而关卡总是固定,你可以砸时间不停试错,熟悉并最终击败每一个陷阱和挑战。这并非好无副作用,存在代码覆盖率和执行路径的问题,但和它带来的好处相比,是一笔划算买卖。
1.3 干扰项来源
这种随机干扰可能来源于 JNI、库函数、系统调用、文件访问等方面。
1.3.1 JNI
程序可以通过 JNI 调用 JAVA 中的各种变值,最常见的是时间和随机数。
时间
@Override
public long callStaticLongMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "java/lang/System->currentTimeMillis()J":{
return System.currentTimeMillis();
}
}
return super.callStaticLongMethodV(vm, dvmClass, signature, vaList);
}
随机数
@Override
public long callStaticLongMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "java/lang/System->currentTimeMillis()J":{
return 1667275121452L;
}
}
return super.callStaticLongMethodV(vm, dvmClass, signature, vaList);
}
1.3.2 库函数
比如 gettimeoday、clock_gettime 等库函数都可以获取时间信息。
先以 gettimeofday 为例,其函数原型如下。
int gettimeofday(struct timeval *tv, struct timezone *tz);
参数 1 tv 是指向 timeval 结构体的指针,这个结构体定义如下。tz 参数使用不多,此处不表。
struct timeval {
long tv_sec; 秒时间戳
long tv_usec; 余下的微秒
};
函数返回 0 代表调用成功,返回 -1 表示调用失败。
当我们只想获取秒级时间戳时,一般像下面这么写代码。
long gettime(){
struct timeval t{};
gettimeofday(&t, nullptr);
long sec = t.tv_sec;
return sec;
}
当我们想获取毫秒时间戳的时候,一般像下面这么写代码。
long long gettime2(){
struct timeval t{};
gettimeofday(&t, nullptr);
long sec = t.tv_sec;
long usec = t.tv_usec;
return (sec * 1000) + (usec / 1000);
}
它是获取时间戳的最常用函数,如果想使用 Frida 在真实环境中固定它,往往会直接替换它。
function hook_gettimeofday() {
var addr_gettimeofday = Module.findExportByName(null, "gettimeofday");
Interceptor.replace(addr_gettimeofday, new NativeCallback(function (ptr_tz, ptr_tzp) {
console.log("hook gettimeofday:", ptr_tz, ptr_tzp, result);
var t = new Int32Array(ArrayBuffer.wrap(ptr_tz, 8));
t[0] = 0xAAAA;
t[1] = 0xBBBB;
}, "int", ["pointer", "pointer"]));
}
除了 gettimeofday,clock_gettime 是另一个高频函数。这是一个语义相当丰富的函数,可以获取真实时间、进程时间、线程时间等多种类型的时间,函数原型如下。
int clock_gettime(clockid_t __clock, struct timespec* __ts);
timespec 比 timeval 更加精确,1 纳秒 = 1000 微秒。
struct timespec {
long tv_sec; // 秒时间戳
long tv_nsec; // 余下的纳秒时间戳
};
如果想和 gettimeofday 一样获取毫秒级时间戳,代码如下。
long long gettime3(){
struct timespec t = {0, 0};
clock_gettime(CLOCK_REALTIME, &t);
return (t.tv_sec*1000) + (t.tv_nsec/1000000);
}
参数 1 即 clockid ,它是所谓的时钟。当设置为 CLOCK_REALTIME 时,意味着获取真实时间信息,此时效果和 gettimeofday 一致,区别仅在于时间精度更高。时钟还有相当多的类型
#define CLOCK_REALTIME 0
#define CLOCK_MONOTONIC 1
#define CLOCK_PROCESS_CPUTIME_ID 2
#define CLOCK_THREAD_CPUTIME_ID 3
#define CLOCK_MONOTONIC_RAW 4
#define CLOCK_REALTIME_COARSE 5
#define CLOCK_MONOTONIC_COARSE 6
#define CLOCK_BOOTTIME 7
#define CLOCK_REALTIME_ALARM 8
#define CLOCK_BOOTTIME_ALARM 9
#define CLOCK_SGI_CYCLE 10
#define CLOCK_TAI 11
CLOCK_MONOTONIC 相关的时钟表示从某个不确定的点开始计时的时间,这个不确定的点往往是开机或开机后的某个时间。
CLOCK_BOOTTIME 相关的时钟表示了开机时间。
CLOCK_PROCESS_CPUTIME_ID 和 CLOCK_THREAD_CPUTIME_ID 代表了 CPU 在当前进程/线程上的耗时,相比较这个进程/线程的维持时间,CPU 耗时会是一个小很多的数字。
简而言之可以分为真实时间、CPU 时间、开机时间、从不确定的某个时间点开始计时这四部分。
除此之外,还有 time 、ftime 库函数,它们基于 gettimeofday,比如 time 的源码如下。
int __fastcall time(_DWORD *a1, int a2)
{
int v3; // r3
int v5[4]; // [sp+0h] [bp-10h] BYREF
v5[0] = (int)a1;
v5[1] = a2;
if ( j_gettimeofday(v5, 0) < 0 )
return -1;
v3 = v5[0];
if ( a1 )
*a1 = v5[0];
return v3;
}
还有 clock 库函数,它基于 clock_gettime,源码如下。
int __fastcall clock(int a1, int a2, int a3)
{
int result; // r0
int v4; // [sp+0h] [bp-10h] BYREF
int v5; // [sp+4h] [bp-Ch]
int v6; // [sp+8h] [bp-8h]
v4 = a1;
v5 = a2;
v6 = a3;
result = j_clock_gettime(2, &v4);
if ( result != -1 )
result = v5 / 1000 + 1000000 * v4;
return result;
}
总体上说,在库函数的层面,有不少函数可以获得时间信息。在真实环境上,我们可以用 Frida 对它们逐个 Hook、拦截和替换。同理,可以在 Unidbg 中 Hook gettimeofday 函数并固定它,但这不是最好的办法。
在获取随机数这件事上也类似,有各种各样的 random 函数可用,不容易在库函数的层面做全面拦截。
1.3.3 系统调用
样本可以通过内联汇编直接调用系统调用,而且绝大多数干扰项所对应的库函数也都基于系统调用,因此如果拦截和处理系统调用,就可以从根本上处理随机干扰项。
比如 gettimeofday 库函数,它其实只是底层同名系统调用的简单封装。
clock_gettime 也同理。
unsigned int __fastcall clock_gettime(clockid_t a1, struct timespec *a2)
{
unsigned int result; // r0
result = linux_eabi_syscall(__NR_clock_gettime, a1, a2);
if ( result > 0xFFFFF000 )
result = j___set_errno_internal(-result);
return result;
}
在 Unidbg 中,这些系统调用都由 Unidbg 自实现,因此可以很方便的做固定或修改。
首先是 gettimeofday,来到src/main/java/com/github/unidbg/unix/UnixSyscallHandler.java
。
protected int gettimeofday(Emulator<?> emulator, Pointer tv, Pointer tz) {
if (log.isDebugEnabled()) {
log.debug("gettimeofday tv=" + tv + ", tz=" + tz);
}
if (log.isDebugEnabled()) {
byte[] before = tv.getByteArray(0, 8);
Inspector.inspect(before, "gettimeofday tv=" + tv);
}
if (tz != null && log.isDebugEnabled()) {
byte[] before = tz.getByteArray(0, 8);
Inspector.inspect(before, "gettimeofday tz");
}
long currentTimeMillis = System.currentTimeMillis();
long tv_sec = currentTimeMillis / 1000;
long tv_usec = (currentTimeMillis % 1000) * 1000;
TimeVal32 timeVal = new TimeVal32(tv);
timeVal.tv_sec = (int) tv_sec;
timeVal.tv_usec = (int) tv_usec;
timeVal.pack();
if (tz != null) {
Calendar calendar = Calendar.getInstance();
int tz_minuteswest = -(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
TimeZone timeZone = new TimeZone(tz);
timeZone.tz_minuteswest = tz_minuteswest;
timeZone.tz_dsttime = 0;
timeZone.pack();
}
if (log.isDebugEnabled()) {
byte[] after = tv.getByteArray(0, 8);
Inspector.inspect(after, "gettimeofday tv after tv_sec=" + tv_sec + ", tv_usec=" + tv_usec + ", tv=" + tv);
}
if (tz != null && log.isDebugEnabled()) {
byte[] after = tz.getByteArray(0, 8);
Inspector.inspect(after, "gettimeofday tz after");
}
return 0;
}
直接将System.currentTimeMillis()
改为固定值即可固定 gettimeofday 系统调用。
clock_gettime 在 Unidbg 中则在src/main/java/com/github/unidbg/linux/ARM64SyscallHandler.java
,如果样本采用 32 位环境,则对应于src/main/java/com/github/unidbg/linux/ARM32SyscallHandler.java
。
private final long nanoTime