Shopee算法分析 - x-sap-ri

博客地址:http://blog.androidcrack.com/index.php/archives/38/

去除干扰项

在上篇文章中说到, 我们主动调用了几次,返回结果都是不同的

相同参数, 我们主动多次call. 可以看到结果是不同的. 只有一个Key不同.

接下来, 引用龙哥的文章

引用自龙哥文章, 我仅仅是对关键信息做加粗

1.1 引言

在使用 Unidbg 模拟执行以及辅助算法还原时,有这样一种需求——入参不变,返回值也不变。本文讨论这个需求的使用场景以及如何实现它。

1.2 问题分析

在代码开发过程中,因为各种各样的原因,常常会引入随机数、时间戳等信息参与运算。这些信息随着时间、系统状态等因素不停变化,它们导致函数入参不变的情况下,计算出的结果不固定。在模拟执行以及算法还原时,我们有控制这些干扰项,使得结果固定的需求。

对于模拟执行而言,如果能固定结果,就能确定 Unidbg 补环境是否符合预期,是否和真实环境完全一样。下面详细展开讨论。

在固定干扰项后,使用 Unidbg 与 Frida 调用目标函数且入参一致时,理论上返回结果也一致,这意味着我们得到了绝对意义上的,完全正确的模拟执行。

具体操作流程如下

  1. 在 Unidbg 中找到和固定干扰项,使得 Unidbg Call 在入参不变的情况下结果固定。
  2. 参考 Unidbg,用 Frida 同等固定干扰项(使用 inline hook / patch / replace 等等),使得 Frida Call 在入参不变的情况下结果固定。
  3. 对比两个固定值是否一致。

在顺序上,选择先在 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 库函数,它其实只是底层同名系统调用的简单封装。

image.png

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 
### Shopee 数据采集方法及工具 #### 方法一:使用 Shopee 开放平台 API 接口 Shopee 提供了开放平台 API,允许开发者通过注册成为开发者并创建应用来获取 App Key 和其他必要信息。完成身份验证后,可以调用 API 获取商品详情数据。此方法适合具备一定技术背景的用户,能够灵活控制数据采集范围和频率[^2]。 #### 方法二:借助第三方 API 服务商 对于不具备开发能力的用户,可以选择市场上已有的第三方 API 服务商。这些服务商通常提供即插即用的服务,简化了数据采集流程。例如 Anzexi58 等公司提供的解决方案,可帮助用户快速接入 Shopee 商品数据流。 #### 方法三:手动抓取网页数据 当所需数据量较小时,可以直接利用浏览器内置的开发者工具(如 Chrome 的 DevTools),分析目标页面发出的网络请求及其参数结构。基于此信息,可以用编程语言(如 Python 或 PHP)编写脚本模拟请求过程,进而提取 JSON 格式的商品详情数据[^3]。 #### 方法四:采用专业的 ERP 工具 像店梯 ERP 这样的管理软件集成了自动化采集功能,支持通过关键词搜索的方式从多个电商渠道收集产品信息。它不仅限于单一平台操作,还允许多店铺同步处理,极大地方便了跨平台运营者的工作需求[^4]。 #### 方法五:运用通用爬虫框架 针对更复杂场景下的大规模数据挖掘任务,则推荐使用 Scrapy、BeautifulSoup 等开源库构建专属爬虫程序。这类方案虽然前期投入较大,但长期来看能带来更高的效率与自定义程度。 ```python import requests def fetch_shopee_product(product_id, access_token): url = f"https://partner.shopeemobile.com/api/v2/product/get?access_token={access_token}&product_id={product_id}" headers = {"Content-Type": "application/json"} response = requests.get(url, headers=headers) if response.status_code == 200: data = response.json() return data['response']['item'] else: raise Exception(f"Failed to retrieve product details: {response.text}") # Example usage of the function with dummy values. try: sample_data = fetch_shopee_product(123456789, 'your_access_token_here') print(sample_data) except Exception as e: print(e) ``` 上述代码片段展示了如何利用 Requests 库向 Shopee 发起 GET 请求以取得指定 ID 下的产品资料实例。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值