GrumpyJellyfish 发表于 2025-2-14 02:53

【2025春节】解题领红包之四 Android 中级题WriteUp

本帖最后由 GrumpyJellyfish 于 2025-2-14 03:57 编辑

# 【2025春节】解题领红包之四 Android 中级题WriteUp

# 0x00 初探虎先锋

> **题目题干如下:**
>
> 出题老师:正己
>
> 题目简介:《黑神话·虎先锋の猴肉108种烹饪方式》通关失败后,张师傅怒摔手柄,突然瞥见室友王大爷的屏幕金光大作:"无限定身术+金刚不坏+暴击999%!"
>
> "年轻人,听说过科学修仙吗?"王大爷反手一个Alt+F4,显示器残留着神秘代码:
>
> 风灵月影宗弟子认证:输入[宗门秘钥]可解锁修改权限
>
> (温馨提示:秘钥格式请参考——flag{我是秘钥},"我是秘钥"的真实内容需要动态运算得出)



第1步:直接进入战斗!然后败了😭

第2步:根据提示点击右上角风灵月影

第3步:输入密钥查看交互信息

第4步:jadx查找字符串“密钥错误,请重试!”


第5步:找到实现代码如下:

```java
    /* renamed from: invoke */
    public final void m5invoke() {
      String BattleScreen$lambda$21;
      BattleScreen$lambda$21 = BattleActivityKt.BattleScreen$lambda$21(this.$activationCode$delegate);
      //只要Check函数返回True就能激活风灵月影
      if (!BattleActivityKt.Check(BattleScreen$lambda$21)) {
            Toast.makeText(this.$context, "秘钥错误,请重试!", 0).show();
            return;
      }
      BattleActivityKt.BattleScreen$lambda$7(this.$playerHp$delegate, BattleActivityKt.BattleScreen$lambda$3(this.$maxHp$delegate));
      BattleActivityKt.BattleScreen$lambda$10(this.$enemyHp$delegate, BattleActivityKt.BattleScreen$lambda$3(this.$maxHp$delegate));
      this.$battleResult$delegate.setValue("");
      this.$context.clearBattleLog();
      BattleActivityKt.BattleScreen$lambda$25(this.$playerAttackPower$delegate, 9999);
      this.$playerDefense.f5315i = 999;
      BattleActivityKt.updateLog(this.$context, "════════════════════");
      BattleActivityKt.updateLog(this.$context, "★ 风灵月影已激活 ★");
      BattleActivityKt.updateLog(this.$context, "➤ 攻击力提升至9999");
      BattleActivityKt.updateLog(this.$context, "➤ 防御力提升至999");
      BattleActivityKt.updateLog(this.$context, "➤ 生命值已重置");
      BattleActivityKt.updateLog(this.$context, "════════════════════");
      BattleActivityKt.BattleScreen$lambda$19(this.$showActivationDialog$delegate, false);
    }
}
```

第6步:算法助手直接hook这个Check函数,直接拿下虎先锋!


第7步:提交flag!欸不对我的flag呢?

# 0x01 攻克Check函数

**jadx中查找Check的声明发现其只有声明,调用了SO层实现**

```JAVA
//声明函数
public static final native boolean Check(String str);
//调用SO
static {
    System.loadLibrary("wuaipojie2025_game");
}
```

解压缩APK拿到libwuaipojie2025_game.so文件,直接上IDA

>分析了arm64-v8a、X86、armeabi-v7a感觉armeabi-v7a的难度较低,推荐armeabi-v7a

# 0x02 IDA分析libwuaipojie2025_game.so

第1步:找到函数实现

> 在export中搜索java没有找到相关函数,初步判定为JNI动态注册。
>
> 看到有JNI_OnLoad函数从他入手

