Command 发表于 2025-2-13 07:09

2025解题领红包 Android中级详细WP

本帖最后由 Command 于 2025-2-13 07:44 编辑

## APK

Jadx打开APK, 搜索"**错误**"可以找到这里

```java
public final class BattleActivityKt$BattleScreen$1$3$1$1$2 extends AbstractC1360j implements InterfaceC1335a {
      // ......
      public final void invoke() {
                String BattleScreen$lambda$21;
                BattleScreen$lambda$21 = BattleActivityKt.BattleScreen$lambda$21(this.$activationCode$delegate);
                if (!BattleActivityKt.Check(BattleScreen$lambda$21)) {
                  Toast.makeText(this.$context, "秘钥错误,请重试!", 0).show();
                  return;
                }
                // ......
      }
}
```

能够发现这里调用了**BattleActivityKt.Check**对输入的Flag进行校验

```java
public static final native boolean Check(String str);
```

发现是Native函数, 那就直接看**libwuaipojie2025_game.so**

## SO

IDA, 启动! (本文结合Unidbg分析ARM64的so)

### 找函数

发现**Check**在so里并未被导出, 找**JNI_OnLoad**

```c
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
__int64 v3; // x0
_QWORD v4; // BYREF
v4 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( (*vm)->GetEnv(vm, (void **)v4, 65542LL) )
    return -1;
v3 = (*(__int64 (__fastcall **)(_QWORD, const char *))(*(_QWORD *)v4 + 48LL))(
         v4,
         "com/zj/wuaipojie2025_game/ui/BattleActivityKt");
if ( !v3 )
    return -1;
if ( (*(int (__fastcall **)(_QWORD, __int64, char **, __int64))(*(_QWORD *)v4 + 1720LL))(
         v4,
         v3,
         off_163510,
         1LL) < 0 )
    return -1;
return 65542;
}
```

(这啥玩意? 算了懒得分析了, 直接Unidbg模拟一下看看)

```java
AndroidEmulator emulator = AndroidEmulatorBuilder
                .for64Bit()
      .addBackendFactory(new Unicorn2Factory(true))
      .build();
Memory memory = emulator.getMemory();
LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver);
VM vm = emulator.createDalvikVM(new File("52pojie.apk"));
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary(new File("libwuaipojie2025_game.so"), true);
dm.callJNI_OnLoad(emulator);
```

由此可知**Check**的地址为**0xe8c54**

```
RegisterNative(com/zj/wuaipojie2025_game/ui/BattleActivityKt, Check(Ljava/lang/String;)Z, RX@0x400e8c540xe8c54)
```

```c
// Check (a3为传入str)
bool __fastcall sub_E8C54(JNIEnv *a1, jobject a2, jstring *a3)
{
const char *InputUTF; // x0
const char *Input; // x21
char v11; // w0
char v14; // w8
void (__fastcall *v15)(_QWORD *, const char *, __int64, _QWORD *); // x22
_QWORD *v16; // x23
_BOOL4 v19; // w22
_QWORD v21; // BYREF
_OWORD v22; // BYREF

InputUTF = (*a1)->GetStringUTFChars(a1, a3, 0LL);
if ( InputUTF )
{
    Input = InputUTF;
    // ......省略部分不太重要的代码, 大概是环境检测? 总之不重要
    v15 = (void (__fastcall *)(_QWORD *, const char *, __int64, _QWORD *))nullsub_1(*(_QWORD *)((unsigned __int64)v22 & 0xFFFFFFFFFFFFFFF7LL | (8LL * (((unsigned __int8)(v11 | v14) ^ (((unsigned int)ao ^ (unsigned int)a) >> 24)) & 1)))); // 计算v15的地址 (其实调用的就是ao, 但不一定能确定, 还得Unidbg)
    dword_16359C = -559038669;
    v21 = 0LL;
    v21 = 0LL;
    v16 = (_QWORD *)operator new[](0x13uLL);
    v15(v21, Input, 19LL, v16); // 调用v15获取v16 对应指令为BLR X22
    v19 = *v16 == 0x72ECF89BAF8F2748LL
       && v16 == 0xB63AE26B0C720798LL
       && *(_QWORD *)((char *)v16 + 11) == 0xF75942B63AE26B0CLL;// 检验
    operator delete[](v16);
    (*a1)->ReleaseStringUTFChars(a1, a3, Input);
}
else
{
    return 0;
}
return v19;
}
```

