App逆向百例|12|某电商App Sign分析

简介: App逆向百例|12|某电商App Sign分析

观前提示:

本文章仅供学习交流,切勿用于非法通途,如有侵犯贵司请及时联系删除

样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMUNtbzZHU1lOUmM4a2l4YkVsT3Rkc1E/cHdkPWxpbm4=

0x1 抓包&定位

打开样本App 直接抓包

本次的受害参数为sign

打开jadx定位到sign的加密位置

加载的so为libjdbitmapkit.so

有了这些信息 上frida hook入参和结果

打开frida 启动样本App发现 App卡死闪退

可能有frida检测 那我们就使用葫芦娃大佬的魔改frida

github: https://github.com/hluwa/strongR-frida-android

继续启动frida 发现还是崩溃

猜测可能还有其他检测方式

根据网上文章给出的frida检测点进行多次尝试

经过多次试错后最后可知 样本App对frida的默认端口27042进行检测

那就需要让frida运行在非默认端口

/data/local/tmp/hluda-server-15.1.17-android-arm64 -l 127.0.0.1:8080

然后映射端口

adb forward tcp:8080 tcp:8080

最后启动frida

frida -H 127.0.0.1:8080

成功运行frida且不闪退后就可以对样本进行hook操作了

function hook_getSignFromJni() {
    Java.perform(function () {
        var Class = Java.use('com.xxxx.common.utils.BitmapkitUtils');
        var Method = "getSignFromJni"
        Class[Method].overload('android.content.Context', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function () {
            var result = this[Method]['apply'](this, arguments);
            console.log('----------------------');
            console.log('arg1:' + arguments[0]);
            console.log('arg2:' + arguments[1]);
            console.log('arg2:' + arguments[2]);
            console.log('arg2:' + arguments[3]);
            console.log('arg2:' + arguments[4]);
            console.log('arg2:' + arguments[5]);
            console.log('result:' + result);
            console.log('----------------------');
            return result;
        }
    })
}
setImmediate(hook_getSignFromJni)

通过hook 拿到了多个结果 选取其中一个进行固定分析

arg1:com.jingdong.app.mall.JDApp@f0dac2f
arg2:hotWords
arg2:{"originHotWord":"0","pageFrom":"1"}
arg2:3036c83c3c4b25a2
arg2:android
arg2:10.2.0
result:st=1664463515894&sign=7ce2045026643e68ea1639c5e291127f&sv=122

0x2 Unidbg黑盒调用

复制粘贴造个轮子运行

根据报错信息开始补环境

vm.resolveClass("android/app/Activity");

getpackagemanager所以需要在前面的基础上再完善一点环境

vm.resolveClass("android/app/Activity",vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);

ApplicationInfo也就是apk的存放位置 可以在RE文件管理器/data/app中找到对应的位置

new StringObject(vm,"/data/app/com.xxxx.app.mall-cd4VeJ0b5yxrR0Zb-io_MA=/base.apk");

unZip 看传入参数是什么

传入了仨参数

arg1->"/data/app/com.xxxx.app.mall-cd4VeJ0b5yxrR0Zb-io_MA=/base.apk"
arg2->"META-INF/"
arg3->".RSA"

根据参数打开APK中中的META-INF搜索RSA结尾的文件

根据这个文件名补即可

new ByteArray(vm,vm.unzip("META-INF/xxxx.RSA"));

vm.resolveClass("sun/security/pkcs/PKCS7").newObject(new PKCS7((byte[]) varArg.getObjectArg(0).getValue()));

PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();  
X509Certificate[] certificates = pkcs7.getCertificates();  
return ProxyDvmObject.createObject(vm,certificates);

这里的objectToBytes是java层的一个方法 直接去复制粘贴就行

new ByteArray(vm,objectToBytes(varArg.getObjectArg(0).getValue()));

补到这里 恭喜你完成初始化了

主动调用getSignFromJni

public void getSignFromJni(){
    ArrayList<Object> args = new ArrayList<>(10);
    args.add(vm.getJNIEnv());
    args.add(0);
    args.add(vm.addLocalObject(vm.resolveClass("android/content/Context").newObject(null)));
    args.add(vm.addLocalObject(new StringObject(vm,"hotWords")));
    args.add(vm.addLocalObject(new StringObject(vm,"{\"originHotWord\":\"0\",\"pageFrom\":\"1\"}")));
    args.add(vm.addLocalObject(new StringObject(vm,"3036c83c3c4b25a2")));
    args.add(vm.addLocalObject(new StringObject(vm,"android")));
    args.add(vm.addLocalObject(new StringObject(vm,"10.2.0")));
    Number number = module.callFunction(emulator, 0x28b5, args.toArray());
    System.out.println(vm.getObject(number.intValue()).getValue().toString());
}

然后接着报错接着补环境

vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());

StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();
return vm.resolveClass("java/lang/StringBuffer").newObject(stringBuffer.append(vaList.getObjectArg(0).getValue()));

Integer integer = new Integer(vaList.getIntArg(0));  
return vm.resolveClass("java/lang/Integer").newObject(integer);

Integer integer = (Integer) dvmObject.getValue();  
return vm.resolveClass("java/lang/Integer").newObject(integer.toString());

StringBuffer stringBuffer = (StringBuffer) dvmObject.getValue();
return new StringObject(vm,stringBuffer.toString());

恭喜你 补到这里就能出结果了

小声bb 写到这里就1k字了还没开始看算法

多次调用sv变动 st变动 sign也变动

0x3 算法分析

IDA打开直接搜索

看到直接是静态注册的

双击跳转过去

这一段全是拼接操作

st生成位置

sv生成位置

固定随机项

idea双击shift搜gettimeofday

改为固定的时间戳

固定了时间 不同的sv算出来的sign结果也不一致

观察代码 sv是通过lrand48生成的

固定lrand48

HookZz instance = HookZz.getInstance(emulator);
instance.wrap(module.findSymbolByName("lrand48"), new WrapCallback<HookZzArm32RegisterContext>() {
    int count=0;
    @Override
    public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
    }
    @Override
    public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
        count+=1;
        if(count==2){
            ctx.setR0(0x1);
        }
        if(count==1){
            ctx.setR0(0x1);
        }
    }
});

就是这俩处位置结果变动使sign的结果随之改变

改了这些就可以为所欲为了

前面拼接了一堆东西的结果传入sub_126AC

先放着继续往下看

sub_126AC传回的结果放入了sub_18B8

进入sub_18B8

看到这里似乎是一个Base64的码表

sub_18B8可能就是一个Base64方法

sub_18B8的结果由v66传出并且传入sub_227C

进入sub_227C

看到似曾相识的东西 这样看可能不是很明显 手动分割一下

  • 0xEFCDAB89
  • 0x67452301
  • 0x10325476
  • 0x98BADCFE

这不就是MD5

从魔数上看 似乎并没有魔改

验证一下前面的Base64和MD5猜想

拿到sub_126AC的结果进行Base64再MD5

验证成功 那重心就在sub_126AC

回到sub_126AC

这里有3个case分别代表三个算法

而算法的走向是由v24决定

v24是由sv2和sv3决定的

根据伪代码逻辑可以翻译为

unk_17440 = [0, 1, 2]
sv1 = 1
sv2 = 1
sv3 = 1
print(unk_17440[(sv2 + sv3) % 3])

根据结果得到下表

sv1 sv2 sv3 sv case
1 0 0 100 0
1 0 1 101 1
1 0 2 102 2
1 1 0 110 1
1 1 1 111 2
1 1 2 112 0
1 2 0 120 2
1 2 1 121 0
1 2 2 122 1

往下看这里先是内存拷贝了一个值然后根据v12 * 40的值进行偏移 其实实现的就是一个数组取值的操作这里根据伪代码可以翻译为

v11 = ['44e715a6e322ccb7d028f7a42fa55040', '7d0069660c9b5d32074facf37c3738a1', '80306f4370b39fd5630ad0529f77adb6']
v13 = v11[v12]

接下来的重点就是在

  • case 1->sub_10E18
  • case 2->sub_10DE4
  • case 0->sub_10E4C

前面手动固定了lrand48 sv为111

