吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1731|回复: 16
收起左侧

[分享] 学破解第223天,《2026春节红包解题》之抄答案都抄不明白

  [复制链接]
小菜鸟一枚 发表于 2026-4-4 20:57
本帖最后由 小菜鸟一枚 于 2026-4-4 21:02 编辑

前言:

  水区的朋友们,年轻就是资本,和我一起学逆向逆天改命吧,我的学习过程全部记录及学习资源:https://www.52pojie.cn/forum.php?mod=viewthread&tid=2093516&page=1#pid54862410

立帖为证!--------记录学习的点点滴滴

0x1 【2026春节】解题领红包之二 {Windows 初级题} 出题老师:云在天

  1.题目在这里下载:活动已结束,题目打包放到爱盘供大家下载学习:https://down.52pojie.cn/Challenge/Happy_New_Year_2026_Challenge.rar

  2.运行程序如下图,随便输入123456,提示如下

1.png

  3.那么身为菜鸟的思路当然是,搜索字符串了,使用ida展开字符串视图,看到好多的提示,这里有两种思路,一种就是从错误的位置开始向上找调用栈,让他不要进入错误流程,因为动态调试时,可以很容易的看到调用链,方便快速找到用户代码;另一种就是从正确的流程向上翻,找到输入后的函数,让他按正确流程进入,这种主要适合流程清晰很容易定位到成功代码的地方,如果有虚函数,混淆,就不太好找了。

.rdata:004D3000        00000018        C        \nPress Enter to exit...
.rdata:004D301A        00000018        C        pojie_2026_HappyNewYear
.rdata:004D3033        00000010        C        2pojie2026Happy
.rdata:004D3044        00000029        C        ========================================
.rdata:004D3070        00000029        C           CrackMe Challenge v2.5 - 2026        
.rdata:004D309C        00000028        C        Keywords: 52pojie, 2026, Happy new year
.rdata:004D30C4        0000001F        C        Hint: Fake flag; length is key
.rdata:004D30E4        00000029        C        ----------------------------------------
.rdata:004D310D        0000001A        C        \n[?] Enter the password: 
.rdata:004D3128        00000026        C        \n[!] Nice try, but not quite right...
.rdata:004D3150        00000034        C        \n[!] Hint: The length is your first real challenge.
.rdata:004D3184        0000002A        C        \n========================================
.rdata:004D31B0        00000029        C                *** SUCCESS! ***                
.rdata:004D31DC        00000025        C        [+] Congratulations! You cracked it!
.rdata:004D3201        00000013        C        [+] Correct flag: 
.rdata:004D3214        0000002C        C        \n[!] Checksum failed! Something is wrong...
.rdata:004D3240        0000001B        C        [!] Expected: 44709, Got: 
.rdata:004D325B        00000014        C        \n[X] Access Denied!
.rdata:004D3270        00000021        C        [X] Wrong password. Keep trying!
.rdata:004D3291        0000001E        C        \n[!] You're getting closer...

  4.定位成功的字符串,Ctrl+x找到调用的地方,然后翻调用栈,记录关键位置,这里借助一下deepseek,得到的如下的数据。

地址 含义
004CD2C0 前16字符校验循环
004CD2FB 长度校验入口
004CD306 比较长度是否为31
004CD34F 长度31分支,调用 sub_4016D0
004CD371 累加和计算入口
004CD39C 比较累加和是否为 0xAEA5
004CD3A3 成功跳转到 loc_4CD450
004CD450 成功打印分支开始
004CD4A2 Congratulations! 字符串地址
004D3032 前16个校验数据的地址

  5.由于我看不懂汇编代码,所以直接动调调试,看看输入输出,先在段首下断点,等会,这地址怎么是动态的,先用studype工具固定基址,重新再来,然后一直单步走,这里我还是输入123456,记录函数调用前后的输入输出,相关内容记录下来。

2.png

//第一次调试
.text:004CD294 call    sub_4C5840   //类似scanf函数,接收我的输入  

.text:004CD2A2 call    sub_401740   //这个函数目前没看出来什么,没有显示输出
.text:004CD2A7 mov     edx, 35h ; '5'//edx=5
.text:004CD2AC mov     ecx, eax
.text:004CD2AE xor     eax, eax
.text:004CD2B0 test    cl, cl
.text:004CD2B2 jz      short loc_4CD2C7

.text:004CD2C7 mov     ecx, [ebp+Str]
.text:004CD2CA cmp     [ecx+eax], dl   //前面eax清零了,dl赋值是5, 运行到这里时ecx是我们输入的123456,所以这里就是校验首字符是不是5开头。
.text:004CD2CD jnz     short loc_4CD2FB

第一次调试失败

//重来第二次调试,因为这里不符合要求,所以重新输入字符串52pojie_2026_HappyNewYear(为什么输入这个,因为前面ida搜到了.rdata:004D301A这个字符串,前面补一个5,说不定它就是flag呢)

.text:004CD2B0 test    cl, cl
.text:004CD2B2 jz      short loc_4CD2C7   //这一次在这里直接走向失败