```cpp
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
jint v2; // r5
int v3; // r0
int v5; // BYREF
//此处v3、v5应该都是JNIEnv *类型;off_134D10应该就是g_methods函数
if ( (*vm)->GetEnv(vm, (void **)&v5, 65542) )
    return -1;
v3 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)v5 + 24))(
         v5,
         "com/zj/wuaipojie2025_game/ui/BattleActivityKt");
v2 = -1;
if ( v3 && (*(int (__fastcall **)(int, int, char **, int))(*(_DWORD *)v5 + 860))(v5, v3, off_134D10, 1) > -1 )
    return 65542;
return v2;
}
```

根据JNI实现经验修改部分变量类型:

```cpp
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
jint v2; // r5
JNIEnv *env; // r0
JNIEnv *v5; // BYREF
//此处v3、v5应该都是JNIEnv *类型;off_134D10应该就是g_methods函数
if ( (*vm)->GetEnv(vm, (void **)&v5, 65542) )
    return -1;
env = (JNIEnv *)(*v5)->FindClass(v5, "com/zj/wuaipojie2025_game/ui/BattleActivityKt");
v2 = -1;
if ( env && (*v5)->RegisterNatives(v5, env, (const JNINativeMethod *)g_methods, 1) > -1 )
    return 65542;
return v2;
}
```

转到g_methods的汇编地址看到如下内容:

```assembly
.data:00134D10 off_134D10      DCD aCheck            ; DATA XREF: LOAD:0000009C↑o
.data:00134D10                                       ; JNI_OnLoad+46↑o ...
.data:00134D10                                       ; "Check"
.data:00134D14               DCD aLjavaLangStrin   ; "(Ljava/lang/String;)Z"
.data:00134D18               DCD sub_BE440+1
.data:00134D1C unk_134D1C      DCB 0x73 ; s            ; DATA XREF: A(void)+20↑o
.data:00134D1C                                       ; A(void)+2E↑o ...
```

可知sub_BE440函数就是Check的实现位置,反编译结果如下:

```cpp
bool __fastcall sub_BE440(int a1, int a2, int a3)
{
int v5; // r4
int v6; // r0
int v7; // r9
int v8; // r4
int v9; // r6
int v10; // r1
unsigned int v11; // r6
char *v12; // r4
_BOOL4 v13; // r8
int v14; // r0
void (__fastcall *v15)(_BYTE *, int, int, void *); // r8
void *v16; // r5
int v17; // r4
const std::nothrow_t *v18; // r1
unsigned __int64 v20; //
_BYTE v21; // BYREF
_QWORD v22; // BYREF

v5 = 0;
v6 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)a1 + 676))(a1, a3, 0);
if ( v6 )
{
    v7 = v6;
    HIDWORD(v20) = a3;
    v8 = A();
    v9 = CNJAK();
    if ( !byte_134E49 )
    {
      afdm::decrypt_buffer((afdm *)byte_134D7E, &byte_4, 0xA8FC3415, v20);
      byte_134E49 = 1;
    }
    v10 = -1;
    if ( v8 )
      v10 = 1;
    v11 = v9 + v10;
    v12 = getenv(byte_134D7E);
    v13 = v12 == 0 || v11 < 3;
    v14 = jgbjkb();
    if ( v11 <= 2 && v12 )
    {
      v13 = 1;
      dword_134D90 = -559038669;
    }
    v22 = *(_QWORD *)&off_12FCE8;
    v22 = *(_QWORD *)&off_12FCF0;
    v15 = (void (__fastcall *)(_BYTE *, int, int, void *))nullsub_9(*(_DWORD *)((unsigned int)v22 | (4 * ((v14 | v13) ^ (unsigned int)sub_BE6CC & 1 ^ (((unsigned int)ao ^ (unsigned int)a) >> 24) & 1))));
    dword_134D90 = -559038669;
    memset(v21, 0, sizeof(v21));
    v16 = (void *)operator new[](0x13u);
    v15(v21, v7, 19, v16);
    v17 = memcmp(v16, &unk_3A0FC, 0x13u);
    operator delete[](v16, v18);
    (*(void (__fastcall **)(int, _DWORD, int))(*(_DWORD *)a1 + 680))(a1, HIDWORD(v20), v7);
    return v17 == 0;
}
return v5;
}
```

