世界征服者3逆向分析

网上几乎都是只有对该游戏勋章等逻辑的修改,没找到过对游戏局内资源的相关分析,所以就来分析一下吧

布豪,我的CE怎么失灵了!

我们首先使用CE桥接我们的Android设备搜索数据,根据对游戏玩法的了解,这里的数据不可能出现浮点数的情况,所以我们直接4字节搜索即可,多次筛选搜索到我们的金币数量,发现直接修改只能修改显示效果,并不能修改实际读取的数据

image-20251116141512000

这里尝试使用CE修改器修改依旧无济于事,就算显示99999但是依旧买不了东西,也尝试过使用GG修改器等工具,但是修改之后只有显示效果,刷新之后我们的资源数据依旧会便会修改之前的值

image-20251116215012255

我们可以选择使用数据的变化来筛选真实数据,但是这样的话这篇文章就太水了,而且并不是所有的游戏的加密数据都可以通过是否变化来确认的

内存断点——谁动了我的蛋糕

这里尝试过一些提供了断点功能的修改器,但是效果都不尽人意,还得是CE更好用

这里直接修改数据只是起显示作用,如果尝试造其他东西点击是无效的,而且下一轮更新之后金币会变回去,我们选择对这里的数据下一个写入断点,因为写入的次数一般会远小于访问次数,而且更接近代码的关键位置,所以相比访问断点,写入断点会让我们的分析更加简单一些

根据访问断点找到是由"libworld-conqueror-3.so"+189927(hex地址)这个地址写入的

image-20251116141654622

我们将唯一一个32位arm的so拖入IDA分析,这里很明显是根据前面的判断来决定是否改写内存地址

image-20251116141856937

我们发现是否赋值与R0 + 0xCR0 + 0x10有关

我们使用Frida进行inlineHook,这样更精准一点,方便我们查看数据的具体情况,这里需要注意这里是Thumb指令集,我们Hook的地址需要 + 1,否则会因为错误指令替换直接让游戏崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Hook_Native1() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
Interceptor.attach(base.add(0x18991C + 1), { // Thumb指令集需要+1
onEnter: function (args) {
console.log("======== Register ========");
console.log("R0: " + this.context.r0);
console.log("R4: " + this.context.r4);
console.log("======== var ========");
var p_r0_c = ptr(this.context.r0).add(0xc);
console.log("word__r0 + 0xc: " + p_r0_c.readU16());
var p_r0_10 = ptr(this.context.r0).add(0x10);
console.log("word__r0 + 0x10: " + p_r0_10.readU32());
console.log("======== Memory ========")
console.log(hexdump(ptr(this.context.r0), { length: 0x20, header: true, ansi: false }));

},
onLeave: function (retval) {

}
});
}
setTimeout(Hook_Native1, 1000);

我们当前金币是318,钢铁是245,我们制造花费120金币,50钢铁的步兵炮,我们可以在其中根据r0 + 0x10的数据找到对金币进行读写的点

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
======== Register ========
R0: 0xcc39da98
R4: 0x956
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 2389
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc39da98 bc da 39 cc 88 9d 58 22 00 00 00 00 00 00 04 00 ..9...X"........
cc39daa8 55 09 00 00 00 00 00 00 08 00 00 00 08 00 00 00 U...............

# 涉及金币的调用
======== Register ========
R0: 0xcc39dcd8
R4: 0xc6
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 318
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc39dcd8 fc dc 39 cc 88 50 38 0a 00 00 00 00 00 00 04 00 ..9..P8.........
cc39dce8 3e 01 00 00 00 00 00 00 0b 00 00 00 0b 00 00 00 >...............

# 涉及钢铁的调用
======== Register ========
R0: 0xcc39dd38
R4: 0xc3
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 245
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc39dd38 5c dd 39 cc 17 bf 33 74 00 00 00 00 00 00 04 00 \.9...3t........
cc39dd48 f5 00 00 00 00 00 00 00 0b 00 00 00 0b 00 00 00 ................