可以发现`v15`的地址不能确定, 直接用Unidbg断在调用`v15`的那里即可

```java
emulator.attach().addBreakPoint(md.base+0xe8f44); // 0xe8f44为指令BLR X22的偏移量, 断下后看X22寄存器减掉基址就行
```

最后可以发现调用函数为**ao** (0xe9f60)

### 分析

```c
// a2就是输入的字符串, a3为19, result为最后结果
void __fastcall ao(__int128 *a1, const char *a2, __int64 a3, _QWORD *result)
{
__int64 i; // x23
__int128 v8; // BYREF
__int64 v9; //

v9 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40); // 没用
v8 = *a1; // 传入a1为定值, 故v8也为定值
if ( a3 )
{
    for ( i = 0LL; i != a3; ++i )// 循环19次, 侧面说明Flag长度为19
    {
      if ( (i & 0xF) == 0 )
      sub_E9954(&v8);// 这里的这个函数其实不用跟进去看...... (和输入啥的都没关系, v8一开始就是定值, v8更新的次数也固定)
      *((_BYTE *)result + i) = *(_BYTE *)((unsigned __int64)&v8 | i & 0xF) ^ a2; // 实际上做的事情 ("^"左侧其实都是确定的值, 不随输入的改变而改变)
    }
}
}
```

由此我们不难发现 (只要没有和我一样一开始就跑偏去看**sub_E9954**), 实际上就是将字符串的每一位与不同的数进行异或然后存入`result`而已

我们只要获取到`i`在0-18时`*(_BYTE *)((unsigned __int64)&v8 | i & 0xF)`的值就好, 我选择使用Unidbg对后面的`EOR`(ARM中异或指令)进行Hook

而在那里的汇编是这样的

```assembly
; ......
ORR             X8, X24, X25
LDRB            W9,
LDRB            W8,
EOR             W8, W8, W9; Addr=0xE9FB8
STRB            W8,
ADD             X23, X23, #1
; ......
```

在这里先简单讲一下`EOR`指令

```assembly
EOR                        W8, W8, W9; 将W8与W9进行异或, 结果存回W8 (在这里W8就是^左边那一长串, W9就是a2)
```



### 获取秘钥

Unidbg指令Hook

```java
emulator.getBackend().hook_add_new(new CodeHook() {
      @Override
      public void hook(Backend backend, long address, int size, Object user) {
                if (address == md.base + 0xe9fb8) {// 0xe9fb8就是刚才说的异或指令的偏移
            // 输出 W8, chr(a2)
                System.out.printf("0x%1$x, %2$c", emulator.getBackend().reg_read(Unicorn.UC_ARM64_REG_W8).intValue(), (char) emulator.getBackend().reg_read(Unicorn.UC_ARM64_REG_W9).intValue());
            System.out.println();
      }
    }

    @Override
    public void onAttach(UnHook unHook) {}
    @Override
      public void detach() {}
}, md.base, md.base + md.size, null);
```

然后我们可以得到每一位要与之异或的值

```python

```

那么现在我们有异或的值, 还需要知道正确的结果才能得到Flag

这时往前看, 回到刚才的函数**Check** (`sub_E8C54`), 有这样一段代码

```c
_QWORD *v16// Tips: 单个_QWORD 8个字节
v16 = (_QWORD *)operator new[](0x13uLL); // 这里申请了一块长度为0x13(19)的内存空间
v15(v21, Input, 19LL, v16); // v16是刚才的result
v19 = *v16 == 0x72ECF89BAF8F2748LL // v16的前8字节
   && v16 == 0xB63AE26B0C720798LL// 又有8字节
   && *(_QWORD *)((char *)v16 + 11) == 0xF75942B63AE26B0CLL; // 这里有可能看不明白, 这其实是从11-19的8个字节, 也就是说包括第二个8字节尾部的5字节
return v19;
```