//看一下sub_401740函数伪代码
bool __cdecl sub_401740(int a1)
{
  int v1; // eax
  char v2; // dl

  v1 = 0;
  v2 = 53;
  do
  {
    if ( *(_BYTE *)(a1 + v1) != v2 )   //比较输入的第一个字符是否等于53就是asii 5,那说明开头是5的逻辑没错,看下一句。
      return 0;
    v2 = byte_4D3019[v1++];     //v1自增,v2每次的值为byte_4D3019的第v1个字符,byte_4D3019就是.rdata:004D3019 32 70 6F 6A 69 65 5F 32 30 32+a2pojie2026Happ db '2pojie_2026_HappyNewYear',0。
  }
  while ( v2 );
  return *(_BYTE *)(a1 + 25) == 0;  //检查第26为是否为0。
}
//再看看我刚刚输的字符串,正好25个字符,第26位默认0,由于这里返回了true,所以我们才走了错误的分析
//所以52pojie_2026_HappyNewYear没问题,但是得增加长度。

//重来第三次调试,输入52pojie_2026_HappyNewYear123,这次后面加个123,再看看,怎么cmp     [ecx+eax], dl这里相等还跳。

.text:004CD2C0 loc_4CD2C0:                             ; CODE XREF: sub_4CD130+1A5↓j
.text:004CD2C0 movzx   edx, ds:byte_4D3032[eax]    //.rdata:004D3033 a2pojie2026happ db '2pojie2026Happy',0
.text:004CD2C0
.text:004CD2C7
.text:004CD2C7 loc_4CD2C7:                             ; CODE XREF: sub_4CD130+182↑j
.text:004CD2C7 mov     ecx, [ebp+Str]
.text:004CD2CA cmp     [ecx+eax], dl
.text:004CD2CD jnz     short loc_4CD2FB
.text:004CD2CD
.text:004CD2CF add     eax, 1
.text:004CD2D2 cmp     eax, 10h
.text:004CD2D5 jnz     short loc_4CD2C0

//原来这是一个循环判断,似乎这里没有下划线,那不是会false去错误分析吗,还好原来这是陷进,前面的逻辑是对的,带下划线才能进正确分支如下。

.text:004CD2FB
.text:004CD2FB loc_4CD2FB:
.text:004CD2FB mov     eax, [ebp+Str]
.text:004CD2FE mov     [esp+78h+Time], eax ; Str    虽然这里写着 Time,但这不是一个时间相关的变量,而是:IDA 自动生成的局部变量名,实际位置是 [esp],用于存储临时参数
.text:004CD301 call    strlen
.text:004CD306 cmp     eax, 1Fh   //判断长度是否是31
.text:004CD309 jz      short loc_4CD34F

//重来第四次调试,输入52pojie_2026_HappyNewYear123456,取消前面的断点,在004CD2FB下断,加快进程,到这里问问deepseek,他说这是sub_4016D0是比较函数
// 分配 0x64 (100) 字节内存
    Block = (unsigned __int8 *)sub_4CB710(0x64u);

    // 初始化/生成数据到 Block
    sub_401620(Block);

    if (a2 <= 0) {
        v4 = 0;
    } else {
        v3 = 0;
        v4 = 0;
        do {
            // 逐字节比较输入字符串和 Block 中的数据
            v5 = *(char *)(a1 + v3) == Block[v3];
            ++v3;
            v4 += v5;  // 如果相等,v4++
        } while (a2 != v3);
    }

    // 释放内存
    j_j_free(Block);

    // 返回是否所有字节都匹配
    return a2 == v4;
}

那么关键就在这个sub_401620函数里面了,看不懂不要紧,开始我们的动调大法,在第一行下断点,输入和前面一样,直接F4到这里看看,就是把固定数据写入到到内存,用result指向它,执行结束后看result指向的那块内存区域,可以看到相应字符串。
_BYTE *__cdecl sub_401620(int a1)
{
  _BYTE *result; // eax

  *(_DWORD *)a1 = 758280311;
  *(_DWORD *)(a1 + 4) = 1663511336;
  *(_DWORD *)(a1 + 8) = 1880974179;
  *(_DWORD *)(a1 + 12) = 494170226;
  *(_DWORD *)(a1 + 16) = 842146570;
  *(_DWORD *)(a1 + 20) = 657202491;
  *(_DWORD *)(a1 + 24) = 658185525;
  *(_BYTE *)(a1 + 30) = 99;
  *(_WORD *)(a1 + 28) = 12323;
  result = (_BYTE *)a1;
  do
    *result++ ^= 0x42u;    //resul指向的地址每次加1,将数据异或0x42
  while ( result != (_BYTE *)(a1 + 31) );  //循环指向a1的第31个元素时结束循环
  *(_BYTE *)(a1 + 31) = 0;     //执行完之后result往回推31个字节可以看到debug021:008111C0 a52pojie2026Hap db '52pojie!!!_2026_Happy_new_year!'
  return result;
}

  6.动态调试这段代码最快可以看到效果,如果不想动调,也可以写解密脚本,得到执行后的数据。

package ctf;

public class test01 {

