1, Foreword
This is the fourth part of the SO reverse entry practical tutorial. The focus of the first part is the supplement environment of Unidbg and the simple magic modification of hash algorithm. The focus of this article is to analyze the encryption algorithm with deep magic modification in Unidbg.
- The common way of algorithm analysis is Frida Hook + IDA. In this series, the role of Frida will be weakened and the route of Unidbg Hook + IDA will be adopted.
- The main introduction, but not limited to the introduction, you will see the shallow and deep magic change encryption algorithm, as well as the OLLVM, SO confrontation and other contents in the sample.
- The analysis of samples is limited to study and research, and resolutely resist black ash production.
- There are 13 articles in total, and one article will be updated in 1-2 days. The information of each article is put in Baidu online disk at the end of the article.
2, Prepare
Xpreauthenencode is the target method that receives three parameters
Parameter 1 is a context, parameter 2 is the input plaintext, parameter 3 is the package name of app, and the return value is a 40 digit hexadecimal number.
Frida actively calls the test sample, parameter 2 is set to "r0ysue", parameter 3 is set to "com.mfw.roadbook", output:
57c043fe945355a64cb9c3d75db4bd767d1bbccb
3, Unidbg simulation execution
The old rule is to put up a shelf first
package com.lession4; import com.github.unidbg.linux.android.dvm.AbstractJni; 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.*; import com.github.unidbg.linux.android.dvm.array.ByteArray; import com.github.unidbg.memory.Memory; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; public class mfw extends AbstractJni{ private final AndroidEmulator emulator; private final VM vm; private final Module module; mfw() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // Create simulator instance final Memory memory = emulator.getMemory(); // Memory operation interface of simulator memory.setLibraryResolver(new AndroidResolver(23)); // Set system class library resolution vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession4\\mafengwo_ziyouxing.apk")); // Create Android virtual machine DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession4\\libmfw.so"), true); // Load so into virtual memory module = dm.getModule(); //Get the handle of this SO module vm.setJni(this); vm.setVerbose(true); dm.callJNI_OnLoad(emulator); }; public static void main(String[] args) throws Exception { mfw test = new mfw(); } }
function
RegisterNative(com/mfw/tnative/AuthorizeHelper, xPreAuthencode(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;, RX@0x4002e301[libmfw.so]0x2e301)
JNI OnLoad runs successfully. The address of our target method is 0x2e301. The three parameters passed in are String or context, which are all of the types mentioned above. We won't repeat them.
package com.lession4; import com.github.unidbg.linux.android.dvm.AbstractJni; 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.*; import com.github.unidbg.memory.Memory; import java.io.File; import java.util.ArrayList; import java.util.List; public class mfw extends AbstractJni{ private final AndroidEmulator emulator; private final VM vm; private final Module module; mfw() { emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.mfw.roadbook").build(); // Create simulator instance final Memory memory = emulator.getMemory(); // Memory operation interface of simulator memory.setLibraryResolver(new AndroidResolver(23)); // Set system class library resolution vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\lession4\\mafengwo_ziyouxing.apk")); // Create Android virtual machine DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\lession4\\libmfw.so"), true); // Load so into virtual memory module = dm.getModule(); //Get the handle of this SO module vm.setJni(this); vm.setVerbose(true); dm.callJNI_OnLoad(emulator); }; public String xPreAuthencode(){ List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // The first parameter is env list.add(0); // The second parameter, the instance method is jobject, and the static method is jclazz. Fill in 0 directly, which is generally unavailable. Object custom = null; DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(custom);// context list.add(vm.addLocalObject(context)); list.add(vm.addLocalObject(new StringObject(vm, "r0ysue"))); list.add(vm.addLocalObject(new StringObject(vm, "com.mfw.roadbook"))); Number number = module.callFunction(emulator, 0x2e301, list.toArray())[0]; String result = vm.getObject(number.intValue()).getValue().toString(); return result; } public static void main(String[] args) throws Exception { mfw test = new mfw(); System.out.println(test.xPreAuthencode()); } }
function
We were pleasantly surprised to find that there was not much interaction with the JAVA layer in the sample, so we ran out of the results smoothly! However, the run out algorithm is not the focus of this article. Let's continue to look at it.
4, Algorithm restore
The test found that no matter how long the input plaintext is, the fixed length result is output, so it is suspected of the hash algorithm. Because the output is always 40 bits, it is also suspected of the SHA1 algorithm in the hash algorithm.
First, make a static analysis. According to the address, jump to 0x2e301 in IDA
Rename and adjust the input parameters
sub_30548 is a signature verification function. There are many reasons for making this judgment
- The parameters are context and package name
- When the return value is false, the entire JNI function returns "illegal signature".
However, when using Unidbg to simulate the execution, we did not feel the trouble of calling JAVA signature verification by native. This is because we passed in APK and Unidbg handled this part of signature verification for us, but Unidbg can not handle signature verification in all cases. Therefore, in some previous examples, we will patch out the signature verification function.
Make further comments on JNI function
The encryption logic must be in sub_312E0 or sub_ In 2e1f4, look at sub from top to bottom_ 2e1f4, its parameter 1 is the input plaintext, and parameter 3 is the plaintext length. What about parameter 2? Like the sample in the previous article, it is buffer. As can be seen from the definition of v13, v13[20] does nothing and directly puts it into the function.
Use HookZz to Hook parameter 1 and parameter 3 before the function enters and Hook parameter 2 after the function goes out.
public void hook_312E0(){ // Get HookZz object IHookZz hookZz = HookZz.getInstance(emulator); // Load hookzz, support inline hook, see the document https://github.com/jmpews/HookZz // enable hook hookZz.enable_arm_arm64_b_branch(); // Test enable_arm_arm64_b_branch, optional // hook MDStringOld hookZz.wrap(module.base + 0x312E0 + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap export function @Override // Before method execution public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer input = ctx.getPointerArg(0); byte[] inputhex = input.getByteArray(0, ctx.getR2Int()); Inspector.inspect(inputhex, "input"); Pointer out = ctx.getPointerArg(1); ctx.push(out); }; @Override // After method execution public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer output = ctx.pop(); byte[] outputhex = output.getByteArray(0, 20); Inspector.inspect(outputhex, "output"); } }); hookZz.disable_arm_arm64_b_branch(); };
The parameter is the plaintext we entered, and the return value is the final result, so we only need to focus on this function.
Press H to convert the value to hexadecimal
Suspected SHA1 algorithm, look at the standard magic number
It can be found that the fourth and fifth of IV have been changed.
Next, we modify and verify the standard algorithm according to the IV in the sample
# 0xffffffff is used to make sure numbers dont go over 32 def chunks(messageLength, chunkSize): chunkValues = [] for i in range(0, len(messageLength), chunkSize): chunkValues.append(messageLength[i:i + chunkSize]) return chunkValues def leftRotate(chunk, rotateLength): return ((chunk << rotateLength) | (chunk >> (32 - rotateLength))) & 0xffffffff def sha1Function(message): # initial hash values h0 = 0x67452301 h1 = 0xEFCDAB89 h2 = 0x98BADCFE h3 = 0x5E4A1F7C h4 = 0x10325476 messageLength = "" # preprocessing for char in range(len(message)): messageLength += '{0:08b}'.format(ord(message[char])) temp = messageLength messageLength += '1' while (len(messageLength) % 512 != 448): messageLength += '0' messageLength += '{0:064b}'.format(len(temp)) chunk = chunks(messageLength, 512) for eachChunk in chunk: words = chunks(eachChunk, 32) w = [0] * 80 for n in range(0, 16): w[n] = int(words[n], 2) for i in range(16, 80): # sha1 # w[i] = leftRotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1) # sha0 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]) # Initialize hash value for this chunk: a = h0 b = h1 c = h2 d = h3 e = h4 # main loop: for i in range(0, 80): if 0 <= i <= 19: f = (b & c) | ((~b) & d) k = 0x5A827999 elif 20 <= i <= 39: f = b ^ c ^ d k = 0x6ED9EBA1 elif 40 <= i <= 59: f = (b & c) | (b & d) | (c & d) k = 0x8F1BBCDC elif 60 <= i <= 79: f = b ^ c ^ d k = 0xCA62C1D6 a, b, c, d, e = ((leftRotate(a, 5) + f + e + k + w[i]) & 0xffffffff, a, leftRotate(b, 30), c, d) h0 = h0 + a & 0xffffffff h1 = h1 + b & 0xffffffff h2 = h2 + c & 0xffffffff h3 = h3 + d & 0xffffffff h4 = h4 + e & 0xffffffff return '%08x%08x%08x%08x%08x' % (h0, h1, h2, h3, h4) plainText = "r0ysue" sha1Hash = sha1Function(plainText) print(sha1Hash)
Unfortunately, the results are not consistent this time! In other words, we have encountered a real magic change to the algorithm, rather than just modifying IV like last class!
How can we find the magic change in hundreds of lines of code? Rename the input parameter and take a look at the code here.
int __fastcall sub_312E0(char *input, int output, int Length) { int v4; // r0 int v5; // r4 unsigned int i; // r1 int v7; // r0 int v8; // r3 int v9; // r1 unsigned int j; // r2 unsigned int v11; // r0 unsigned int v12; // r4 int v13; // r3 int v14; // r0 int v15; // r2 unsigned int v16; // r5 int v17; // r2 int v18; // r0 int v19; // r4 int v20; // r0 int v21; // r0 int v22; // r4 int v23; // r3 int k; // r0 int v27; // [sp+20h] [bp-84h] char v28; // [sp+28h] [bp-7Ch] BYREF char v29[12]; // [sp+2Ch] [bp-78h] BYREF int v30[5]; // [sp+38h] [bp-6Ch] BYREF unsigned int v31; // [sp+4Ch] [bp-58h] int v32; // [sp+50h] [bp-54h] unsigned __int8 v33[63]; // [sp+54h] [bp-50h] BYREF char v34; // [sp+93h] [bp-11h] int v35; // [sp+94h] [bp-10h] v30[1] = 0xEFCDAB89; v30[0] = 0x67452301; v30[2] = 0x98BADCFE; v30[3] = 0x5E4A1F7C; v30[4] = 0x10325476; v4 = 0; v32 = 0; v31 = 0; if ( Length ) { v5 = Length - 1; for ( i = 0; ; i = v31 ) { v31 = i + 8; if ( i >= 0xFFFFFFF8 ) v32 = ++v4; v32 = v4; v7 = (i >> 3) & 0x3F; v8 = 0; if ( v7 == 63 ) { v34 = *input; sub_3151C(v30, v33); v7 = 0; v8 = 1; } qmemcpy(&v33[v7], &input[v8], v8 ^ 1); if ( !v5 ) break; --v5; ++input; v4 = v32; } } v9 = 0; for ( j = 0; j != 8; ++j ) { v29[j] = (unsigned int)v30[(j < 4) + 5] >> (~(_BYTE)v9 & 0x18); v9 += 8; } v28 = 0x80; v11 = v31; v12 = v31 + 8; v31 += 8; v13 = v32; if ( v11 >= 0xFFFFFFF8 ) v13 = ++v32; v32 = v13; v14 = (v11 >> 3) & 0x3F; v15 = 0; if ( v14 == 63 ) { v34 = 0x80; sub_3151C(v30, v33); v14 = 0; v15 = 1; v12 = v31; } qmemcpy(&v33[v14], (const void *)((unsigned int)&v28 | v15), v15 ^ 1); if ( (v12 & 0x1F8) == 448 ) { v16 = v12; } else { v16 = v12; do { v17 = 0; v28 = 0; v16 += 8; v31 = v16; v18 = v32; if ( v12 >= 0xFFFFFFF8 ) v18 = ++v32; v32 = v18; v19 = (v12 >> 3) & 0x3F; if ( v19 == 63 ) { v19 = 0; v34 = 0; sub_3151C(v30, v33); v16 = v31; v17 = 1; } qmemcpy(&v33[v19], (const void *)((unsigned int)&v28 | v17), v17 ^ 1); v12 = v16; } while ( (v16 & 0x1F8) != 448 ); } v31 = v16 + 64; v20 = v32; if ( v16 >= 0xFFFFFFC0 ) v20 = ++v32; v32 = v20; v21 = (v16 >> 3) & 0x3F; v22 = 0; if ( (unsigned int)(v21 + 8) < 0x40 ) { v23 = 0; } else { v27 = 64 - v21; qmemcpy(&v33[v21], v29, 64 - v21); sub_3151C(v30, v33); v23 = v27; v21 = 0; } qmemcpy(&v33[v21], &v29[v23], 8 - v23); for ( k = 0; k != 20; ++k ) { *(_BYTE *)(output + k) = *(unsigned int *)((char *)v30 + (k & 0xFFFFFFFC)) >> (~(_BYTE)v22 & 0x18); v22 += 8; } return _stack_chk_guard - v35; }
Sub appears more than once in the code_ 3151c, click in and have a look
The amount of code is five or six hundred lines, which should be the operation part of the function. Plus the previous part, a total of seven or eight hundred lines of code, so how can we find out where the magic change takes place? even to the extent that? Does it have no magic algorithm process, but XOR with a KEY after the standard operation?
The solution to this problem depends on the in-depth understanding of the hash algorithm process. Those interested can take a look at the part about the principle and implementation of cryptography in the SO basic course or the video course of Kangkang Unidbg series. It is not easy to explain the text clearly.
A hash algorithm can be simply divided into two parts: filling and encryption. Directly Hook the encryption function, look at its input parameters, and judge whether the filling part has changed.
public void hook_3151C(){ // Get HookZz object IHookZz hookZz = HookZz.getInstance(emulator); // Load hookzz, support inline hook, see the document https://github.com/jmpews/HookZz // enable hook hookZz.enable_arm_arm64_b_branch(); // Test enable_arm_arm64_b_branch, optional // hook MDStringOld hookZz.wrap(module.base + 0x3151C + 1, new WrapCallback<HookZzArm32RegisterContext>() { // inline wrap export function @Override // Before method execution public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { // Similar to Frida args[0] Pointer input = ctx.getPointerArg(0); byte[] inputhex = input.getByteArray(0, 20); Inspector.inspect(inputhex, "IV"); Pointer text = ctx.getPointerArg(1); byte[] texthex = text.getByteArray(0, 64); Inspector.inspect(texthex, "block"); ctx.push(input); ctx.push(text); }; @Override // After method execution public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { Pointer text = ctx.pop(); Pointer IV = ctx.pop(); byte[] IVhex = IV.getByteArray(0, 20); Inspector.inspect(IVhex, "IV"); byte[] outputhex = text.getByteArray(0, 64); Inspector.inspect(outputhex, "block out"); } }); hookZz.disable_arm_arm64_b_branch(); };
function
[08:53:06 577]IV, md5=b70ca24521f790e6bf3c4a16ba868a03, hex=0123456789abcdeffedcba987c1f4a5e76543210 size: 20 0000: 01 23 45 67 89 AB CD EF FE DC BA 98 7C 1F 4A 5E .#Eg........|.J^ 0010: 76 54 32 10 vT2. ^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [08:53:06 578]block, md5=c8e3dfac5d04ac7fb62160cd976bb01c, hex=72307973756580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030 size: 64 0000: 72 30 79 73 75 65 80 00 00 00 00 00 00 00 00 00 r0ysue.......... 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30 ...............0 ^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [08:53:06 592]IV, md5=eb8ea7f8f507f692ef0778f13a59a330, hex=fe43c057a6555394d7c3b94c76bdb45dcbbc1b7d size: 20 0000: FE 43 C0 57 A6 55 53 94 D7 C3 B9 4C 76 BD B4 5D .C.W.US....Lv..] 0010: CB BC 1B 7D ...} ^-----------------------------------------------------------------------------^ >-----------------------------------------------------------------------------< [08:53:06 592]block out, md5=c8e3dfac5d04ac7fb62160cd976bb01c, hex=72307973756580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030 size: 64 0000: 72 30 79 73 75 65 80 00 00 00 00 00 00 00 00 00 r0ysue.......... 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 30 ...............0 ^-----------------------------------------------------------------------------^
Hook results reflect such a problem
The magic change is the algorithm itself. Because the input parameters of the operation function are normal and filled in plaintext, there is no possibility of custom filling or transformation of plaintext. The output parameters are the output results. Therefore, the algorithm does not make some custom steps after the standard process. What it modifies is the algorithm itself.
At this time, we should consider what constitutes the operation part of SHA1 algorithm? SHA1 and MD5 adopt the same structure. Each 512 bit packet requires one round of operation. Our input length does not exceed the length of one packet, so only one round of operation is considered. One round of operation is 80 steps, and every 20 steps is a mode.
First, record the results that should be obtained under normal conditions in each of the 80 steps
0x5e1444aa 0xecb6ad5e 0x4066d34 0xed08cc85 0xe8f28c34 0x237ebcb7 0xeecacf3d 0xaf1a9fa8 0x921750fc 0x4380efc5 0xff26c559 0xe3d49cd6 0x517dcdd6 0x22a2bc19 0x3eaf6dc2 0x4891169b 0x20c32ce1 0x8556c446 0xdd2c894f 0x5420ba17 0x6ec4e797 0x91e5d34b 0xba26ad8 0xef34ad50 0xd1126575 0x7dd310e7 0x6b52d1f9 0xe7768a2 0xac273146 0x694146b8 0xebe5e627 0xfa712f50 0x10bfabc0 0x4cb1379b 0x665c4398 0xb2b46868 0x2ac8a949 0xb65eae61 0x3524a2e5 0x72ac7756 0x7f0e6c94 0x2928a555 0x7ed33fde 0x46a8f7fc 0x66ff0f01 0x52cfa822 0x4b18fa72 0xe39f852e 0xe0a3043a 0x9729af47 0xc142ad63 0x77c7096f 0x94602ecb 0x3e7202e5 0x89c7a8f2 0xbd2782bb 0xe6f058a3 0x8ca5906 0xe5cb4077 0x4a238672 0xe93aa2e 0xcf4dd760 0x111f600f 0x3853e9bf 0x7e375ab5 0xe4ba4774 0x9e39f23 0x4041ea20 0x82265213 0x9f37f728 0x3adf0819 0x586ac5e9 0xe5675b10 0xfb192c0e 0xc885ea1b 0x30628c48 0x833f6da5 0x5d958b47 0x2b11a368 0xc5611c9d
Next, verify the results of 80 steps in the sample through inline Hook. This process requires a deep understanding of the principle and programming implementation of the encryption algorithm
You can use HookZz to implement Inline hook in the following way
public void hook_315B0(){ IHookZz hookZz = HookZz.getInstance(emulator); hookZz.enable_arm_arm64_b_branch(); hookZz.instrument(module.base + 0x315B0 + 1, new InstrumentCallback<Arm32RegisterContext>() { @Override public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // Through the base+offset inline wrap internal function, it can be seen as sub in IDA_ XXX those System.out.println("R2:"+ctx.getR2Long()); } }); }
But on the whole, we need to do more than ten or even dozens of inline hook s. In this case, it's a little inconvenient to use HookZz. You might as well try Unidbg's console debugger.
In such a repetitive work, we found that in the first 16 steps, the encryption process of the sample is consistent with that of the standard algorithm, and we parted ways from step 17. We use Python code to represent
80 step operation of standard process
for t in range(80): if t <= 19: K = 0x5a827999 f = (b & c) ^ (~b & d) elif t <= 39: K = 0x6ed9eba1 f = b ^ c ^ d elif t <= 59: K = 0x8f1bbcdc f = (b & c) ^ (b & d) ^ (c & d) else: K = 0xca62c1d6 f = b ^ c ^ d
80 step operation of samples
for t in range(80): if t <= 15: K = 0x5a827999 f = (b & c) ^ (~b & d) elif t <= 19: K = 0x6ed9eba1 f = b ^ c ^ d elif t <= 39: K = 0x8f1bbcdc f = (b & c) ^ (b & d) ^ (c & d) elif t <= 59: K = 0x5a827999 f = (b & c) ^ (~b & d) else: K = 0xca62c1d6 f = b ^ c ^ d
In the standard process, K and nonlinear function are switched in 20 steps. There are four modes in total. In the sample, K and nonlinear function are switched every 16 steps. There are five modes, but they are still four modes in the standard process in essence, because one mode is used twice.
Verify the results and you're done.
5, Epilogue
The writing process of this article is very awkward. Encryption algorithm is a difficult thing. I want to explain the sample of deep magic change encryption algorithm in a limited space. The content described in this article is not enough to really understand this magic change sample. Readers can get a real understanding of the magic modification samples in the samples through one of the following paths.
- Read SHA1 WIKI + official English document + handwritten C implementation
- Sign up for my class, watch the live broadcast and laugh together
In addition, there is also a magic change Hmac in the sample. You can learn and study it.
Data link: https://pan.baidu.com/s/1MHe0Oen6KKsdWru0YWTUzQ
Extraction code: ro1x