也就是, 输入正确的情况下`v16`里应该是这样的

```
v16:
0000: 48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6
0010: 42 59 F7 00 00 00 00 00 00 00 00 00 00 00 00 00

数字对应的字节 (小端序)
0x72ECF89BAF8F2748LL: 48 27 8F AF 9B F8 EC 72
0xB63AE26B0C720798LL: 98 07 72 0C 6B E2 3A B6
0xF75942B63AE26B0CLL: 0C 6B E2 3A B6 42 59 F7
后两个明显可以看到重合部分
```

正确结果有了, 异或的值有了, 直接写Python脚本解密

```python
Flag = ''

for i in zip(, ):
    # print(i ^ i)
    Flag += chr(i ^ i)
print(Flag)
```

最后输出结果

```
flag{md5(uid+202eö
```

好像最后那点不太对? 不过猜也能猜出来是`uid+2025` 毕竟2025年嘛

实际上应该是`flag{md5(uid+2025)}` ......吧? (但我从apk里输入进去也不对啊......)

(顺带提一嘴, 秘钥输入框需要在进入战斗后点右上角那个圆形才会出现... 应该不会有人和我一样直到快做完题才发现吧)

正己 发表于 2025-2-13 15:59

ao是错误方法,这里加密的秘钥和正确的秘钥只差几个字节,导致异或出来的结果可以猜出来。另外前面的检测方法没过掉就走ao这个错误方法

JOKERWING 发表于 2025-2-24 09:55

前面的思路基本和我一样,也到0xe8c54了,本来是打算断掉调试看能不能行,但由于IDA捣鼓半天也没捣鼓出调试,就放弃了。光啃代码啃半天进度缓慢就放弃了,哎。。可能是我还在学,还有好多方法或者工具不知道,所以没啥思路了。等我继续看完安卓逆向这档事吧,目前还没到frida{:1_924:}

nanaqilin 发表于 2025-2-13 08:46

我也是搞到这儿,然后提交了好多次也没成功,其中也试过2025,看来这道题跟我无缘啊{:1_923:}

Command 发表于 2025-2-13 08:47

nanaqilin 发表于 2025-2-13 08:46
我也是搞到这儿,然后提交了好多次也没成功,其中也试过2025,看来这道题跟我无缘啊

你应该是把这个交上去了? 需要把UID和2025拼接到一起然后MD5再提交的

nanaqilin 发表于 2025-2-13 08:51

Command 发表于 2025-2-13 08:47
你应该是把这个交上去了? 需要把UID和2025拼接到一起然后MD5再提交的

是拼起来?我直接取的加和,然后再求的Md5{:1_909:}

cattie 发表于 2025-2-13 08:56

那个2025不出来应该是你第二轮的异或密钥找错了,他在0x10位的时候重新获取了一次密钥。
可以看看我的writeup


还有建议把几个帖子全部合到一起,这样看起来更方便。

风子09 发表于 2025-2-13 10:09

安卓中级题在app上提交的flag的格式是什么样的告诉我flag{md5(uid+2025)},提交半天都是错的

提交需要带flag吗?拼接时要带加号吗?

Command 发表于 2025-2-13 10:13

本帖最后由 Command 于 2025-2-13 10:21 编辑

风子09 发表于 2025-2-13 10:09
安卓中级题在app上提交的flag的格式是什么样的告诉我flag{md5(uid+2025)},提交半天都是错的

提交需要 ...
刚试了一下app上好像咋提交都不对; 但按照这个flag的格式往服务端上提交是没问题的

伤城幻化 发表于 2025-2-13 10:43

风子09 发表于 2025-2-13 10:09
安卓中级题在app上提交的flag的格式是什么样的告诉我flag{md5(uid+2025)},提交半天都是错的

提交需要 ...

uid+2025 是字符串上的拼接

风子09 发表于 2025-2-13 10:48

app里面怎么提交都是错的,不知道为什么

LONG65041 发表于 2025-2-13 11:21

菜鸟围观一下!!!!!
页: [1] 2
查看完整版本: 2025解题领红包 Android中级详细WP