        public static void main(String[] args) {

                // 写入加密数据(小端序)
                byte[] jiami_buffer = {
                            // 0x2D327077 的实际内存顺序(小端序)
                            0x77, 0x70, 0x32, 0x2D,  // 偏移 0-3   
                            // 0x63272B28
                            0x28, 0x2B, 0x27, 0x63,              // 偏移 4-7
                            // 0x701D6363
                            0x63, 0x63, 0x1D, 0x70,              // 偏移 8-11
                            // 0x1D747072
                            0x72, 0x70, 0x74, 0x1D,              // 偏移 12-15    
                            // 0x3232230A
                            0x0A, 0x23, 0x32, 0x32,              // 偏移 16-19 
                            // 0x272C1D3B
                            0x3B, 0x1D, 0x2C, 0x27,              // 偏移 20-23
                            // 0x273B1D35
                            0x35, 0x1D, 0x3B, 0x27,              // 偏移 24-27
                            // 0x3023
                            0x23, 0x30,                          // 偏移 28-29
                            // 0x63
                            0x63                                 // 偏移 30
                        };

                // XOR 0x42 解密
                for (int i = 0; i < 31; i++) {
                        jiami_buffer[i] ^= 0x42;
                }

                System.out.println("解密后:"+new String(jiami_buffer));
        }
}

运行输出:解密后:52pojie!!!_2026_Happy_new_year!

0x2 (失败)【2026春节】解题领红包之三 {Android 初级题} 出题老师:正己

  1.下载后,运行,是游戏形式,我是靠玩游戏形式拿到的flag,丢人,所以先拜读一下论坛大佬们的帖子,稍后回来。

3.png

  2.奈何看不懂,试试AI的力量,使用jadx打开,搜索界面上已有的提示“重新开始”,定位到public final class m extends K1.l implements J1.f 这个类,定位到 @Override // J1.fpublic final Object d0(Object obj, Object obj2, Object obj3) 这个方法,在重新开始的前面翻一下,定位到如下代码。

                if (f8 || D4 == c0769s) {
                    c0286i10 = c0286i11;
                    S s2 = new S(g3, 4);
                    c0810u.j0(s2);
                    obj6 = s2;
                } else {
                    c0286i10 = c0286i11;
                    obj6 = D4;
                }
                c0810u.t(false);
                H.m a4 = androidx.compose.ui.graphics.a.a(b4, (J1.c) obj6);
                c0286i3 = c0286i10;
                c0286i4 = c0286i9;
                interfaceC0772V = interfaceC0772V5;
                j02 = j04;
                M1.a.b(Q2, "查看FLAG", a4, null, g6, 0.0f, null, c0810u, 24632, 104);
                z2 = true;
                z3 = false;

  3.deepseek告诉我C0826g 就是拼图游戏的主 ViewModel,然后还要干下面两件事,贴出来之后告诉我找的地方不对,然后告诉我搜索搜索 check、complete、win 等方法,搜索complete时候,看到一段可以代码,还好稍微有点英文基础,能认识tiles标题,moveCount移动步数,isCompleted已完成两三个单词,这段代码所在的C0690b类的代码应该很关键。

1.EnumC0821b 枚举类

2.I0.B 类(拼图数据逻辑)

public final String toString() {
        return "PuzzleState(tiles=" + this.f7182a + ", emptyIndex=" + this.f7183b + ", isCompleted=" + this.f7184c + ", moveCount=" + this.f7185d + ", elapsedMs=" + this.f7186e + ", isRunning=" + this.f7187f + ", startTimeMs=" + this.f7188g + ')';
    }

  4.丢给AI后,isCompleted 字段(f7184c)就是判断拼图是否完成的标志,根本看不懂代码逻辑,所以只能尝试hook一下。

public final class C0690b {
    public final List f7182a;      // tiles — 拼图格子排列
    public final int f7183b;       // emptyIndex — 空格位置
    public final boolean f7184c;   // isCompleted — 是否完成!
    public final int f7185d;       // moveCount — 步数
    public final long f7186e;      // elapsedMs — 用时(毫秒)
    public final boolean f7187f;   // isRunning — 是否进行中
    public final long f7188g;      // startTimeMs — 开始时间
}

  5.Hook准备,我的环境是16.6.6版本的frida。

# 1. 重启 adb
adb kill-server
adb start-server

# 2. 查看设备
adb devices

# 3. 进入模拟器 shell 并启动 frida-server
adb shell
su
cd /data/local/tmp
chmod 755 frida-server
./frida-server &
exit

# 4. 转发端口
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043

# 5. 测试连接(用本地地址)
frida-ps -H 127.0.0.1

# 6. 如果成功,运行 hook
adb shell am start -n com.zj.wuaipojie2026/.MainActivity
frida -H 127.0.0.1 com.zj.wuaipojie2026 -l hook.js

# 7. 这里我不知道为什么一直失败,启动备选方案用 adb 获取 PID,用 PID 附加
adb shell ps | findstr wuaipojie2026
frida -H 127.0.0.1 3617 -l hook.js

  6.混淆的太厉害了,hook成功了也只是拼图完成了,然后播放音乐,就结束了,没看到flag,懵逼了,继续会看大佬的内容,按照这个推测flag存在这里f7615q。

