0x0 前言
由大佬 esterTion 编写的iOS插件 ArcaeaCountdownScore 用于lowiro
制作的音游Arcaea
。此插件的作用是将游戏分数计算改为倒计分制,即开局满分,产生每个far
或者lost
时分数递减。这种计分制度对我这种菜鸡即为有用,可以更容易的提前许多预估出本局打歌是否可以达到理想的分数并选择继续还是重试。而616截止至目前(写文时3.6.4)都没有推出具有此功能的搭档。
不幸的是,插件在很久前就失效了,表现为什么反应都没有,而鸽王一直都没有更新,我也一直在等待。此时的我并未加入任何音游相关的群组。直至616推出了3.6.0版本更新,此版本中在请求头中新增了X-Random-Challenge
字段来验证请求,与此相关的代码逆出来看着简直像一坨shit。于是我想尝试加入一些音游群组向大佬们取经。
进入一些群组后才发现原来 esterTion
已然弃坑许久(具体原因可参见其B站动态),那么大佬们不再维护ArcaeaCountdownScore
,就只能自己撸起袖子上了。。。
小知识:616在iOS 3.6.0版本请求头中同时也加入了X-Client-Certificate-Subject-CN
以及EnableObfuseCheck
,而安卓则在后续版本中加入
趣事:616在iOS 3.6.0版本中意外(?)打包进了Algo.zip
0x1 方法注入
拿到 ArcaeaCountdownScore 源码后首先看看hook的方法:
ArcUtil_calculateScore = (unsigned long)MSFindSymbol(NULL, "__ZN7ArcUtil14calculateScoreEiiiii");
怪不得没有任何反应了,iOS二进制文件中已经没有了符号,插件无从找起,方法都没有被替换,当然什么反应都没有了。那么当务之急就是找到新的方法位置进行hook。
iOS中没有符号了,那么掏出安卓版3.6.0的二进制想办法对比定位。找到ArcUtil::calculateScore()
,然后按x
寻找其xref
引用,可见一堆调用了calculateScore()
的方法。接下来就是在这些方法中找出可以用于在iOS二进制中搜索的信息,由于我之前就有看过OnlineManager::submitScoreInternal()
方法,并且此方法中也调用了calculateScore()
,所以直接选择从此方法入手。伪代码:
v133 = std::string::append((int)&v550, "&score=", 7u);
v134 = *(_OWORD *)v133;
v553 = *(void **)(v133 + 16);
v552 = v134;
*(_QWORD *)(v133 + 8) = 0LL;
*(_QWORD *)(v133 + 16) = 0LL;
*(_QWORD *)v133 = 0LL;
v135 = (std::__ndk1 *)ArcUtil::calculateScore(
(ArcUtil *)*(unsigned int *)(a4 + 32),
*(_DWORD *)(a4 + 28),
*(_DWORD *)(a4 + 24),
*(_DWORD *)(a4 + 20),
*(_DWORD *)(a4 + 28) + *(_DWORD *)(a4 + 32) + *(_DWORD *)(a4 + 24),
*(_BYTE *)(a4 + 132));
std::to_string(v135, v136);
可以看到调用calculateScore()
方法前有个字符串拼接,其中部分字符串为&score=
,那么就可以在iOS的二进制中搜索此字符串:
找到后查找其引用进入的即为OnlineManager::submitScoreInternal()
方法(如果使用其他引用进行查找发现多个调用,则需要对比找到其在iOS中对应方法),伪代码:
v72 = std::string::append(&v328, "&score=");
v73 = *(_OWORD *)v72;
v331 = *(_QWORD *)(v72 + 16);
v330 = v73;
*(_QWORD *)(v72 + 8) = 0LL;
*(_QWORD *)(v72 + 16) = 0LL;
*(_QWORD *)v72 = 0LL;
v74 = (std::__1 *)sub_10025F358();
std::to_string(v74, v75);
v76 = std::string::append();
则ArcUtil::calculateScore()
在iOS中为位置在base + 0x25F358
,查看该方法的汇编对应二进制:
CMP W2, W4
B.NE loc_10025F370
CBNZ W5, loc_10025F370
MOV W8, #0x989680
5F 00 04 6B A1 00 00 54 85 00 00 35 08 D0 92 52
将原代码中通过符号查找ArcUtil::calculateScore()
修改为通过二进制查找。摘取部分代码,对应 ArcaeaCountdownScore中第173-186行:
uint8_t calculateScore_inst[] = {
0x5f, 0x00, 0x04, 0x6b,
0xa1, 0x00, 0x00, 0x54,
0x85, 0x00, 0x00, 0x35,
0x08, 0xd0, 0x92, 0x52
};
// ------------摘取部分代码-------------
if (instcmp((uint8_t*)ptr, calculateScore_inst, 16)) {
ArcUtil_calculateScore = (unsigned long)ptr;
found_calculateScore = true;
NSLog(@"[arc-score] found calculateScore at %lx", ArcUtil_calculateScore);
}
那么对此方法的寻址就修改完成了,值得一提的是,此方法参数列表末尾新增了一个布尔参数,顺便加上:
int (*orig_ArcUtil_calculateScore)(int lost, int far, int pure, int shiny_pure, int note_count, bool param_6);
int hacked_ArcUtil_calculateScore(int lost, int far, int pure, int shiny_pure, int note_count, bool param_6) {
}
至此,ArcUtil::calculateScore()
的注入修改完成了,接下来就是对ScoreState::init()
,ScoreState::hitNote()
以及ScoreState::missNote()
进行类似的寻找及修改了。
ScoreState::init()
可通过10ScoreState
找到_ZTV10ScoreState DCQ 0
,接着查看其引用发现sub_10013DC88()
,此方法伪代码:
*(_QWORD *)v7 = off_100924018;
*(_QWORD *)(v7 + 36) = 0x42C8000000000000LL;
*(_WORD *)(v7 + 44) = 256;
*(_DWORD *)(v7 + 176) = 0;
*(_BYTE *)(v7 + 180) = 0;
*(_QWORD *)(v7 + 184) = 0LL;
*(_BYTE *)(v7 + 200) = 0;
*(_OWORD *)(v7 + 48) = 0u;
*(_OWORD *)(v7 + 64) = 0u;
*(_QWORD *)(v7 + 92) = 0LL;
*(_QWORD *)(v7 + 84) = 0LL;
*(_QWORD *)(v7 + 124) = 0LL;
*(_QWORD *)(v7 + 116) = 0LL;
*(_BYTE *)(v7 + 132) = 0;
if ( (unsigned int)sub_10022E8B8(v7, a1, a2, v8, a4) ) <---- 这里就是ScoreState::init()
{
sub_10052C380(v7);
}
else
{
(*(void (__fastcall **)(__int64))(*(_QWORD *)v7 + 8LL))(v7);
v7 = 0LL;
}
ScoreState::hitNote()
查找31CharacterAbilityGaugeRateModify
-> _ZTI31CharacterAbilityGaugeRateModify DCQ
。
查看其引用发现sub_10022ED60+A0 ADRL X2, _ZTI31CharacterAbilityGaugeRateModify;
,此方法即为ScoreState::hitNote()
ScoreState::missNote()
查找17LogicLongNoteBase
-> _ZTI17LogicLongNoteBase DCQ
。调用此类型的方法有点多,3.6.0中发现ScoreState::missNote
对应的是sub_1001CBCC0+38 ADRL X1, _ZTI17LogicLongNoteBase;
将上面3个方法都找出并将其对应二进制作为特征进行暴力搜索查找注入。
另外因为hitNote()
和missNote()
都调用了ArcUtil::calculateScore()
,因此需要补上新增的布尔参数。查看hitNote()
的伪代码发现有调用,如下第六个参数即为我们需要的:
result = sub_10025F358(
*(unsigned int *)(a1 + 96),
*(_DWORD *)(a1 + 92),
*(_DWORD *)(a1 + 88),
*(_DWORD *)(a1 + 84),
*(_DWORD *)(*(_QWORD *)(a1 + 160) + 160LL),
*(unsigned __int8 *)(a1 + 200));
对插件进行修改,拿到此变量传入hacked_ArcUtil_calculateScore()
就完成了
bool _bool = *(bool*)(((char*)a1) + 200);
int score = hacked_ArcUtil_calculateScore(scoreState->lost, scoreState->far, scoreState->pure, scoreState->shiny_pure, scoreState->noteCount, _bool);
至此,3.6.0的方法注入就修改完成了。
0x2 ScoreState结构体
好耶,能正确寻找到方法注入了,可以完事睡大觉了对吧?
...
进入歌曲,闪退。
...
打打log,发现这行导致的闪退:
ScoreState *scoreState = (ScoreState*)a1;
那么应该就是ScoreState
这个结构体有变,嗯..去看看看... 噫?IDA里找不到这个struct,那就看看成员调用猜吧... 完全看不懂.jpg 那就看内存吧。
我iPad中已经装有插件DLGMemor-Injected
,oslog
及frida
等常用插件,现在既然已经成功注入方法,就直接使用DLGMemor-Injected
查看内存好了。注释掉修改后方法的方法体,在hacked_ScoreState_hitNote()
中打印a1,即ScoreState
的地址,并调用原方法返回:
NSLog(@"[arc-score ScoreState:hitNote] ScoreState *scoreState = (ScoreState*)a1; a1:0x%lx", a1);
long returnVal = orig_ScoreState_hitNote(a1, a2, a3, a4, a5);
return returnVal;
在hacked_ArcUtil_calculateScore()
中打印lost
,far
等参数
NSLog(@"[arc-score ArcUtil_calculateScore] lost:%d far:%d pure:%d shiny_pure:%d note_count:%d score:%d param_6:%d", lost, far, pure, shiny_pure, note_count, returnVal, param_6);
重新编译安装插件,打一会儿歌,补写本文时打的是Dot to Dot
,随意打几个far
和lost
后暂停,查看log:<Notice>: [arc-score ScoreState:hitNote] ScoreState *scoreState = (ScoreState*)a1; a1:0x283a98b60
掏出DLGMemor-Injected
,开启对Arcaea
的注入,点击下方memory
,输入此地址点search
即跳转至对应位置了。复制记录下此时的内存。
顺便写个Quality of Life脚本处理下地址值:
base_addr = 0x283A98B60
dump = """283A98B60 40 87 EB 02 01 00 00 00 @.......
283A98B68 01 00 00 00 00 00 00 00 ........
283A98B70 E3 02 00 00 48 00 00 00 ....H...
283A98B78 75 F2 96 00 D1 50 32 00 u....P2.
283A98B80 49 4D 84 42 00 00 00 00 IM.B....
283A98B88 00 00 C8 42 00 01 00 00 ...B....
283A98B90 00 00 00 00 00 00 00 00 ........
283A98B98 00 00 00 00 00 00 00 00 ........
283A98BA0 00 00 00 00 00 00 00 00 ........
283A98BA8 00 00 00 00 00 00 00 00 ........
283A98BB0 12 00 00 00 6A 00 00 00 ....j...
283A98BB8 6F 00 00 00 04 00 00 00 o.......
283A98BC0 06 00 00 00 11 00 00 00 ........
283A98BC8 16 00 00 00 08 00 00 00 ........
283A98BD0 24 00 00 00 03 00 00 00 $.......
283A98BD8 01 00 00 00 03 00 00 00 ........
283A98BE0 02 00 00 00 00 00 00 00 ........
283A98BE8 68 F1 56 3E 00 00 00 00 h.V>....
283A98BF0 00 00 00 00 00 00 00 00 ........
283A98BF8 02 00 00 00 00 00 00 00 ........
283A98C00 00 C0 E8 83 02 00 00 00 ........
283A98C08 00 00 00 00 00 00 00 00 ........
283A98C10 D0 5C 00 00 00 00 00 00 .\......
283A98C18 83 00 00 00 24 00 00 00 ....$...
283A98C20 80 CF F2 82 02 00 00 00 ........
283A98C28 00 00 00 00 00 00 00 00 ........
283A98C30 CC 00 00 00 00 00 00 00 ........
283A98C38 00 00 00 00 00 00 80 BF ........
283A98C40 00 00 00 C0 00 00 80 3F .......?
"""
qol = ""
for line in dump.split("\n"):
parts = line.split(" ", maxsplit=1)
if len(parts) == 2:
addr, content = parts
addr = int(addr, 16) - base_addr
qol += str(addr).zfill(3) + " " + content + "\n"
print(qol)
000 40 87 EB 02 01 00 00 00 @.......
008 01 00 00 00 00 00 00 00 ........
016 E3 02 00 00 48 00 00 00 ....H...
024 75 F2 96 00 D1 50 32 00 u....P2.
032 49 4D 84 42 00 00 00 00 IM.B....
040 00 00 C8 42 00 01 00 00 ...B....
048 00 00 00 00 00 00 00 00 ........
056 00 00 00 00 00 00 00 00 ........
064 00 00 00 00 00 00 00 00 ........
072 00 00 00 00 00 00 00 00 ........
080 12 00 00 00 6A 00 00 00 ....j...
088 6F 00 00 00 04 00 00 00 o.......
096 06 00 00 00 11 00 00 00 ........
104 16 00 00 00 08 00 00 00 ........
112 24 00 00 00 03 00 00 00 $.......
120 01 00 00 00 03 00 00 00 ........
128 02 00 00 00 00 00 00 00 ........
136 68 F1 56 3E 00 00 00 00 h.V>....
144 00 00 00 00 00 00 00 00 ........
152 02 00 00 00 00 00 00 00 ........
160 00 C0 E8 83 02 00 00 00 ........
168 00 00 00 00 00 00 00 00 ........
176 D0 5C 00 00 00 00 00 00 .\......
184 83 00 00 00 24 00 00 00 ....$...
192 80 CF F2 82 02 00 00 00 ........
200 00 00 00 00 00 00 00 00 ........
208 CC 00 00 00 00 00 00 00 ........
216 00 00 00 00 00 00 80 BF ........
224 00 00 00 C0 00 00 80 3F .......?
接着回游戏再打一会儿歌暂停,再次记录内存。
000 40 87 EB 02 01 00 00 00 @.......
008 01 00 00 00 00 00 00 00 ........
016 E3 02 00 00 7A 00 00 00 ....z...
024 B7 88 96 00 92 2D 32 00 .....-2.
032 69 19 A5 42 00 00 00 00 i..B....
040 00 00 C8 42 00 01 00 00 ...B....
048 00 00 00 00 00 00 00 00 ........
056 00 00 00 00 00 00 00 00 ........
064 00 00 00 00 00 00 00 00 ........
072 00 00 00 00 00 00 00 00 ........
080 7A 00 00 00 DE 00 00 00 z.......
088 E7 00 00 00 06 00 00 00 ........
096 07 00 00 00 25 00 00 00 ....%...
104 2E 00 00 00 0C 00 00 00 ........
112 2A 00 00 00 04 00 00 00 *.......
120 02 00 00 00 05 00 00 00 ........
128 04 00 00 00 00 00 00 00 ........
136 68 F1 56 3E 00 00 00 00 h.V>....
144 00 00 00 00 00 00 00 00 ........
152 02 00 00 00 00 00 00 00 ........
160 00 C0 E8 83 02 00 00 00 ........
168 00 00 00 00 00 00 00 00 ........
176 12 B2 00 00 00 00 00 00 ........
184 58 01 00 00 5B 00 00 00 X...[...
192 80 CF F2 82 02 00 00 00 ........
200 00 00 00 00 00 00 00 00 ........
208 CC 00 00 00 00 00 00 00 ........
216 00 00 00 00 00 00 80 BF ........
224 00 00 00 C0 00 00 80 3F .......?
使用任意diff工具diff一下:
结合esterTion
已完成的结构变量一起分析:
位置016
的数值未进行变化,且和原代码中noteCount
位置接近,0x2E3
即为739
,确实为本曲物量。
剩下的数值就参照记录下的far
,lost
等进行猜测。猜不出的则也对ArcaeaCountdownScore
来说无用。全部跳过就可以了。
其中原代码中的skill_type
为血条类型,0
为普通血条,1
为简单血条,2
为困难血条,6
为暴风血条。通过切换角色后可以发现152
处的数值与此相符。
那么修改后的ScoreState
如下:
typedef struct ScoreState {
pointer_t a1; // 0
char a2[8]; // +8 8
int noteCount; // +16 10
int maxCombo; // +20 14
int score; // +24 18
int score_chk; // +28 1c score / 3
float hp; // +32 20
char a3[11 * 4];// 跳过
int combo; // +80 50
int shiny_pure; // +84 54
int pure; // +88 58
int far; // +92 5c
int lost; // +96 60
char gap[13 * 4];// 跳过
int skill_type; // +152 98
} ScoreState;
至此,插件中ARM64部分修复完毕。我没有ARM设备,未进行修改。另外顺便优化了下游戏启动时的插件弹窗,提示未找到并注入的方法。截止至补写文章时Arcaea
版本为3.6.4,此插件依然可用。
5 条评论
想請問一下理論上這個改法對於Arcaea目前為止所有版本都可行嗎
被shadow ban后许久没有玩过了,不太确认了哦。不知道616有没有进行安全加强,如果没有那理论上是一直可行的。
第二(tql
话说arm64设备可以模拟arm的吧,还是说苹果只给了arm64的库
只有arm64,另外好像从iOS11开始就强制不支持32位应用了
第一!即刻进行一个太强了是大佬我死了