吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 2125|回复: 9
收起左侧

[Android CTF] 2025春节红包之Android中级题Writeup

  [复制链接]
Doratmon 发表于 2025-2-28 11:19

战斗了几次,天命人伤害值1,虎先锋伤害值135,两者都是1000血量,根本没法打赢。
1.png

0x00 定位Check()方法

activity记录器定位到战斗页面是com.zj.wuaipojie2025_game.ui.BattleActivity
2.png

猜测点击“开始战斗”触发了战斗的代码逻辑,如果能够hook修改天命人的伤害点数,或许可以胜利?丢进Jadx分析,逛了一圈没找到Button相关的代码,还是想的太简单了。

点点点发现了输入秘钥的入口,随便输几个字母提示“秘钥错误,请重试”。搜索错误提示发现了Check()方法,功能是做秘钥验证的,如果返回true,风灵月影就激活了。
3.png 4.png

使用frida让Check()方法始终返回为true,代码如下:

function hookCheck(){
    let BattleActivityKt = Java.use("com.zj.wuaipojie2025_game.ui.BattleActivityKt");
    BattleActivityKt["Check"].implementation = function (str) {
        console.log(`BattleActivityKt.Check is called: str=${str}`);
        let result = this["Check"](str);
        console.log(`BattleActivityKt.Check result=${result}`);
        return true;
    };
}

虎先锋被秒了!等等,我的flag呢???这个思路有点问题,或许输入的秘钥可能就是flag。
5.png

0x01 JNI动态注册识别

自然而然想到去审计Check()方法,这是一个native方法。导入libwuaipojie2025_game.so到IDA,搜索“Check”,没有形如Java_xxx_Check() 的方法,说明不是这个native方法不是静态注册的。

关于JNI静态、动态注册的知识忘记了,参考这篇帖子复习了一下:安卓逆向基础知识之JNI开发与so层逆向基础

动态注册的话找到JNI_OnLoad()方法,(*(_QWORD *)v4[0] + 1720LL)是通过 JNIEnv 指针获取 RegisterNatives 方法的地址,所以14~18行调用RegisterNatives方法对Check()方法进行了注册。

6.png