InterfaceC2344J0 interfaceC2344J03 = this.f7615q;
String str = (String) interfaceC2344J03.getValue();
if (str != null) {
    // 显示"查看FLAG"按钮
}

  7.然后deepseek根据f7615q推测查看这两个实例的字段,执行这两个hook后喂给它,猜测字段 o 是 A{Completed}@877fd1a。这个 A 类很可能就是存储 FLAG 的容器!

//u1.m 查看实例有哪些字段:
Java.perform(function() {
    console.log(" Listing u1.m fields...");

    setTimeout(function() {
        Java.perform(function() {
            Java.choose("u1.m", {
                onMatch: function(instance) {
                    console.log(" Found u1.m instance");
                    console.log(" Instance: " + instance);

                    // 列出所有字段
                    var fields = instance.class.getDeclaredFields();
                    console.log(" Fields:");
                    for (var i = 0; i < fields.length; i++) {
                        var fieldName = fields[i].getName();
                        try {
                            var value = instance[fieldName].value;
                            console.log("    " + fieldName + " = " + value);
                        } catch(e) {
                            console.log("    " + fieldName + " = [cannot read]");
                        }
                    }
                },
                onComplete: function() {

                    console.log("Done");
                }
            });
        });
    }, 2000);
});

//w1.g 查看实例有哪些字段:
Java.perform(function() {
    console.log(" Listing w1.g fields...");

    setTimeout(function() {
        Java.perform(function() {
            Java.choose("w1.g", {
                onMatch: function(instance) {
                    console.log(" Found w1.g instance");
                    console.log(" Instance: " + instance);

                    var fields = instance.class.getDeclaredFields();
                    console.log(" Fields:");
                    for (var i = 0; i < fields.length; i++) {
                        var fieldName = fields[i].getName();
                        try {
                            var value = instance[fieldName].value;
                            console.log("    " + fieldName + " = " + value);
                        } catch(e) {
                            console.log("    " + fieldName + " = [cannot read]");
                        }
                    }
                },
                onComplete: function() {
                    console.log("Done");
                }
            });
        });
    }, 2000);
});

  8.疯了,ai和大佬的帖子都救不了我,切换jeb再挣扎一下,编辑-寻找,输入查看FLAG,定位到u1.m.d0方法,打上断点,报错了,解压之后是可以看到正确拼图的,每一个图片做一个标识,3乘3排列,再让deepseek试一下,标记图片位置和九宫格位置,始终不成功,放弃了。

