【说明】 本人一直是ctf菜鸡,我的能力并不强,今年来做题练练手,这篇文章作为我本人的纪念和心得,求大佬们轻喷。由于时间问题没有在中级和高级题深入研究,这几天也在学习,感谢各位大佬能够分享思路!
解题领红包之二 {Windows 初级题}
这题是我目前见到过的打卡题目里面比较复杂的了,打开程序观察:
(这题走了弯路,我其实应该直接IDA拆的)
========================================
CrackMe Challenge v2.5 - 2026
========================================
Keywords: 52pojie, 2026, Happy new year
Hint: Fake flag; length is key
----------------------------------------
目前得到的信息: 几个关键词、假Flag、长度
根据简单题目的尿性,首先会有长度判断,然后才是验证逻辑,所以我先想到了动调直接绕过长度判定。在此之前试了一下52pojie2026Happynewyear发现有里程碑
[!] You're getting closer...
后面回看发现是假flag
首先查壳:*Unknown exe , EP : C7 05 .. , 09 sections [ Linker 2.36 ] [ CRC Set ] - GCC exeHeader Big sec. 01 [ .text ] ,基本判定为无壳GCC,同时exeinfo提示这是32位程序
直接动态调试搜字符串[!],搜出几个字符串(筛选flag、fl@g之类的发现无结果,跳过)
地址=010CD2C0 "52pojie2026Happy"
地址=010CD2D7 "\n[!] You're getting closer..."
地址=010CD30B "\n[!] Hint: The length is your first real challenge."
地址=010CD3A9 "\n[!] Checksum failed! Something is wrong..."
地址=010CD3C9 "[!] Expected: 44709, Got: "
地址=010CD3FC "\n[!] Nice try, but not quite right..."
地址=010CD415 "\n[X] Access Denied!"
地址=010CD435 "[X] Wrong password. Keep trying!"
地址=010CD450 "\n========================================"
地址=010CD470 " *** SUCCESS! *** "
地址=010CD489 "========================================"
地址=010CD4A2 "[+] Congratulations! You cracked it!"
地址=010CD4BB "[+] Correct flag: "
目前得到的信息: 可疑字符串52pojie2026Happy、存在长度判定、存在flag完整性检验
右键转到字符串,发现了长度判定的代码
010CD301 | E8 B23BF5FF | call <JMP.&strlen> |
010CD306 | 83F8 1F | cmp eax,1F |
010CD309 | 74 44 | je 0x010CD34F |
不难看出此处为长度判定,判断输入字符串长度是否为0x1F,如果是的话跳转,那么我们现在就知道了flag长度为31。
继续往下翻,翻到了判定和checksum验证:
010CD364 | E8 6743F3FF | call 0x10016D0 |
010CD369 | 84C0 | test al,al |
010CD36B | 0F84 A4000000 | je 0x10CD415 | ->密码错误消息
…
010CD39C | 817D A0 A5AE0000 | cmp dword ptr ss:[ebp-60],AEA5 |
010CD3A3 | 0F84 A7000000 | je 0x10CD450 | ->继续流程
目前得到的信息: flag长度31、某个checksum0xAEA5
查看了call的方法,发现全是一大堆运算,逐个分析太浪费时间,所以我们掏出IDA直接开拆。Shift+F12打开字符串面板,找到密码错误提示那里双击转到对应的部分,按X打开引用,找到对应引用部分按F5拆出伪代码:
But then, I had a great idea! I used F5! Using F5 gave me a whole new perspective and i was able to find out how to see more text!
——Kenadian(这段话是玩梗,与题目无关)
……
SetConsoleOutputCP(0xFDE9u);
sub_4C7E50((int)&dword_4D27C0, "========================================");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, " CrackMe Challenge v2.5 - 2026 ");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "========================================");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "Keywords: 52pojie, 2026, Happy new year");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "Hint: Fake flag; length is key");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "----------------------------------------");
((void (*)(void))sub_4015B0)();
v22 = &v24;
v23 = 0;
v24 = 0;
v16 = 1;
v13 = (int *)&v22;
sub_4C7E50((int)&dword_4D27C0, "\n[?] Enter the password: ");
v13 = (int *)&v22;
v2 = sub_401560();
v13 = (int *)&v22;
sub_4C5840(&dword_4D25E0, &v22, v2);
Str = v22;
v3 = sub_401740(v22);
v4 = 53;
v5 = v3;
v6 = 0;
if ( !v5 )
{
while ( Str[v6] == v4 )
{
if ( ++v6 == 16 )
{
v16 = 1;
sub_4C7E50((int)&dword_4D27C0, "\n[!] You're getting closer...");
v16 = 1;
goto LABEL_9;
}
v4 = byte_4D3032[v6];
}
if ( strlen(Str) != 31 )
{
v16 = 1;
sub_4C7E50((int)&dword_4D27C0, "\n[!] Hint: The length is your first real challenge.");
goto LABEL_9;
}
v16 = 1;
if ( (unsigned __int8)sub_4016D0(Str, 31) )
{
Str = 0;
v8 = *v22;
if ( !*v22 )
goto LABEL_16;
v9 = 0;
do
{
Str += ++v9 * v8;
v8 = v22[v9];
}
while ( v8 );
if ( Str != (char *)44709 )
{
LABEL_16:
v16 = 1;
sub_4C7E50((int)&dword_4D27C0, "\n[!] Checksum failed! Something is wrong...");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "[!] Expected: 44709, Got: ");
sub_47DE70(Str);
sub_4015B0(v10);
LABEL_17:
v16 = 1;
goto LABEL_10;
}
v16 = 1;
sub_4C7E50((int)&dword_4D27C0, "\n========================================");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, " *** SUCCESS! *** ");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "========================================");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "[+] Congratulations! You cracked it!");
((void (*)(void))sub_4015B0)();
v11 = sub_4C7E50((int)&dword_4D27C0, "[+] Correct flag: ");
sub_4C4330(v11, v22, v23);
}
else
{
v16 = 1;
sub_4C7E50((int)&dword_4D27C0, "\n[X] Access Denied!");
((void (*)(void))sub_4015B0)();
sub_4C7E50((int)&dword_4D27C0, "[X] Wrong password. Keep trying!");
}
((void (*)(void))sub_4015B0)();
goto LABEL_17;
}
sub_4C7E50((int)&dword_4D27C0, "\n[!] Nice try, but not quite right...");
LABEL_9:
sub_4015B0(Time);
LABEL_10:
sub_4017C0();
sub_4B1AE0(v13);
sub_40C600(v15);
return 0;
}
不难发现这里的判断逻辑,如果你完全不懂的话也可以发给ai分析,现在的ai真的是越来越强了。
我们观察代码可以得到以下信息:
| 验证点 |
条件 |
提示信息 |
| 1 |
假Flag |
"You're getting closer..." |
| 2 |
字符串总长度是否等于 31 |
"The length is your first real challenge" |
| 3 |
sub_4016D0(Str, 31) 返回值是否为真 |
"Access Denied" |
| 4 |
Checksum计算结果是否等于 44709 |
"Checksum failed" |
Checksum算法:
v9 = 0;
do {
Str += ++v9 * v8; // 累加:位置×字符值
v8 = v22[v9];
} while (v8);
// 最终 Str 应该等于 44709
即:
$$\sum_{i=1}^{n} (i \times ASCII(s[i])) = 44709$$
接下来我们双击sub_4016D0,转到判断函数内,再次F5,拆出伪代码:
bool __cdecl sub_4016D0(int a1, int a2)
{
unsigned __int8 *Block; // ebp
int v3; // eax
int v4; // ebx
bool v5; // dl
Block = (unsigned __int8 *)sub_4CB710(0x64u); // 分配100字节内存
sub_401620(Block); // 用正确密码填充Block
if ( a2 <= 0 )
{
v4 = 0;
}
else
{
v3 = 0;
v4 = 0;
do
{
v5 = *(char *)(a1 + v3) == Block[v3]; // 逐字符比较
++v3;
v4 += v5; // 统计匹配字符数
}
while ( a2 != v3 );
}
j_j_free(Block);
return a2 == v4; // 所有字符都匹配才返回true
}
那么接下来就很简单了,直接拆出sub_401620就是密码的生成逻辑:
_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;
while ( result != (_BYTE *)(a1 + 31) );
*(_BYTE *)(a1 + 31) = 0;
return result;
}
这个函数生成的逻辑就是:写入DWORD → 异或0x42 → 得到密码
我们直接让ai帮我们写一个答案生成的脚本,然后运行一下就行了。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 原始DWORD/WORD/BYTE值
data = [
758280311, # DWORD 0-3
1663511336, # DWORD 4-7
1880974179, # DWORD 8-11
494170226, # DWORD 12-15
842146570, # DWORD 16-19
657202491, # DWORD 20-23
658185525, # DWORD 24-27
12323, # WORD 28-29
99, # BYTE 30
]
# 转换为字节流 (小端序)
bytes_list = []
for i, val in enumerate(data):
if i < 7: # DWORD (4字节)
bytes_list.extend(val.to_bytes(4, 'little'))
elif i == 7: # WORD (2字节)
bytes_list.extend(val.to_bytes(2, 'little'))
else: # BYTE (1字节)
bytes_list.append(val)
print(f"原始字节长度: {len(bytes_list)}")
# XOR 0x42 解密
flag = ''.join(chr(b ^ 0x42) for b in bytes_list)
print(f"🚩 Flag: {flag}")
print(f"📏 长度: {len(flag)}")
# 验证Checksum
checksum = sum((i + 1) * ord(flag[i]) for i in range(len(flag)))
print(f"🔢 Checksum: {checksum} (期望: 44709)")
运行之后得到:
原始字节长度: 31
🚩 Flag: 52pojie!!!_2026_Happy_new_year!
📏 长度: 31
🔢 Checksum: 44709 (期望: 44709)
完美符合我们得出的状态和预想的结果,flag52pojie!!!_2026_Happy_new_year!!
解题领红包之三 {Android 初级题}
这一题就是简单的华容道拼图,没想那么多,几步就做出来了。(图片字体看着奇怪是因为改了系统字体)
flag:flag{Wu41_P0j13_2026_Spr1ng_F3st1v4l}
解题领红包之四 {Windows 初级题}
这一题是简单的pyinstaller题目,看到图标一眼pyinstaller打包的,直接开拆。
刷到过的一篇很不错的教程:PyInstaller逆向——解包问题与工具使用
拆包工具:extremecoders-re/pyinstxtractor: PyInstaller Extractor
拆包后发现一个很显眼的入口点crackme_easy.pyc,uncompyle6开拆(python3.14,也可以用PyLingual在线拆):
# filename = 'crackme_easy.py'
# Bytecode_version = '3.14rc3 (3627)'
import hashlib
import base64
import sys
def xor_decrypt(data, key):
"""XOR解密"""
result = bytearray()
for i, byte in enumerate(data):
result.append(byte ^ key ^ i & 255)
return result.decode('utf-8', errors='ignore')
def get_encrypted_flag():
"""获取加密的flag"""
enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
return base64.b64decode(enc_data)
def generate_flag():
"""动态生成flag"""
encrypted = get_encrypted_flag()
key = 78
result = bytearray()
for i, byte in enumerate(encrypted):
result.append(byte ^ key)
return result.decode('utf-8')
def calculate_checksum(s):
"""计算校验和"""
total = 0
for i, c in enumerate(s):
total += ord(c) * (i + 1)
return total
def hash_string(s):
"""计算字符串哈希"""
return hashlib.sha256(s.encode()).hexdigest()
def verify_flag(user_input):
"""验证flag"""
correct_flag = generate_flag()
if len(user_input)!= len(correct_flag):
return False
else:
for i in range(len(correct_flag)):
if user_input[i]!= correct_flag[i]:
return False
return True
def fake_check_1(user_input):
"""假检查1"""
fake_hash = 'a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890'
return hash_string(user_input) == fake_hash
def fake_check_2(user_input):
"""假检查2"""
fake_hash = '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'
return hash_string(user_input) == fake_hash
def main():
"""主函数"""
print('==================================================')
print(' CrackMe Challenge - Python Edition')
print('==================================================')
print('Keywords: 52pojie, 2026, Happy New Year')
print('Hint: Decompile me if you can!')
print('--------------------------------------------------')
user_input = input('\n[?] Enter the password: ').strip()
if fake_check_1(user_input):
print('\n[!] Nice try, but not quite right...')
input('\nPress Enter to exit...')
return None
else:
if fake_check_2(user_input):
print('\n[!] You\'re getting closer...')
input('\nPress Enter to exit...')
else:
if verify_flag(user_input):
checksum = calculate_checksum(user_input)
expected_checksum = calculate_checksum(generate_flag())
if checksum == expected_checksum:
print('\n==================================================')
print(' *** SUCCESS! ***')
print('==================================================')
print('[+] Congratulations! You cracked it!')
print(f'[+] Correct flag: {user_input}')
else:
print('\n[!] Checksum failed!')
else:
print('\n[X] Access Denied!')
print('[X] Wrong password. Keep trying!')
input('\nPress Enter to exit...')
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print('\n\n[!] Interrupted by user')
sys.exit(0)
相比第二题,注释能还原出来就很方便了,直接编写flag生成代码:
import base64
enc_data = 'e3w+fiRvfW18fnx4ZAZ6Pj43YwB9OWMXfXo8Dg4O'
# base64 解码
encrypted = base64.b64decode(enc_data)
print(f"Base64: {encrypted.hex()}")
# XOR78
key = 78
result = bytearray()
for byte in encrypted:
result.append(byte ^ key)
flag = result.decode('utf-8', errors='replace')
print(f"Flag: {flag}")
运行后得到Flag:
Base64: 7b7c3e7e246f7d6d7c7e7c7864067a3e3e3763007d3963177d7a3c0e0e0e
Flag: 52p0j!3#2026*H4ppy-N3w-Y34r@@@
直接得到了我们需要的结果:flag:52p0j!3#2026*H4ppy-N3w-Y34r@@@!
解题领红包之六 {番外篇 初级题}
首先我们观察压缩包结构
$ ls
CatchTheCat.exe* credits.txt msvcp120.dll*
OpenAL32.dll* license.txt lua51.dll* msvcr120.dll*
SDL2.dll* love.dll* mpg123.dll*
第一眼没法看出这是什么框架的程序,发现有credits和licence,直接打开,查看发现程序是love2d写的,在网上搜索love2d discompiler,搜到一个love2d-unpacker.py的文件:
import argparse
import os, os.path
import zipfile
import io
def readui32(file):
bytes = file.read(4)
number = bytes[0]
number += bytes[1] << 8
number += bytes[2] << 16
number += bytes[3] << 24
return number
def skip2zip(file):
SIGNATURE = b'PK\x05\x06'
# retrieve the file size
file.seek(0, 2)
filesize = file.tell()
# scan the last 65k (2^16) for the zip signature
signature_position = filesize
while signature_position > filesize - (2 << 16):
file.seek(signature_position, 0)
data = file.read(len(SIGNATURE))
if data == SIGNATURE:
break
signature_position -= 1
else:
raise ValueError("Corrupted zip archive.")
# skip 8 bytes
file.seek(8, 1)
# read size and offset of central directory
size = readui32(file)
offset = readui32(file)
# Calculate beginning of the zip file:
# There is a "central directory" with the size 'size' located at 'offset' (relative to the zip
# file). The signature is appended directly after the central directory. We have already found
# the signature start and know the size of the central directory, so we can calculate the
# beginning of the central directory via 'signature_position - size'. The result is the "real"
# offset inside the packed executable. The supposed offset inside the zip file is stored at
# 'offset', so we can calculate the beginning of the zip-file.
start = (signature_position - size) - offset
# seek to the beginning position
file.seek(start, 0)
def unpack(executablename, unzipdestination=None, lovefilename=True):
with open(executablename, 'rb') as executable:
skip2zip(executable)
data = executable.read()
if lovefilename:
with open(lovefilename, 'wb') as lovefile:
lovefile.write(data)
if unzipdestination:
if not os.path.isdir(unzipdestination):
os.makedirs(unzipdestination)
zipdata = io.BytesIO(data)
with zipfile.ZipFile(zipdata, 'r') as zip:
zip.extractall(unzipdestination)
if __name__=="__main__":
parser = argparse.ArgumentParser(description="Unpack a love game executable which has been " \
"fused by appending the .love file to a love binary. Choose one of two modes: either " \
"just separate the .love file from the binary, using the '--love' (or '-l') option, or " \
"extract the contained files into a directory specified with '--extract' (or '-x'). You " \
"can also specify both options.")
parser.add_argument('-x', '--extract', metavar="UNZIPDESTINATION", type=str, help="Unzip " \
"files to this folder (it will be created if it doesn't exist, otherwise the contents " \
"will be overwritten!)")
parser.add_argument('-l', '--love', metavar="LOVEFILE", type=str, help="Split off the .love " \
"file and save it to this file (it will be overwritten if it already exists!)")
parser.add_argument('executable', metavar="EXECUTABLE", type=str, help="The love game " \
"executable to unpack.")
args = parser.parse_args()
unpack(args.executable, unzipdestination=args.extract, lovefilename=args.love)
程序里面有说明,直接python3 ../temp/love2d-unpacker.py CatchTheCat.exe -x ./CatchTheCat_unpacked拆包,拆完之后:
$ ls -R
.:
assets/ conf.lua main.lua
./assets:
cat/ flag.dat
./assets/cat:
bottom_left/ left/ top_left/
直接打开flag.dat发现不可识读,打开main.lua搜索flag.dat看看能不能搜到解密逻辑:
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
直接运行:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def getWinMessage():
try:
with open("CatchTheCat_unpacked/assets/flag.dat", "rb") as f:
encrypted_data = f.read()
except FileNotFoundError:
return
key = "52pojie".encode('utf-8')
key_len = len(key_bytes)
result = []
for i, byte in enumerate(encrypted_data):
key_byte = key[i % key_len]
result.append(byte ^ key_byte)
flag = bytes(result).decode('utf-8', errors='replace')
print("Flag:", flag)
if __name__ == "__main__":
getWinMessage()
轻松拿下flag: flag{52pojie_2026_Happy_New_Year!_>w<}!