======== Register ========
R0: 0xcc39da98
R4: 0x957
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 2390
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc39da98 bc da 39 cc 88 9d 58 22 00 00 00 00 00 00 04 00 ..9...X"........
cc39daa8 56 09 00 00 00 00 00 00 08 00 00 00 08 00 00 00 V...............

根据数据我们首先对内存数据做一定的猜测

1
2
3
4
5
6
======== Register ========
R0: object address
R4: state code
======== var ========
word__r0 + 0xc: ???
word__r0 + 0x10: option data

在非购买状态下该函数的数据在持续递增,在购买的时候又会和我们的资源数量有关,而且不同状态下使用的R0又不相同。所以可能不是对同一个玩家对象的操作,所以这很有可能是一个公用的数据处理函数,我们需要寻找上层的调用

我们尝试打印调用栈(这里如果在函数头部和改写数据处下两个inline Hook会导致0x18991C 的原始指令被第一个 hook 破坏,导致崩溃,所以直接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
function Hook_Native1() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
Interceptor.attach(base.add(0x189914 + 1), { // Thumb指令集需要+1
onEnter: function (args) {
console.log('========called from========\n' +
Thread.backtrace(this.context, Backtracer.FUZZY)
.map(DebugSymbol.fromAddress).join('\n') + '\n');

},
onLeave: function (retval) {
console.log("======== Register ========");
console.log("R0: " + this.context.r0);
console.log("R2: " + this.context.r2);
console.log("======== var ========");
var p_r0_c = ptr(this.context.r0).add(0xc);
console.log("word__r0 + 0xc: " + p_r0_c.readU16());
var p_r0_10 = ptr(this.context.r0).add(0x10);
console.log("word__r0 + 0x10: " + p_r0_10.readU32());
console.log("======== Memory ========")
console.log(hexdump(ptr(this.context.r0), { length: 0x20, header: true, ansi: false }));
}
});

}
setTimeout(Hook_Native1, 1000);

我们再次尝试购买物品打印堆栈

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
# 这里应该是时间或者显示的调用,不涉及核心资源,我们无需关心
========called from========
0xce2e41c5 libworld-conqueror-3.so!0xcc1c5
0xce2d9a34 libworld-conqueror-3.so!0xc1a34
0xce2e871c libworld-conqueror-3.so!0xd071c
0xce2e2918 libworld-conqueror-3.so!0xca918
0xce2e2cec libworld-conqueror-3.so!0xcacec
0xce2d2bb8 libworld-conqueror-3.so!Java_com_easytech_wc3_ecRender_nativeRender+0x43
0xce71e148 base.odex!com.android.billingclient.api.zzaj.nativeOnBillingServiceDisconnected [DEDUPED]+0x5f
0xce73da6c base.odex!com.easytech.wc3.ecRender.onDrawFrame+0x43
0x75328184 boot-framework.oat!android.opengl.GLSurfaceView$GLThread.guardedRun+0xe13
0x753296e0 boot-framework.oat!android.opengl.GLSurfaceView$GLThread.run+0xb7
0xecebb292 libc.so!je_arena_tdata_get_hard+0xe9
0xec5d4976 libart.so!art_quick_invoke_stub_internal+0x45
0xec5adad4 libart.so!art_quick_invoke_stub+0xe3
0xeceaa690 libc.so!je_arena_tcache_fill_small+0x257
0xec262c68 libart.so!_ZN3art9ArtMethod6InvokeEPNS_6ThreadEPjjPNS_6JValueEPKc+0x8b
0xec50ab80 libart.so!_ZN3art12_GLOBAL__N_118InvokeWithArgArrayERKNS_33ScopedObjectAccessAlreadyRunnableEPNS_9ArtMethodEPNS0_8ArgArrayEPNS_6JValueEPKc+0x37

