App逆向百例|12|某电商App Sign分析
2022-10-17 09:57:56 Author: 逆向lin狗(查看原文) 阅读量:159 收藏

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

样本:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMUNtbzZHU1lOUmM4a2l4YkVsT3Rkc1E/cHdkPWxpbm4=

打开样本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'](thisarguments);
            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:[email protected]
arg2:hotWords
arg2:{"originHotWord":"0","pageFrom":"1"}
arg2:3036c83c3c4b25a2
arg2:android
arg2:10.2.0
result:st=1664463515894&sign=7ce2045026643e68ea1639c5e291127f&sv=122

复制粘贴造个轮子运行

根据报错信息开始补环境

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也变动

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 = [012]
sv1 = 1
sv2 = 1
sv3 = 1
print(unk_17440[(sv2 + sv3) % 3])

根据结果得到下表

sv1sv2sv3svcase
1001000
1011011
1021022
1101101
1111112
1121120
1201202
1211210
1221221

往下看这里先是内存拷贝了一个值然后根据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 = [0x370x920x440x680xA50x3D0xCC0x7F0xBB0x0F0xD90x880xEE0x9A0xE90x5A]
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即可

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

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

点赞 在看 分享是你对我最大的支持
逆向lin狗

文章来源: http://mp.weixin.qq.com/s?__biz=MzUxMjU3ODc1MA==&mid=2247485239&idx=1&sn=a059748eba368c2d69f429f111d283a0&chksm=f96302e6ce148bf07f848cad76f1e04217c31a88f47dedda8e507bee9beeea774ffad3584c70#rd
如有侵权请联系:admin#unsafe.sh