腾讯游戏安全大赛2023安卓初赛

不算非常难的题,但是对于没有app逆向经验的新人来说是挺好的上手题目

dump libli2cpp.so

进来先随便看看,unity题先看看c#的东西,il2cpp被加密过,ida里解析不出来,考虑运行时dump
直接用frida读,注意到对frida默认的端口做了检测,所以要转发到别的端口上

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
/*
如何使用
设置一个倒计时后dump指定的so文件,timeout单位是ms,默认为3000ms
*/
function dump(soName: string, timeout: number = 3000) {
setTimeout(() => {
let libSo = Process.getModuleByName(soName);
let base = libSo.base;
let size = libSo.size;
let sectionRanges = libSo.enumerateRanges("");
for (let i = 0; i < sectionRanges.length; i++) {
console.log(sectionRanges[i].base.sub(base), sectionRanges[i].size, sectionRanges[i].base.add(sectionRanges[i].size).sub(base), sectionRanges[i].protection);
Memory.protect(sectionRanges[i].base, sectionRanges[i].size, 'rwx');
let buffer = sectionRanges[i].base.readByteArray(sectionRanges[i].size);
console.log(`write ${sectionRanges[i].size} bytes sections`);
send(["dumpso", soName], buffer);
if (i + 1 < sectionRanges.length && sectionRanges[i].base.add(sectionRanges[i].size).compare(sectionRanges[i + 1].base) !== 0) {
let gap = Memory.alloc(sectionRanges[i + 1].base.sub(sectionRanges[i].base.add(sectionRanges[i].size)).toUInt32());
let buffer = gap.readByteArray(sectionRanges[i + 1].base.sub(sectionRanges[i].base.add(sectionRanges[i].size)).toUInt32());
console.log(`write ${sectionRanges[i + 1].base.sub(sectionRanges[i].base.add(sectionRanges[i].size)).toUInt32()} bytes gap`);
send(["dumpso", soName], buffer);
}
}
console.log("base: ", base);
console.log("size: ", size);
console.log("base + size: ", base.add(size));
}, timeout);
}
dump("libil2cpp.so");
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
import frida
import sys
import time


def handleResigerNatives(message):
print("----------------------------------------")
print("Native Method in class: {}".format(message['payload'][1]))
print("Native Method name: {}".format(message['payload'][2]))
print("Native Method signature: {}".format(message['payload'][3]))
print("Native Method address: {}".format(message['payload'][4]))
print("Which file register It: {}".format(message['payload'][5]))


def handleDumpSo(message, data):
with open("dump_"+message['payload'][1], "ab") as f:
f.write(data)


def onMessage(message, data):
if message['type'] == 'send':
if message['payload'][0] == "registerNatives":
handleResigerNatives(message)
elif message['payload'][0] == "dumpso":
handleDumpSo(message, data)
else:
print("[!!] Message from target: ", message['payload'])
else:
print("Not a send type Message", message['stack'])


with open('dumpso.js', "r", encoding="utf-8") as f:
jscode = f.read()

targetProcessPackName = "com.com.sec2023.rocketmouse.mouse"
device = frida.get_device_manager().add_remote_device("127.0.0.1:12345")
try:
pid = device.spawn(targetProcessPackName)
session = device.attach(pid)
print(pid)
except frida.ProcessNotFoundError:
print("No such process")
sys.exit(0)

# device.resume(pid)
# input("continue")
script = session.create_script(jscode)
script.on('message', onMessage)

script.load()
device.resume(pid)

sys.stdin.read()


这样so基本都出来了,再结合ll2cppDumper和apk里找到的global-metadata修复一下c#符号信息

但是因为是dump的,所以偏移和节符号之类的多少有些损坏,这样直接扔进ida里是无法识别的,所以要修复elf,如果手动修的话就是把so扔进010里先跑一遍elf模板识别关键字段,然后把progame_table段里的物理地址值改成虚拟地址对应的值,把文件中的段大小改成内存中的段大小,具体原理就是因为dump下来的elf中数据的偏移都是还在内存中的偏移,同时因为内存中的elf会把一些像stack_segment之类只有在运行时才会占据实际空间的段拓展,所以有些段的位置会被挤歪,也就是物理地址和虚拟地址不符,而ida只识别物理地址,所以要把物理地址的值改成虚拟地址对应的值,还有把在文件中的大小改为在内存中的大小,这样才能定位真实的偏移