所以走的是case 2

进入sub_10DE4 只有三个方法

算法在sub_12ECC中 双击进来

根据hook入参得到以下结果

a2->80306f4370b39fd5630ad0529f77adb6
a3->0x1
a4->functionId=hotWords&body={"originHotWord":"0","pageFrom":"1"}&uuid=3036c83c3c4b25a2&client=android&clientVersion=10.2.0&st=1664689004670&sv=111
a5->0x8f

由入参可知a4为拼接后的明文 a5是a4的长度

所以这个if是必定成立的 else后面的那一块可以忽略不看

这一段主要在计算v21

这里的a3对应的是sv1 而sv1固定为1 所以同理 不用理else部分

这里就是sub_12ECC的核心计算位置

其中

v16 = &v21[7] + (v15 & 0xF);
v18 =*(v16 - 20);

从汇编中可知R0=(SP+0x20-0x14)+(R3&0xf)

所以这段实现的操作是v21[v15&0xf]

v21结果为SP+0xC

也就是前面小端结果

往下看

v17 = v15++ & 7;
result = ((v18 ^ *a4 ^ *(a2 + v17)) + v18);

此处为取值进行异或操作

LOBYTE(v18) = v18 ^ result;
*a4++ = v18;
*(a4 - 1) = v18 ^ *(a2 + v17);

将异或后的结果取低位然后再与a2[v17]进行运算最后算出结果

整个循环翻译成py简简单单没有难点

v15 = 0
v21 = [0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A]
while v15 != a5:
    v18 = v21[v15 % 16]
    v17 = v15 & 7
    result = (v18 ^ a4[v15] ^ a2[v17]) + v18
    v18 = v18 ^ result % 256
    a4[v15] = v18 ^ a2[v17]
    v15 += 1

运算后的结果Base64再MD5即为sign值

这是简单的case 2

接下来将前面固定lrand48的返回值改为0x2 使得sv为122

当sv为122时 走case 1

进入sub_10E18 和前面一样进来就是三个方法

但是不同的是出现了一个nullsub_1 那就分析不了吗?

并不然 从前面的分析结合这里可知 sub_125F0可能为初始化 sub_12510可能为计算核心方法 那nullsub_1就可能是释放 所以并不需要理睬nullsub_1

进入sub_12510

入参和前面基本一致 a2变为7d0069660c9b5d32074facf37c3738a1

这里循环每次取8个字节进入sub_10EA4计算 一共循环a5 >> 3

进入sub_10EA4 955行代码 有点多 不过大部分都能直接复制

这里一堆与操作的目的就是将传入的8位分别和0x80 0x40 0x20 0x10 8 4 2 1与操作扩展至64位

接着就是一堆赋值 直接Ctrl+C Ctrl+V

这里为遍历a2然后进行判断走不同分支

其中出现了一些goto操作

Python本身是没有的 但是可以依靠一个库goto-statement来实现

pip install goto-statement

https://www.w3cschool.cn/article/3069641.html

这里就是将前面计算好并且重新赋值后的64位循环4次计算 每次取16位

每次循环更改2位 循环4次一共8位

实现了goto 其他操作只需要复制粘贴复制粘贴并稍微改改就能实现 反正全靠肝

回到上层 这里一共循环0x8f >> 3 = 17 但是似乎还有部分明文并没有参与计算 而最后得出的结果显示 全部都参与了计算

hook验证猜想

确实 只循环了17次 后面还会有&sv=122没有参与计算 但是从最终结果来看 确实是计算到了 那是哪里计算了呢?

直接traceWrite

emulator.traceWrite(0x4021c080L,0x4021c080L+10L);

跳转0xfbd0

这里就是赋值位置 但是 这里居然有3509行 这谁顶得住

先不管 看一下sub_E7FC的交叉引用

只一个 跳转过来

(a4 & 7) - 1可知

这。。这段不就是根据未参与计算的明文的长度走不同的方法 而且每个方法都有上千行 留给有肝的人还原吧

将之前还原好的做个验证没问题

接下来将前面固定lrand48的返回值改为0x0 使得sv为100

当sv为100时 走case 0

