某md5魔改样本学习

在吾爱破解上看到个合适的案例,学习一下

初始化

先搭一个 unidbg 的架子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.md5;
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.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

//必须继承AbstractJni
public class modMd5 extends AbstractJni {
    public static AndroidEmulator emulator;  // 静态属性,以后对象和类都可以直接使用
    public static Memory memory;
    public static VM vm;
    public static DvmClass MainActivity;
    public static Module module;
    // 1 构造方法--》用来初始化
    public modMd5(){
        // 1.创建设备(32位或64位模拟器), 具体看so文件在哪个目录。 在armeabi-v7a就选择32位
        // 传进设备时,如果是32位,后面so文件就要用32位,同理需要用64位的
        // 这个名字可以随便写,一般写成app的包名    以后可能会动
        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("").build();

        // 2.获取内存对象(可以操作内存)
        memory = emulator.getMemory();

        //3.设置安卓sdk版本(只支持19、23)
        memory.setLibraryResolver(new AndroidResolver(23));

        // 4.创建虚拟机(运行安卓代码需要虚拟机,就想运行py代码需要python解释器一样)    以后会动
        vm = emulator.createDalvikVM(new File("apks/md5/md5-release.apk"));
        vm.setJni(this); // 后期补环境会用,把要补的环境,写在当前这个类中,执行这个代码即可,但是必须继承AbstractJni
        vm.setVerbose(true); //是否展示调用过程的细节

        // 5.加载so文件
        DalvikModule dm = vm.loadLibrary("native-lib", false);   // 以后会动
        dm.callJNI_OnLoad(emulator); // jni开发动态注册,会执行JNI_OnLoad,如果是动态注册,需要执行一下这个,如果静态注册,这个不需要执行,车智赢案例是静态注册

        MainActivity = vm.resolveClass("com.littleq.cryptography.md5.MainActivity");
        // 6.dm代表so文件,dm.getModule()得到module对象,基于module对象可以访问so中的成员。
        module = dm.getModule(); // 把so文件加载到内存后,后期可以获取基地址,偏移量等,该变量代指so文件
    }
    
    //2 sign 成员方法--》主要用来解密
    public void sign(){
        String str = "123456";
        String result = MainActivity.newObject(null).callJniMethodObject(emulator,"sign(Ljava/lang/String;)Ljava/lang/String;",str).getValue().toString();
        System.out.println(result);
    }

    public void hookConsoleByDebugger(){
//        emulator.attach().addBreakPoint(module.base+0xA3C);
        emulator.attach().addBreakPoint(module.base+0x8D8);
    }

    // 3 main方法---》右键直接运行
    public static void main(String[] args) {
        modMd5 md5=new modMd5();
        md5.hookConsoleByDebugger();
        md5.sign();
    }
}

直接运行就可以出结果,当然,我是来学习算法分析的

分析

接着打开 ida,改一下类型,这样好看一点

首先通过 GetStringUTFChars 得到 java 层的字符串,转成 c 语言字符串,得到输入的值给 v4
接着分配内存空间,给 v6,将 v4 传给 sub_A3C 加密后给 v6,最后返回 v6 的值
所以,毫无疑问, sub_A3C 就是加密函数

进来之后,就看到了 md5 的模数值,但是得到的结果并不是标准 md5 加密的结果,说明被魔改了,接下来就来看看是哪儿进行了魔改

首先需要直到一个 md5 函数可能魔改哪些地方?

  • 初始化常量
  • K 表
  • 非线性函数
  • 常量转换 首先看一下初始化常量,正常的初始化常量是:
1
2
3
4
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;

由上图可知,初始化常量它并没有改,那就只能看其他的部分了

第一轮魔改

首先先来看第一轮运算,我把变量名改了一下,这样我看得清楚一点

可以很清楚的看到非线性函数改了,第一轮应该是 FF 函数,他的非线性函数是: (b & c) | ((~b) & d) 被修改成了 (d & c) | ((~d) & b),但是这样不太好看,我选择在 unidbg 中打印寄存器的值
经过分析汇编,我知道了:

  • A 对应着 W26 寄存器
  • B 对应着 W25 寄存器
  • C 对应着 W24 寄存器
  • D 对应着 W23 寄存器 在 unidbg 中对日志进行打印
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public void hookDebugger(){
	long baseAddr = module.base;
	System.out.printf("SO基地址: 0x%x%n", baseAddr);
	emulator.attach().addBreakPoint(module.base + 0xBBC, new BreakPointCallback() {
		@Override
		public boolean onHit(Emulator<?> emulator, long address) {
			int a = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X26).intValue();
			int b = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X25).intValue();
			int c = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X24).intValue();
			int d = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X23).intValue();
			System.out.println("===魔改md5第一轮运算结果===");
			System.out.printf("a: 0x%08x, b: 0x%08x, c: 0x%08x, d: 0x%08x\n", a, b, c, d);
			return true;
		}
	});
}

得到结果如下:

在上面我已经知道非线性函数给改了,我也跟着他改一下,发现结果并不对
那应该是其他地方也改了,比如说,K 表

