AES白盒加密与密钥还原

AES白盒加密主流的方法就是通过将原本的字节替换,行移位,列混淆和轮密钥加等操作用查表的方法实现,轮密钥则被合并到这些表中。可以通过DFA(Differential Fault Analysis)差分故障分析来获取到正确的轮密钥,通过轮密钥结合密钥扩展算法得到原始密钥。

常规AES加密

此so的encryptPhoneNum函数会将输入的phoneNumber的末尾四个字节和其余字节分别进行加密并将加密结果拼接返回

末尾的四个字节加密算法为:MD5( sha384(number) + number + "9037HAND" )

而其余字节则是使用AES进行加密

AES的key是54 15 24 6E ED 9A EA 94 77 EB 68 05 42 E4 8D DA

Nr = 密钥位数/32,这里key是128位(AES128),所以对应的Nr是4。然后调用key_expansion进行密钥扩展,扩展为Nr+6+1(11)个轮密钥,最后调用cipher进行加密。

  1. cipher会先将输入数据转换到4*4的矩阵中
  2. 先进行一次轮密钥加
  3. 9轮重复加密,每轮加密都会进行字节替换,行移位,列混淆和轮密钥加
  4. 最后进行一轮加密,此轮加密没有列混淆
  5. 将状态矩阵中的加密结果返回

因为上述AES加密是常规加密,并且key我们已经已知了。现在使用DFA差分故障分析看还原的密钥和原始的是否相等。DFA差分故障分析是通过在倒数第一轮列混淆和倒数第二轮列混淆之前改变状态矩阵中的任意一个字节,这样最后得到的加密结果会有4个字节与原始加密结果不同,通过得到多组错误的加密结果还原原始的最后一轮的轮密钥。这里可以通过hookadd_round_key((uint8_t *)state, w, k);轮密钥加函数,他的参数k就是重复轮的加密轮数,需要在k等于8的时候随机修改状态矩阵中的一个字节来得到错误的加密结果。unidbg模拟执行代码如下:

// D1D2
// void __fastcall add_round_key(uint8_t *state, uint8_t *w, uint8_t r)
hook.wrap(dm.getModule().base + 0xD1D3, new WrapCallback<HookZzArm32RegisterContextImpl>() {
    boolean isStart;
    UnidbgPointer aes_state;
    public void preCall(Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) {
        aes_state = ctx.getPointerArg(0);
        isStart = false;
        if(ctx.getIntArg(2) == 8){
            isStart = true;
        }
    };
    @Override
    public void postCall(Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) {
        if(isStart){
            // 修改状态矩阵中的字节
            aes_state.setByte(randInt(0, 15), (byte) randInt(0, 255));
        }
    }
});

// 98F8
// int __fastcall encrypt_phone_number(unsigned __int8 *number, unsigned __int8 *encrypt, int *cipher_length)
hook.wrap(dm.getModule().base + 0x98F9, new WrapCallback<HookZzArm32RegisterContextImpl>() {
    UnidbgPointer aes_encrypt_data;
    public void preCall(Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) {
        aes_encrypt_data = ctx.getPointerArg(1);
    };
    @Override
    public void postCall(Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) {
        System.out.println(Hex.encodeHexString(aes_encrypt_data.getByteArray(0, 16)));
    }
});

//Java_com_mucfc_honeybee_CollectorManager_encryptPhoneNum
DvmClass Honeybee_dvmclass = vm.resolveClass("com/mucfc/honeybee/CollectorManager");
DvmObject resultobj = null;
for (int i = 1; i <= 32; i++){
    resultobj = Honeybee_dvmclass.callStaticJniMethodObject(emulator,"encryptPhoneNum(Ljava/lang/String;I)[B;","191371327525418", 16);
}

最后得到若干组错误的加密结果使用phoenixAES得到最后一轮正确的轮密钥。需要注意得到的错误加密结果只有四个字节与原始加密结果不同,如果大于四个字节说明修改状态矩阵的时机过早,如果过少说明修改状态矩阵的时机过晚。

