某幸咖啡 “标准“白盒AES+sign

现在app so aes的加密几乎都是白盒了,之前也写过很多篇白盒AES,后续如果这个app只有一个单独的白盒aes,那我大概率不会在出这个文章了,已经讲过很多次了,无论是标准的还是魔改程度很大的.这个的标题标准我打了一个引号,说它标准是因为加密结果是标准的,说它不标准是因为它打乱了标准AES的加密流程,比如标准的首个加密是轮秘钥加,而它上来就是循环左移,后面碰到我们再说吧.

抓包

抓个包先,这个app无论是登录接口还是搜索接口都是一样的加密,sign值和q值,这个sign值不是32位,是一直变化的,37,38,39,40都有,hash算法直接排除,肯定是做过什么处理的.
在这里插入图片描述

小公司都喜欢加壳,它也不例外.
在这里插入图片描述
360加固,可以打开这个apk看看
在这里插入图片描述
bttets很明显的360整体加固,直接上fart脱壳机,最后在需要的地方点几下,确保关键位置的函数都加载了.
dex已经上传到123云盘 https://www.123pan.com/s/4O7Zjv-dhnBd.html

关键代码定位

直接搜是搜不到的,都是一些第三方组件的.带huawei字眼,肯定不是了
在这里插入图片描述
其实是字符串加密了,字符串加密也很常见的,还有其他方法.
我这里hook的是hashmap,因为表单里很喜欢这样一个键,一个值这样添加上去.

function call(){
Java.perform(function (){
    var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b) {
    if(a!=null && a.equals("sign")){ 
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()))
        console.log("hashMap.put: ", a, b);
    }
    return this.put(a, b);
}}
}

frida附加启动
在这里插入图片描述
报错了,这个报错是无法附加上去的意思,典型的双进程保护反调试,以重启的方式即可解决.
frida -U -f 包名 -l js文件
以Spawne方式成功注入.这里java.perform外面最好在包一个函数,防止刚进入的时候类还没有加载.
在这里插入图片描述

看下调用堆栈,下面一行
在这里插入图片描述
往hashmap里添加了4组数据,应该正好对应表单里的4个.主动调用一下看看加密的字符串是什么,顺便也可以看到哪个是结果

function call_so(){
    Java.perform(function(){
        var Class = Java.use('com.stub.StubApp');
        var result = Class['getString2']('7719');
        console.log(result);  // cryptoDD
    })
}

7719对应的是sign,457对应的是q,4005是cid,16944是uid.
cid和uid可固定,接下来先看q值吧,最终sign需要这个q.q来自上面的b2
一路跟下来,没什么难的,到了这两个native方法
在这里插入图片描述

两个都有可能所以都hook下,下面的md5_crypt就是sign跟过去的,后面就不说了.下看下加载自哪个so,字符串也是加密的,30491解密出来是cryptoDD
找到这个so,只有32位的
在这里插入图片描述

搜下java看看是不是静态注册,看来是动态注册了.
在这里插入图片描述
同时可以看到上面圈起来的,控制流平坦化的标志,属于ollvm混淆,但是不用怕,只要关注关键函数就好了.
下来hook下libart来找动态注册函数

// 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
var addrRegisterNatives = null;
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols[i];
    if (symbol.name.indexOf("art") >= 0 &&
        symbol.name.indexOf("JNI") >= 0 &&
        symbol.name.indexOf("RegisterNatives") >= 0 &&
        symbol.name.indexOf("CheckJNI") < 0) {

        addrRegisterNatives = symbol.address;
        console.log("RegisterNatives is at ", symbol.address, symbol.name);
        break
    }
}
if (addrRegisterNatives) {
    Interceptor.attach(addrRegisterNatives, {
        onEnter: function (args) {
            var env = args[0];        // jni对象
            var java_class = args[1]; // 类
            var class_name = Java.vm.tryGetEnv().getClassName(java_class);
            var taget_class = "com.luckincoffee.safeboxlib.CryptoHelper";   //111 某个类中动态注册的so
            if (class_name === taget_class) {
                console.log("\n[RegisterNatives] method_count:", args[3]);
                var methods_ptr = ptr(args[2]);
                var method_count = parseInt(args[3]);
                for (var i = 0; i < method_count; i++) {
                    // Java中函数名字的
                    var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    // 参数和返回值类型
                    var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    // C中的函数内存地址
                    var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
                    var name = Memory.readCString(name_ptr);
                    var sig = Memory.readCString(sig_ptr);
                    var find_module = Process.findModuleByAddress(fnPtr_ptr);
                    // 地址、偏移量、基地址
                    var offset = ptr(fnPtr_ptr).sub(find_module.base);
                    console.log('class_name:',class_name,"name:", name, "sig:", sig,'module_name:',find_module.name ,"offset:", offset);
                }
            }
        }
    });
}
// 动态注册函数地址
// frida -U -f com.lucky.luckyclient -l hook_so_register.js  