Java.perform(function() {
    console.log(" === ULTRA-PRECISE PUZZLE SOLUTION ===");
    console.log(" CURRENT LAYOUT:");
    console.log(" [0]人1 [1]人2 [2]新");
    console.log(" [3]感  [4]空格 [5]人3");  
    console.log(" [6]人4 [7]年  [8]别");
    console.log(" TARGET LAYOUT:");
    console.log(" [0]新 [1]年 [2]别");
    console.log(" [3]人1 [4]人2 [5]感");
    console.log(" [6]人3 [7]人4 [8]冒");
    console.log(" VALUE MAPPING: 新=1, 年=2, 别=3, 人1=4, 人2=5, 感=6, 人3=7, 人4=8, 冒=9, 空格=0");

    var Toast = Java.use("android.widget.Toast");
    Toast.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function(ctx, text, duration) {
        var msg = text.toString();
        console.log("\n[TOAST] " + msg);
        if (msg.indexOf("flag") !== -1 || msg.indexOf("{") !== -1 || msg.toLowerCase().indexOf("congrat") !== -1) {
            console.log("\n ========== FLAG FOUND ==========");
            console.log(" Message: " + msg);
        }
        return this.makeText(ctx, text, duration);
    };

    // SET THE EXACT DESIRED FINAL STATE
    setTimeout(function() {
        Java.perform(function() {
            Java.choose("s1.b", {
                onMatch: function(instance) {
                    try {
                        console.log(" Setting EXACT desired state...");

                        // Current state: [人1,人2,新,感,空格,人3,人4,年,别] -> [4,5,1,6,0,7,8,2,3]
                        // Desired state: [新,年,别,人1,人2,感,人3,人4,冒] -> [1,2,3,4,5,6,7,8,9]

                        var tiles = instance.a.value;
                        var targetState = [1, 2, 3, 4, 5, 6, 7, 8, 9]; // 新,年,别,人1,人2,感,人3,人4,冒
                        var emptyIndex = 8; // Empty space should be at position 8 (where 冒 will appear)

                        console.log(" Setting tiles to: " + targetState);
                        console.log(" Empty index will be: " + emptyIndex);

                        // Method 1: Direct modification using set() if available
                        if (tiles && typeof tiles.set === 'function') {
                            for (var i = 0; i < targetState.length; i++) {
                                tiles.set(i, targetState[i]);
                            }
                            instance.b.value = emptyIndex; // empty at position 8
                            instance.c.value = true; // completed
                            instance.f.value = false; // not running

                            console.log(" ✅ Used tiles.set() method");

                        } else if (tiles && tiles.length === 9) {
                            // Method 2: Direct array assignment
                            for (var i = 0; i < targetState.length; i++) {
                                tiles[i] = targetState[i];
                            }
                            instance.b.value = emptyIndex;
                            instance.c.value = true;
                            instance.f.value = false;

                            console.log(" ✅ Used direct array assignment");

                        } else {
                            // Method 3: Try to find the correct field to modify
                            console.log(" ❗ Tiles not accessible via standard methods, trying alternative...");

                            // Let's force the completion state regardless of tiles
                            instance.c.value = true; // Mark as completed
                            instance.f.value = false; // Stop running
                            instance.b.value = 8; // Empty at target position

                            console.log(" ✅ Forced completion state");

                            // Also try to access via reflection if needed
                            try {
                                var clazz = instance.getClass();
                                var fields = clazz.getDeclaredFields();

                                for (var i = 0; i < fields.length; i++) {
                                    fields[i].setAccessible(true);
                                    var fieldName = fields[i].getName();
                                    var fieldValue = fields[i].get(instance);

                                    console.log(" Field: " + fieldName + " = " + fieldValue);

                                    // If we find an array field with length 9, modify it
                                    if (fieldValue && Array.isArray(fieldValue) && fieldValue.length === 9) {
                                        console.log(" Found tiles array in field: " + fieldName);
                                        for (var j = 0; j < targetState.length; j++) {
                                            fieldValue[j] = targetState[j];
                                        }
                                        console.log(" ✅ Modified tiles via reflection");
                                        break;
                                    }
                                }
                            } catch (reflError) {
                                console.log(" Reflection failed: " + reflError);
                            }
                        }

                        console.log(" 🎯 Puzzle state set to: [新,年,别,人1,人2,感,人3,人4,冒]");
                        console.log(" 🎯 Empty space at position 8");
                        console.log(" 🎯 Completion flag set to true");

                    } catch (e) {
                        console.log("[!] Critical error: " + e);
                        console.log("[!] Stack trace: " + e.stack);
                    }
                },
                onComplete: function() {
                    console.log(" Puzzle state modification completed");
                }
            });
        });
    }, 500);

    // TRIGGER VIEWMODEL TO UPDATE UI
    setTimeout(function() {
        Java.perform(function() {
            console.log(" Updating ViewModel to reflect changes...");

            Java.choose("w1.g", {
                onMatch: function(vm) {
                    try {
                        // Update the state immediately
                        if (vm.i && vm.i.value) {
                            var state = vm.i.value;
                            if (state.k && state.getValue) {
                                var currentState = state.getValue();
                                console.log(" Current state value: " + currentState);

                                // Create new state to force update
                                state.k(currentState);
                                console.log(" ✅ State updated via setState");
                            }
                        }

                        // Call completion methods
                        var completionMethods = ['e', 'b', 'd', 'a', 'c'];
                        completionMethods.forEach(function(method) {
                            if (vm[method] && typeof vm[method] === 'function') {
                                try {
                                    vm[method]();
                                    console.log(" ✅ Called ViewModel." + method);
                                } catch (e) {
                                    console.log(" Method " + method + " failed: " + e);
                                }
                            }
                        });

                        // Force the flag visibility
                        if (vm.j) { // assuming j might control flag visibility
                            try {
                                vm.j.value = true;
                                console.log(" ✅ Forced flag visibility");
                            } catch (e) {}
                        }

                    } catch (e) {
                        console.log("[!] ViewModel update error: " + e);
                    }
                },
                onComplete: function() {
                    console.log(" ViewModel updates completed");
                }
            });
        });
    }, 1000);

    // FORCE UI REFRESH AND BUTTON CLICK
    setTimeout(function() {
        Java.perform(function() {
            console.log(" Refreshing UI and searching for FLAG button...");

            // Force UI refresh
            var View = Java.use("android.view.View");
            var Handler = Java.use("android.os.Handler");
            var Looper = Java.use("android.os.Looper");

            var handler = Handler.$new(Looper.getMainLooper());

            handler.post(Java.use("java.lang.Runnable").$new({
                run: function() {
                    try {
                        // Find and invalidate main compose view
                        Java.choose("androidx.compose.ui.platform.AndroidComposeView", {
                            onMatch: function(composeView) {
                                composeView.invalidate();
                                console.log(" ✅ Compose view invalidated");
                            },
                            onComplete: function() {
                                console.log(" UI refresh completed");
                            }
                        });
                    } catch (e) {
                        console.log("[!] UI refresh error: " + e);
                    }
                }
            }));

            // Search for FLAG button again
            setTimeout(function() {
                Java.perform(function() {
                    Java.choose("android.view.View", {
                        onMatch: function(view) {
                            try {
                                var text = view.getText ? view.getText() + "" : "";
                                var desc = view.getContentDescription ? view.getContentDescription() + "" : "";
                                var combined = (text + " " + desc).toLowerCase();

                                if (combined.includes("查看") || combined.includes("flag") || combined.includes("解") || combined.includes("密")) {
                                    console.log(" 🎯 FOUND FLAG-RELATED ELEMENT: '" + (text || desc) + "'");

                                    // Click it!
                                    view.performClick();
                                    console.log(" ✅ CLICKED FLAG ELEMENT!");
                                }
                            } catch (e) {}
                        },
                        onComplete: function() {
                            console.log(" Button search completed");

                            // Final attempt: call the puzzle's own completion verification
                            Java.choose("s1.b", {
                                onMatch: function(puzzle) {
                                    try {
                                        // If there's a method that validates the puzzle state, call it
                                        // This might trigger the flag automatically
                                        if (puzzle.a && puzzle.b && puzzle.c) {
                                            // Just ensure all states are properly set
                                            puzzle.c.value = true; // completion flag

                                            console.log(" ✅ Ensured puzzle completion state");
                                        }
                                    } catch (e) {
                                        console.log("[!] Final validation error: " + e);
                                    }
                                },
                                onComplete: function() {
                                    console.log(" All operations completed!");
                                    console.log("\n🎯 EXPECTED RESULT:");
                                    console.log("✅ Layout: 新 年 别 / 人 人 感 / 人 人 冒");
                                    console.log("✅ Empty space at bottom-right");
                                    console.log("✅ '查看FLAG' button should appear");
                                    console.log("✅ Clicking it should reveal the flag");
                                }
                            });
                        }
                    });
                });
            }, 500);
        });
    }, 1500);
});

  9.有没有大佬出个更详细的零基础教程,我太菜了,跟不上。

