白盒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<Object> 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<HookZzArm32RegisterContext>() {
            @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<Object> 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秘钥的方法

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容