import phoenixAES
data = """
64e5e2199a4e739d8b38943e3af8a4eb
64e5cc199a41739dd338943e3af8a49f
84e5e2199a4e73c78b385e3e3a19a4eb
64e51b199a80739d5738943e3af8a48a
64e5e2a19a4e729d8bb7943e59f8a4eb
64e5e2ed9a4ed39d8b2d943e86f8a4eb
6463e2197b4e739d8b38949f3af8ffeb
64e5e2389a4e7c9d8ba7943e7cf8a4eb
64e5e2f09a4eca9d8b99943e9bf8a4eb
64e50c199a93739d0938943e3af8a4fc
64e5e2a09a4e6e9d8bc5943e50f8a4eb
39e5e2199a4e738c8b38603e3af2a4eb
64f4e219ee4e739d8b38947c3af8a6eb
64e5e2aa9a4ef49d8bde943e6cf8a4eb
64e59e199aff739d9338943e3af8a4ae
cfe5e2199a4e73668b38b63e3a9ea4eb
644ae219674e739d8b3894b03af8f9eb
6ee5e2199a4e73b98b38a93e3aeca4eb
64e549199af4739dd938943e3af8a4d4
64b2e219084e739d8b3894303af8f7eb
3ae5e2199a4e73588b388b3e3ae6a4eb
64e5e2aa9a4ef89d8bcf943e4ef8a4eb
645ee2190b4e739d8b3894e93af800eb
64e501199aa3739dbe38943e3af8a4e2
f8e5e2199a4e735e8b38453e3a95a4eb
64e5e25e9a4eab9d8bfd943e22f8a4eb
64e5e20c9a4e509d8bb2943e66f8a4eb
64e5e2489a4e2d9d8b35943eaef8a4eb
15e5e2199a4e736d8b38a73e3a13a4eb
64e5e2739a4e149d8b96943e24f8a4eb
641ee2191b4e739d8b3894fc3af892eb
35e5e2199a4e73c18b38ee3e3a3da4eb
64e522199a39739d2838943e3af8a46b
"""
with open('crackfile', 'w') as fp:
    fp.write(data)

phoenixAES.crack_file('crackfile', [], True, False, verbose=2)

运行得到最后一轮轮加密的轮密钥C25F7DEE426D66DF2FA3531798F2D47B

然后使用aes_keyschedule得到原始密钥为5415246EED9AEA9477EB680542E48DDA与ida中看到的key相等

AES白盒加密

还是上面的so文件,函数desensitization会对输入的数据进行AES白盒加密

输入的数据每16个字节(4*4)为一组调用encryptBlock进行加密

encryptBlock会先将初始状态矩阵进行一次异或加密,然后调用encdec加密,最后解密后的状态矩阵再进行一次异或加密。

encdec会先将状态矩阵进行转置,然后进行重复的9轮加密。

可以hook encdec得到状态矩阵参数,然后通过汇编层次上判断如果到第8次重复轮加密的时候对状态矩阵进行修改,最后得到错误的加密结果。

使用unidbg可以得到多组错误数据

/*
void __fastcall encdec(
W128b *state,
const int (*shiftOp)[16],
W32XTB (*edXTab)[10][4][6],
W32XTB (*edXTabEx)[2][15][4],
AES_TB_TYPE1 (*edTab1)[2][16],
AES_TB_TYPE2 (*edTab2)[10][16],
AES_TB_TYPE3 (*edTab3)[10][16])
    */
// 9E58
final UnidbgPointer[] aes_state = new UnidbgPointer[1];
hook.wrap(dm.getModule().base + 0x9E59, new WrapCallback<HookZzArm32RegisterContextImpl>() {
    public void preCall(Emulator<?> emulator, final HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) {
        aes_state[0] = ctx.getPointerArg(0);
    };
    @Override
    public void postCall(Emulator<?> emulator, HookZzArm32RegisterContextImpl ctx, HookEntryInfo info) {
        System.out.println(Hex.encodeHexString(aes_state[0].getByteArray(0, 16)));
    }
});