在这里插入图片描述
上面hook过了只会走localAESWork4Api和md5_crypt,偏移分别是0x1b1cd和0x1a981
分析算法优先用unidbg,unidbg跑起来的话对算法帮助很大.

unidbg搭架子

package com.rxkf;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;

import javax.xml.bind.DatatypeConverter;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Random;

public class rxkf2 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    rxkf2(){
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/rxkf/rxkf5.0.01.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
    public static void main(String[] args) {
        rxkf2 rxkf = new rxkf2();
    }
}

执行后有结果
在这里插入图片描述
但是上面有一条load dependency libandroid.so failed日志,缺少libandroid.so,这个so依赖很多so,不好加载进来,这里可以用 VirtualModule,加上这句new AndroidModule(emulator, vm).register(memory);就好了,这里即使你不处理也是没问题的,但是其他地方就不好说了.

package com.rxkf;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;

import javax.xml.bind.DatatypeConverter;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Random;

public class rxkf2 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    rxkf2(){
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/rxkf/rxkf5.0.01.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        new AndroidModule(emulator, vm).register(memory);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
    public static void main(String[] args) {
        rxkf2 rxkf = new rxkf2();
    }
}

加上后就没有那个日志了.
接下来开始调用函数

public void callaes(){
        // args list
        List<Object> list = new ArrayList<>(10);
        // jnienv
        list.add(vm.getJNIEnv());
        // jclazz
        list.add(0);
        // str1
        String str = "yangruhua";
        byte[] byteArray = str.getBytes();
        list.add(vm.addLocalObject(new ByteArray(vm,byteArray)));
        // 最后的int
        list.add(0);

        Number number = module.callFunction(emulator, 0x1b1cd, list.toArray());
        ByteArray resultArr = vm.getObject(number.intValue());
        String base64String = Base64.getEncoder().encodeToString(resultArr.getValue());
        System.out.println("aesresult:"+base64String);
    };

这里入参你可以选java层的入参,加密结果是一样的,我这里为了方便就把入参缩短了,其实这也是一个技巧,如果真是对称加密的话,就只需要加密1轮就好了,一些关键函数就只会调用一次,这样不容易犯迷糊.
直接调用就出值了,不需要补环境.

白盒aes

在这里插入图片描述
这里是目标函数,地址0x1b1cd,可以看左下角略微有些虚假指令和控制流,这种带很多while循环的千万不要从上往下看,因为你这样看看不出来他要走哪个分支.直接看关键函数就好了,有些符号没去掉的话你应该可以猜到这是关键函数.就比如这个.
在这里插入图片描述
有两处android_native_wbaes_jni,反正都至少会走一次,直接点进去看就行了,不确定走几次可以unidbg下断看下.
在这里插入图片描述
进来后看左下角也是有混淆的,并且看到了pkcs5padding,PKCS5Padding 和 PKCS7Padding 实际上是相同的填充方案,只是在命名上有所不同.了解填充方式对算法分析也有很大帮助,因为有些算法填充后下一步会做什么一般是固定的(本篇除外).
这个so关键函数名字都没去掉,直接看关键函数就好了.
在这里插入图片描述
wbaes_encrypt_ecb,白盒aes ecb模式,ecb模式好啊,不用找iv了,但防止它虚幻一枪可以做个验证.先看看入参是什么,unidbg中下断.

public void HookByConsoleDebugger() {
        Debugger debugger = emulator.attach();
        debugger.addBreakPoint(module.base+0x17BD4);
    }

