APK
Jadx打开APK, 搜索"错误"可以找到这里
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进行校验
public static final native boolean Check(String str);
发现是Native函数, 那就直接看libwuaipojie2025_game.so
SO
IDA, 启动! (本文结合Unidbg分析ARM64的so)
找函数
发现Check在so里并未被导出, 找JNI_OnLoad
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
__int64 v3;
_QWORD v4[2];
v4[1] = *(_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[0] + 48LL))(
v4[0],
"com/zj/wuaipojie2025_game/ui/BattleActivityKt");
if ( !v3 )
return -1;
if ( (*(int (__fastcall **)(_QWORD, __int64, char **, __int64))(*(_QWORD *)v4[0] + 1720LL))(
v4[0],
v3,
off_163510,
1LL) < 0 )
return -1;
return 65542;
}
(这啥玩意? 算了懒得分析了, 直接Unidbg模拟一下看看)
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@0x400e8c54[libwuaipojie2025_game.so]0xe8c54)
bool __fastcall sub_E8C54(JNIEnv *a1, jobject a2, jstring *a3)
{
const char *InputUTF;
const char *Input;
char v11;
char v14;
void (__fastcall *v15)(_QWORD *, const char *, __int64, _QWORD *);
_QWORD *v16;
_BOOL4 v19;
_QWORD v21[2];
_OWORD v22[2];
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))));
dword_16359C = -559038669;
v21[0] = 0LL;
v21[1] = 0LL;
v16 = (_QWORD *)operator new[](0x13uLL);
v15(v21, Input, 19LL, v16);
v19 = *v16 == 0x72ECF89BAF8F2748LL
&& v16[1] == 0xB63AE26B0C720798LL
&& *(_QWORD *)((char *)v16 + 11) == 0xF75942B63AE26B0CLL;
operator delete[](v16);
(*a1)->ReleaseStringUTFChars(a1, a3, Input);
}
else
{
return 0;
}
return v19;
}
可以发现v15
的地址不能确定, 直接用Unidbg断在调用v15
的那里即可
emulator.attach().addBreakPoint(md.base+0xe8f44);
最后可以发现调用函数为ao (0xe9f60)
分析
void __fastcall ao(__int128 *a1, const char *a2, __int64 a3, _QWORD *result)
{
__int64 i;
__int128 v8;
__int64 v9;
v9 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v8 = *a1;
if ( a3 )
{
for ( i = 0LL; i != a3; ++i )
{
if ( (i & 0xF) == 0 )
sub_E9954(&v8);
*((_BYTE *)result + i) = *(_BYTE *)((unsigned __int64)&v8 | i & 0xF) ^ a2[i];
}
}
}
由此我们不难发现 (只要没有和我一样一开始就跑偏去看sub_E9954), 实际上就是将字符串的每一位与不同的数进行异或然后存入result
而已
我们只要获取到i
在0-18时*(_BYTE *)((unsigned __int64)&v8 | i & 0xF)
的值就好, 我选择使用Unidbg对后面的EOR
(ARM中异或指令)进行Hook
而在那里的汇编是这样的
; ......
ORR X8, X24, X25
LDRB W9, [X21,X23]
LDRB W8, [X8]
EOR W8, W8, W9 ; Addr=0xE9FB8
STRB W8, [X19,X23]
ADD X23, X23, #1
; ......
在这里先简单讲一下EOR
指令
EOR W8, W8, W9 ; 将W8与W9进行异或, 结果存回W8 (在这里W8就是^左边那一长串, W9就是a2[i])
获取秘钥
Unidbg指令Hook
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
if (address == md.base + 0xe9fb8) {
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);
然后我们可以得到每一位要与之异或的值
[0x2e, 0x4b, 0xee, 0xc8, 0xe0, 0x95, 0x88, 0x47, 0xb0, 0x72, 0x1b, 0x68, 0x40, 0xd0, 0xa, 0x84, 0x27, 0xaf, 0xf3]
那么现在我们有异或的值, 还需要知道正确的结果才能得到Flag
这时往前看, 回到刚才的函数Check (sub_E8C54
), 有这样一段代码
_QWORD *v16
v16 = (_QWORD *)operator new[](0x13uLL);
v15(v21, Input, 19LL, v16);
v19 = *v16 == 0x72ECF89BAF8F2748LL
&& v16[1] == 0xB63AE26B0C720798LL
&& *(_QWORD *)((char *)v16 + 11) == 0xF75942B63AE26B0CLL;
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脚本解密
Flag = ''
for i in zip([0x48, 0x27, 0x8f, 0xaf, 0x9b, 0xf8, 0xec, 0x72, 0x98, 0x7, 0x72, 0xc, 0x6b, 0xe2, 0x3a, 0xb6, 0x42, 0x59, 0xf7], [0x2e, 0x4b, 0xee, 0xc8, 0xe0, 0x95, 0x88, 0x47, 0xb0, 0x72, 0x1b, 0x68, 0x40, 0xd0, 0xa, 0x84, 0x27, 0xaf, 0xf3]):
Flag += chr(i[0] ^ i[1])
print(Flag)
最后输出结果
flag{md5(uid+202eö
好像最后那点不太对? 不过猜也能猜出来是uid+2025
毕竟2025年嘛
实际上应该是flag{md5(uid+2025)}
......吧? (但我从apk里输入进去也不对啊......)
(顺带提一嘴, 秘钥输入框需要在进入战斗后点右上角那个圆形才会出现... 应该不会有人和我一样直到快做完题才发现吧)