The Martian,中文名“火星救援”,是一款安卓平台上的文字冒险类游戏。
Android逆向破解-The Martian(已完成)
简介
The Martian,中文名“火星救援”,是一款安卓平台上的文字冒险类游戏。
目标
- 解密并提取剧本内容
- 游戏为了真实,有些地方设计的等待时间过长;因此尝试修改代码,消除过长的时间等待
Dump剧本
寻找线索
首先是直接全局搜索任意关键词,发现不存在任何明文剧本,猜测被加密了。
观察Java包,发现org.cryptonode.jncryptor
加密包。
RNCryptor/JNCryptor: Java implementation of RNCryptor (github.com)
并了解到AES256JNCtypor
对象用于初始化加密/解密。
因此全局搜索,发现在com.littlelabs.storyengine.engine.TwineParser
里面调用了这个对象,位于方法parseTwineStoryAtPath
内。
1 | JNCryptor deCryptor = new AES256JNCryptor(); |
this.pass
是一个静态变量(太逆天了)
1 | private String pass = "asdasd"; |
而outputStream
合理猜测,应该就是加密后的文本。
这里没有具体描述在哪里,因此自己直接去assets
文件夹里面去找。
定位到一个可疑的包:
1 | The Martian_en.enc |
这应该就是存储在本地的,默认的英文剧本包了。刚刚的那个parseTwineStoryAtPath
里面其实还有一些联网逻辑,猜测应该是根据语言去服务器上下载其他语言的包。不过这个APK太古老了,就不管了。
解密
JNCryptor
的这个加密后的数据比较特殊,不是直接的AES,里面似乎还添加了一些自定义的头文件数据。比如第一个字节可能会存放0x2或者0x3,用于判断JNCryptor
使用的版本。
因此直接用Python梭哈的方式失败了。
下面有三种方法:
- 阅读源码,用Python复现解密逻辑
- Github上面下载
JNCryptor
源码进行复现 - 用Frida直接野蛮调用现成的
JNCryptor
对象
我选择了第三个方案。
直接上脚本:其中复杂的点其实是Js与Java之间的类型转换,有点折磨人。
1 | function hook_open() { |
这样就成功dump到/data/data
的私有目录了,然后用MT文件浏览器取出来就好了。
修改时间
这里主要修改的时间是基于剧本中AUTO
标识的时间延迟。
TIMER
则应该是我们不去打扰的东西,因为TIMER
一般对应了一些突发限时事件,点错了就会进badend的那种,所以我们应该希望时间不变,或者更加充裕。
重点关注对象
parseLineForLink
CheckInOnCharacter
TwineTimerService
links
System.currentTimeMillis
resolveLinkAction
TwineTimerService.EXTRA_DELAY_SECONDS
CheckInOnCharacter
这个函数是点击Mark is away...
按钮后触发的。
伪代码:
1 | public void checkInOnCharacter() { |
其中关于if (this.checkInReleaseDate.compareTo(date) > 0) {
的逻辑,大概3878行开始:
1 | .line 1557 |
首先删掉基于Date的时间判断,即将
1 | if-lez v3, :cond_3e |
给删去。这样就可以实现在点击Mark is away...
按钮后,立刻触发Mark后续的对话,少掉了部分的等待时间。
但是即便如此,上面还是有一块逻辑无法在这里修改:
1 | if (this.currentPassage.links.size() <= 1) { |
这里的原因是因为links
对象里面确实没有message可供调用。因此要继续寻找可行的办法。
TwineTimerService
尝试1
这是一个用于设置timer的东西。不出意外的话,应该就是用于阻塞下一个message的发送的。
1 | protected void onHandleIntent(Intent intent) { |
最后有一个Log.w
,已经很明确了下一个Passage要发送的延迟。这个在Logcat里面能查到。
因此,尝试修改这里:
1 | if (shouldFastForward) { |
让shouldFastForward
里的逻辑永远成立,立刻执行。
只要把 if-eqz v4, :cond_22
给删掉即可,位于第588行。
1 | .line 60 |
结果
确实是立刻显示了,但是其他所有的Message都是瞬间显示,游玩体验不好了。
尝试2
继续去看代码,关注
1 | long triggerAtMillis = getTriggerAtMillisFromIntent(intent); |
这个决定了Message将会在哪个时候发送。
同时
1 | Log.w(getClass().getSimpleName(), "Scheduled Passage [" + intent.getStringExtra(EXTRA_PASSAGE_NAME) + "] for deployment in [" + (((float) (triggerAtMillis - System.currentTimeMillis())) / 1000.0f) + "] seconds; [" + intent.getLongExtra(EXTRA_DELAY_SECONDS, 0L) + "] seconds expected."); |
中的
1 | intent.getLongExtra(EXTRA_DELAY_SECONDS, 0L) |
也是控制了较长等待时的时间。
1 | private long getTriggerAtMillisFromIntent(Intent intent) { |
想法:主要修改这里:
1 | if (intent.hasExtra(EXTRA_DELAY_SECONDS)) { |
将
1 | return triggerAtMillis + (intent.getLongExtra(EXTRA_DELAY_SECONDS, 0L) * 1000); |
修改成固定的
1 | return triggerAtMillis + 4000; |
Smali在这里:第466行
1 | .line 122 |
最简单的修改:
1 | .line 122 |
直接把v4
乘4,然后最后绕过v2
,把0xfa0
加到v0
上。
结果
非常好。比较自然,没有很突兀。
但是后面可能会有一些问题,就是有一些特殊事件会触发TIMER
类别的事件,这里也会统一改成4s,时间会太短了。
因此可能还是需要更细节的修改。
resolveLinkAction
通过对TwineTimerService.EXTRA_DELAY_SECONDS
的交叉引用,发现了这个函数。
这个函数就是用于给每一个Message设置Timer的。通过从这里修改,可以更具体地,分类别地修改时间。
1 | public void resolveLinkAction(TwinePassage passage, double secondsPassed) { |
尝试3
具体而言,修改7994行左右的代码:
1 | .line 340 |
大概就是这里的代码:
1 | intent.putExtra(TwineTimerService.EXTRA_DELAY_SECONDS, Math.round(duration - secondsPassed)); |
这里则是给duration
赋值的代码:位于8153行
1 | .line 337 |
修改逻辑决定为这里:
1 | if (!TwineWaitPassage.class.getSimpleName().equals(passage.getType()) && ((passage.links.size() >= 2 && passage.links.elementAt(1).isCheckInLink()) || passage.links.elementAt(0).durationInSeconds > getLongestTypingSpeed())) { |
其中,最简单的方式便是想办法不进入else
里面,这样就会全部默认使用duration = MIN_LINK_DURATION;
,即最短延迟时间,为2s。
这样的方法会简单一点,只要把7971行的代码给删去即可:
1 | check-cast v12, Lcom/littlelabs/storyengine/model/link/TwineLink; |
删掉 if-lez v12, :cond_109
即可。
结果
不太好,变成负数了,然后永远也不触发了。
尝试4(成功)
找到link.durationInSeconds;
设置的方法
1 | private TwineLink buildLinkFromRegex(String curLine, Matcher linkMatch) { |
其实主要是要把m
和h
,分别代表分钟和小时,这样太长的时间给改掉,因此这里修改应该就够了。
然后呢,经过我检查剧本,发现TIMER没有用上m
和h
的,所以不用担心。
TwineParser
里面修改第430行:
1 | .line 1027 |
改成其他更少的数字,比如0x5。
以及597行:
1 | .line 1035 |
然后第10行的:
1 | .field public static final HOUR_MULTIPLIER:I = 0xe10 |
也修改了。
全部改成0x5可能会比较好。
然后还有一个函数也要做类似修改:
1 | private int parseLineForTime(String curLine, Matcher linkMatch) { |
修改1636行:
1 | .line 1075 |
修改1707行:
1 | .line 1081 |
全部看情况改,改成0x5可能不错。
结果
效果不错。
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2023/10/10/Android逆向破解-The-Martian/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!