boomshakalaka-3 && easy-app
这两道题都尝试动态去看了,调试过程中遇到很多这样那样的问题,过几天回过头来看居然没遇到什么阻碍,甚至忘记当初遇到什么问题了(?)这就是人生吧(?)
0ctf boomshakalaka-3#
打飞机游戏
主活动可以看出 Cocos2 的 java 层一般只做初始化,通过重写 native 的组件来调用主要逻辑。
public class FirstTest extends Cocos2dxActivity {
@Override // org.cocos2dx.lib.Cocos2dxActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
a haha = new a(this, "flag");
haha.d("YmF6aW5nYWFhYQ==");
a hehe = new a(this, "Cocos2dxPrefsFile");
hehe.d("N0");
}
@Override // org.cocos2dx.lib.Cocos2dxActivity
public Cocos2dxGLSurfaceView onCreateView() {
Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
a hehe = new a(this, "Cocos2dxPrefsFile");
hehe.d("MG");
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
return glSurfaceView;
}
static {
System.loadLibrary("cocos2dcpp");
}
}
class a
public class a {
private SharedPreferences editor;
public a(Context arg1, String arg2) {
this.editor = null;
this.editor = arg1.getSharedPreferences(arg2, 0);
}
public void b() {
this.editor.edit().putString("DATA", "").commit();
}
public String c() {
return this.editor.getString("DATA", "");
}
public void d(String arg1) {
this.editor.edit().putString("DATA", String.valueOf(String.valueOf(c())) + arg1).commit();
}
}
用到 SharedPreferences,玩一把游戏去 /data/data/com.example.plane/shared_prefs 看一眼
sagit:/data/data/com.example.plane/shared_prefs # cat Cocos2dxPrefsFile.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="DATA">MGN0ZntDMGNvUzJkX0FuRHJvMWRfMGN0ZntDMGNvUzJkX0FuRHJvMWdz99</string>
<boolean name="isHaveSaveFileXml" value="true" />
<int name="HighestScore" value="500" />
</map>
解码得0ctf{C0coS2d_AnDro1d_0ctf{C0coS2d。明显不对
MGN0ZntDMGNvUzJkX0FuRHJvMW Rf MGN0ZntDMGNvUzJkX0FuRHJvMW dz99
观察这个东西的结构,第一个部分和第三个部分一样一样,ida 里面搜一下 Rf 和 dz99
int __fastcall GameOverLayer::init(GameOverLayer *this)
{
v80 = &unk_EEFBEEEB;
v1 = 0;
v81 = 0;
v83 = 0;
v82 = 0;
do
{
*(&v82 + v1) = *(&v80 + v1) ^ 0xAF;
++v1;
}
while ( v1 != 4 );
v3 = cocos2d::CCUserDefault::sharedUserDefault(this);
sub_E137F4D8(v78, &byte_E13D52A0, v75);
cocos2d::CCUserDefault::getStringForKey(v77, v3, &v82, v78);
v4 = sub_E137DDDC(v78);
v5 = cocos2d::CCUserDefault::sharedUserDefault(v4);
sub_E137EF30(v79, v77);
sub_E137ECB4(v79, "dz99");
cocos2d::CCUserDefault::setStringForKey(v5, &v82, v79);
...
在 GameOverLayer::init() 里面找到,那就是说游戏结束会把 dz99 加到 SharedPreference 里。到这里可以猜测字符片段是随游戏进行加进来的,而且显然会有一个判断对部分字符进行拦截,这个判断不难想到跟分数有关。扫一眼 setStringForKey 的 xref,找到
cocos2d::CCUserDefault *__fastcall ControlLayer::updateScore(cocos2d::CCUserDefault *this, const char *a2)
{
int v2; // r3
int v4; // r7
cocos2d::CCUserDefault *v5; // r0
int v6; // r5
_BYTE *v7; // r0
int v8; // r5
int v9; // r5
int v10; // r5
int v11; // r5
int v12; // r5
int v13; // r5
int v14; // r5
int v15; // r5
int v16; // r5
int v17; // r0
cocos2d::CCUserDefault *v18; // [sp+4h] [bp-50h]
_BYTE v19[4]; // [sp+Ch] [bp-48h] BYREF
_BYTE v20[4]; // [sp+10h] [bp-44h] BYREF
_BYTE v21[4]; // [sp+14h] [bp-40h] BYREF
_BYTE v22[4]; // [sp+18h] [bp-3Ch] BYREF
_BYTE v23[4]; // [sp+1Ch] [bp-38h] BYREF
_BYTE v24[4]; // [sp+20h] [bp-34h] BYREF
_BYTE v25[4]; // [sp+24h] [bp-30h] BYREF
_BYTE v26[4]; // [sp+28h] [bp-2Ch] BYREF
_BYTE v27[4]; // [sp+2Ch] [bp-28h] BYREF
_BYTE v28[4]; // [sp+30h] [bp-24h] BYREF
_BYTE v29[4]; // [sp+34h] [bp-20h] BYREF
_BYTE v30[4]; // [sp+38h] [bp-1Ch] BYREF
_BYTE v31[4]; // [sp+3Ch] [bp-18h] BYREF
char v32[8]; // [sp+40h] [bp-14h] BYREF
int v33; // [sp+48h] [bp-Ch] BYREF
char v34; // [sp+4Ch] [bp-8h]
strcpy(v32, "data");
v2 = 0;
v34 = 0;
v18 = this;
v33 = 0;
do
{
*(&v33 + v2) = v32[v2] ^ 0x20;
++v2;
}
while ( v2 != 4 );
if ( a2 <= 0x3B9ACA00 )
{
v4 = cocos2d::CCUserDefault::sharedUserDefault(this);
sub_E137F4D8(v21, &byte_E13D52A0, v19);
cocos2d::CCUserDefault::getStringForKey(v20, v4, &v33, v21);
v5 = sub_E137DDDC(v21);
if ( a2 == 100 )
{
v6 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v22, v20, "MW");
cocos2d::CCUserDefault::setStringForKey(v6, &v33, v22);
v7 = v22;
}
else if ( a2 == 600 )
{
v8 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v23, v20, "Rf");
cocos2d::CCUserDefault::setStringForKey(v8, &v33, v23);
v7 = v23;
}
else if ( a2 == 700 )
{
v9 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v24, v20, "Rz");
cocos2d::CCUserDefault::setStringForKey(v9, &v33, v24);
v7 = v24;
}
else if ( a2 == 3000 )
{
v10 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v25, v20, "Bt");
cocos2d::CCUserDefault::setStringForKey(v10, &v33, v25);
v7 = v25;
}
else if ( a2 == 5600 )
{
v11 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v26, v20, "RV");
cocos2d::CCUserDefault::setStringForKey(v11, &v33, v26);
v7 = v26;
}
else if ( a2 == 9900 )
{
v12 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v27, v20, "9Z");
cocos2d::CCUserDefault::setStringForKey(v12, &v33, v27);
v7 = v27;
}
else if ( a2 == 18000 )
{
v13 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v28, v20, "b1");
cocos2d::CCUserDefault::setStringForKey(v13, &v33, v28);
v7 = v28;
}
else if ( a2 == 88800 )
{
v14 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v29, v20, "Vf");
cocos2d::CCUserDefault::setStringForKey(v14, &v33, v29);
v7 = v29;
}
else if ( a2 == 100000 )
{
v15 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v30, v20, "S2");
cocos2d::CCUserDefault::setStringForKey(v15, &v33, v30);
v7 = v30;
}
else
{
if ( a2 != 1000000000 )
{
LABEL_25:
v17 = cocos2d::CCString::createWithFormat("%d", a2);
(*(**(v18 + 66) + 428))(*(v18 + 66), *(v17 + 20));
return sub_E137DDDC(v20);
}
v16 = cocos2d::CCUserDefault::sharedUserDefault(v5);
std::operator+<char>(v31, v20, "4w");
cocos2d::CCUserDefault::setStringForKey(v16, &v33, v31);
v7 = v31;
}
sub_E137DDDC(v7);
goto LABEL_25;
}
return this;
}
那就做完了
0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}
虚调用#
虚调用溯源#
可以猜测到这题是通过 cocos2d 组件调用核心逻辑,xref 看不到东西说明是虚调用
.text:E113BC58 ; _DWORD GameScene::init(GameScene *__hidden this)
.text:E113BC58 EXPORT _ZN9GameScene4initEv
.text:E113BC58 _ZN9GameScene4initEv ; DATA XREF: .data.rel.ro:`vtable for'GameScene+20↓o
.text:E113BC58
.text:E113BC58 var_1C= -0x1C
.text:E113BC58 var_18= -0x18
.text:E113BC58 var_14= -0x14
.text:E113BC58 var_10= -0x10
.text:E113BC58 var_8= -8
.text:E113BC58
.text:E113BC58 ; __unwind { // sub_E1392F8C
.text:E113BC58 PUSH {R4-R6,LR}
.text:E113BC5A LDR R3, =0x796C797C
以这个函数为例,在 prologue 的下一句,即 LDR 下断点,断下后 LR 的值即为 caller。也可以 Stack Trace 直接看
虚调用分析#
溯源到 GameOverScene::create() 调用 GameScene::init() 的地方(E1131B68)
.text:E1131B4C ; GameOverScene *__fastcall GameOverScene::create(GameOverScene *this, int)
.text:E1131B4C EXPORT _ZN13GameOverScene6createEi
.text:E1131B4C _ZN13GameOverScene6createEi ; CODE XREF: PlaneLayer::RemovePlane(void)+14↓p
.text:E1131B4C ; __unwind { // sub_E1388F8C
.text:E1131B4C PUSH {R3-R5,LR}
.text:E1131B4E MOVS R5, R0
.text:E1131B50 MOVS R0, #0xEC ; unsigned int
.text:E1131B52 BLX _Znwj ; operator new(uint)
.text:E1131B56 MOVS R4, R0
.text:E1131B58 BL _ZN13GameOverSceneC2Ev ; GameOverScene::GameOverScene(void)
.text:E1131B5C MOVS R3, R4
.text:E1131B5E ADDS R3, #0xE8
.text:E1131B60 STR R5, [R3]
.text:E1131B62 LDR R3, [R4]
.text:E1131B64 MOVS R0, R4
.text:E1131B66 LDR R3, [R3,#0x18]
.text:E1131B68 BLX R3
稍微看一眼
E1131B4C PUSH {R3-R5,LR}
E1131B4E MOVS R5, R0 ; (1) R5 = arg0(可能是某个外部指针)
E1131B50 MOVS R0, #0xEC ; (2) 分配 0xEC 字节
E1131B52 BLX _Znwj ; (3) new 一个新对象,R0 = operator new(0xEC)
E1131B56 MOVS R4, R0 ; (4) R4 = this(新对象地址)
E1131B58 BL _ZN13GameOverSceneC2Ev ; (5) 调用 GameOverScene 构造函数(C2)
E1131B5C MOVS R3, R4
E1131B5E ADDS R3, #0xE8
E1131B60 STR R5, [R3] ; (6) *(this+0xE8) = R5(写一个成员)
E1131B62 LDR R3, [R4] ; (7) R3 = *(this) = vptr
E1131B64 MOVS R0, R4 ; (8) R0 = this,作为成员函数第一个参数
E1131B66 LDR R3, [R3,#0x18] ; (9) R3 = *(vptr + 0x18) = vtable[0x18/4]
E1131B68 BLX R3 ; (10) call R3(this)
那么这段就相当于
GameOverScene* obj = new GameOverScene(); // operator new + C2
obj->someMember = arg0; // *(this+0xE8)=R5
obj->vtable[6](obj); // 虚调用第 6 个槽位
构造函数分析#
看一眼构造函数
.text:E1131B28 ; void __fastcall GameOverScene::GameOverScene(GameOverScene *this)
.text:E1131B28 EXPORT _ZN13GameOverSceneC2Ev
.text:E1131B28 _ZN13GameOverSceneC2Ev ; CODE XREF: GameOverScene::create(int)+C↓p
.text:E1131B28 PUSH {R4,LR}
.text:E1131B2A MOVS R4, R0
.text:E1131B2C BL _ZN7cocos2d7CCSceneC2Ev ; cocos2d::CCScene::CCScene(void)
.text:E1131B30 LDR R3, =(_ZTV13GameOverScene_ptr - 0xE1131B3A)
.text:E1131B32 MOVS R2, R4
.text:E1131B34 ADDS R2, #0xE4
.text:E1131B36 ADD R3, PC ; _ZTV13GameOverScene_ptr
.text:E1131B38 LDR R3, [R3] ; `vtable for'GameOverScene ...
.text:E1131B3A MOVS R0, R4
.text:E1131B3C ADDS R3, #8
.text:E1131B3E STR R3, [R4]
.text:E1131B40 MOVS R3, #0
.text:E1131B42 STR R3, [R2]
.text:E1131B44 STR R3, [R2,#4]
.text:E1131B46 POP {R4,PC}
.text:E1131B46 ; End of function GameOverScene::GameOverScene(void)
.text:E1131B46
.text:E1131B46 ; ---------------------------------------------------------------------------
.text:E1131B48 off_E1131B48 DCD _ZTV13GameOverScene_ptr - 0xE1131B3A
.text:E1131B48 ; DATA XREF: GameOverScene::GameOverScene(void)+8↑r
.text:E1131B48 ; } // starts at E1131ACC
可以看出来 GameOverScene 继承自 cocos2d 的 CCScene。调用完基类构造函数之后的几步:
.text:E1131B30 LDR R3, =(_ZTV13GameOverScene_ptr - 0xE1131B3A)
...
.text:E1131B36 ADD R3, PC ; _ZTV13GameOverScene_ptr
Thumb 状态下 PC = 当前指令地址 + 4
因此这段相当于
C = (_ZTV13GameOverScene_ptr - 0xE1131B3A)
R3 = C
PC = E1131B36 + 4 = 0xE1131B3A
R3 = C + PC = _ZTV13GameOverScene_ptr
这样才能正确拿到 vtable,之后写入对象头
.text:E1131B38 LDR R3, [R3] ; `vtable for'GameOverScene ...
.text:E1131B3A MOVS R0, R4
.text:E1131B3C ADDS R3, #8
.text:E1131B3E STR R3, [R4]
后面就是对两个字段进行清理
太湖杯 easy-app#
adb install 报错
Performing Streamed Install
adb: failed to install easy_app.apk: Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES: Scanning Failed.: No signature found in package of version 2 or newer for package com.example.myapplication]
apksigner 再签一下就行
apksigner sign -ks ~/Downloads/my-release.keystore --out signed.apk easy_app.apk
魔改点主要在 base64,对部分字节进行了一个循环位移。调试找规律很容易注意到。
def reverse_check1(output:bytes):
"""
Check1:
A = input[5:21]; B = input[21:37];
out1[i] = (ord(A[i]) & 0x0F) | (ord(B[i]) & 0xF0);
out2[i] = (ord(A[i]) & 0xF0) | (ord(B[i]) & 0x0F);
"""
A = output[0:16]
B = output[16:32]
in1 = bytearray(len(A))
in2 = bytearray(len(A))
for i in range(len(A)):
in1[i] = (A[i] & 0x0F) | (B[i] & 0xF0)
in2[i] = (A[i] & 0xF0) | (B[i] & 0x0F)
return in1, in2
def reverse_tea(v0:bytearray, v1:bytearray):
key = [0x00000042, 0x00000037, 0x0000002C, 0x00000021]
delta = 0x9E3779B9
v0 = int.from_bytes(v0, 'little')
v1 = int.from_bytes(v1, 'little')
sum = (delta * 32) & 0xFFFFFFFF
for _round in range(32):
v1 = (v1 - (((v0 << 4) + key[2]) ^ (v0 + sum) ^ ((v0 >> 5) + key[3]))) & 0xFFFFFFFF
v0 = (v0 - (((v1 << 4) + key[0]) ^ (v1 + sum) ^ ((v1 >> 5) + key[1]))) & 0xFFFFFFFF
sum = (sum - delta) & 0xFFFFFFFF
return v0.to_bytes(4, 'little'), v1.to_bytes(4, 'little')
def base64_encode(input_bytes: bytes):
import base64
base64_chars = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
encoded_bytes = base64.b64encode(input_bytes)
translated_str = encoded_bytes.decode().translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", base64_chars))
result = [''] * len(translated_str)
for i in range(len(translated_str)):
if (i+1) % 4 == 0:
result[i] = translated_str[i]
else:
result[i] = translated_str[i + 1 if (i + 2) % 4 != 0 else i - 2]
return result
print("Base64 Encode Test: ", [hex(ord(h)) for h in base64_encode(bytes([0xBB, 0x11, 0x61, 0x2F, 0xA0, 0xEF, 0x7F, 0x60, 0x2F, 0xF1,
0x72, 0x7F, 0x0C, 0x4F, 0xBC, 0xEA, 0x1C, 0xBC]))])
def base64_decode(input_str):
import base64
base64_chars = "abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ+/"
output_bytes = bytearray()
input_bytes = bytes(input_str, 'utf-8')
aligned_input = [b''] * len(input_bytes)
for i in range(len(input_bytes)):
if (i+1) % 4 == 0:
aligned_input[i] = input_bytes[i]
else:
aligned_input[i] = input_bytes[i - 1 if i % 4 != 0 else i + 2]
aligned_str = bytes(aligned_input).decode('utf-8')
decoded_bytes = base64.b64decode(aligned_str.translate(str.maketrans(base64_chars, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")))
output_bytes.extend(decoded_bytes)
print("Decoded Bytes:", output_bytes)
return output_bytes
def solve(encoded_str):
decoded_bytes = base64_decode(encoded_str)
print("len(decoded_bytes): ", len(decoded_bytes))
for i in range(len(decoded_bytes)//8):
v0, v1 = reverse_tea(decoded_bytes[8*i:8*i+4], decoded_bytes[8*i+4:8*i+8])
decoded_bytes[8*i:8*i+4] = v0
decoded_bytes[8*i+4:8*i+8] = v1
print("After TEA Decryption:", decoded_bytes)
print("decoded_bytes[0]: ", hex(decoded_bytes[0]))
print("decoded_bytes[16]: ", hex(decoded_bytes[16]))
result = reverse_check1(decoded_bytes)
return result[0] + result[1]
encoded_str = "e)n*pNe%PQy!^oS(@HtkUu+Cd$#hmmK&ieytiWwYkIA="
original_input = solve(encoded_str)
print("flag: ", original_input)