节表的部分在dump时都会损坏,要去未解密的so里把节表的部分复制过来
同理对节表(section_table)里的元素也做修正偏移的操作

注意到节表里有个储存各字段名称的部分由elf头里的string_table_index定位

这个值是节表的索引,对应的节表元素里储存了所有表头的名称,这个部分在dump的时候是会丢失的,手动修复就要去原来apk里的so中把对应的部分复制出来(因为表头名称是不加密的)

这些都修完后按理就修好了,修的过程中发现有些elf的部分没识别出来是正常情况,因为没修好010模板无法识别,边修边f5就会逐渐把符号表,节表之类的都显示出来

但是主播主播,你的操作太麻烦了,有没有一把梭的方法

有的有的,直接上soFixer,修之前记得先用ll2cpp dumper提取符号,然后在用soFixer修,不然修完后的ll2cpp dumper就不识别符号了,用soFixer只要输入dump时的基址就能一键修正偏移了

修完后就是用ll2cppdumper自带的ida脚本把符号信息导入ida,在ida的script file里选择ida_with_struct_py3.py这个脚本,然后按照提示依次选择script.json和il2cpp.h两个文件导入脚本,就可以在ida里恢复符号
记得ida里也要设定基址,在edit->segment->rebase一栏中填入dump时的基址
这样跑完后该有的符号就都有了,因为题目要求做注册机,token是用小键盘输入的,所以先搜下keyboard试试看能不能找到相关函数

分析注册机输入


可以看到还是找到了,拿frida hook一下看看调用

这里虽然做的时候软件一直崩溃,但还是拿到了hook数据

这里是按了一下1按了一下enter,可以看到最后都调用了SmallKeyboard__iI1Ii,看看这个函数什么情况

可以看到这里针对keyType的三种不同情况做不同处理,其中type=2的情况又调用了别的函数,所以重点hook SmallKeyboard__iI1Ii_469567457968看看这个v23是什么,怀疑是我们的输入

hook结果如下,这个0x7b就是我们输入的123,点进这个函数看,发现最终调用了一个导入函数,如果跳过去是jumpout就手动c一下把数据转成代码,可能是ida误判没有把代码反编译出来

那么这里很有可能是加密了,我们得跳到sec2023这个so的导出表里找这个函数,这里指的是导出表的第十个元素
上文的hook脚本如下

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
setTimeout(() => {
let lib = Process.getModuleByName("libil2cpp.so");
console.log(lib.base);
let offset = 0x6EF9C28000;
let addr = [0x0000006EFA08E300,
0x0000006EFA08E18C,
0x0000006EFA08D880,
0x0000006EFA08DAB0,
0x0000006EFA08DFDC,
0x0000006EFA08DE90,
0x0000006EFA08E184,
0x0000006EFA08E2F8,
0x0000006EFA08E3B0,
0x0000006EFA08E3A8
]
var name = ["SmallKeyboard___ctor(SmallKeyboard_o *this, const MethodInfo *method)",
"SmallKeyboard__Start(SmallKeyboard_o *this, const MethodInfo *method)",
"void SmallKeyboard__iI1Ii(SmallKeyboard_o *this, SmallKeyboard_iII1i_o *info, const MethodInfo *method)",
"SmallKeyboard__iI1Ii_469567457968(SmallKeyboard_o *this, uint64_t i1I, const MethodInfo *method)",
"void SmallKeyboard__iI1Ii_476641288156(SmallKeyboard_o *this, UnityEngine_GameObject_o *go, const MethodInfo *method",
"SmallKeyboard__oO0oOo0(SmallKeyboard_o *this, const MethodInfo *method)",
"SmallKeyboard__oO0oOoO(SmallKeyboard_o *this, const MethodInfo *method)",
"SmallKeyboard___c__DisplayClass14_0___ctor(SmallKeyboard___c__DisplayClass14_0_o *this, const MethodInfo *method)",
"SmallKeyboard___c__DisplayClass14_0___Start_b__0(SmallKeyboard___c__DisplayClass14_0_o *this, const MethodInfo *method)",
"SmallKeyboard_iII1i___ctor(SmallKeyboard_iII1i_o *this, const MethodInfo *method)"
]
for (let i = 0; i < addr.length; i++) {
console.log("hook " + name[i]);
Interceptor.attach(lib.base.add(addr[i]).sub(offset), {
onEnter: function () {
console.log("called " + name[i]);
}
})
}
Interceptor.attach(lib.base.add(0x0000006EFA4839C4).sub(offset), {
onLeave(retval) {
console.log(retval);
},
})
}, 3000);



