白盒AES加密DFA逆向

白盒AES加密DFA逆向

样本下载

DFA(Differential Fault Analysis)

2591919-74f335f407cad32c.png

简单来说就是在倒数第一轮列混合和倒数第二轮列混合之间(在AES-128中也就是第8轮和第9轮之间),修改此时中间密文的一个字节,会导致最终密文和正确密文有4个字节的不同。通过多次的修改,得到多组错误的密文,然后通过正确密文和这些错误密文能够推算出第10轮的密钥(加密模式下),继而能推算出原始密钥。

实例

public class Wbaes extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    public Wbaes() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("").build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM();
        vm.setJni(this);
        vm.setVerbose(false);
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/wbaes/ex2/libhoneybee.so"), true);
        module = dm.getModule();
    }

    public void call_wb() {
        List list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(0);
        list.add(vm.addLocalObject(new ByteArray(vm, "everhu".getBytes())));
        Number ret = module.callFunction(emulator, 0x92cd, list.toArray());
        byte[] bytes = (byte[]) vm.getObject(ret.intValue()).getValue();
        System.out.println(Hex.encodeHexString(bytes));
    }

    public static void main(String[] args) {
        Wbaes test = new Wbaes();
        test.call_wb();
    }
}

2591919-92252e061ed5bb14.png

接下来分析白盒AES加密

Java_com_mucfc_honeybee_DataProcessor_desensitization

2591919-0c89c55ca2c9f377.png

wbEncrypt

2591919-3d0b24de67ad8dc6.png

encryptBlock

2591919-1fb59f3a191749cc.png

可以看出encryptBlock并不完全是块加密,它在加密的前后进行了一些异或操作,encdec才是真正执行块加密的函数

encdec

2591919-f850a5cd6b5c31f2.png

下断点看看encryptBlock的输入输出

2591919-a206af5804b76bc0.png
2591919-e1a3503f63a5472c.png
2591919-6d752b93cc4b9d5b.png

同样的,看看encdec的输入输出

2591919-88eab0bf19066ffb.png
2591919-55846479a36be126.png
2591919-3f3fb2de29d3987f.png

流程如下

2591919-ba45140e685132a7.png

接下来,进行dfa攻击

2591919-abec2c40aaa2b3d2.png
2591919-038f2a6c313cd73b.png

在for循环执行第9轮时,随机修改state的一个字节。

public class Wbaes extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    public Wbaes() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("").build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM();
        vm.setJni(this);
        vm.setVerbose(false);
        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/wbaes/ex2/libhoneybee.so"), true);
        module = dm.getModule();
    }

    public int randInt(int min, int max) {
        Random rand = new Random();
        return rand.nextInt(max - min) + min;
    }

    public void patch() {
        // patch log
        emulator.getMemory().pointer(module.base + 0x9342).setInt(0, 0xbf00bf00);
        emulator.getMemory().pointer(module.base + 0x9354).setInt(0, 0xbf00bf00);
    }

    public void dfa() {
        IHookZz hookZz = HookZz.getInstance(emulator);
        hookZz.wrap(module.base + 0x9e59, new WrapCallback() {
            @Override
            public void preCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                final Pointer output = ctx.getR0Pointer();
                ctx.push(output);

                emulator.attach().addBreakPoint(module.base + 0xA2AC, new BreakPointCallback() {
                    int count = 0;
                    @Override
                    public boolean onHit(Emulator emulator, long address) {
                        count += 1;
//                        System.out.println(count);
                        if (count == 9) {
                            output.setByte(randInt(0, 15), (byte) randInt(0, 255));
                        }
                        return true;
                    }
                });
            }

            @Override
            public void postCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                Pointer output = ctx.pop();
                byte[] outputHex = output.getByteArray(0, 16);
                System.out.println(Hex.encodeHexString(outputHex));
            }
        });
    }

    public void call_wb() {
        List list = new ArrayList<>(10);
        list.add(vm.getJNIEnv());
        list.add(0);
        list.add(vm.addLocalObject(new ByteArray(vm, "everhu".getBytes())));
        Number ret = module.callFunction(emulator, 0x92cd, list.toArray());
        byte[] bytes = (byte[]) vm.getObject(ret.intValue()).getValue();
//        System.out.println(Hex.encodeHexString(bytes));
    }

    public static void main(String[] args) {
        Wbaes test = new Wbaes();
        test.patch();
        test.dfa();
        for (int i = 0; i<32; i++) {
            test.call_wb();
        }
    }
}