0x3 【2026春节】解题领红包之六 {番外篇 初级题} 出题老师:Coxxs

  1.把所有文件名丢给ai,让他看看,说是lua语言编写的,有2个思路,一是调试exe,二是解压exe文件看看,把解压后的文件名称丢给deepseek。

解压出 flag.dat、conf.lua、main.lua 后,题目思路就更明确了。这是典型的 LÖVE 2D 游戏结构,Flag 很可能藏在 flag.dat 中,而解密逻辑在 Lua 脚本里。

  2.用ida打开main.lua,然后全部数据丢给ai,得到如下结果。

我已经看到关键逻辑:Flag 藏在 assets/flag.dat,只有 Hard 难度通关才会解密显示 Flag,且解密函数是 XOR,密钥是 "52pojie"。

//在 main.lua 中找到 getWinMessage() 函数:

local function getWinMessage()
    local content = nil
    if love.filesystem.getInfo("assets/flag.dat") then
        content = love.filesystem.read("assets/flag.dat")
    end

    if not content or currentDifficulty ~= "hard" then
        return "You WIN!"
    end

    local key = "52pojie"
    local keyLen = #key
    local result = {}
    local bit = require("bit")

    for i = 1, #content do
        local b = string.byte(content, i)
        local k = string.byte(key, ((i - 1) % keyLen) + 1)
        table.insert(result, string.char(bit.bxor(b, k)))
    end

    return table.concat(result)
end

  3.然后deepseek告诉我先到assets这个目录,确保和flag.dat在同一个目录,然后直接在命令行执行解密脚本,果然拿到flag了。

cd /d "C:\Users\LENOVO\Desktop\【2026春节】解题领红包之六 {番外篇 初级题} 出题老师:Coxxs\CatchTheCat - 副本\assets"
python -c "data=open('flag.dat','rb').read();key=b'52pojie';print(bytes([data[i]^key[i%len(key)] for i in range(len(data))]))"

C:\Users\LENOVO\Desktop\【2026春节】解题领红包之六 {番外篇 初级题} 出题老师:Cox
xs\CatchTheCat - 副本\assets>python -c "data=open('flag.dat','rb').read();key=b'
52pojie';print(bytes([data[i]^key[i%len(key)] for i in range(len(data))]))"
b'flag{52pojie_2026_Happy_New_Year!_>w<}'

0x4 【2026春节】解题领红包之七 {Windows 中级题} 出题老师:爱飞的猫

  1.解压文件,得到一个exe,一个加密的数据,现在要找到正确的flag解密这个加密文件,并且给出了flag的格式。

4.png

  2.用die工具查壳,CM1.exe 是 64 位程序,加了 UPX 5.10 版本的壳,且压缩方式使用了 NRV(Not Really Vanished)算法,压缩级别是 best。接着使用study pe工具固定基址,随机地址太讨厌了。然后使用x64dbg载入,下断点bpx VirtualProtect,F9直到触发断点,按照下面步骤找到oep。

//触发断点后,Ctrl+F9返回,然后看到多个pop指令,后面接一个jmp大跳。