这里有间接跳转,可以把so的data段改成只读,这样bn会自动计算部分跳转地址,当然还是不怎么可看,最好结合trace分析控制流

CRC校验绕过

我们hook sub_3b8cc这个函数时会发现app会跳出,可能是crc校验检测到了inline hook,去hook open,openat等都没有发现相关调用,感觉可能是使用svc来避免相关函数被frida hook,使用stackplz对内核跟踪后发现调用了openat

这个时候根据调用栈去找检测的位置,基本上每个调用下面都紧跟一个cmp,我们hook 参数看看是否像是在判断检测的最终结果,不断排查可以往前跳到sub_353e0这个函数

可以看到这个sleep其实就已经很说明问题了,因为app不是马上跳出,而是过一会再跳出的
这个时候我们把0x3649c这个位置的w0给hook成0,app就不跳出了,说明找对位置了,继续分析加密

加密1

第一处加密的参数是输入的高32位,点进去看看像是对每个字节分别做了两次计算


可以hook下验证下想法

就是些计算,hook下看下具体计算流程,然后就可以写解密了

1
2
3
4
5
6
7
8
9
10
11
# enc = [0x17, 0x6d, 0x22, 0x89]
enc = [0x89, 0x22, 0x6d, 0x17]

enc[0] = enc[0]+0x1c
enc[1] = (enc[1]+8) ^ 0xd3 ^ 1
enc[2] = ((enc[2]+16)+0x5e) ^ 2
enc[3] = (enc[3]+24) ^ 0x86 ^ 3

for i in range(0, 4):
print(hex(enc[i] & 0xFF))


然后是一个byteswap宏,就是把加密完的部分的字节序反过来

加密2


在下面是sub_3a924是调用native调用dex加密,hook GetStaticMethodID这个函数去找加密函数在dex中的位置

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
/*
如何使用
这个gadget通过hook libart.so中的getStaticMethodID函数来获取so层获取的dex函数信息
一般直接运行即可,注意本gadget依赖libart.so的加载
配套python解析message的函数
def handleGetStaticMethodID(message):
print("----------------------------------------")
print("JAVA method name: {}".format(message['payload'][1]))
print("JAVA method signature: {}".format(message['payload'][2]))
print("Which file get It: {}".format(message['payload'][3]))


*/
function hookJNIgetStaticMethodID() {
console.log("hook JNIgetStaticMethodID");
let symbols = Module.load("libart.so").enumerateSymbols();
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("GetStaticMethodID") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
console.log(symbol.name);
Interceptor.attach(symbol.address, {
onEnter: function (args) {
var Name = args[2].readUtf8String();
var sig = args[3].readUtf8String();
let whoCallIt = DebugSymbol.fromAddress(this.returnAddress).toString();
send(["getStaticMethodID", Name, sig, whoCallIt]);
}
})
}
}
}
setTimeout(hookJNIgetStaticMethodID, 3000);

用frida-dexdump把dex dump下来,然后可以都扔进jeb里了,加密部分加了平坦化混淆,jeb效果好一点

就是循环右移后进行异或和加法,可以写解密了

1
2
3
4
5
6
7
8
9
10
11
12
13
key = [50, -51, -1, -104, 25, -78, 0x7C, -102]
b1 = [0xf8, 0xd0, 0x17, 0x96]
b1 = list(reversed(b1))
for i in range(0, 4):
b1[i] = (b1[i]-i) & 0xFF
b1[i] = (b1[i] ^ key[i]) & 0xFF
v = (b1[0] << 24) | (b1[1] << 16) | (b1[2] << 8) | b1[3]
v = v & 0xFFFFFFFF
v1 = ((v << 7) | (v >> 25)) & 0xFFFFFFFF
print(hex(v1))
# ret: 6d98b6d2
# [dexcall] f8d01796

低32位

注意虽然反编译没有直接显示低32位被扔进了enc1里,但其实低32位也经历了一次enc1,应该是类型识别导致的问题
然后是把低32位字节序反过来,再一样扔进那个dexcall里
最后是把高32位放在低位,低32位放在高位拼起来返回

跨文件跳转


看到这里还根据一个全局导出变量做了跳转,但是静态不好算这个变量,hook一下看看位置

1
2
3
4
5
Interceptor.attach(lib.base.add(0x311a0), {
onEnter: function () {
console.log(DebugSymbol.fromAddress(this.context.x2));
}
})


