前言:
水区的朋友们,年轻就是资本,和我一起学逆向逆天改命吧,我的学习过程全部记录及学习资源: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,提示如下
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,记录函数调用前后的输入输出,相关内容记录下来。
//第一次调试
.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,丢人,所以先拜读一下论坛大佬们的帖子,稍后回来。
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的格式。
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解密后的数据(密码都错的,上哪拿解密成功的数据,最后让我去爆破,改跳转,晕,这有什么用。
0x5 总结
1、第四题,第五题都是python,而且版本高,我老古董电脑win7运行不了,所以学习不了。
2、deepseek分析到核心数据的时候,得基于自己的判断,把存疑的代码全部喂给它,还得自己理清逻辑,才能全面分析。
3、后面题就不试了,我这水平看着大佬的答案都抄不明白,有deepseek我也抓瞎,这篇文章当做我失败的经验给大家分享一下了。