进入sub_10E4C 一样的三个方法

进入sub_12580 看到核心方法是一样的sub_10EA4 不过16变成了32

其余的和前面分析case2的一致

到此整一个流程就基本走完了 最后再拼接成st=xxx&sign=xxx&sv=xxx即可


-恭喜你 看完了这篇又水又长的东西-

感谢各位大佬观看感谢大佬们的文章分享 如有错误 还请海涵共同进步 带带弟弟

相关文章
|
5月前
|
JSON 监控 数据格式
1688 item_search_app 关键字搜索商品接口深度分析及 Python 实现
1688开放平台item_search_app接口专为移动端优化,支持关键词搜索、多维度筛选与排序,可获取商品详情及供应商信息,适用于货源采集、价格监控与竞品分析,助力采购决策。
|
5月前
|
缓存 监控 Android开发
京东 item_get_app 接口深度分析及 Python 实现
京东item_get_app接口可获取商品原始详情数据,包含更丰富的字段和细节,适用于电商分析、价格追踪等场景。需通过认证获取权限,支持字段筛选和区域化数据查询。
|
6月前
|
缓存 数据挖掘 API
淘宝 item_get_app 接口深度分析及 Python 实现
淘宝item_get_app接口是淘宝开放平台提供的移动端商品详情数据获取接口,相较PC端更贴近APP展示效果,支持获取APP专属价格、促销活动及详情页结构,适用于电商导购、比价工具、数据分析等场景。接口采用appkey+appsecret+session认证机制,需申请相应权限。本文提供Python调用示例及使用注意事项,帮助开发者高效对接移动端商品数据。
|
9月前
|
搜索推荐 开发工具 UED
apptrace 三大策略,助力电商 App 在 618 突围​
随着“618”电商大促预售开启,各大平台投入百亿流量与现金争夺用户。然而,网络购物市场增量空间趋于饱和,电商App亟需突破曝光、拉新与转化瓶颈。apptrace提供三大增长策略:精准曝光通过智能广告监测优化投放;裂变拉新简化流程,助力社交传播;高效转化实现一键直达活动页面,提升用户体验与留存率。这些技术优势助力开发者和运营者在618大战中抢占先机,实现用户增长与商业价值最大化。
|
8月前
|
JavaScript
TypeOrmModule 从 app.module.ts 抽离到 database.module.ts 后出现错误的原因分析
本文分析了TypeORM实体元数据错误的成因,主要涉及实体注册方式、路径解析差异及模块结构变化导致的关系解析问题,并提供了具体解决方案和最佳实践建议。
191 56
|
5月前
|
缓存 供应链 开发者
1688 item_get_app 接口深度分析及 Python 实现
1688平台item_get_app接口专为移动端设计,提供商品原始详情数据,包含批发价格、起订量、供应商信息等B2B特有字段,适用于采购决策、供应链分析等场景。接口需通过appkey+access_token认证,并支持字段筛选,返回结构化数据,助力企业实现智能采购与供应商评估。
|
6月前
|
数据采集 数据可视化 API
驱动业务决策:基于Python的App用户行为分析与可视化方案
驱动业务决策:基于Python的App用户行为分析与可视化方案
|
10月前
|
数据采集 数据可视化 数据挖掘
基于Python的App流量大数据分析与可视化方案
基于Python的App流量大数据分析与可视化方案
|
11月前
|
监控 数据可视化 数据挖掘
【开发者必看—电商篇】数据赋能电商类App转化率循序增长
通过友盟+ 数据分析工具,团队深入分析了用户行为路径、转化漏斗、停留时间及错误事件等关键数据,定位到用户体验与产品性能的问题。经过精准优化,包括简化购物流程、修复技术故障及提升稳定性,最终显著提高了用户转化率。这一案例展示了数据驱动在产品优化中的重要作用。
【开发者必看—电商篇】数据赋能电商类App转化率循序增长
|
11月前
|
监控 搜索推荐 数据挖掘
【开发者必看—电商篇】数据赋能电商App活跃度重焕新生
通过友盟+数据分析工具的综合数据分析和个性化推送功能,解决APP用户活跃度迅速下降的问题。