======== Register ========
R0: 0xe6691ab8
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 4550
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e6691ab8 dc 1a 69 e6 88 9d 58 22 00 00 00 00 00 00 04 00 ..i...X"........
e6691ac8 c6 11 00 00 00 00 00 00 08 00 00 00 08 00 00 00 ................

# 下方是金币和钢铁逻辑的调用信息
========called from========
0xce2e41c5 libworld-conqueror-3.so!0xcc1c5
0xce2e3cfa libworld-conqueror-3.so!0xcbcfa
0xce3551b4 libworld-conqueror-3.so!0x13d1b4
0xce355db2 libworld-conqueror-3.so!0x13ddb2
0xce2ef470 libworld-conqueror-3.so!0xd7470
0xce354cda libworld-conqueror-3.so!0x13ccda
0xce3562f4 libworld-conqueror-3.so!0x13e2f4
0xce326460 libworld-conqueror-3.so!0x10e460
0xce33a2e0 libworld-conqueror-3.so!0x1222e0
0xce2e6e3e libworld-conqueror-3.so!0xcee3e
0xce30865a libworld-conqueror-3.so!0xf065a
0xce2e9842 libworld-conqueror-3.so!0xd1842
0xec46ca78 libart.so!_ZN3artL12FindMethodIDERNS_18ScopedObjectAccessEP7_jclassPKcS5_b+0xb3
0xec44cba0 libart.so!_ZN3art3JNI20CallStaticIntMethodVEP7_JNIEnvP7_jclassP10_jmethodIDSt9__va_list+0x1bf
0xec46c808 libart.so!_ZN3art9JNIEnvExt17AddLocalReferenceIP11_jthrowableEET_NS_6ObjPtrINS_6mirror6ObjectEEE+0x23
0xec446c7c libart.so!_ZN3art3JNI17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS6_+0x1cf

======== Register ========
R0: 0xe6691cf8
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 58
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e6691cf8 1c 1d 69 e6 88 50 38 0a 00 00 00 00 00 00 04 00 ..i..P8.........
e6691d08 3a 00 00 00 00 00 00 00 0b 00 00 00 0b 00 00 00 :...............
========called from========
0xce2e41c5 libworld-conqueror-3.so!0xcc1c5
0xce2e3cfa libworld-conqueror-3.so!0xcbcfa
0xce3551be libworld-conqueror-3.so!0x13d1be
0xce355db2 libworld-conqueror-3.so!0x13ddb2
0xce2ef470 libworld-conqueror-3.so!0xd7470
0xce354cda libworld-conqueror-3.so!0x13ccda
0xce3562f4 libworld-conqueror-3.so!0x13e2f4
0xce326460 libworld-conqueror-3.so!0x10e460
0xce33a2e0 libworld-conqueror-3.so!0x1222e0
0xce2e6e3e libworld-conqueror-3.so!0xcee3e
0xce30865a libworld-conqueror-3.so!0xf065a
0xce2e9842 libworld-conqueror-3.so!0xd1842
0xec46ca78 libart.so!_ZN3artL12FindMethodIDERNS_18ScopedObjectAccessEP7_jclassPKcS5_b+0xb3
0xec44cba0 libart.so!_ZN3art3JNI20CallStaticIntMethodVEP7_JNIEnvP7_jclassP10_jmethodIDSt9__va_list+0x1bf
0xec46c808 libart.so!_ZN3art9JNIEnvExt17AddLocalReferenceIP11_jthrowableEET_NS_6ObjPtrINS_6mirror6ObjectEEE+0x23
0xec446c7c libart.so!_ZN3art3JNI17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS6_+0x1cf

======== Register ========
R0: 0xe6691d58
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
word__r0 + 0x10: 160
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e6691d58 7c 1d 69 e6 17 bf 33 74 00 00 00 00 00 00 04 00 |.i...3t........
e6691d68 a0 00 00 00 00 00 00 00 0b 00 00 00 0b 00 00 00 ................

so层分析——抽丝剥茧