跳回libil2cpp了,和基址算一下偏移再去找跳到哪了

再跳过去

发现其实就是之前跳到sec2023.so的加密的下面
也就是说加密完的64位数被弄到这个函数里处理了

il2cpp中的加密


前面搞了一大坨初始化,传入输入也被分为高低各两份放进了v21这个结构体里,跟踪这个结构体,最后是走到了一个vm加密里,至于为什么断定这是vm因为里面进去就是一个大while(1),而且看着也不像其他加密

vm里面这一大坨重点关注那个get_Item的库函数,hook他的第二个参数看看下标都是什么

看到种类不多,接下来该去找handle的实现了

1
2
3
4
5
6
Interceptor.attach(ll2.base.add(0x46AE1C), {
onEnter: function () {
console.log("[handle]", ptr(this.context.x9).sub(ll2.base).add(0x6D54207000))
}
})

这里简单trace下拿下handle的函数地址

vm分析

handle里面有些和数值计算相关的地方用了mba算式混淆,ida9.0内置了解mba混淆的插件

拿完handle后一个个写trace,把指令和参数拿下来

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
function main() {
var ll2 = Process.getModuleByName("libil2cpp.so");

var idabase = 0x6D54207000;
var basePtr = ll2.base.sub(idabase);
Interceptor.attach(basePtr.add(0x6D5466CBD0), {
onEnter() {
console.log("low32 of input: ", this.context.x0);
}
})
Interceptor.attach(basePtr.add(0x6D5466CBF4), {
onEnter() {
console.log("hig32 of input: ", this.context.x0);
}
})
Interceptor.attach(basePtr.add(0x6D5466CC3C), {
onEnter() {
console.log("ret: ", this.context.x21, this.context.x22);
}
})
Interceptor.attach(basePtr.add(0x6D546725CC), {
// pushU16toStack
onEnter() {
console.log(`stack[${this.context.x10}] = ${this.context.x8}`);
}
})
var heapIdx = 0, stackIdx = 0;
// popStacktoHeap
Interceptor.attach(basePtr.add(0x6D54672548), {
onEnter() {
stackIdx = this.context.x9;
heapIdx = this.context.x8;
let value = ptr(this.context.x10).add(this.context.x9 * 4 + 0x20).readU32();
console.log(`heap[${heapIdx}] = stack[${stackIdx}] // stack[${stackIdx}] = ${value}`)
}
})
// pushHeaptoStack
Interceptor.attach(basePtr.add(0x6D54672438), {
onEnter() {
heapIdx = this.context.x8;
}
})
Interceptor.attach(basePtr.add(0x6D54672460), {
onEnter() {
stackIdx = this.context.x10;
console.log(`stack[${stackIdx}] = heap[${heapIdx}] // heap[${heapIdx}] = ${this.context.x8}`)
}
})
// shiftR
Interceptor.attach(basePtr.add(0x6D54672088), {
onEnter() {
console.log(`push ${this.context.x10} >> ${this.context.x11} to stack // ${this.context.x10 >> this.context.x11}`)
}
})
// and
Interceptor.attach(basePtr.add(0x6D54672104), {
onEnter() {
console.log(`push ${this.context.x10} & ${this.context.x11} to stack // ${this.context.x10 & this.context.x11}`)
}
})
// sub
Interceptor.attach(basePtr.add(0x6D54671F14), {
onEnter() {
console.log(`push ${this.context.x10} - ${this.context.x11} to stack // ${this.context.x10 - this.context.x11}`)
}
})
// less
Interceptor.attach(basePtr.add(0x6D5467220C), {
onEnter() {
console.log(`push ${this.context.x10} < ${this.context.x11} to stack // ${this.context.x10 < this.context.x11}`)
}
})
// xor
Interceptor.attach(basePtr.add(0x6D54672180), {
onEnter() {
console.log(`push ${this.context.x10} ^ ${this.context.x12} to stack // ${this.context.x10 ^ this.context.x12}`)
}
})
// loadi32toStack
Interceptor.attach(basePtr.add(0x6D546723D8), {
onEnter() {
heapIdx = this.context.x10;
let value = ptr(this.context.x11).add(this.context.x10 * 4 + 0x20).readU32();
console.log(`load heap[${heapIdx}] to stack // heap[${heapIdx}] = ${value}`)
}
})
// shiftL
Interceptor.attach(basePtr.add(0x6D54672008), {
onEnter() {
console.log(`push ${this.context.x10} << ${this.context.x11} to stack // ${this.context.x10 << this.context.x11}`)
}
})
// add
Interceptor.attach(basePtr.add(0x6D54671E98), {
onEnter() {
console.log(`push ${this.context.x10} + ${this.context.x11} to stack // ${this.context.x10 + this.context.x11}`)
}
})
Interceptor.attach(basePtr.add(0x6D5467232C), {
onEnter() {
console.log("jmp if zero");
}
})
Interceptor.attach(basePtr.add(0x6D546722C4), {
onEnter() {
console.log("jmp if not zero");
}
})
}