00000001400263EA | 48:8D87 AF010000         | lea rax,qword ptr ds:[rdi+1AF]          | rdi+1AF:"郩PX1"
00000001400263F1 | 8020 7F                  | and byte ptr ds:[rax],7F                |
00000001400263F4 | 8060 28 7F               | and byte ptr ds:[rax+28],7F             |
00000001400263F8 | 4C:8D4C24 20             | lea r9,qword ptr ss:[rsp+20]            |
00000001400263FD | 4D:8B01                  | mov r8,qword ptr ds:[r9]                |
0000000140026400 | 48:89DA                  | mov rdx,rbx                             |
0000000140026403 | 48:89F9                  | mov rcx,rdi                             |
0000000140026406 | FFD5                     | call rbp                                |
0000000140026408 | 48:83C4 28               | add rsp,28                              |
000000014002640C | C605 2D000000 FC         | mov byte ptr ds:[140026440],FC          |
0000000140026413 | 48:8D8E 00F0FFFF         | lea rcx,qword ptr ds:[rsi-1000]         |
000000014002641A | 6A 01                    | push 1                                  |
000000014002641C | 5A                       | pop rdx                                 |
000000014002641D | 4D:31C0                  | xor r8,r8                               |
0000000140026420 | 50                       | push rax                                |
0000000140026421 | E8 1A000000              | call cm1.140026440                      |
0000000140026426 | 58                       | pop rax                                 |
0000000140026427 | 5D                       | pop rbp                                 |
0000000140026428 | 5F                       | pop rdi                                 |
0000000140026429 | 5E                       | pop rsi                                 |
000000014002642A | 5B                       | pop rbx                                 |
000000014002642B | 48:8D4424 80             | lea rax,qword ptr ss:[rsp-80]           |
0000000140026430 | 6A 00                    | push 0                                  |
0000000140026432 | 48:39C4                  | cmp rsp,rax                             |
0000000140026435 | 75 F9                    | jne cm1.140026430                       |
0000000140026437 | 48:83EC 80               | sub rsp,FFFFFFFFFFFFFF80                |
000000014002643B | E9 90AFFDFF              | jmp cm1.1400013D0                       |

//取消断点,F4运行到000000014002643B这一行,再按一下F8,来到了00000001400013D0,oep。

00000001400013D0 | 48:83EC 28               | sub rsp,28                              |
00000001400013D4 | 48:8B05 95970000         | mov rax,qword ptr ds:[14000AB70]        |
00000001400013DB | C700 01000000            | mov dword ptr ds:[rax],1                |
00000001400013E1 | E8 9AFDFFFF              | call cm1.140001180                      |
00000001400013E6 | 90                       | nop                                     |
00000001400013E7 | 90                       | nop                                     |
00000001400013E8 | 48:83C4 28               | add rsp,28                              |
00000001400013EC | C3                       | ret                                     |

  3.使用自带的插件Scylla x64,默认oep填的就是我们当前RIP停留的位置,然后依次点击IAT Autosearch,Get Imports,Dump,Fix Dump,有一个dll识别失败,直接删掉。

  4.搜索flag.png.encrypted字符串,定位到这里,把汇编代码喂给deepseek,让我接下来分析sub140008720函数。

0000000140007B46 | lea rdi, [rsp+160]           ; 缓冲区
0000000140007B4E | mov rax, rbp                 ; 清零
0000000140007B51 | mov edx, 2                   ; 控件 ID(可能是输入文件路径框)
0000000140007B65 | call GetDlgItemTextW         ; 获取输入文件名(flag.png.encrypted)
0000000140007BDC | mov r12, [_wfopen]           ; _wfopen
0000000140007BE3 | lea rdx, [14000A072]         ; "rb" (读二进制)
0000000140007BED | call r12                     ; fopen 打开加密文件
0000000140007C05 | call GetDlgItemTextW         ; 获取输出文件名(flag.png)
0000000140007C17 | lea rdx, [14000A0A8]         ; "wb" (写二进制)
0000000140007C1A | call r12                     ; fopen 创建输出文件
0000000140007C34 | call GetDlgItemTextA         ; 获取密码框内容
0000000140007C4E | call sub_140008720           ; ⭐⭐⭐ 核心解密函数 ⭐⭐⭐
0000000140007C61 | call fclose                  ; 关闭输入文件
0000000140007C69 | call fclose                  ; 关闭输出文件
0000000140007C84 | test r12d, r12d              ; 检查解密是否成功
0000000140007C9C | call sub_140008D70           ; 显示"成功"消息框

  5.把sub140008720这个函数继续喂进去,告诉我这个函数大致是做这么个事,让我把sub1400081E0给它,到了核心逻辑不能完全跟着它走了,deepseek会产生推测幻想的,基于已有的信息,按照我的思维140008580你说的是可能,那我不放心,sub_140008310验证解密头部很重要,1400081E0和1400082E0核心解密和验证也重要,一个个扣代码,喂的内容越全面才会越准确。

1. 初始化缓冲区 (sub_140008640)
2. 用密钥 "52pojie_2026_" 调用 sub_140008500 (两次)
3. 调用 sub_140008580 (可能生成密钥流)
4. 读取输入文件前 16 字节
5. 调用 sub_140008310 (验证/解密头部)
6. 跳转到文件末尾,读取剩余数据
7. 调用 sub_1400081E0 (核心解密)
8. 调用 sub_1400082E0 (验证)
9. 写入解密后的数据到输出文件

  6.全部喂出去后,140008080和140008480这两个函数,结论还是可能,说明需要继续精确判断,把这两个代码喂给它。