根据已知内容更改变量类型与变量名:

```cpp
jboolean __fastcall check(JNIEnv *env, jobject obj, jstring jkey) //此处根据JNI实现和Check声明修改变量类型
{
jboolean v5; // r4
const char *key; // r0
const char *key1; // r9
int v8; // r4
int v9; // r6
int v10; // r1
unsigned int v11; // r6
char *v12; // r4
_BOOL4 v13; // r8
int v14; // r0
void (__fastcall *fun_enc)(_BYTE *, const char *, int, void *); // r8
void *v16; // r5
int v17; // r4
const std::nothrow_t *v18; // r1
unsigned __int64 v20; //
_BYTE v21; // BYREF
_QWORD v22; // BYREF

v5 = 0;
key = (*env)->GetStringUTFChars(env, jkey, 0);
if ( key )
{
    key1 = key;
    HIDWORD(v20) = jkey;                        // 让v20变量的前8位赋值位jkey
    v8 = A();
    v9 = CNJAK();
    if ( !byte_CA839E49 )
    {
      afdm::decrypt_buffer((afdm *)byte_CA839D7E, (char *)4, 0xA8FC3415, v20);
      byte_CA839E49 = 1;
    }
    v10 = -1;
    if ( v8 )
      v10 = 1;
    v11 = v9 + v10;
    v12 = getenv(byte_CA839D7E);
    v13 = v12 == 0 || v11 < 3;
    v14 = jgbjkb();
    if ( v11 <= 2 && v12 )
    {
      v13 = 1;
      dword_CA839D90 = -559038669;
    }
    //上面这一大段都搞不清楚是干嘛的,一开始看名字还以为是解密函数,浪费了好多时间。
    //正己大佬说这个是反调试,如果没过就会走错误的ao方法,过了走a正确方法
    v22 = *(_QWORD *)&off_C16B1CE8;// 指向函数a的指针
    v22 = *(_QWORD *)&off_C16B1CF0;// 指向函数ao的指针
    //这个应该就是加密函数,通过动态计算赋值fun_enc的地址    fun_enc = (void (__fastcall *)(_BYTE *, const char *, int, void *))nullsub_9(*(_DWORD *)((unsigned int)v22 | (4 * ((v14 | v13) ^ (unsigned int)sub_CA7C36CC & 1 ^ (((unsigned int)ao ^ (unsigned int)a) >> 24) & 1))));
    //fun_enc = (void (__fastcall *)(...)) nullsub_9(*(_DWORD *)( (unsigned int)v22 | (4 * ( ... )) ));
    //关键逻辑:表达式 (unsigned int)v22 | (4 * (...)) 用于计算 v22 数组的索引偏移
      //v22 是数组基址,4 * (...) 是索引偏移(QWORD 类型占8字节,但此处可能因对齐或混淆设计为4字节步进)
      //实际偏移由 (v14 | v13) ^ ... 的位运算结果决定,最终目标为 0(选a)或 1(选ao)

    dword_CA839D90 = -559038669;
    memset(v21, 0, sizeof(v21));      //把v21的值全部赋0
    v16 = (void *)operator new[](0x13u);      //调用加密函数,这里新申请了一段0x13u的内存v16应该是把结果返回到v16
    fun_enc(v21, key1, 19, v16);      //这里判断v16和&unk_CA73F0FC,共0x13(十进制19)位应该就是校验flag是否正确,如果两值前19位相等的话的话v17赋值0
    v17 = memcmp(v16, &unk_CA73F0FC, 0x13u);
    operator delete[](v16, v18);
    (*env)->ReleaseStringUTFChars(env, (jstring)HIDWORD(v20), key1);
    //当v17的值0时返回true
    return v17 == 0;
}
return v5;
}
```