setTimeout(main, 1000);

这里注意有有几条指令(比如load i32 to stack) 执行操作的位置离ret非常近,如果frida inline hook的点选的不好会导致程序直接崩溃,因为注入点没有足够的空间容纳跳转指令


trace完大概长这样,大约350条

然后就可以手撕了,能复原出来加密算法如下

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
low32 = 0x038217c2
hig32 = 0xae869c8a
a1 = [0, 0, 0, 0]
for i in range(24, -1, -8):
a1[i//8] = (low32 >> i) & 0xff
a2 = [0, 0, 0, 0]
a2[0] = a1[0]-0x1b
a2[1] = a1[1] ^ 0xc2
a2[2] = a1[2]+0xa8
a2[3] = a2[3] ^ 0x36
res1 = 0
for i in range(0, 25, 8):
res1 |= (((a2[i//8] ^ i) & 0xFF) << i)

a3 = [0, 0, 0, 0]
for i in range(24, -1, -8):
a3[i//8] = (hig32 >> i) & 0xff

a4 = [0, 0, 0, 0]
a4[0] = a3[0]-0x2f
a4[1] = a3[1] ^ 0xb6
a4[2] = a3[2]+0x37
a4[3] = a3[3] ^ 0x98

res2 = 0
for i in range(0, 25, 8):
res2 |= (((a4[i//8] + i) & 0xFF) << i)

然后解密如下

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
def decodeVM(ret1, ret2):
a1 = [0, 0, 0, 0]
a2 = [0, 0, 0, 0]
a3 = [0, 0, 0, 0]
a4 = [0, 0, 0, 0]
for i in range(0, 25, 8):
a4[i//8] = (ret2 >> i) & 0xff
a4[i//8] = a4[i//8]-i
a3[0] = a4[0]+0x2f
a3[1] = a4[1] ^ 0xb6
a3[2] = a4[2]-0x37
a3[3] = a4[3] ^ 0x98
for i in range(0, 25, 8):
a2[i//8] = (ret1 >> i) & 0xff
a2[i//8] = a2[i//8] ^ i
a1[0] = a2[0]+0x1b
a1[1] = a2[1] ^ 0xc2
a1[2] = a2[2]-0xa8
a1[3] = a2[3] ^ 0x36
res1 = 0
res2 = 0
for i in range(0, 25, 8):
res1 |= (((a1[i//8] & 0xFF) << i))
res2 |= (((a3[i//8] & 0xFF) << i))
print("low", hex(res1))
print("hig", hex(res2))
return res1, res2

xtea

魔改了一个xtea,这个随便逆下就行了,xtea的解密没什么好说的,注意用了两个sum以及sum的初始值
key就动调拿一下就行

1
2
3
4
5
6
7
Interceptor.attach(ll2.base.add(0x465C60), {
onEnter() {
for (let i = 0; i < 12; i++) {
console.log(`key[${i}]: `, this.context.x0.add(i * 4).readU32().toString(16));
}
}
})
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
void xteaDecrypt(uint32_t *v, uint32_t *k)
{
uint32_t sum = 0, v0 = v[0], v1 = v[1];
uint32_t sum1 = -1091584273;
uint32_t sum2 = -1650623010;
uint32_t delta = 559038737;
sum1 -= 64 * delta;
sum2 -= 64 * delta;
sum = delta * 32;
uint32_t ks2 = (sum2 >> 13) & 3;
for (int i = 0; i < 64; i++)
{
sum2 += delta;
v1 -= (sum2 + k[(sum2 >> 13) & 3]) ^ (((v0 << 8) ^ (v0 >> 7)) - v0);
sum1 += delta;
v0 -= (sum1 - k[sum1 & 3]) ^ (((v1 << 7) ^ (v1 >> 8)) + v1);
}
v[0] = v0;
v[1] = v1;
}
int main()
{
uint32_t key[4] = {0x7b777c63, 0xc56f6bf2, 0x2b670130, 0x76abd7fe};
uint32_t plaintext[2] = {71807475, 0}; // {token , 0}
xteaDecrypt((uint32_t *)plaintext, key);
printf("%08X %08X", plaintext[0], plaintext[1]);
return 0;
}

总结解密

输入转成64位整数先跳到libsec2023.so中分高低两份分别进行简单计算加密和字节翻转和dex中的混淆加密,然后高低两份调换位置重新拼成64位传入libil2cpp.so中传入一个vm算法中加密,然后结果再传入xtea中加密,加密完后高位部分和0比较,低位部分和token比较

把上面的解密拼一下

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
71
72
73
74
75
76
77
78
79
80
81
def decodeVM(ret1, ret2):
a1 = [0, 0, 0, 0]
a2 = [0, 0, 0, 0]
a3 = [0, 0, 0, 0]
a4 = [0, 0, 0, 0]
for i in range(0, 25, 8):
a4[i//8] = (ret2 >> i) & 0xff
a4[i//8] = a4[i//8]-i
a3[0] = a4[0]+0x2f
a3[1] = a4[1] ^ 0xb6
a3[2] = a4[2]-0x37
a3[3] = a4[3] ^ 0x98
for i in range(0, 25, 8):
a2[i//8] = (ret1 >> i) & 0xff
a2[i//8] = a2[i//8] ^ i
a1[0] = a2[0]+0x1b
a1[1] = a2[1] ^ 0xc2
a1[2] = a2[2]-0xa8
a1[3] = a2[3] ^ 0x36
res1 = 0
res2 = 0
for i in range(0, 25, 8):
res1 |= (((a1[i//8] & 0xFF) << i))
res2 |= (((a3[i//8] & 0xFF) << i))
print("low", hex(res1))
print("hig", hex(res2))
return res1, res2


def decode1(enc):
print(hex(enc))
enc = [(enc >> 24) & 0xFF, (enc >> 16) &
0xFF, (enc >> 8) & 0xFF, enc & 0xFF]
print(list(map(hex, enc)))
# 65b0c4b07ac8b1d9
enc[0] = enc[0]+0x1c
enc[1] = (enc[1]+8) ^ 0xd3 ^ 1
enc[2] = ((enc[2]+16)+0x5e) ^ 2
enc[3] = (enc[3]+24) ^ 0x86 ^ 3

print(hex((enc[0] << 24) | (enc[1] << 16) | (enc[2] << 8) | enc[3]))
return (enc[0] << 24) | (enc[1] << 16) | (enc[2] << 8) | enc[3]


def decodeDex(enc):
key = [50, -51, -1, -104, 25, -78, 0x7C, -102]
b1 = [(enc >> 24) & 0xFF, (enc >> 16) &
0xFF, (enc >> 8) & 0xFF, enc & 0xFF]
print(enc)
print(list(map(hex, b1)))
# b1 = [0x0b, 0x9e, 0x7f, 0xc7]
b1 = list(reversed(b1))
for i in range(0, 4):
b1[i] = (b1[i]-i) & 0xFF
b1[i] = (b1[i] ^ key[i]) & 0xFF
v = (b1[0] << 24) | (b1[1] << 16) | (b1[2] << 8) | b1[3]
v = v & 0xFFFFFFFF
v1 = ((v << 7) | (v >> 25)) & 0xFFFFFFFF
print(hex(v1))
return v1


def byteswap(x):
return ((x & 0x000000FF) << 24) | ((x & 0x0000FF00) << 8) | ((x & 0x00FF0000) >> 8) | ((x & 0xFF000000) >> 24)


low32 = 0x7CD0D74B
hig32 = 0xEB77B65B
low32, hig32 = decodeVM(low32, hig32)
low32, hig32 = hig32, low32
low32 = decodeDex(low32)
low32 = byteswap(low32)
low32 = decode1(low32)
print("low32:", hex(low32))
hig32 = decodeDex(hig32)
hig32 = byteswap(hig32)
hig32 = decode1(hig32)
print("hig32:", hex(hig32))
print(hex((hig32 << 32) | low32))
print((byteswap(hig32) << 32) | byteswap(low32))

把xtea中解出来的部分分别扔进low32,hig32里,然后跑就行了

Author

SGSG

Posted on

2025-04-18

Updated on

2025-05-12

Licensed under