在这里插入图片描述
看函数的参数入参1是input,入参2是0x10,应该是一个分组长度16个字节,参数3是output,4是模式0
在这里插入图片描述
打印一下入参,确实是我们的入参,并且是填充过的,剩7个字节刚好填充7个07.上面我们说过要验证是不是ecb,怎么验证呢?
把明文弄成刚好两个分组长度32字节,如果是ecb的话,两个分组的加密结果应该是一样的.明文yangruhualoveyouyangruhualoveyou,再次运行,这是入参
在这里插入图片描述
输入blr下临时断点到函数刚刚执行完的地址,再按c执行到该位置.
在这里插入图片描述
之前的入参3地址是0x402d2030,unidbg中直接看这个地址就好了m0x402d2030
在这里插入图片描述
是ecb没错了.
接下来按照上面所讲的思路接着分析,可以定位到下面的函数位置
在这里插入图片描述
左下角也是严重的控制流平坦化,俗称ollvm混淆,但不是很严重,我也不会还原,都是硬刚的.
往下滑,看到循环左移字眼
在这里插入图片描述
除此以外,中间也有几个boxes,符合白盒aes查表的特征
在这里插入图片描述
点过去这个表也是很大的,916256字节
在这里插入图片描述
还好这里有循环左移的函数,除此以外,似乎没有看到什么关键的函数了,所以可以以这个函数为切入点,循环左移的入参是state块,左移后结果也存在里面.所以DFA就可以攻击这个位置.在此之前,先确定下是否真的是10轮运算.忘了说了,前面验证了ecb后改一下入参,确保只有一轮加密,否则很多函数都会调用不止一次.

public void HookByConsoleDebugger() {
        Debugger debugger = emulator.attach();
                debugger.addBreakPoint(module.base+0x154e8,new BreakPointCallback() {  //0x15AD6不走
            RegisterContext context = emulator.getContext();
            int num = 1;
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                System.out.println("num:"+num);
                emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    return true;
                }
            });
            num+=1;
            return true;
            }
        });
    }

在这里插入图片描述
确实是10轮,接下来DFA攻击不想多说了,很简单直接上代码

package com.rxkf;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.debugger.Debugger;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.virtualmodule.android.AndroidModule;

import javax.xml.bind.DatatypeConverter;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Random;

public class rxkf2 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    rxkf2(){
        // 创建模拟器实例,进程名建议依照实际进程名填写,可以规避针对进程名的校验
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.lucky.luckyclient").build();
        // 获取模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机,传入APK,Unidbg可以替我们做部分签名校验的工作
        vm = emulator.createDalvikVM(new File("unidbg-android/apks/rxkf/rxkf5.0.01.apk"));
        // 设置JNI
        vm.setJni(this);
        // 打印日志
        vm.setVerbose(true);
        new AndroidModule(emulator, vm).register(memory);
        // 加载目标SO
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        //获取本SO模块的句柄,后续需要用它
        module = dm.getModule();
        // 调用JNI OnLoad
        dm.callJNI_OnLoad(emulator);
    };
    public void callDfa(){
        Debugger debugger = emulator.attach();
        debugger.addBreakPoint(module.base+0x14f98,new BreakPointCallback() {
            UnidbgPointer pointer;
            RegisterContext context = emulator.getContext();

            int num = 1;
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                pointer = context.getPointerArg(0);
                if(num%9==0){
                    pointer.setByte(randint(0,15),(byte) randint(0,0xff));
                }
                num+=1;
                return true;
            }

        });
    }
    public void call_wbaes(){
        MemoryBlock inputBlock = emulator.getMemory().malloc(16,true);
        UnidbgPointer inputPtr = inputBlock.getPointer();
        MemoryBlock ouputBlock = emulator.getMemory().malloc(16,true);
        UnidbgPointer ouputPtr = ouputBlock.getPointer();
        byte[] byteArray = hexToBytes("68656C6C6F0B0B0B0B0B0B0B0B0B0B0B");
        assert byteArray != null;
        inputPtr.write(0,byteArray,0,byteArray.length);
        module.callFunction(emulator,0x17bd5, inputPtr,16,ouputPtr,0);
        String res = bytesToHex(ouputPtr.getByteArray(0,0x10));
        System.out.println(res);
        inputBlock.free();
        ouputBlock.free();
    }
    public static void main(String[] args) {
        rxkf2 rxkf = new rxkf2();
        for (int i = 0; i < 200; i++) {
            rxkf.callDfa();
            rxkf.call_wbaes();
        }
    }
    public static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            int unsignedInt = b & 0xff;
            String hex = Integer.toHexString(unsignedInt);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
    public static byte[] hexToBytes(String hexString) {
        // 将十六进制字符串转换为字节数组
        return DatatypeConverter.parseHexBinary(hexString);
    }
    public static int randint(int min,int max){
        Random rand = new Random();
        return rand.nextInt((max-min)+1)+min;
    }
}