off_163510是注册的jni方法,类型为JNINativeMethod,参考jni.h源码(https://github.com/openjdk/jdk/blob/3ebed78328bd64d2e18369d63d6ea323b87a7b24/src/java.base/share/native/include/jni.h#L1817) 如下:

//RegisterNatives方法原型
jint RegisterNatives(jclass clazz, const JNINativeMethod *methods, jint nMethods) {
    return functions->RegisterNatives(this,clazz,methods,nMethods);
}

//JNINativeMethod结构体定义
typedef struct {
    const char* name;        // 本地方法在Java中的名称
    const char* signature;   // 本地方法的签名,描述参数和返回类型
    void*       fnPtr;       // 指向本地方法实现的函数指针
} JNINativeMethod;

这里只关注第三个值fnPtr,因为它保存了注册方法在so里的函数地址。对应到IDA里就是sub_E8C54

7.png

修改sub_E8C54()函数参数类型为JNIEnv *a1, jclass a2, jstring a3。大概的逻辑是:输入秘钥a3在sub_E8C54()内部处理后与真实秘钥比较,正确就返回true,天命人得到“风灵月影”,反之验证失败。

分析a3数据流的结果:a3->v5->v6->v15,函数指针v15指向了什么函数最终决定了a3经过什么样的处理

辨析:函数指针是指向函数的指针变量,而指针函数是返回指针类型的函数。

8.png
9.png

第51行代码的作用是给函数指针v15赋值,从而决定在56行执行的函数是哪个。第51行代码的执行结果十分关键,不过这行代码我不知道咋分析。

我个人的分析到这里就没能继续往下了,后面的工作参考了吾爱破解上的大佬的帖子:

【2025春节】解题领红包之四 Android 中级题WriteUp - 吾爱破解 - 52pojie.cn

0x02 动态调试分析秘钥

静态分析卡住了,可以上动态调试看一下结果。参考这两篇帖子:

《安卓逆向这档事》十二、大佬帮我分析一下 - 吾爱破解 - 52pojie.cn

[超级详细]实战分析一个Crackme的过程 - 吾爱破解 - 52pojie.cn

动态调试的准备工作:

  1. adb push IDA dbgsrv目录下载的android server到手机的/data/local/tmp目录。arm64-v8a选择android_server64

  2. 修改android_server64权限为777,然后XAppDebug hook目标app

    或者使用android:debuggable="true"

  3. 手机端以root权限运行./android_server64adb forward tcp:23946 tcp:23946

    手机端运行要调试的app。

    adb shell am start -n com.zj.wuaipojie2025_game/.MainActivity

    或者直接点击运行

动态调试步骤:

  1. IDA端

10.png

  1. Debugger -> Process Options

11.png

  1. Debugger->Attach Process

    如果找不到目标app,请解锁手机,并切换到手机app,保持可见状态。

动态调试时找不到对应的so文件,两种解决办法:

  1. 把so文件拷贝到app安装目录下的lib文件夹,先用pm path 包名确定路径,然后拷贝到对应lib目录,拷贝了lib后,需要重新启动app。
  2. 使用android:extractNativeLibs="true"属性。

实际动态调试过程中,发现代码v15(v21, v6, 19LL, v16);始终会跳转到ao函数执行,正己大佬说这是个反调试,过了反调试才会走正确的a函数。

正己大佬提示:

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

帖子链接:2025解题领红包 Android中级详细WP - 吾爱破解 - 52pojie.cn

涉及到函数跳转赋值的关键的代码简化后如下:

  v10 = getenv(byte_7767FEA58A);
  v11 = jgbjkb();

  ...

  if ( v10 )
    v13 = v12 >= 3;
  else
    v13 = 0;
  v14 = !v13;

  ...

  v22[1] = *(_OWORD *)off_7767FE1638;
  v22[0] = *(_OWORD *)off_7767FE1628;

  v15 = (void (__fastcall *)(__int64 *, __int64, __int64, _QWORD *))nullsub_2(*(_QWORD *)(
(unsigned __int64)v22 & 0xFFFFFFFFFFFFFFF7LL | //第四步:v22 & 0xFFFFFFFFFFFFFFF7LL |8*(v11|14) ^ 0
(8LL * (((unsigned __int8)(v11 | v14) //第三步:8*(v11|14) ^ 0 
^ (((unsigned int)ao ^ (unsigned int)a) >> 24)) //第一步:(0x7764532F60^0x77645328A0)>>24 ==> 0
& 1))));//第二步:0&1 ==> 0

数组v22的内存布局如下:

12.png

所以要让v15执行函数a的逻辑,需要第三步的结果为8,也就是(v11|v14)的值为1,最终第四步的结果为v22,这样v15就指向了v22[0],然后(_QWORD*)进行8字节转换,v15也就指向了a函数。

分析到这里,我的思路是让(v11|v14)的值为1,数据流回溯一下就是需要控制下面两行代码

v10 = getenv(byte_7767FEA58A); //让v10等于0,这样v14就等于1
v11 = jgbjkb()//让v11等于0

//最终v11|v14 ==> 0|1  ==> 1

具体来说,就是在动态调试的时候修改函数的返回值,对应到寄存器X0。实际动态调试过程中,返回值并不可控,比如v10 = getenv(byte_7767FEA58A);在执行完成后v10为0,但是执行完给v15赋值的语句,v14的值又发生了变化,且执行完第a行,单步调试后可能会返回去执行第a-1行。加上我对ARM汇编也不是很熟悉,这种思路就暂时放弃了。

换种思路,直接去审计a()函数和ao()函数的实现逻辑,可结合deepseek分析代码。以下是a()函数的实现:

//v6是Check()方法的入参,v16是一个大小为19的QWORD数组
v15(v21, v6, 19LL, v16);
....
/*
a方法实现
a2是Check()方法的入参,a3=19,a4是一个大小为19的QWORD数组
*/
__int128 *__fastcall a(__int128 *result, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 i; // x23
  char v8; // w9
  __int64 v9; // x25
  __int128 v10; // [xsp+0h] [xbp-20h] BYREF
  __int64 v11; // [xsp+18h] [xbp-8h]

  v11 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v10 = *result;
  if ( a3 )
  {
    for ( i = 0LL; i != a3; ++i )//循环19次
    {
      v9 = i & 0xF;
      if ( (i & 0xF) == 0 )//i=0或i=16时满足
        result = (__int128 *)sub_7767F70954(&v10);

      //下面两行代码等价于a4[i] = v10[v9]^a2[i]
      v8 = *(_BYTE *)((unsigned __int64)&v10 | v9) ^ *(_BYTE *)(a2 + i);//a2每个字节与(v10|v9)异或计算
      *(_BYTE *)(a4 + i) = v8;//异或计算结果赋值给a4数组

      /*
      v10是__int128类型,128位,占用16字节,在内存追中16字节对齐
      因为要保证16字节对齐,在内存中v10的地址是类似于0x1000、0x1010这种形式,即低4位为0
      而v9的取值范围为0~15
      所以下面代码的效果为修改以v10为起始地址的16个字节的值,等价于v10[v9] = v8
      */
      *(_BYTE *)((unsigned __int64)&v10 | v9) = v8;
    }
  }
  return result;
}

以下时ao()函数的实现。分析后发现,ao()方法与a()方法的作用都是通过异或运算更改a4指向内存的取值,区别是v10在每次循环中均会被更新。当i=16时,第二次调用sub_7767F70954()会导致a()函数的v10和ao()函数的v8取值不同,进而在最后三次异或运算中:v10[0]^a2[16]、v10[1]^a2[17]、v10[2]^a2[18],a()函数和ao()函数的计算结果会不同。

__int128 *__fastcall ao(__int128 *result, __int64 a2, __int64 a3, __int64 a4)
{
  __int64 i; // x23
  __int128 v8; // [xsp+0h] [xbp-20h] BYREF
  __int64 v9; // [xsp+18h] [xbp-8h]

  v9 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v8 = *result;
  if ( a3 )
  {
    for ( i = 0LL; i != a3; ++i )
    {
      if ( (i & 0xF) == 0 )
          result = (__int128 *)sub_7767F70954(&v8);
      *(_BYTE *)(a4 + i) = *(_BYTE *)((unsigned __int64)&v8 | i & 0xF) ^ *(_BYTE *)(a2 + i);
      //a4[i] = v8[i&0xF] ^ a2[i]

     }
  }
  return result;
}

总结来说,a()函数的v10或ao()函数的v8是加密密钥,区别在于这两个密钥的索引16~18不同(索引从0开始计算)。

暂时没有办法执行a()方法,就用ao()方法凑合一下,密钥差别也不是很大。

当i=0时,v8取值如下:

13.png

当i=0x10时,v8取值如下

14.png

注意libwuaipojie_2025.so是arm65 little endian,遵循低字节低地址,所以密钥需要逆序,再拼接上i=0x10时的低三字节,得到ao()函数使用的加密密钥为2E 4B EE C8 E0 95 88 47 B0 72 1B 68 40 D0 0A 84 27 AF F3

结合异或运算的特点,a^b=c,已知任意a、b、c任意两个可以计算出第三个。现在有正确的加密密钥,如果有正确的加密结果就可以得到正确的输入秘钥,即正确的风灵月影秘钥。

回到sub_E8C54()函数,也就是Check()方法的native实现。基于返回true的前提,反推v16的前8字节为0x72ECF89BAF8F2748,9~16字节为0xB63AE26B0C720798,最后三字节为0xF75942

15.png

再根据小端存储调整一下顺序:48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7

现在有了正确的加密密钥,也有了正确的加密结果,可以写代码计算出正确的输入秘钥了。

偷个懒,用大模型算一下hhh

16.png

最后三字节是错的,由于是25年,最终的结果为flag{md5(uid+2025)}

但是把flag{md5(uid+2025)}作为秘钥输入到app还是会提示秘钥错误,因为ap没还是走的ao()函数的逻辑。

0x03 计算最终flag值

计算最终flag值这步由于系统关闭了,就没没法验证了。看各位大佬的帖子说flag{md5(uid+2025)}并不是最终提交到系统的flag,还需要处理一下。

uid应该就是每个人吾爱破解账号的uid,和2025拼接做一下md5计算,这样每个人提交的flag都不一样,妙啊。

我的uid是2140960,计算后得到的结果为flag{9ec9300a37560732409b878ecfbbcf96}

17.png

0x04 最后

看其他大佬的帖子还有用frida和unicorn完成的,打算后续也实操一下。

🙏感谢正己大佬辛苦出题,学到了很多知识,也感谢其他发帖的大佬提供的思路,欢迎大家评论区交流、互相学习。

免费评分

参与人数 4威望 +2 吾爱币 +103 热心值 +3 收起 理由
ML2025 + 1 + 1 用心讨论,共获提升!
xuezhang18 + 1 我很赞同!
正己 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
小菜鸟一枚 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

正己 发表于 2025-3-3 15:29
让它强制走a方法,就能验证成功啦
无闻无问 发表于 2025-3-1 07:41
你用a方法去解密,flag就是flag{md5(uid+2025)},用ao去解密末尾3个乱码,他们故意的…
 楼主| Doratmon 发表于 2025-3-1 13:26
无闻无问 发表于 2025-3-1 07:41
你用a方法去解密,flag就是flag{md5(uid+2025)},用ao去解密末尾3个乱码,他们故意的…

是的,我想动态调试走a()函数的逻辑,没有成功,最后将就走ao()的逻辑,最后三字节相当于是猜的,不过如果a()和ao()代码逻辑差太多这种方法就不适用。
伤城幻化 发表于 2025-3-3 12:29
好像是64位的库有问题,得用32的库
 楼主| Doratmon 发表于 2025-3-4 13:57
伤城幻化 发表于 2025-3-3 12:29
好像是64位的库有问题,得用32的库

是么?我对64位的库用hook的方法成功了。
 楼主| Doratmon 发表于 2025-3-4 13:58
正己 发表于 2025-3-3 15:29
让它强制走a方法,就能验证成功啦

用frida可以强制走a,IDA修改内存强制走a没成功,可能是动态调试不熟悉吧
laoshenshila 发表于 2025-3-7 19:30
很好的一个技术分享!
kaijinaini 发表于 2025-3-12 15:49
好像是64位的库有  问题,得用32的库
Simwillbetter 发表于 2025-3-14 14:08
学到了,感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-3-25 03:23

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表