反调试分析

```cpp
//根据下面这个表达式返回的值确定fun_enc指向的函数地址
(unsigned int)v22 | (4 * (
    (v14 | v13) ^
    (sub_CA7C36CC & 1) ^
    (((ao ^ a) >> 24) & 1)
))
//返回只有两种可能v22或v22,其中v22 = *(_QWORD *)&off_C16B1CE8;(指向函数a的指针)v22 = *(_QWORD *)&off_C16B1CF0;(指向函数ao的指针)

//下面这个表达式返回的就决定了是v22指向的是还是
    (v14 | v13) ^         //其中v13、v14在上面进行了赋值;v13=(getenv()==0),v14=jgbjkb()
    (sub_CA7C36CC & 1) ^         //sub_CA7C36CC只return 0,所以&1结果为0
    (((ao ^ a) >> 24) & 1)      //ao ^ a默认没改的话为0,“>>24”取高8位也为0,所以&1结果为0
```

所有这里的反调试关键点在于上面需要满足以下两个条件:

- getenv()!=0
- jgbjkb()==0


加密函数

```cpp
//调用代码fun_enc(v21, key1, 19, v16);
//这里times为19
void __fastcall fun_enc(_BYTE *a1, char *a2, int times, void *a4)
{
__int64 v5; // d17
int i; // r6
int v9; // r5
char v10; // r0
_BYTE v11; // BYREF

v5 = *((_QWORD *)a1 + 1);
*(_QWORD *)v11 = *(_QWORD *)a1;      //把a1赋值给v11其实就是上面父函数的v21,输入的时候v21是全0的
*(_QWORD *)&v11 = v5;
if ( times )
{
    for ( i = 0; i != times; ++i )
    {
      v9 = i & 0xF;      //当循环到i=0、i=16时v9会等于0进入下面的if
      if ( (i & 0xF) == 0 )
      fun_enc2(v11);      //这个函数十分复杂(分析了大半天看不懂放弃了),会改变v11的内容
      v10 = a2 ^ v11;      //v10的值为a2异或v11,其中a2就是flag,v9是0到15,v11是一个16字节的数组,会在v9=0时发生变化。
      *((_BYTE *)a4 + i) = v10;      //赋值a4,就是父函数的v16
      v11 = v10;      //改变v11的第v9位
    }
}
}
```

&unk_CA73F0FC的值如下:

```assembly
.rodata:0003A0FC unk_3A0FC       DCB 0x48 ; H            ; DATA XREF: sub_BE440+216↓o
.rodata:0003A0FD               DCB 0x27 ; '
.rodata:0003A0FE               DCB 0x8F
.rodata:0003A0FF               DCB 0xAF
.rodata:0003A100               DCB 0x9B
.rodata:0003A101               DCB 0xF8
.rodata:0003A102               DCB 0xEC
.rodata:0003A103               DCB 0x72 ; r
.rodata:0003A104               DCB 0x98
.rodata:0003A105               DCB    7
.rodata:0003A106               DCB 0x72 ; r
.rodata:0003A107               DCB0xC
.rodata:0003A108               DCB 0x6B ; k
.rodata:0003A109               DCB 0xE2
.rodata:0003A10A               DCB 0x3A ; :
.rodata:0003A10B               DCB 0xB6
.rodata:0003A10C               DCB 0x42 ; B
.rodata:0003A10D               DCB 0x59 ; Y
.rodata:0003A10E               DCB 0xF7
//48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7
```

**结论:**

> 要想Check返回True需要v16=48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7
>
> v16的值在函数fun_enc中赋值,其赋值形式是flag与fun_enc内的数组v11异或(可逆)
>
> 那么只需要知道v11的内容与48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7异或回去即可得到flag。