调用200次后得到故障密文,不过这么多也够了.

import phoenixAES
with open('tracefile', 'wb') as t:  # 第一行是正确密文 后面是故障密文
    t.write("""92e2de1032a142467477d6e2efa27d58 
fae2de1032a1423a747753e2efe87d58
92e2b110320b42463b77d6e2efa27d94
85e2de1032a142cb74778ce2ef007d58
93e2de1032a142ef747778e2ef887d58
92e2de0432a1dd4674f7d6e276a27d58
f4e2de1032a14290747726e2efd47d58
92e29410325342468477d6e2efa27d10
92e2751032ac42460277d6e2efa27dba
92e2a210324942463277d6e2efa27dac
922cde1013a142467477d676efa2a458
92e23010324b4246d477d6e2efa27d5f
92e2721032fe42462277d6e2efa27d01
43e2de1032a142ce747770e2efbe7d58
92e20610320b42464f77d6e2efa27d93
92e2651032804246e077d6e2efa27d47
c6e2de1032a142e97477fbe2efda7d58
71e2de1032a1427574779ce2ef4c7d58
922dde10a6a142467477d6ceefa2fd58
92e2aa1032574246f677d6e2efa27d32
92e2dee032a1b4467419d6e2dfa27d58
92e2d11032e84246b077d6e2efa27d57
     """.encode('utf8'))
phoenixAES.crack_file('tracefile', [], True, False, 3) # 第3个参数传False代表解密

在这里插入图片描述
用phoenixAES库得到第10轮秘钥869D92BBB700D0D25BD9FD3E224B5DF2
在这里插入图片描述
再用aes_keyschedule拿到初始秘钥644A4C64434A69566E44764D394A5570
最后用cyberchef验证一下结果是对的.
前面我说了这个aes不太标准.
来看下循环左移的第一次入参

public void HookByConsoleDebugger() {
        Debugger debugger = emulator.attach();
        debugger.addBreakPoint(module.base+0x154E8);
    }

在这里插入图片描述
划线部分,下面的不是,没任何关系.来看下标准的10轮
在这里插入图片描述
第一个state是轮秘钥加,这个shiftrows之前state经过了轮秘钥加和字节替换怎么可能还是之前的填充过的明文,几乎不可能有这样的巧合.也就是说它state块第一次处理是循环左移,并且加密的结果是标准的,这是怎么实现的呢?
下面是龙哥提供的一份标准aes python版,无iv,无填充,只加密一组