1. sub_140008580 (CRC/哈希类函数)
asm
; 计算某种校验和/哈希值
; 循环4次,每次处理一个字节
; 使用查找表 (r9 + rbx*8 + C608)
; 最后返回 NOT(结果)
这是一个自定义的哈希/校验函数,类似 CRC 或滚动哈希。

2. sub_140008310 (头部验证)
asm
; 检查文件头部是否为 "CM26" (0x36324D43)
cmp dword ptr [r8], 0x36324D43  ; "CM26"
关键发现:加密文件的前 4 字节必须是 "CM26",这是文件格式魔数。

3. sub_1400081E0 (核心解密循环)
asm
; 以 8 字节为单位循环解密
; 调用 sub_140008080 (可能是 XOR 或解密函数)
; 调用 sub_140008480 (可能是写入结果)
这是真正的解密核心,每 8 字节处理一次。

4. sub_1400082E0 (最终验证)
asm
; 检查解密后的校验和是否匹配
; 如果匹配返回 1,否则返回 0

  7.现在已经得出准确的结论了,deepseek让我去0x140008857下断点,断下的时候dump数据。

1. sub_140008080 (核心解密函数)
这个函数每 8 字节处理一次,做了以下操作:

asm
; 第一部分:对 8 字节数据进行变换(ROL、XOR、查表等)
rol rax, 3          ; 循环左移 3 位
shr rax, 38         ; 右移 56 位
or rax, 0x221300    ; 或常数
shl r9, 8           ; 左移 8 位
movzx eax, [rbx+rax] ; 查表
or rax, r9          ; 组合结果

; 第二部分:XOR 混淆
xor al, [rcx+0x5450]
xor al, [rcx+0x5440]
这是一个自定义的块加密算法,结合了位运算、查表和 XOR。

2. sub_140008480 (CRC32 类函数)
asm
; 这是标准的 CRC32 算法!
; 多项式 0xEDB88320
xor eax, edx
mov edx, 8
loop:
    shr r8d, 1
    and eax, 1
    neg eax
    and eax, 0xEDB88320
    xor eax, r8d
确认:这是 CRC32 校验,用于验证解密是否正确。

解密流程:
1. 读取加密文件,验证头部是否为 "CM26"
2. 跳过前 16 字节(头部 + 校验信息)
3. 以 8 字节为单位,调用 sub_140008080 解密
4. 每解密一块,更新 CRC32 校验
5. 最后验证 CRC32 是否匹配
6. 写入解密后的数据到输出文件

dump 数据
在 x64dbg 中,在 0x140008857(call fwrite)下断点
运行程序,触发解密
断下时,查看寄存器:
rcx = 数据缓冲区指针
r8 = 数据长度
r9 = 文件指针
在 dump 窗口跟随 rcx,右键 → Save to File 保存为 flag.png

  8.人懵了,deepseek开始胡说八道了,一会说随便输入密码,dump解密后的数据(密码都错的,上哪拿解密成功的数据,最后让我去爆破,改跳转,晕,这有什么用。

5.png

0x5 总结

  1、第四题,第五题都是python,而且版本高,我老古董电脑win7运行不了,所以学习不了。

  2、deepseek分析到核心数据的时候,得基于自己的判断,把存疑的代码全部喂给它,还得自己理清逻辑,才能全面分析。

  3、后面题就不试了,我这水平看着大佬的答案都抄不明白,有deepseek我也抓瞎,这篇文章当做我失败的经验给大家分享一下了。

免费评分

参与人数 8吾爱币 +13 热心值 +8 收起 理由
Hmily + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
junjia215 + 1 + 1 用心讨论,共获提升!
xueren114 + 1 + 1 热心回复!
ycxy + 1 + 1 我很赞同!
liuqm + 1 + 1 热心回复!
clzdlz + 1 + 1 我很赞同!
唐小样儿 + 1 + 1 我很赞同!
bai0du101 + 1 用心讨论,共获提升!

查看全部评分

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

geesehoward 发表于 2026-4-9 10:29
Android初级题也是挺奇葩的,第一次游戏过了,右上角出现了宝箱,点一下拿到了flag,之后再完成直接重置,不给flag了,由于游戏太简单了,根本没看代码,估计是有标志位,拿到过flag的直接重置了吧,哪位不是玩游戏解出来的大神来说一下
L0u1s 发表于 2026-4-4 21:42
darkvanfan 发表于 2026-4-4 22:00
liltn 发表于 2026-4-4 22:08
学习了,感谢楼主分享
kokoi7 发表于 2026-4-5 08:58
虚心学习
dev4mobile 发表于 2026-4-5 09:15
学习一下,感谢分享
nameisadam 发表于 2026-4-5 15:50
年轻真的好~~~
wh785496332 发表于 2026-4-5 17:57
哈哈看不懂
amwquhwqas128 发表于 2026-4-5 20:51
感谢详细解答文章
tzx1022559120 发表于 2026-4-6 00:11
正在努力学习
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-5-14 06:56

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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