**需注意!!!v11是一个16个字节元素的数组,48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7(有19个字节)。而v11在第1次和17次使用时会被fun_enc2改变,所以动态调试的关键点在v11的两次变化过程的值**

# 0x03 上动态调试

第1步:改apk包,lib目录只留下armeabi-v7a那个文件夹,然后更改权限增加下面两个权限。

> android:debuggable="true"
>
> android:extractNativeLibs="true"

第2步:IDA动态调试


第3步:先异或获取前16位flag

```python
#i=0时v11的值为
#2E 4B EE C8 E0 95 88 47 B0 72 1B 68 40 D0 0A 84
#与&unk_CA73F0FC异或
#48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7

# 输入的16进制字符串
hex_str1 = "2E 4B EE C8 E0 95 88 47 B0 72 1B 68 40 D0 0A 84"
hex_str2 = "48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6"

# 将16进制字符串转换为字节列表
bytes1 = bytes.fromhex(hex_str1.replace(" ", ""))
bytes2 = bytes.fromhex(hex_str2.replace(" ", ""))

# 确保两个字节序列长度相同
assert len(bytes1) == len(bytes2), "字节序列长度不匹配"

# 异或操作并转换为字符
result = ''.join(chr(b1 ^ b2) for b1, b2 in zip(bytes1, bytes2))

print("XOR结果转换为字符:", result)
#输出结果:XOR结果转换为字符: flag{md5(uid+202

xor_bytes = bytes(b1 ^ b2 for b1, b2 in zip(bytes1, bytes2))
print("XOR结果的16进制表示:", ' '.join(f'{b:02X}' for b in xor_bytes))
#输出结果:XOR结果的16进制表示: 66 6C 61 67 7B 6D 64 35 28 75 69 64 2B 32 30 32
```

