初见
题目附件为一个apk。
提示信息为:play the game, get the highest score
在SDK创建的模拟器下按照apk后运行失败:
想到电脑里还装了雷电模拟器,apk在雷电模拟器下可以正常按照运行。
运行app后,是一个打飞机的游戏,然后我就玩了两把。。。。。。
暂时不清楚这个游戏和解题有什么关系,题目的提示信息说要拿到最高分,但解题方法应该不是玩到最高分^_^。
接下来静态分析apk文件看看。
静态分析apk
用jadx分析apk,发现apk中有两个包:
第一个包,com.example.plane应该是作者开发的。主要有两个类,a和FirstTest。
第二个包,org.cocos2dx.lib,看名字就像个第三方的,百度一下,发现是个游戏开发的库,包括“我是MT”等许多游戏都是用这个库开发的。作者应该就是用这个库开发了这个打飞机的游戏。
没有发现MainActivity,需要先确定app的主Activity是哪个。故解码AndroidManifest.xml看看哪个是主Activity。
主Activity
apk内的xml文件都是编码过的,需要用apktool解码:
apktool.bat d C:\Users\leo\Desktop\9ff03a7a4b364ca3be452d08771ae7fb.apk
解码后,AndroidManifest.xml部分内容为:
<activity android:configChanges="orientation" android:label="Plane" android:name=".FirstTest" android:screenOrientation="portrait" android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
可以看到主Activity是FirstTest类。
FirstTest类
FirstTest类继承自Cocos2dxActivity类:
public class FirstTest extends Cocos2dxActivity{
... ...
}
加载了cocos2dcpp库:
static {
System.loadLibrary("cocos2dcpp");
}
可以猜测,继承Cocos2dxActivity类应该是使用cocos2dx库开发游戏的方式。
FirstTest类内部只有两个方法:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new a(this, "flag").d("YmF6aW5nYWFhYQ==");
new a(this, "Cocos2dxPrefsFile").d("N0");
}
public Cocos2dxGLSurfaceView onCreateView() {
Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
new a(this, "Cocos2dxPrefsFile").d("MG");
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
return glSurfaceView;
}
这两个方法都是对Cocos2dxActivity类中方法的重载,应该是在游戏运行的特定阶段会被调用的。名字都有Create,应该是在游戏运行的开始阶段被调用。
这两个方法内部都用到了a类的d方法,接下来看一看这个方法。
a类
a类的主要功能是使用SharedPreferences接口进行数据存储:
public class a {
private SharedPreferences editor;
public a(Context arg1, String arg2) {
this.editor = null;
this.editor = arg1.getSharedPreferences(arg2, 0);
}
public void d(String arg1) {
this.editor.edit().putString("DATA", String.valueOf(String.valueOf(c())) + arg1).commit();
}
}
SharedPreferences接口是一个轻量级的存储类,用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件存放在/data/data/<package name>/shared_prefs目录下。
shared_prefs目录
我来到雷电模拟器的/data/data/com.example.plane/shared_prefs目录下,果然看到了xml文件。
文件名就是FirstTest类调用a类构造函数的第二个参数:
这里有两个xml文件。
flag.xml
首先看了下flag.xml文件,内容为:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="DATA">YmF6aW5nYWFhYQ==</string>
</map>
这就是FirstTest类的onCreate方法调用a类的d方法时传入的参数,也印证了a类就是用SharedPreferences接口进行数据存储的。
这里“YmF6aW5nYWFhYQ==”有明显的BASE64特征,解码为:bazingaaaa。提交错误,这不是flag。
接下来重点看看Cocos2dxPrefsFile.xml。
Cocos2dxPrefsFile.xml
这个文件内容为:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<int name="HighestScore" value="34300" />
<string name="DATA">MGN0ZntDMGNvUzJkX0FuRHJvMWdz99ZntDMGNvUzJkX0FuRHJvMWRzRVdz99</string>
<boolean name="isHaveSaveFileXml" value="true" />
</map>
里面记录了我在游戏里的最高分“34300”,还有一串DATA字符串。
这个字符串的部分内容是由FirstTest类写入的,FirstTest类的onCreate和onCreateView分别往Cocos2dxPrefsFile.xml文件写入了“N0”和“MG”。正是这段字符串的前四个字符。
我之后重启了几遍app,发现,每次启动app都会在DATA字符串中追加“MG”和“N0”。
“MGN0”之后的字符串分为多组,每组的固定模式为:
ZntDMGNvUzJkX0FuRHJv + xxx + dz99
其中xxx会变化。
通过多次打游戏,并分析Cocos2dxPrefsFile.xml文件内容的变化,可以得到。每次超过前一次的最高分时,都会在该xml文件中增加一条上述模式的内容。
想找一找增加这些内容的代码。
在jadx中没找到关于“ZntDMGNvUzJkX0FuRHJv”和“dz99”字符串,估计在lib中,接下来使用ida分析apk里的“libcocos2dcpp.so”。
libcocos2dcpp.so
分析这个库文件有点懵,因为我本身没有游戏开发经验。想找一篇合适的介绍cocos2d的文章也没找到,看了多篇资料后,得到一些粗略的理解:
- 所谓动态的游戏,就是一个一个画面的交替。
- cocos2dx通过定时器来定时更新我们看到的画面。
- cocos2dx中负责响应定时器的函数名叫update()。
也就是我们移动飞机,敌机位置变化,子弹移动,消灭敌机,增加分数等都是和update()函数有关。
update()
在IDA搜索了一下update()函数,发现有许多函数名中有update的,但函数名为update的只有一个:GameLayer::update()。
浏览一下GameLayer::update()函数的伪代码,发现一个名字为ControlLayer::updateScore()的函数。
感觉这到题目和这个分数有很大的关系,题目的提示信息也说了要得到最高分,ControlLayer::updateScore()应该是个重要函数。
但这两个函数一个是GameLayer类的,一个是ControlLayer类的。搜索了以下游戏层和控制层的关系,大致就是游戏层负责显示相关,控制层负责事件处理(触摸屏幕、子弹移动到敌机从而消灭飞机)。这只是粗略的理解,因为没有对cocos2d进行广泛学习。
updateScore()
这个函数的反编译代码很多,粗略浏览一下,发现一些有趣的数字操作:
if ( (unsigned int)a2 <= 1000000000 )
{
... ...
switch ( a2 )
{
case 100:
v6 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v22, v20, "MW");
cocos2d::CCUserDefault::setStringForKey(v6, &v33, v22);
v7 = v22;
break;
case 600:
v8 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v23, v20, "Rf");
cocos2d::CCUserDefault::setStringForKey(v8, &v33, v23);
v7 = v23;
break;
猜测这里的a2就是获得的分数,这个1000000000应该就是最高分。
在switch分支中可以看到,当分数为100时,给一个字符串附加“MW”,想这会不会就是Cocos2dxPrefsFile.xml中ZntDMGNvUzJkX0FuRHJv + xxx + dz99里的xxx呢。
与其看代码,不如来一把游戏。。。。。。
因为分数为历史最高分时才会在Cocos2dxPrefsFile.xml中写入内容,所以我先在模拟器中将Cocos2dxPrefsFile.xml中的最高分修改为0,并删除现有的DATA字符串内容:
重新运行游戏,达到100分,也就是消灭一个小飞机(小飞机100分,大飞机600分):
此时Cocos2dxPrefsFile.xml内容为:
DATA的内容正好是“ZntDMGNvUzJkX0FuRHJv + MW + dz99”。
同样道理,得分为600时(必须是消灭6个小飞机,不能是一个小飞机+一个大飞机,分数必须有过600这个状态),DATA内容为“ZntDMGNvUzJkX0FuRHJv + MWRf + dz99”。
按照这个逻辑,一直玩到1000000000分,经过updateScore()内的各个分支(switch和之后的if),将得到一个最长DATA字符串。我们按照分数高低拼接出这个字符串,内容为:
ZntDMGNvUzJkX0FuRHJv + MWRfRzBtRV9Zb1VfS24w + dz99
这东西和答案有什么关系呢,得猜一下。
虽然这段字符串结尾没有“=”,但全是可显字符,再结合flag.xml里的DATA字符串是BASE64编码的,所以怀疑这个也是BASE64编码的。
试一下,发现BASE64解码失败。
这时看到了DATA字符串最前面的MGN0,也就是app运行时填入的4个字符,加上这四个字符,完整字符串为:“MGN0ZntDMGNvUzJkX0FuRHJvMWRfRzBtRV9Zb1VfS24wdz99”。
对这个完整串BASE64解码成功,结果为“0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}”。
总结
这道题的难点在cocos2d游戏库相关知识,短期内想查到解题有用资料费不少功夫。
我现在也还是只了解了个大概皮毛。所以对这个题目的so库文件、so库文件如何与java部分的代码配合,还不了解。
如果是懂游戏开发的大神,应该能给出原理更清晰的解答。