dfa随机修改了state的一个字节,并在encdec返回时打印state的值。此外patch了日志打印,方便复制。

2591919-933bf3de30128b61.png

可以看出每个错误密文和正确密文只有4个字节的不同,说明我们hook的时机是对的;如果全部不同,说明时机太早了;只有一个不同则说明时机太晚了。

接下来就是用JeanGrey/phoenixAES根据错误密文还原密钥,第一行为正确密文,其他行为错误密文。

import phoenixAES

data = """3ddf3ef1c257e990555ee834a1c6f3f2
3d103ef17c57e990555ee883a1c6cbf2
3ddf3cf1c243e9909e5ee834a1c6f37f
3ddf3ee2c2570a90557ae834f1c6f3f2
3ddf3ec0c257df905597e83473c6f3f2
73df3ef1c257e904555e9134a1d9f3f2
3ddf3ef9c2570e9055cde8340ac6f3f2
3df43ef1e957e990555ee859a1c659f2
3ddf75f1c2dce990755ee834a1c6f320
3ddf3ed4c2577a905542e834e1c6f3f2
dddf3ef1c257e9f0555ef434a1d8f3f2
2ddf3ef1c257e9f4555edf34a101f3f2
3ddf3e51c2576f9055b1e8342ec6f3f2
3ddf98f1c2dae990045ee834a1c6f316
42df3ef1c257e9cc555e7d34a1b2f3f2
3ddfbbf1c280e990d25ee834a1c6f3be
3ddf3eb9c257ed90551de8347ac6f3f2
3ddf3ef4c257bb90554ae834cac6f3f2
3d433ef11757e990555ee82ba1c698f2
3ddf3eebc2575b905586e83477c6f3f2
3ddf13f1c2b4e990ae5ee834a1c6f399
3d203ef1a157e990555ee811a1c646f2
3ddf3e2cc257509055ace83418c6f3f2
9ddf3ef1c257e94c555e2534a19bf3f2
3ddf6bf1c204e990c05ee834a1c6f345
45df3ef1c257e9ce555e5434a141f3f2
3ddf3e8ec2570090556fe83449c6f3f2
3d4c3ef15657e990555ee84ea1c605f2
3db13ef1ac57e990555ee8aea1c67bf2
3ddf3e89c25716905527e83441c6f3f2
3ddf3e7bc257ec9055e4e83408c6f3f2
3d533ef11757e990555ee85ca1c610f2
3ddf7df1c2b7e990785ee834a1c6f377
"""

with open('crackfile', 'w') as fp:
    fp.write(data)

phoenixAES.crack_file('crackfile', [], True, False, verbose=2)
2591919-94636e33d457e8be.png

然后并没有还原出密钥,一开始怀疑是错误密文少了,开始加,从50、100、200、500到1000,并不能正确还原。

但是phoenixAES显示了每个错误密文对应的group,说明我们进行dfa攻击的时机是没问题的,所以还是要看看so里面的实现。

2591919-e6d0dabc61158df2.png
2591919-10359fe925ab5f64.png

函数开始和结束都有个idxTranspose函数

2591919-860630be2dde8293.png

实际上,state常用4x4的矩阵来展示,而该函数则是对其进行了转置。而在dfa中,错误密文共有4种情况,这些数组转置后错误的位置是不变的,所以才会出现上面的情况,明明phoenixAES显示了每个错误密文对应的group,但是却不能正确推算出密钥。

2591919-8e23b79ad746f351.png
import phoenixAES