分析堆栈数据我们发现资源调用和显示调用上层都在0xcc1c5这个地址,再上一层开始出现了分歧

  • 可能是计时调用:0xc1a34
  • 资源变化调用:0xcbcfa

但是我们跳转到0xcbcfa什么都没有发现,这几个函数也没找到我们感兴趣的信息

image-20251116163302506

我们尝试去到调用栈的更上一层0x13d1b4

image-20251116163420554

我们尝试进入到这里的3个函数发现了很有意思的东西(其实这里已经可以带着修改器直接搜索修改加密数据了)

image-20251116163541891image-20251116163555553

image-20251116163612101

后面分析的时候回来F5刷新了一下变成这样了……但是没什么大的影响,后面Hook的时候就明白这里数据的意义了

image-20251116192942629

因为三个函数结构几乎一模一样,使用我们以money的函数为例进行简单的猜测

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall sub_13C9B8(int result, int a2)
{
int v3; // r0

*(_DWORD *)(result + 36) = a2 ^ 0x7EAD3; // 对数据进行解密 or 加密
if ( !*(_BYTE *)(result + 104) )
{
v3 = sub_CCD74(); // 获取object
return sub_CC194(v3, "BattleMoney", a2); // 传入数据和操作进行运算
}
return result;
}

然后我们进入sub_CC194函数,在这里我们找到了我们之前下断点分析到的0x18991C函数

image-20251116171125099

这时我们知道了我们最初分析的函数的参数意义,它的作用就是将数据回填到object中

1
sub_189914(object, (int)resource_string, (int)data);

我们就可以借助frida快速分析我们前面Hook的数据的实际意义了,我们这时就已经不需要再打印调用栈了,只需要输出我们的cstring即可解析每一次操作的实际意义

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
function Hook_Native1() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
Interceptor.attach(base.add(0x189914 + 1), { // Thumb指令集需要+1
onEnter: function (args) {
console.log("======== string data ========");
// read r1 cstring
console.log("r1 cstring: " + ptr(this.context.r1).readCString());

},
onLeave: function (retval) {
console.log("======== Register ========");
console.log("R0: " + this.context.r0);
console.log("R2: " + this.context.r2);
console.log("======== var ========");
var p_r0_c = ptr(this.context.r0).add(0xc);
console.log("word__r0 + 0xc: " + p_r0_c.readU16());
var p_r0_10 = ptr(this.context.r0).add(0x10);
console.log("dword__r0 + 0x10: " + p_r0_10.readU32());
console.log("======== Memory ========")
console.log(hexdump(ptr(this.context.r0), { length: 0x20, header: true, ansi: false }));
}
});

}
setTimeout(Hook_Native1, 1000);

根据输出的数据,很明显我们就可以看出来我们之前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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
======== string data ========
r1 cstring: GameDate
======== Register ========
R0: 0xcc1838e8
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
dword__r0 + 0x10: 202
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc1838e8 0c 39 18 cc 88 9d 58 22 00 00 00 00 00 00 04 00 .9....X"........
cc1838f8 ca 00 00 00 00 00 00 00 08 00 00 00 08 00 00 00 ................


======== string data ========
r1 cstring: BattleMoney
======== Register ========
R0: 0xcc183b28
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
dword__r0 + 0x10: 10
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc183b28 4c 3b 18 cc 88 50 38 0a 00 00 00 00 00 00 04 00 L;...P8.........
cc183b38 0a 00 00 00 00 00 00 00 0b 00 00 00 0b 00 00 00 ................


======== string data ========
r1 cstring: BattleSteel
======== Register ========
R0: 0xcc183b88
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
dword__r0 + 0x10: 80
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc183b88 ac 3b 18 cc 17 bf 33 74 00 00 00 00 00 00 04 00 .;....3t........
cc183b98 50 00 00 00 00 00 00 00 0b 00 00 00 0b 00 00 00 P...............