// 0xA2Af
/*
.text:0000A2AC C2 98                       LDR             R0, [SP,#0x480+var_178]
.text:0000A2AE 08 28                       CMP             R0, #8
.text:0000A2B0 00 F3 A4 81                 BGT.W           loc_A5FC
.text:0000A2B4 FF E7                       B               loc_A2B6
*/
// 添加一个指令集hook回调
emulator.getBackend().hook_add_new(new com.github.unidbg.arm.backend.CodeHook() {
    @Override
    public void hook(Backend backend, long address, int size, Object user) {
        if(backend.reg_read(ArmConst.UC_ARM_REG_R0).byteValue() == 8){
            aes_state[0].setByte(randInt(0, 15), (byte) randInt(0, 255));
        }
    }

    @Override
    public void onAttach(UnHook unHook) {
    }

    @Override
    public void detach() {
    }
}, dm.getModule().base + 0xA2Af,dm.getModule().base + 0xA2b1,null);

// Java_com_mucfc_honeybee_DataProcessor_desensitization
DvmClass Honeybee_dvmclass = vm.resolveClass("com/mucfc/honeybee/DataProcessor"); //先把类找到,这里的原理很像反射
DvmObject resultobj = null;
byte[] input_data = {0x74, 0x68, 0x69, 0x73, 0x69, 0x73, 0x61, 0x65, 0x73, 0x77, 0x68, 0x69, 0x74, 0x65, 0x65};
for (int i = 0; i < 32; i++){
    resultobj = Honeybee_dvmclass.callStaticJniMethodObject(emulator,"desensitization([B;)[B;",input_data);
}

phoneixAES跑一下错误数据没有得到正确的轮密钥,但是每一组错误数据都正确识别了。

原因是在状态矩阵参与AES加密之前和加密后进行了矩阵转置,正确的做法应该是在状态矩阵加密后矩阵转置之前获取错误数据,或者使用脚本将转置的错误数据在转置回去即可。

最后得到最后一轮轮密钥为C25F7DEE426D66DF2FA3531798F2D47B,这和之前分析常规AES加密的轮密钥相同,所以使用aes_keyschedule得到原始密钥也是5415246EED9AEA9477EB680542E48DDA

混淆的AES白盒加密

r2pay

这个是之前做OWASP的一道题,题目要求得到白盒加密的keyhttps://www.cnblogs.com/revercc/p/17277997.html#uncrackable-level1。关键函数就是__int64 __fastcall sub_7D124(JNIEnv *a1, __int64 a2, __int64 input, unsigned __int8 a4),此函数被ollvm混淆了。

参数input是输入的数据,跟踪此参数的访问位置可以看到最后input会被复制到v6788中,而v6788地址处是通过malloc申请的内存。

然后继续寻找v6788地址访问的位置,可以看到会赋值给v6779[284]

v6779[284]会直接参与对应的查表运算,说明其就是状态矩阵,因为v6779[284] = v6788,所以对应通过malloc申请的内存v6788存放的就是状态矩阵

这里因为混淆的比较验证,不太好寻找9轮重复轮加密的插入点。因为状态矩阵在9轮重复轮运算中的值变化一定是成规律的,可以通过跟踪状态矩阵的变化找到第8轮重复轮加密并进行插入代码修改状态矩阵得到错误数据。

/*
.text:000000000007D1C8                 BL              .malloc
.text:000000000007D1CC                 STR             X0, [X19,#0x2DD0+var_E20]
.text:000000000007D1D0                 LDR             X8, [X19,#0x2DD0+var_E00]
.text:000000000007D1D4                 LDR             X9, [X8]
    */
// 添加一个指令集hook回调
final UnidbgPointer[] aes_state = new UnidbgPointer[1];
emulator.getBackend().hook_add_new(new com.github.unidbg.arm.backend.CodeHook() {
    @Override
    public void hook(Backend backend, long address, int size, Object user) {
        long tmp_status = backend.reg_read(Arm64Const.UC_ARM64_REG_X0).longValue();
        aes_state[0] = UnidbgPointer.pointer(emulator, tmp_status);
        emulator.getBackend().hook_add_new(new com.github.unidbg.arm.backend.WriteHook() {
            @Override
            public void hook(Backend backend, long address, int size, long value, Object user) {
                System.out.println(Hex.encodeHexString(aes_state[0].getByteArray(0, 16)));
            }

            @Override
            public void onAttach(UnHook unHook) {
            }

            @Override
            public void detach() {
            }
        }, tmp_status, tmp_status + 0x10, null);
    }

    @Override
    public void onAttach(UnHook unHook) {
    }

    @Override
    public void detach() {
    }
}, dm.getModule().base + 0x7D1CC,dm.getModule().base + 0x7D1D0,null);

