这两道题都尝试动态去看了,调试过程中遇到很多这样那样的问题,过几天回过头来看居然没遇到什么阻碍,甚至忘记当初遇到什么问题了(?)这就是人生吧(?)

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)