======== string data ========
r1 cstring: GameDate
======== Register ========
R0: 0xcc1838e8
R2: 0x3f
======== var ========
word__r0 + 0xc: 0
dword__r0 + 0x10: 203
======== Memory ========
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
cc1838e8 0c 39 18 cc 88 9d 58 22 00 00 00 00 00 00 04 00 .9....X"........
cc1838f8 cb 00 00 00 00 00 00 00 08 00 00 00 08 00 00 00 ................

所以这里基本就清晰了,这里类似解析object获取field的操作来根据操作的资源类型获取其字段,而字段偏移0x10就是存储的数据

image-20251116185159815

所以我们就可以根据分析的结果写出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
function Hook_Native1() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x189914 + 1), { // Thumb指令集需要+1
onEnter: function (args) {
console.log("======== data string ========");
console.log("r1 cstring: " + ptr(this.context.r1).readCString());
if (ptr(this.context.r1).readCString() == "BattleOil") {
resource = "BattleOil";
}
else if (ptr(this.context.r1).readCString() == "BattleSteel") {
resource = "BattleSteel";
}
else if (ptr(this.context.r1).readCString() == "BattleMoney") {
resource = "BattleMoney";
}

},
onLeave: function (retval) {
console.log("======== Register ========");
console.log("R0: " + this.context.r0);
console.log("R2: " + this.context.r2);
console.log("======== var ========");
var p_r0_c = ptr(this.context.r0).add(0xc);
console.log("word__r0 + 0xc: " + p_r0_c.readU16());
var p_r0_10 = ptr(this.context.r0).add(0x10);
console.log("dword__r0 + 0x10: " + p_r0_10.readU32());
console.log("======== Memory ========")
console.log(hexdump(ptr(this.context.r0), { length: 0x20, header: true, ansi: false }));
if (resource != null) {
p_r0_10.writeU32(0x1000);
console.log("Modified " + resource + " to 0x1000");
resource = null;
}
}
});

}
setTimeout(Hook_Native1, 1000);

Hook修改之后虽然显示确实为4096,可是依旧只有显示效果,无法实际修改真实数据,下一轮游戏或者实际购买的时候资源数量又会变回修改之前的数据

image-20251116190804671

再探so层——什么叫我改的数据是假的

这时候我们仔细思考前面我们遇到了一个加密数据的解密操作,而且我们下的断点本来就是显示数据的写入断点,而非实际数据的写入断点

image-20251116191058165

但是我们不必沮丧,因为它如果想要显示这个数据,那这个数据一定有真实来源,这时候我们想到数据解密的上一层就是分别对我们的资源进行操作

image-20251116191526026

我们向上可以找到这里对v10的初始化

image-20251116194220294

我们更换一下脚本进行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
function Hook_Native2() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x0D743A + 1), { // Thumb指令集需要+1
onEnter: function (args) {


},
onLeave: function (retval) {

var p_r0_c = ptr(this.context.r0).add(0x40);
console.log("dword__r0 + 0x40: " + p_r0_c.readU32());

}
});

}
function Hook() {
// Hook_Native1();
Hook_Native2();
}

setTimeout(Hook, 1000);
1
2
3
4
5
6
7
8
9
base:0xcddd8000
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80
dword__r0 + 0x40: 80

我们发现前面的猜想的确存在问题,这里的参数2是我们花费的价格

这里我们如果查看交叉引用会有很多地方对这三个函数进行的调用,但是这里我们在修改数据的时候根据堆栈追上来的,所以只有这里才进行了实际的数据写操作

所以我们只需要顺着这一条调用链一直向上追踪,大概率是可以找到对实际数据的操作的

因为我们是下写入断点追上来的,所以其它处的调用我们不需要关心,这样只会让我们的分析难度倍增

原来的函数调用:

1
2
3
sub_13C9F0(object, -price[16]);
sub_13CA5C(object, -price[17]);
sub_13CAC8(object, -price[18]);

那我们只要将花费的价格修改为相反数,我们将程序patch为这样,将取反操作修改为NOP指令即可验证这里的是否能实际修改我们内存中的数据:

