白盒AES加密DFA逆向
DFA(Differential Fault Analysis)
簡單來說就是在倒數(shù)第一輪列混合和倒數(shù)第二輪列混合之間(在AES-128中也就是第8輪和第9輪之間),修改此時中間密文的一個字節(jié),會導(dǎo)致最終密文和正確密文有4個字節(jié)的不同。通過多次的修改,得到多組錯誤的密文,然后通過正確密文和這些錯誤密文能夠推算出第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();
}
}
接下來分析白盒AES加密
Java_com_mucfc_honeybee_DataProcessor_desensitization
wbEncrypt
encryptBlock
可以看出encryptBlock
并不完全是塊加密,它在加密的前后進(jìn)行了一些異或操作,encdec
才是真正執(zhí)行塊加密的函數(shù)
encdec
下斷點看看encryptBlock
的輸入輸出
同樣的,看看encdec
的輸入輸出
流程如下
接下來,進(jìn)行dfa攻擊
在for循環(huán)執(zhí)行第9輪時,隨機(jī)修改state的一個字節(jié)。
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隨機(jī)修改了state的一個字節(jié),并在encdec
返回時打印state的值。此外patch了日志打印,方便復(fù)制。
可以看出每個錯誤密文和正確密文只有4個字節(jié)的不同,說明我們hook的時機(jī)是對的;如果全部不同,說明時機(jī)太早了;只有一個不同則說明時機(jī)太晚了。
接下來就是用JeanGrey/phoenixAES根據(jù)錯誤密文還原密鑰,第一行為正確密文,其他行為錯誤密文。
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)
然后并沒有還原出密鑰,一開始懷疑是錯誤密文少了,開始加,從50、100、200、500到1000,并不能正確還原。
但是phoenixAES顯示了每個錯誤密文對應(yīng)的group,說明我們進(jìn)行dfa攻擊的時機(jī)是沒問題的,所以還是要看看so里面的實現(xiàn)。
函數(shù)開始和結(jié)束都有個idxTranspose
函數(shù)
實際上,state常用4x4
的矩陣來展示,而該函數(shù)則是對其進(jìn)行了轉(zhuǎn)置。而在dfa中,錯誤密文共有4種情況,這些數(shù)組轉(zhuǎn)置后錯誤的位置是不變的,所以才會出現(xiàn)上面的情況,明明phoenixAES
顯示了每個錯誤密文對應(yīng)的group,但是卻不能正確推算出密鑰。
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)
得到了第10輪的密鑰,然后使用SideChannelMarvels/Stark還原初始密鑰
因此5415246EED9AEA9477EB680542E48DDA
就是AES的密鑰,驗證一下
cryptor = AES.new(bytes.fromhex('5415246EED9AEA9477EB680542E48DDA'), AES.MODE_ECB)
data = cryptor.decrypt(bytes.fromhex('3ddf3ef1c257e990555ee834a1c6f3f2'))
print(data.hex())
同樣需要對輸入輸出進(jìn)行轉(zhuǎn)置
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())
和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
強(qiáng)推白龍的知識星球,里面的白盒加密系列深入淺出,讓人很方便理解dfa的原理,原文見于第三講——差分故障攻擊的原理
或者也可以看看一種還原白盒AES秘鑰的方法