Sbox = (
    0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
    0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
    0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
    0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
    0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
    0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
    0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
    0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
    0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
    0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
    0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
    0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
    0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
    0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
    0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
    0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16,
)
Rcon = (0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36)
def text2matrix(text):
    matrix = []
    for i in range(16):
        byte = (text >> (8 * (15 - i))) & 0xFF
        if i % 4 == 0:
            matrix.append([byte])
        else:
            matrix[i // 4].append(byte)
    return matrix
def shiftRound(array, num):
    return array[num:] + array[:num]
def g(array, index):
    # 首先循环左移1位
    array = shiftRound(array, 1)
    # 字节替换
    array = [Sbox[i] for i in array]
    # 首字节和rcon中对应元素异或
    array = [(Rcon[index] ^ array[0])] + array[1:]
    return array
def xorTwoArray(array1, array2):
    '''
    返回两个数组逐元素异或的新数组
    :param array1: 一个array
    :param array2: 另一个array
    :return:
    '''
    assert len(array1) == len(array2)
    return [array1[i] ^ array2[i] for i in range(len(array1))]
def showRoundKeys(round_keys):
    # 将轮密钥从44*4转成11*16
    kList = [[] for i in range(11)]
    for i in range(len(round_keys)):
        kList[i // 4] += round_keys[i]
    # for i in range(len(kList)):
    #     print("K%02d:" % i + "".join("%02x" % k for k in kList[i]))
def keyExpand(key):
    master_key = text2matrix(key)
    round_keys = [[0] * 4 for i in range(44)]
    # 规则一(图中红色部分)
    for i in range(4):
        round_keys[i] = master_key[i]
    for i in range(4, 4 * 11):
        # 规则二(图中红色部分)
        if i % 4 == 0:
            round_keys[i] = xorTwoArray(g(round_keys[i - 1], i // 4), round_keys[i - 4])
        # 规则三(图中橙色部分)
        else:
            round_keys[i] = xorTwoArray(round_keys[i - 1], round_keys[i - 4])
    showRoundKeys(round_keys)
    return round_keys
def AddRoundKeys(state, roundKey):
    result = [[] for i in range(4)]
    for i in range(4):
        result[i] = xorTwoArray(state[i], roundKey[i])

    return result
def SubBytes(state):
    result = [[] for i in range(4)]
    for i in range(4):
        result[i] = [Sbox[i] for i in state[i]]
    return result
def ShiftRows(s):
    s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1]
    s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2]
    s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3]
    return s
def mul_by_02(num):
    if num < 0x80:
        res = (num << 1)
    else:
        res = (num << 1) ^ 0x1b
    return res % 0x100
def mul_by_03(num):
    return mul_by_02(num) ^ num
def MixColumns(state):
    for i in range(4):
        s0 = mul_by_02(state[i][0]) ^ mul_by_03(state[i][1]) ^ state[i][2] ^ state[i][3]
        s1 = state[i][0] ^ mul_by_02(state[i][1]) ^ mul_by_03(state[i][2]) ^ state[i][3]
        s2 = state[i][0] ^ state[i][1] ^ mul_by_02(state[i][2]) ^ mul_by_03(state[i][3])
        s3 = mul_by_03(state[i][0]) ^ state[i][1] ^ state[i][2] ^ mul_by_02(state[i][3])
        state[i][0] = s0
        state[i][1] = s1
        state[i][2] = s2
        state[i][3] = s3

    return state
def state2Text(state):
    text = sum(state, [])
    return "".join("%02x" % k for k in text)
def encrypt(input_bytes, kList):
    state = text2matrix(input_bytes)
    # 初始轮密钥加
    state = AddRoundKeys(state , kList[0:4])
    for i in range(1, 10):
        state = SubBytes(state)
        print('ShiftRows前:',state2Text(state))
        state = ShiftRows(state)
        state = MixColumns(state)
        state = AddRoundKeys(state, kList[4 * i:4 * (i + 1)])

    state = SubBytes(state)
    state = ShiftRows(state)
    state = AddRoundKeys(state, kList[40:44])
    return state
key = 0x644A4C64434A69566E44764D394A5570
input_bytes = 0x79616e67727568756107070707070707
kList = keyExpand(key)
# 秘钥编排
cipherState = encrypt(input_bytes, kList)
# 转16进制
cipher = state2Text(cipherState)
print('encrypt:',cipher)

这里的步骤是一比一按照上面那副图还原的,有什么办法可以做到使循环左移在最前面并且不影响最后的结果呢.
首先就是字节替换和循环左移是可以调换顺序的,他两随便换都不影响最终结果,因为你先字节替换再循环左移和先循环左移再字节替换是一样的.于是循环左移被放到了上面,最上面还有一个轮秘钥加,如何把他降下来呢.想要做到这一点首个轮秘钥加必须在循环左移和字节替换中间,如果先字节替换再轮秘钥加想都不用想后面全乱了.轮秘钥加的函数state = AddRoundKeys(state , kList[0:4]),现在要让state 先循环左移,又要保证对应顺序的异或,只能是kList[0:4]也循环左移,state和它是一样的结构.
代码如下:

def encrypt(input_bytes, kList):
    state = text2matrix(input_bytes)
    for i in range(1, 10):
        if (i == 1):
            print('ShiftRows前:', state2Text(state))
            state = ShiftRows(state)
            state = AddRoundKeys(state, ShiftRows(kList[0:4]))
            state = SubBytes(state)
            state = MixColumns(state)
            state = AddRoundKeys(state, kList[4 * i:4 * (i + 1)])
        else:
            print('ShiftRows前:', state2Text(state))
            state = ShiftRows(state)
            state = SubBytes(state)
            state = MixColumns(state)
            state = AddRoundKeys(state, kList[4 * i:4 * (i + 1)])
    state = SubBytes(state)
    state = ShiftRows(state)
    state = AddRoundKeys(state, kList[40:44])
    return state

对比下
在这里插入图片描述
两次的最终结果都一样,可以看到右边第一次循环左移的第一个就是填充过的数据,和unidbg中的对应上了,但是第二次的对应不上,不知道它又做了什么操作,感兴趣的可以研究下,白盒的可操作性很大.
说了这么多,这个q值应该是说清楚了,接下来看sign值,这个值不是固定的位数,肯定不简单.

public void callmd5(){
        // args list
        List<Object> list = new ArrayList<>(10);
        // jnienv
        list.add(vm.getJNIEnv());
        // jclazz
        list.add(0);
        // str1
        String str = "yangruhua";
        byte[] byteArray = str.getBytes();
        list.add(vm.addLocalObject(new ByteArray(vm,byteArray)));
        // 最后的int
        list.add(1);

        Number number = module.callFunction(emulator, 0x1a981, list.toArray());

        ByteArray resultArr = vm.getObject(number.intValue());
        String md5result = new String(resultArr.getValue(), StandardCharsets.UTF_8);
        System.out.println("md5result:"+md5result);
    };

和上面分析q的流程一样,同样有控制流平坦化和虚假指令,按照上面的分析思路就可以了.
上面的字符串加密的结果是324265480520097001235419948921476047 36位
在这里插入图片描述
最终跟到这个函数,unidbg下断看下.

public void HookByConsoleDebugger() {
        Debugger debugger = emulator.attach();
        debugger.addBreakPoint(module.base+0x14D54);
    }

在这里插入图片描述
可以看到入参后面加了点字符串,盐值dJLdCJiVnDvM9JUpsom9
在这里插入图片描述
还是上面说过的命令,blr下临时断点到函数结束的位置,再按c跳过去,m0xbffff6f4,0xbffff6f4是之前的入参2,应该是拿来当返回值的.
在这里插入图片描述
这里视乎是小端序,看到40你应该可以想到unidbg中的基地址是400…什么的,所以这个应该是一个指针,指向的地址是0x402D2000,注意小端续是以字节为单位的,正常的顺序402D2000就是大端序.
m0x402D2000看下结果
在这里插入图片描述

结果和上面的能对应上,就是这里没错了.
在这里插入图片描述

开头这里有个md5函数,0x13E3C,hook看一下是标准的md5,这里就不写了.
在这里插入图片描述

幸好是标准的,要是魔改了这里的控制流应该挺折磨人的,完全看不出64轮运算.
在这里插入图片描述
md5加密的结果给了v14,后续就是拼接的操作,这里写的很清楚了,关键是有一个bytesToInt函数,点过去看看
在这里插入图片描述
只有最后一行是有用的.按他的写法化简成最终的python代码如下,应该有更好的写法,我这里是按着他这里写的.

def getsign():
    res = ''
    for i in range(4):
        encode_str = 'ecac19f8e0fff3170e08392c36ec9bcf'
        i*=4
        result = (int(encode_str[(i+1)*2:(i+1)*2+2],16)<<16) | (int(encode_str[i*2:i*2+2],16)<<24) | (int(encode_str[(i+2)*2:(i+2)*2+2],16)<<8) | int(encode_str[(i+3)*2:(i+3)*2+2],16)
        result &= 0xffffffff
        if result & 0x80000000:
            result = -((result ^ 0xffffffff) + 1)
        if result<0:
            result = -result
        res+=str(result)
    print(res)
getsign()

自此,sign值和q值全部分析完毕!
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请联系作者立即删除!

总结

1 主要就是一个白盒aes,现在白盒aes已经是常态化了.
2 sign值是对md5的结果进行了处理.
3 如有讲解不到位的欢迎指出,字数有点多如有错别字还请见谅!
技术交流+lyaoyao__i(两个_)

最后

微信公众号
在这里插入图片描述
知识星球
在这里插入图片描述
如果你觉得这篇文章对你有帮助也可请作者喝一杯咖啡
在这里插入图片描述

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨如画.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值