image-20251116200623112

实际尝试后发现这里的确有效果,然后就是……不要好的不要对的,只要贵的!

image-20251116201703242

Frida解析——兄弟,看看你的对象

我们发现这里的确是对我们的数据进行了实际的修改的函数调用,那么我们的Hook为什么还是只能改变显示数据,而不能修改实际数据呢?

我们向内部寻找,发现这里它将我们的数据赋值给了object

image-20251116204944910

我们可以尝试一下在它赋值给object之后,传参给sub_CC194的时候修改我们需要支付的金额,看看会不会生效,看看这里的决定性因素是否为object[9]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Hook_Native3() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0xCC194+1), { // Thumb指令集需要+1
onEnter: function (args) {
this.context.r2 = ptr(10000);


},
onLeave: function (retval) {

}
});

}

Hook之后发现这里只能修改显示数值,使用数据之后输入仍然会恢复修改之前的值,所以关键性的数据其实是这里的object[9],它的数据直接导致了我们资源数量的变化,所以这里的object很有可能就是我们真实的object地址,我们可以对它进行修改

但是在我们不知道实际意义的情况下乱改大概率游戏会直接崩溃,我们直接读这个object里面的指针尝试自己解析这个object的实际意义

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

function Hook_Native5() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x13C9B8+1), { // Thumb指令集需要+1
onEnter: function (args) {
console.log("======== r0 struct onEnter ========");
var p_r0 = ptr(this.context.r0).add(0x04);
console.log("dword__r0 + 0x4: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x08);
console.log("dword__r0 + 0x8: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x0c);
console.log("dword__r0 + 0xc: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x10);
console.log("dword__r0 + 0x10: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x14);
console.log("dword__r0 + 0x14: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x18);
console.log("dword__r0 + 0x18: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x1c);
console.log("dword__r0 + 0x1c: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x20);
console.log("dword__r0 + 0x20: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x24);
console.log("dword__r0 + 0x24: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x28);
console.log("dword__r0 + 0x28: " + (p_r0.readU32() ^ 0x7EAD3));

},
onLeave: function (retval) {

console.log("======== r0 struct onLeave ========");
var p_r0 = ptr(this.context.r0).add(0x04);
console.log("dword__r0 + 0x4: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x08);
console.log("dword__r0 + 0x8: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x0c);
console.log("dword__r0 + 0xc: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x10);
console.log("dword__r0 + 0x10: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x14);
console.log("dword__r0 + 0x14: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x18);
console.log("dword__r0 + 0x18: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x1c);
console.log("dword__r0 + 0x1c: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x20);
console.log("dword__r0 + 0x20: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x24);
console.log("dword__r0 + 0x24: " + (p_r0.readU32() ^ 0x7EAD3));
var p_r0 = ptr(this.context.r0).add(0x28);
console.log("dword__r0 + 0x28: " + (p_r0.readU32() ^ 0x7EAD3));
}
});

}

function Hook() {
// Hook_Native1();
// Hook_Native2();
// Hook_Native3();
// Hook_Native4();
Hook_Native5();
}

setTimeout(Hook, 1000);

解析出的数据如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[MI 8::世界征服者3 ]-> base:0xce410000
======== r0 struct onEnter ========
dword__r0 + 0x4: -865798469
dword__r0 + 0x8: 518865
dword__r0 + 0xc: 518866
dword__r0 + 0x10: 518871
dword__r0 + 0x14: 518867
dword__r0 + 0x18: 518867
dword__r0 + 0x1c: -906161933
dword__r0 + 0x20: -910901549
dword__r0 + 0x24: 2346
dword__r0 + 0x28: 970
======== r0 struct onLeave ========
dword__r0 + 0x4: 518907
dword__r0 + 0x8: 518867
dword__r0 + 0xc: 95059
dword__r0 + 0x10: 57875
dword__r0 + 0x14: -865846573
dword__r0 + 0x18: -860606353
dword__r0 + 0x1c: 518867
dword__r0 + 0x20: -1066104750
dword__r0 + 0x24: -1066104750
dword__r0 + 0x28: -914469197

根据我们的观察,dword__r0 + 0x24及其之后的数据和我们的实际资源数量一致,所以我们在最上层这3个函数任意一个设置Hook即可,这里传入的就是真实的object

image-20251116214529190

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Hook_Native5() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x13C9F0+1), { // Thumb指令集需要+1
onEnter: function (args) {



var p_money = ptr(this.context.r0).add(0x24);
p_money.writeU32(0x7EAD3 ^ 6666);
var p_iron = ptr(this.context.r0).add(0x28);
p_iron.writeU32(0x7EAD3 ^ 6666);
var p_oil = ptr(this.context.r0).add(0x2C);
p_oil.writeU32(0x7EAD3 ^ 6666);


},
onLeave: function (retval) {
}
});

}

然后建造任意物品即可,修改真实有效,完美收工!

image-20251116214649446

最后附上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
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
112
113
114
115
116
117
118
119
120
var resource = null;

function Hook_Native1() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
Interceptor.attach(base.add(0x189914 + 1), { // Thumb指令集需要+1
onEnter: function (args) {
console.log("======== data string ========");
console.log("r1 cstring: " + ptr(this.context.r1).readCString());
if (ptr(this.context.r1).readCString() == "BattleOil") {
resource = "BattleOil";
}
else if (ptr(this.context.r1).readCString() == "BattleSteel") {
resource = "BattleSteel";
}
else if (ptr(this.context.r1).readCString() == "BattleMoney") {
resource = "BattleMoney";
}

},
onLeave: function (retval) {
console.log("======== Register ========");
console.log("R0: " + this.context.r0);
console.log("R2: " + this.context.r2);
console.log("======== var ========");
var p_r0_c = ptr(this.context.r0).add(0xc);
console.log("word__r0 + 0xc: " + p_r0_c.readU16());
var p_r0_10 = ptr(this.context.r0).add(0x10);
console.log("dword__r0 + 0x10: " + p_r0_10.readU32());
console.log("======== Memory ========")
console.log(hexdump(ptr(this.context.r0), { length: 0x20, header: true, ansi: false }));
}
});

}

function Hook_Native2() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x0D743A + 1), { // Thumb指令集需要+1
onEnter: function (args) {


},
onLeave: function (retval) {

var p_r0_c = ptr(this.context.r0).add(0x40);
console.log("dword__r0 + 0x40: " + p_r0_c.readU32());

}
});

}
function Hook_Native3() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0xCC194+1), { // Thumb指令集需要+1
onEnter: function (args) {
// 将r2寄存器的值修改为10000
this.context.r2 = ptr(10000);


},
onLeave: function (retval) {

}
});

}
function Hook_Native4() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x13C9B8+1), { // Thumb指令集需要+1
onEnter: function (args) {
// 将r2寄存器的值修改为10000
this.context.r1 = ptr(10000);


},
onLeave: function (retval) {

}
});

}
function Hook_Native5() {
var base = Module.findBaseAddress("libworld-conqueror-3.so");
console.log("base:" + base);
var resource = null;
Interceptor.attach(base.add(0x13C9F0+1), { // Thumb指令集需要+1
onEnter: function (args) {


var p_money = ptr(this.context.r0).add(0x24);
p_money.writeU32(0x7EAD3 ^ 6666);
var p_iron = ptr(this.context.r0).add(0x28);
p_iron.writeU32(0x7EAD3 ^ 6666);
var p_oil = ptr(this.context.r0).add(0x2C);
p_oil.writeU32(0x7EAD3 ^ 6666);


},
onLeave: function (retval) {
}
});

}

function Hook() {
// Hook_Native1();
// Hook_Native2();
// Hook_Native3();
// Hook_Native4();
Hook_Native5();
}

setTimeout(Hook, 1000);

神秘so

The End