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即可


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

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

相关文章
|
6月前
|
安全 搜索推荐 数据挖掘
电商类app开发定制,电商app制作开发
在电商行业的蓬勃发展下,越来越多的企业和个体商户开始向移动化方向转型,希望借助电商App提升销售额和用户粘性。然而,纷繁复杂的电商App市场,如何设计出一个功能强大又符合自身需求的电商App成为了迫切需要解决的问题。
|
24天前
|
数据采集 小程序 网络安全
云擎技术---分析工信部APP备案的“传闻”
APP备案并非新事物,自2005年起已有非经营性互联网信息服务备案制度。备案针对的是网站主办者,而非用户,不涉及个人用户网络访问。网络接入服务提供者包括ISP和IDC,不限于三大运营商。通知要求不为未备案网站提供接入,但不影响国外软件使用。个人开发者不能涉及经营性内容,备案审核时长1-20个工作日。境内服务器和国内应用商店需备案,境外则无需。手机厂商不会开启白名单制,仅实行黑名单制。APP备案与民营经济发展壮大意见不冲突,工信部有权颁布相关规定。该政策不存在逐步试探底线情况,所有解读均有法律依据。
25 3
云擎技术---分析工信部APP备案的“传闻”
|
2月前
|
网络协议 算法 Android开发
安卓逆向 -- 实战某峰窝APP(动态分析)
安卓逆向 -- 实战某峰窝APP(动态分析)
32 4
|
2月前
|
算法
某圈app算法分析
某圈app算法分析
18 0
|
2月前
|
算法 安全 数据安全/隐私保护
某影视APP算法逆向分析
某影视APP算法逆向分析
19 0
|
2月前
|
算法 Java
某江app算法分析
某江app算法分析
17 0
|
2月前
|
算法 数据挖掘 数据安全/隐私保护
某合伙人app算法分析
某合伙人app算法分析
15 0
|
4月前
|
数据可视化 数据挖掘
【数据分析与可视化】使用pyecharts对App下载量数据进行可视化分析(附源码)
【数据分析与可视化】使用pyecharts对App下载量数据进行可视化分析(附源码)
46 0
|
4月前
|
缓存 安全 NoSQL
App开放接口api安全:Token签名sign的设计与实现
在app开放接口api的设计中,避免不了的就是安全性问题,因为大多数接口涉及到用户的个人信息以及一些敏感的数据,所以对这些 接口需要进行身份的认证,那么这就需要用户提供一些信息,比如用户名密码等,但是为了安全起见让用户暴露的明文密码次数越少越好,我们一般在web项目 中,大多数采用保存的session中,然后在存一份到cookie中,来保持用户的回话有效性。
|
7月前
|
算法 数据安全/隐私保护
App逆向百例|17|某音乐App分析
App逆向百例|17|某音乐App分析
159 0