【转载】第七讲——使用Unidbg进行DFA攻击

转载 24 / 25

说说在前面

本文转载龙哥星球 白盒 AES 密码学系列,相关文件代码等,可加入星球下载


一、引言

这一篇中,我们简单讨论使用Unidbg处理DFA。

二、案例

案例就是前文所处理过的案例,在前面我们说,Unidbg中有另一个办法可以帮助”找时机“。我先卖个关子,大家先往下看。

首先和前文一样,Call wbaes_encrypt_ecb。

package com.luckin;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;

import java.io.File;

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

    public LKAES(){
        emulator = AndroidEmulatorBuilder.for32Bit().build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分

        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/luckin/ruixingkafei_4.9.3.apk"));
        vm.setVerbose(true);
        // 加载so到虚拟内存
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        module = dm.getModule();
        // 设置JNI
        vm.setJni(this);
        dm.callJNI_OnLoad(emulator);
    }

    public static byte[] hexStringToBytes(String hexString) {
        if (hexString.isEmpty()) {
            return null;
        }
        hexString = hexString.toLowerCase();
        final byte[] byteArray = new byte[hexString.length() >> 1];
        int index = 0;
        for (int i = 0; i < hexString.length(); i++) {
            if (index  > hexString.length() - 1) {
                return byteArray;
            }
            byte highDit = (byte) (Character.digit(hexString.charAt(index), 16) & 0xFF);
            byte lowDit = (byte) (Character.digit(hexString.charAt(index + 1), 16) & 0xFF);
            byteArray[i] = (byte) (highDit << 4 | lowDit);
            index += 2;
        }
        return byteArray;
    }

    public static String bytesTohexString(byte[] bytes) {
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if(hex.length() < 2){
                sb.append(0);
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    public void call_wbaes(){
        MemoryBlock inblock = emulator.getMemory().malloc(16, true);
        UnidbgPointer inPtr = inblock.getPointer();
        MemoryBlock outblock = emulator.getMemory().malloc(16, true);
        UnidbgPointer outPtr = outblock.getPointer();
        byte[] stub = hexStringToBytes("30313233343536373839616263646566");
        assert stub != null;
        inPtr.write(0, stub, 0, stub.length);
        module.callFunction(emulator, 0x17bd5, inPtr, 16, outPtr, 0);
        String ret = bytesTohexString(outPtr.getByteArray(0, 0x10));
        System.out.println("white box result:"+ret);
        inblock.free();
        outblock.free();
    }

    public static void main(String[] args) {
        LKAES lkaes = new LKAES();
        lkaes.call_wbaes();
    }
}

运行结果

[15:41:39 547]  INFO [com.github.unidbg.linux.AndroidElfLoader] (AndroidElfLoader:464) - libcryptoDD.so load dependency libandroid.so failed
JNIEnv->FindClass(com/luckincoffee/safeboxlib/CryptoHelper) was called from RX@0x4001b43b[libcryptoDD.so]0x1b43b
JNIEnv->RegisterNatives(com/luckincoffee/safeboxlib/CryptoHelper, RW@0x400dfd0c[libcryptoDD.so]0xdfd0c, 4) was called from RX@0x4001b3e3[libcryptoDD.so]0x1b3e3
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, localAESWork([BI[B)[B, RX@0x4001984d[libcryptoDD.so]0x1984d)
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, localConnectWork([B[B)[B, RX@0x4001978d[libcryptoDD.so]0x1978d)
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, md5_crypt([BI)[B, RX@0x4001a981[libcryptoDD.so]0x1a981)
RegisterNative(com/luckincoffee/safeboxlib/CryptoHelper, localAESWork4Api([BI)[B, RX@0x4001b1cd[libcryptoDD.so]0x1b1cd)
white box result:b0f59c0d48c145915fc8f6a842c4d5eb

可以发现计算结果完全一致。

接下来我们要作图,读者可能会困惑,做什么图?事实上,因为白盒加密的主要实现方式是查表法,所以加密主体就是大量的内存访问。那么记录函数对内存的访问以及发起访问的地址(PC指针),绘制成折线图,就可以较好的反映加密流程。

使用Unidbg的ReadHook

public void traceAESRead(){
    emulator.getBackend().hook_add_new(new ReadHook() {
        @Override
        public void hook(Backend backend, long address, int size, Object user) {
            long now = emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_PC).intValue();
            if((now>module.base) & (now < (module.base+module.size))){
                System.out.println(now - module.base);
            }
        }

        @Override
        public void onAttach(UnHook unHook) {

        }

        @Override
        public void detach() {

        }
    }, module.base, module.base+module.size, null);
}

规则如下:监控整个SO地址范围内的内存读取操作,记录其发起地址,我减去了SO基地址,只打印偏移,这样呈现效果更好。

traceAESRead 函数放于如下位置

public static void main(String[] args) {
    LKAES lkaes = new LKAES();
    lkaes.traceAESRead();
    lkaes.call_wbaes();
}

运行结果如图

将这几千条记录拷贝出来,保存在trace.txt 中,在Python中做可视化,这十分方便。需要安装matplotlib以及numpy库。

import matplotlib.pyplot as plt
import numpy

input = numpy.loadtxt("trace.txt", int)

plt.plot(range(len(input)), input)
plt.show()

运行后生成折线图,将其放大是如下效果

X轴的计数单位是次数,表示当前是第几次内存访问,如图,在程序的运行过程中,发生了1400余次对SO内存的读操作。Y轴是发起访问的偏移地址。需要注意,X与Y轴的数值表示为十进制。图上可得,Y主要在80000-100000之间,我们修改Y轴范围,增强呈现效果。

import matplotlib.pyplot as plt
import numpy

input = numpy.loadtxt("trace.txt", int)

# 限制Y
plt.ylim(80000,100000)
plt.plot(range(len(input)), input)
plt.show()

运行后

似乎还可以缩小到85000-90000之间,再次缩小Y的范围

这样看着顺眼多了,我们对它进行分析。

首先,可以比较明显的看到,存在十个重复的模式,这代表了十轮运算。这一点是有用的,可用于区分AES-128/192/256,分别对应10/12/14 轮。

除此之外,我们发现每轮运算的起点是一个较低的地址,具体在86000附近左右,转成十六进制就是0x14FF0附近。

在IDA 中查看该处,我们发现正是上一篇中所分析的wbShiftRows中。这就是我说的”Unidbg中的另一个好办法“,这得益于Unidbg中获取执行流非常容易。

接下来就再次来到故障攻击的部分,整个代码近似于Frida Hook代码的复刻。

package com.luckin;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.ReadHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
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.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import unicorn.ArmConst;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Random;

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

    public LKAES(){
        emulator = AndroidEmulatorBuilder.for32Bit().build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分

        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/luckin/ruixingkafei_4.9.3.apk"));
        vm.setVerbose(true);
        // 加载so到虚拟内存
        DalvikModule dm = vm.loadLibrary("cryptoDD", true);
        module = dm.getModule();
        // 设置JNI
        vm.setJni(this);
        dm.callJNI_OnLoad(emulator);
    }

    public static byte[] hexStringToBytes(String hexString) {
        if (hexString.isEmpty()) {
            return null;
        }
        hexString = hexString.toLowerCase();
        final byte[] byteArray = new byte[hexString.length() >> 1];
        int index = 0;
        for (int i = 0; i < hexString.length(); i++) {
            if (index  > hexString.length() - 1) {
                return byteArray;
            }
            byte highDit = (byte) (Character.digit(hexString.charAt(index), 16) & 0xFF);
            byte lowDit = (byte) (Character.digit(hexString.charAt(index + 1), 16) & 0xFF);
            byteArray[i] = (byte) (highDit << 4 | lowDit);
            index += 2;
        }
        return byteArray;
    }

    public static String bytesTohexString(byte[] bytes) {
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if(hex.length() < 2){
                sb.append(0);
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    public void call_wbaes(){
        MemoryBlock inblock = emulator.getMemory().malloc(16, true);
        UnidbgPointer inPtr = inblock.getPointer();
        MemoryBlock outblock = emulator.getMemory().malloc(16, true);
        UnidbgPointer outPtr = outblock.getPointer();
        byte[] stub = hexStringToBytes("30313233343536373839616263646566");
        assert stub != null;
        inPtr.write(0, stub, 0, stub.length);
        module.callFunction(emulator, 0x17bd5, inPtr, 16, outPtr, 0);
        String ret = bytesTohexString(outPtr.getByteArray(0, 0x10));
        System.out.println(ret);
        inblock.free();
        outblock.free();
    }

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

    public void dfaAttack(){
        emulator.attach().addBreakPoint(module.base + 0x14F98 + 1, new BreakPointCallback() {
            int count = 0;
            UnidbgPointer pointer;
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                count += 1;
                RegisterContext registerContext = emulator.getContext();
                pointer = registerContext.getPointerArg(0);
                emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
                    @Override
                    public boolean onHit(Emulator<?> emulator, long address) {
                    if(count % 9 == 0){
                        pointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));
                    }
                    return true;
                    }
                });

                return true;
            }
        });
    }

    public void traceAESRead(){
        emulator.getBackend().hook_add_new(new ReadHook() {
            @Override
            public void hook(Backend backend, long address, int size, Object user) {
                long now = emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_PC).intValue();
                if((now>module.base) & (now < (module.base+module.size))){
                    System.out.println(now - module.base);
                }
            }

            @Override
            public void onAttach(UnHook unHook) {

            }

            @Override
            public void detach() {

            }
        }, module.base, module.base+module.size, null);
    }

    public static void main(String[] args) {
        LKAES lkaes = new LKAES();
//        lkaes.traceAESRead();
        lkaes.dfaAttack();
        for(int i =0;i<200;i++){
            lkaes.call_wbaes();
        }

    }
}

将正确密文与运行出的故障密文放入phoenixAES以及stark,可以得到和前文相同的结果。

二、尾声

后续会再补充三个左右样例。

暂无评论
本文作者:
本文链接: https://www.qinless.com/?p=1659
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 qinless 的博客!
100

发表评论

返回顶部