**获取到前16位flag为flag{md5(uid+202:**

第4步:获取剩下的3位flag

把“flag{md5(uid+202123”加上随机的3位输入到程序中,查看第17轮即i=16时fun_enc会把v11改成什么


发现v11变成了77 70 8A 5F D8 7D B0 5C 90 E6 35 8C D0 4C F9 BB,把新3位放回脚本算出答案

```python
# 输入的16进制字符串
hex_str1 = "2E 4B EE C8 E0 95 88 47 B0 72 1B 68 40 D0 0A 84 77 70 8A"
hex_str2 = "48 27 8F AF 9B F8 EC 72 98 07 72 0C 6B E2 3A B6 42 59 F7"

# 将16进制字符串转换为字节列表
bytes1 = bytes.fromhex(hex_str1.replace(" ", ""))
bytes2 = bytes.fromhex(hex_str2.replace(" ", ""))

# 确保两个字节序列长度相同
assert len(bytes1) == len(bytes2), "字节序列长度不匹配"

# 异或操作并转换为字符
result = ''.join(chr(b1 ^ b2) for b1, b2 in zip(bytes1, bytes2))

print("XOR结果转换为字符:", result)
#XOR结果转换为字符: flag{md5(uid+2025)}
```

**得出最后答案 flag{md5(uid+2025)}**

# 0x04 拿下flag

**flag的坑**

算出答案非常开心,就去算uid+2025的md5

```python
#UID2121487+2025即2123512
hashlib.md5(str(2123512).encode('utf8')).hexdigest()
'4ac37b3b46367455af865e516fdef7d0'
#得出flag{4ac37b3b46367455af865e516fdef7d0}
#结果提交错误了
```

最后琢磨了以下应该是字符串相加

```python
#UID2121487+2025即21214872025
hashlib.md5(str(21214872025).encode('utf8')).hexdigest()
'c9ca680ed9f6b0a6f0cd49cebb626bc0'
```

最终答案flag{c9ca680ed9f6b0a6f0cd49cebb626bc0}

# 0x05 推广

[**【2025春节】解题领红包之番外篇一二三WriteUp**](https://www.52pojie.cn/thread-2005805-1-1.html)

[**《安卓逆向这档事》十二、大佬帮我分析一下**](https://www.52pojie.cn/thread-1809646-1-1.html)

# 0x06 【新年祝福】致52pojie的所有逆向勇士们

转眼又到新年钟声响起时,回想这一年我们熬过的夜——从OD到x64dbg的丝滑切换,从花指令对抗到反调试过招,发际线在堆栈中稳步后移(~~bushi~~)。愿新的一年里:

> ① 功力暴涨如IDA反编译F5秒出伪代码
> ② 灵感迸发像VS装好了Resharper般丝滑
> ③ 遇坑必填,玄学bug自动退散
> ④ Hook人生如Monitor精准捕获每个高光时刻
> ⑤ 在逆向的星辰大海里,永远做那个手握IDA和WinHex的追风少年

祝各位:
      **永无蓝屏,一调就过!**
      **栈不溢出,堆不泄漏!**
      **早日拿下内核,轻松搞定协议栈!**
(防杠声明:本祝福已通过ASLR随机化处理,祝大家逆向功力突破熵增定律)

让咱们继续用代码改变世界,用逆向探索未知!新年卷起来~(*手动狗头保命*)

❤❤最后感谢论坛大佬的无私奉献❤❤

GrumpyJellyfish 发表于 2025-2-16 00:05

本帖最后由 GrumpyJellyfish 于 2025-2-16 17:08 编辑

leger1210 发表于 2025-2-14 07:03
反调试那边能再讲讲吗,v13 | v14 应该随便哪个为0都行吧
# 关于程序里面的反调试(检测到异常并不会退出,而是引导到错误的加密函数里面去):

## 0x01 分析校验是否异常的判断逻辑

``` cpp
fun_enc = (void(__fastcall *)                   // 函数返回情况声明
         (_BYTE *, const char *, int, void *) // 函数参数需求声明
         )
    nullsub_9(//函数实现方式,这里这个nullsub_9就是个假函数,其直接返回给他的参数值也就是下面这一串
      *(_DWORD *)(
            (unsigned int)v22 | (4 * ((v14 | v13) ^ (unsigned int)sub_C173F6CC & 1 ^ (((unsigned int)ao ^ (unsigned int)a) >> 24) & 1)) //那么关键点就在这里了,单独拎出来分析
      )
    );
```

**根据运算优先级“按位异或^”优先级高于“按位或|”给他们分段**

```cpp
(unsigned int)v22 | //第四步:这里是通过“数组地址 ‘或’ 偏移量”达到指向数组元素的目的。这里反编译应该是有点问题,v22这个数组应该是4字节的,那么0号元素是a,1号元素应该是ao。
(4 * (
    (v14 | v13) ^   //第三步:所以结果看这里。因为是异或,所以v14|v13的结果决定了这个括号的返回
    (unsigned int)sub_C173F6CC & 1 ^    //第一步:这个函数地址是0xC173F6CC,所以&1结果是1
    (((unsigned int)ao ^ (unsigned int)a) >> 24) & 1)   //第二步:因为ao跟a声明的地址挨的很近,所以他们两个函数地址异或后的的高8位(应该一共32位,右移24位即高8位)应该都是0。0与1结果还是0,a的函数C173FCEC,ao的函数C17402F0
)
//所以当(v14|v13)=1才能走a,那么需要v14=1或v13=1
//根据上面代码
//v14 = jgbjkb(); 这个函数不知道是干啥的,他会返回1
//v13= (v12 == 0 || v11 < 3);后面这个v11应该是一共很大的数,它不会小于3的,所以只看v12
//v12 = getenv(byte_C17B5D7E); 这个byte_C17B5D7E会变成“/proc/cpuinfo”没太搞懂这个什么意思,他在我的环境里面会返回0,所以v13就是1了
```


## 0x02 下面分析下两个函数的差异

```cpp
// 这个是这个正确函数
void __fastcall a(_QWORD *a1, int a2, int a3, int a4)
{
    __int64 v5;
    int i;
    _QWORD v9;
    v5 = a1;
    v9 = *a1;
    v9 = v5;
    if (a3)
    {
      for (i = 0; i != a3; ++i)
      {
            if ((i & 0xF) == 0)
                sub_BED58((uint8_t *)v9);
            *(_BYTE *)(a4 + i) = *(_BYTE *)(a2 + i) ^ *((_BYTE *)v9 + (i & 0xF));
            *((_BYTE *)v9 + (i & 0xF)) = *(_BYTE *)(a2 + i) ^ *((_BYTE *)v9 + (i & 0xF));
            // 正确函数这里多了一个对v9的赋值,如果v9不变那么第16次循环v9新的值前三位会有问题
      }
    }
}

// 这个是这个是校验没过的时候跑的错误函数
void __fastcall ao(_QWORD *a1, int a2, int a3, int a4)
{
    __int64 v5;
    int i;
    _QWORD v9;
    v5 = a1;
    v9 = *a1;
    v9 = v5;
    if (a3)
    {
      for (i = 0; i != a3; ++i)
      {
            if ((i & 0xF) == 0)
                sub_BED58((uint8_t *)v9);
            *(_BYTE *)(a4 + i) = *(_BYTE *)(a2 + i) ^ *((_BYTE *)v9 + (i & 0xF));
      }
    }
}
```

大佬帮忙看下我这样分析对不对@正己 ,也请教下这两个问题:
1、getenv(/proc/cpuinfo)这个东西是干嘛的
2、v22这个数组这里是乘4,不应该乘8吗?

抱薪风雪雾 发表于 2025-2-14 09:28

晓年过了,看到了这个红包

Hmily 发表于 2025-2-14 06:21

你这最后是不是gpt生成的,哈哈哈。

leger1210 发表于 2025-2-14 07:03

反调试那边能再讲讲吗,v13 | v14 应该随便哪个为0都行吧

GrumpyJellyfish 发表于 2025-2-14 07:55

Hmily 发表于 2025-2-14 06:21
你这最后是不是gpt生成的,哈哈哈。

大佬早上好呀,这么快就被发现了吗?{:1_886:}
这祝福是不是太有梗了,有没有直戳心窝子呀:lol

GrumpyJellyfish 发表于 2025-2-14 08:03

leger1210 发表于 2025-2-14 07:03
反调试那边能再讲讲吗,v13 | v14 应该随便哪个为0都行吧

这个应该是得两个都0,晚上下班回去再仔细分析下

heptamemory 发表于 2025-2-14 09:45

忘记搭了 o(╥﹏╥)o

Dahl 发表于 2025-2-14 18:44

要怎样动态调试呀 有没有啥教程可以看看 刚开始学还不知道怎么动调安卓的so文件 {:1_923:}

GrumpyJellyfish 发表于 2025-2-14 18:46

本帖最后由 GrumpyJellyfish 于 2025-2-14 18:47 编辑

Dahl 发表于 2025-2-14 18:44
要怎样动态调试呀 有没有啥教程可以看看 刚开始学还不知道怎么动调安卓的so文件
嘿嘿,终于有人问了,你看我推广里面的第二个,正己大佬的教程有说动态调试,如果基础不好的话建议重头开始看。加油哦

Dahl 发表于 2025-2-14 19:14

GrumpyJellyfish 发表于 2025-2-14 18:46
嘿嘿,终于有人问了,你看我推广里面的第二个,正己大佬的教程有说动态调试,如果基础不好的话建议重头开 ...

好的好的 这就去学习学习 谢谢大佬{:1_919:}
页: [1] 2 3
查看完整版本: 【2025春节】解题领红包之四 Android 中级题WriteUp