data = """3ddf3ef1c257e990555ee834a1c6f3f2
3d103ef17c57e990555ee883a1c6cbf2
3ddf3cf1c243e9909e5ee834a1c6f37f
3ddf3ee2c2570a90557ae834f1c6f3f2
3ddf3ec0c257df905597e83473c6f3f2
73df3ef1c257e904555e9134a1d9f3f2
3ddf3ef9c2570e9055cde8340ac6f3f2
3df43ef1e957e990555ee859a1c659f2
3ddf75f1c2dce990755ee834a1c6f320
3ddf3ed4c2577a905542e834e1c6f3f2
dddf3ef1c257e9f0555ef434a1d8f3f2
2ddf3ef1c257e9f4555edf34a101f3f2
3ddf3e51c2576f9055b1e8342ec6f3f2
3ddf98f1c2dae990045ee834a1c6f316
42df3ef1c257e9cc555e7d34a1b2f3f2
3ddfbbf1c280e990d25ee834a1c6f3be
3ddf3eb9c257ed90551de8347ac6f3f2
3ddf3ef4c257bb90554ae834cac6f3f2
3d433ef11757e990555ee82ba1c698f2
3ddf3eebc2575b905586e83477c6f3f2
3ddf13f1c2b4e990ae5ee834a1c6f399
3d203ef1a157e990555ee811a1c646f2
3ddf3e2cc257509055ace83418c6f3f2
9ddf3ef1c257e94c555e2534a19bf3f2
3ddf6bf1c204e990c05ee834a1c6f345
45df3ef1c257e9ce555e5434a141f3f2
3ddf3e8ec2570090556fe83449c6f3f2
3d4c3ef15657e990555ee84ea1c605f2
3db13ef1ac57e990555ee8aea1c67bf2
3ddf3e89c25716905527e83441c6f3f2
3ddf3e7bc257ec9055e4e83408c6f3f2
3d533ef11757e990555ee85ca1c610f2
3ddf7df1c2b7e990785ee834a1c6f377
"""

idx = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15]

def transpose(data):
    return bytes([data[idx[i]] for i in range(16)])

with open('crackfile', 'w') as fp:
    for item in data.splitlines():
        if item:
            item = transpose(bytes.fromhex(item)).hex()
            fp.write(item + '\n')

phoenixAES.crack_file('crackfile', [], True, False, verbose=2)
2591919-25d2e1073f89ad17.png

得到了第10轮的密钥,然后使用SideChannelMarvels/Stark还原初始密钥

2591919-f661d3c28b4939b6.png

因此5415246EED9AEA9477EB680542E48DDA就是AES的密钥,验证一下

cryptor = AES.new(bytes.fromhex('5415246EED9AEA9477EB680542E48DDA'), AES.MODE_ECB)
data = cryptor.decrypt(bytes.fromhex('3ddf3ef1c257e990555ee834a1c6f3f2'))
print(data.hex())
2591919-80366be4200fbc7c.png

同样需要对输入输出进行转置

from Crypto.Cipher import AES

idx = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15]

def transpose(data):
    return bytes([data[idx[i]] for i in range(16)])

cryptor = AES.new(bytes.fromhex('5415246EED9AEA9477EB680542E48DDA'), AES.MODE_ECB)
data = cryptor.decrypt(transpose(bytes.fromhex('3ddf3ef1c257e990555ee834a1c6f3f2')))
print(data.hex())
print(transpose(data).hex())
2591919-9617647c561ff69e.png

encdec的输入一致。

代码

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

idx = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15]

_KEY = bytes.fromhex('5415246EED9AEA9477EB680542E48DDA')

def transpose(data):
    return bytes([data[idx[i]] for i in range(16)])

def encrypt(data):
    cryptor = AES.new(_KEY, AES.MODE_ECB)
    data = pad(data, AES.block_size)
    bucket = []
    for x in range(0, len(data), 16):
        block = data[x: x+16]
        block = bytes(block[i]^(block[i-1] if i else 0xa6) for i in range(16))
        block = transpose(block)
        block = cryptor.encrypt(block)
        block = transpose(block)
        bucket.append(block[0] ^ 0xe)
        for i in range(1, 16):
            bucket.append(block[i] ^ (bucket[-1] if i!=1 else block[0]))
    return bytes(bucket)

if __name__ == '__main__':
    data = encrypt(b'everhu')
    print(data.hex())

Reference

强推白龙的知识星球,里面的白盒加密系列深入浅出,让人很方便理解dfa的原理,原文见于第三讲——差分故障攻击的原理

或者也可以看看一种还原白盒AES秘钥的方法

你可能感兴趣的:(白盒AES加密DFA逆向)