// __int64 __fastcall sub_7D124(JNIEnv *a1, __int64 a2, __int64 input, unsigned __int8 a4)
// public native byte[] gXftm3iswpkVgBNDUp(byte[] bArr, byte b2);
DvmClass pwnme_dvmclass = vm.resolveClass("re/pwnme/MainActivity"); //先把类找到,这里的原理很像反射
DvmObject resultobj = null;
byte[] input_data = {0x30, 0x30, 0x30, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x38, 0x37, 0x36};
byte input_data_len = 0x10;
for (int i = 0; i < 1; i++){
    resultobj = pwnme_dvmclass.callStaticJniMethodObject(emulator,"gXftm3iswpkVgBNDUp([BB)[B",input_data, input_data_len);
    // System.out.println("resultobj:"+resultobj);
    byte[] encrypt_data =  (byte[]) resultobj.getValue();
    BytePrintAsString(encrypt_data, 1);
}

最后寻找状态矩阵的变化规律,找到第9轮加密开始的头部,往上寻找状态矩阵的值作为修改状态矩阵的时机,这里选择835c1ba929b9f5a61162b5926295fbf1

修改状态矩阵的内存写hook代码,并重复调用函数获取多组错误数据。

public void hook(Backend backend, long address, int size, long value, Object user) {
    System.out.println(Hex.encodeHexString(aes_state[0].getByteArray(0, 16)));
    //System.out.println(Hex.encodeHexString(aes_state[0].getByteArray(0, 16)));
    if(Hex.encodeHexString(aes_state[0].getByteArray(0, 16)).equals("835c1ba929b9f5a61162b5926295fbf1")){
        aes_state[0].setByte(randInt(0, 15), (byte) randInt(0, 255));
    }
}

使用phoneixAES跑出最后一轮轮密钥为768D19E17F62A01EB0CDD39A28E1798F

aes_keyschedule得到原始密钥为723270347931734E3077536563757233也就是r2p4y1sN0wSecur3

500_Obfuscated_AES

此例子的也是被ollvm混淆过的,其中TfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq函数会参数1是输入数据,参数2是输出的白盒加密的数据,首先看一下参数1被赋值给了参数2

看一下参数2会直接参与查表运算,所以证明参数1和2对应地址处就是状态矩阵。

通过hookTfcqPqf1lNhu0DC2qGsAAeML0SEmOBYX4jpYUnyT8qYWIlEq函数获取状态矩阵地址,通过unidbg内存写hook跟踪状态矩阵的变化。最后找到第9轮加密之前的状态变量的值作为修改状态变量的时机,这里选择的561b4f908f448b90e826cd25ba7d10ed

对应的unidbg hook代码为

public UnidbgPointer input;
public UnidbgPointer output;
public void preCall(Emulator<?> emulator, HookZzArm64RegisterContextImpl ctx, HookEntryInfo info) {
    input = ctx.getPointerArg(0);
    output = ctx.getPointerArg(1);
    // System.out.println("4CAC input1 : " + Hex.encodeHexString(input.getByteArray(0, 16)));

    // output
    emulator.getBackend().hook_add_new(new com.github.unidbg.arm.backend.WriteHook() {
        @Override
        public void hook(Backend backend, long address, int size, long value, Object user) {
            // System.out.println(Hex.encodeHexString(output.getByteArray(0, 16)));
            if(Hex.encodeHexString(output.getByteArray(0, 16)).equals("561b4f908f448b90e826cd25ba7d10ed")){
                output.setByte(randInt(0, 15), (byte) randInt(0, 255));
            }
        }

        @Override
        public void onAttach(UnHook unHook) {
        }

        @Override
        public void detach() {
        }
    }, output.toUIntPeer(), output.toUIntPeer() + 0x10, null);
};

最后得到轮密钥为4fa8b87384c7f9bd7240629a31d932cd

对应的原始密钥为6C2893F21B6185E8567238CB78184945

posted @ 2024-03-08 12:34  怎么可以吃突突  阅读(186)  评论(0编辑  收藏  举报