v14 看到上面赋值是在 0x12D8 的位置,第一次取需要 v14-40x12D8-0x10=0x12C8 的位置
从 0x12C8 的位置,开始找,k 表果然被魔改了

最后撸下来的 k 表就是这个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
self.T = [
	# Round 1
	0x8A51407D,
	0x6A88995D,
	0xFD7025F4,
	0xA7553036,
	0x489E15C1,
	0xF5CDB84B,
	0xC0FFBCF6,
	0x253F7D7E,
	0xE93FD535,
	0xD6CD6448,
	0x01220AE4,
	0xD806D023,
	0xE84E6EA9,
	0x230135D8,
	0xC27AE834,
	0xF5292BF4,
	# Round 2
	0x46711AC1,
	0xA90A840A,
	0xFD1BBEF0,
	0x687810E5,
	0x8C37FC1B,
	0xFFFD6EC6,
	0x8867BEAC,
	0x6C96FED4,
	0xFDBF77AC,
	0xA59C8134,
	0x4AC99BE5,
	0xF66D568A,
	0xBF80B2C1,
	0x277D05E4,
	0xEA2C8E1F,
	0xD58FA982,
	# Round 3
	0x03661ADB,  # 57023195
	0xD93BE6CA,  # -650385718
	0xE7585F52,  # -413638830
	0x20C23A76,  # 549599862
	0xC3F22CE1,  # -1007538975
	0xF47FB4D2,  # -192957230
	0x4442B612,  # 1145222674
	0xAABC73EB,  # -1430490133
	0xFCC24453,  # -54377389
	0x66657006,  # 1717923846
	0x8E1BE7C3,  # -1910773821
	0xFFF5BB27,  # -672985
	0x867B8079,  # -2038726535
	0x6EA336BB,  # 1856190139
	0xFE09B282,  # -32918910
	0xA3E07FDA,  # -1545568294
	# Round 4
	0x4CF3A208,  # 1291035144
	0xF708037E,  # -150469762
	0xBDFDD143,  # -1107439293
	0x29B9C389,  # 700040073
	0xEB1494A7,  # -350972761
	0xD44DA632,  # -733108686
	0x05AA195E,  # 95033694
	0xDA6CA20A,  # -630414838
	0xE65DAC20,  # -430068704
	0x1E8296E0,  # 511874784
	0xC5658374,  # -983202956
	0xF3D1564B,  # -204384693
	0x4212F2E5,  # 1108538085
	0xAC6AF723,  # -1402276061
	0xFC63B7E7,  # -60573721
	0x6450C165,  # 1683013989
]

k 表替换之后,结果就正确了

但是并没有完全对,第一轮对了

第二轮魔改

首先看一下第二轮的代码

一来就看见非线性函数给改了
标准的非线性函数是: (B & D) | (C & ~D),而这儿魔改之后就变成了 (D & B) | (C & ~B) 看一下魔改非线性函数之后得到的结果
接着,我去 unidbg 打印一下魔改之后的 A,B,C,D 的值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
emulator.attach().addBreakPoint(module.base + 0xC94, new BreakPointCallback() {
	@Override
	public boolean onHit(Emulator<?> emulator, long address) {
		int a = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X26).intValue();
		int b = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X25).intValue();
		int c = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X24).intValue();
		int d = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X23).intValue();
		System.out.println("===魔改md5第二轮运算结果===");
		System.out.printf("a: 0x%08x, b: 0x%08x, c: 0x%08x, d: 0x%08x\n", a, b, c, d);
		return true;
	}
});

打印结果:

1
2
===魔改md5第二轮运算结果===
a: 0xa586a548, b: 0x021d429d, c: 0x63694d00, d: 0x251515d5

一致,接着看第三轮运算的魔改

第三轮魔改

先来看一下在 ida 中第三轮魔改的代码

发现并没有魔改非线性函数,他左移的位数(常量转换)也是对的,貌似值魔改了 k 表对应的值
到 unidbg 中打印一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
emulator.attach().addBreakPoint(module.base + 0xEE4, new BreakPointCallback() {
	@Override
	public boolean onHit(Emulator<?> emulator, long address) {
		int a = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_W17).intValue();
		int b = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_W23).intValue();
		int c = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_W7).intValue();
		int d = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_W24).intValue();
		System.out.println("===魔改md5第三轮运算结果===");
		System.out.printf("a: 0x%08x, b: 0x%08x, c: 0x%08x, d: 0x%08x\n", a, b, c, d);
		return true;
	}
});

得到的值:

到标准算法改了过后的算法中打印一下

一致,接着就需要看第四轮魔改算法了

第四轮魔改

看一下魔改的代码呢

发现他是魔改了非线性函数的逻辑,左移位数也是对的 原来的逻辑是: C ^ (B | ~D) 经过修改之后变成: (D | ~B) ^ C
替换进去
正常魔改的结果是:

经过我算法魔改之后的结果:

刚好一致,unidbg 都不用验证了