打 CTF 一定要用 Windows!✍️

ROIS 的恩情还不完 ✋😭🤚
Info

Writer: W4t3rm310nabc from Hacklz Team

banner 来自 bilibili@Sth_Sunly

小鲸鱼我爱你喵 😍

DeepSeek 得了 MVP!我是躺赢狗!

话说今年 Web 这么难吗,我队两位 Webers 两天竟只做出一题

Web - 签到?

Background

Wells 写了个签到系统,人都到了但人都没到

请协助 Wells 排查一下系统,作为奖励只要你签到上了就可以拿到 flag 了

原始解者与思路口述:SZ_KC

F12 开启开发者工具,会看到 HTML 里一个很长的注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 如果是处理点击坐标的请求
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['clicks'])) {
$clicks = json_decode($_POST['clicks'], true);

if (!is_array($clicks) || count($clicks) !== 3) {
echo json_encode(['status' => 'error', 'message' => '数据格式不正确']);
exit;
}

// 获取图片尺寸
$imagePath = $_SERVER['DOCUMENT_ROOT'] . '/img/lock.jpg';
if (file_exists($imagePath)) {
$imageSize = getimagesize($imagePath);
$imageWidth = $imageSize[0];
$imageHeight = $imageSize[1];

// 定义正确的点击区域(以图片百分比表示)
// 格式: [x百分比, y百分比, 允许误差百分比]
$correctAreas = [
[25, 30, 5], // 第一个点击区域
[50, 60, 5], // 第二个点击区域
[75, 40, 5] // 第三个点击区域
];

$allCorrect = true;

// 检查所有点击是否都在正确区域内
for ($i = 0; $i < 3; $i++) {
// 获取点击的相对坐标(百分比)
$xPercent = floatval($clicks[$i]['xPercent']);
$yPercent = floatval($clicks[$i]['yPercent']);

// 计算正确区域的百分比位置
$correctXPercent = $correctAreas[$i][0];
$correctYPercent = $correctAreas[$i][1];
$errorMarginPercent = $correctAreas[$i][2]; // 误差范围(百分比)

// 计算点击位置与正确位置的距离(百分比)
$distancePercent = sqrt(pow($xPercent - $correctXPercent, 2) + pow($yPercent - $correctYPercent, 2));

if ($distancePercent > $errorMarginPercent) {
$allCorrect = false;
break;
}
}

if ($allCorrect) {
// 所有点都正确
echo json_encode(['status' => 'success', 'message' => '解锁成功!正在跳转...']);
} else {
// 至少有一个点不正确
echo json_encode(['status' => 'error', 'message' => '解锁图案不正确,请重试']);
}
} else {
echo json_encode(['status' => 'error', 'message' => '找不到锁定图片']);
}
exit;
}

根据给出的坐标写模拟点击代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取图片元素
const lockImage = document.getElementById('lockImage');
const rect = lockImage.getBoundingClientRect();
// 定义要模拟点击的 3 个点(百分比坐标)
const mockClicks = [
{ xPercent: 25, yPercent: 30 }, // 第 1 个正确点
{ xPercent: 50, yPercent: 60 }, // 第 2 个正确点
{ xPercent: 75, yPercent: 40 } // 第 3 个正确点
];
// 模拟点击
mockClicks.forEach((point) => {
// 计算实际像素坐标
const x = (point.xPercent / 100) * rect.width;
const y = (point.yPercent / 100) * rect.height;
// 创建并触发点击事件
const clickEvent = new MouseEvent('click', {
clientX: rect.left + x,
clientY: rect.top + y,
bubbles: true,
cancelable: true
});
lockImage.dispatchEvent(clickEvent);
});
// 自动解锁
document.getElementById('submitBtn').click();

在控制台输入代码,回车,即可跳转到含有 flag 的页面。

flag 为 flag{c6359246-9276-454f-8404-df974d1ee877}

Reverse - easy re

Background

题目描述:欢迎来到 FCTF,速来拿分!

手速题,基本没有什么加密的地方。

DIE 查一下:

无壳,拖到 IDA 反编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
char Str[100]; // [esp+58h] [ebp-74h] BYREF
size_t i; // [esp+BCh] [ebp-10h]

__main();
scanf("%s", Str);
for ( i = 0; i < strlen(Str); ++i )
Str[i] ^= i;
if ( !strcmp(Str, arr) )
printf("You are right!");
else
printf("wrong!");
return 0;
}

单纯的异或加密。点一下 arr,转换到 BYTE 类型数组:

1
2
3
4
5
6
7
8
9
unsigned char _arr[100] = {
0x46, 0x42, 0x56, 0x45, 0x7F, 0x4C, 0x52, 0x58, 0x41, 0x5A, 0x55, 0x4E, 0x4D, 0x5E, 0x57, 0x50,
0x47, 0x5E, 0x40, 0x58, 0x35, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};

写出解密函数(甚至伪代码可以直接用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h>

int main()
{
unsigned char _arr[100] = {
0x46, 0x42, 0x56, 0x45, 0x7F, 0x4C, 0x52, 0x58, 0x41, 0x5A, 0x55, 0x4E, 0x4D, 0x5E, 0x57, 0x50,
0x47, 0x5E, 0x40, 0x58, 0x35, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
};
for (int i = 0; i < strlen(_arr); ++i ){
_arr[i] ^= i;
printf("%c", _arr[i]);
}
return 0;
}

得到 flag:FCTF{IT_IS_EASY_WORK!}


Mobile - androidRe1

这道题的最大阻碍之一竟然是环境配置……

本题的工具主要为 jadx 和一个安卓模拟器(如果你有用电脑玩明日方舟的话,应该是已经有一个了

在模拟器中安装并打开提供的 click.apk,会显示:

Snipaste_2025-05-02_21-26-341

我勒个豆!竟然要点 20220422 下才有 flag,还有退出清零!我才不干。

打开 jadx,选择 click.apk,打开 “文本搜索” 窗口(大放大镜图标),搜索 20220422,得到:

只有一处代码用到了这个数字,点击跳转:

1
2
3
if (this$0.ccccccccc() == 20220422) {
tv.setText("flag{" + this$0.decrypt("whyysqwmstoryhzcontinues") + "}");
}

下面就是我们要的 flag 了(加密后的)。

点击 decrypt 跳转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public final String decrypt(String str) throws IllegalBlockSizeException, BadPaddingException {
byte[] bytes;
byte[] bytes2;
Object ddd = null;
try {
Class<?> a = Class.forName(d("amF2YS5zZWN1cml0eS5NZXNzYWdlRGlnZXN0"));
Method b = a.getMethod(d("Z2V0SW5zdGFuY2U="), String.class);
Object c = b.invoke(null, d("TUQ1"));
Method e = a.getMethod(d("dXBkYXRl"), byte[].class);
e.invoke(c, key0.getBytes());
Method f = a.getMethod(d("ZGlnZXN0"), new Class[0]);
byte[] k = (byte[]) f.invoke(c, new Object[0]);
Class<?> aa = Class.forName(d("amF2YXguY3J5cHRvLnNwZWMuREVTS2V5U3BlYw=="));
Constructor<?> desKeySpecConstructor = aa.getConstructor(byte[].class);
Object dsk = desKeySpecConstructor.newInstance(k);
Class<?> bb = Class.forName(d("amF2YXguY3J5cHRvLlNlY3JldEtleUZhY3Rvcnk="));
bytes = null;
try {
Method cc = bb.getMethod(d("Z2V0SW5zdGFuY2U="), String.class);
Object keyFactory = cc.invoke(null, "DES");
Method dd = bb.getMethod(d("Z2VuZXJhdGVTZWNyZXQ="), KeySpec.class);
Object key = dd.invoke(keyFactory, dsk);
Class<?> ee = Class.forName(d("amF2YXguY3J5cHRvLnNwZWMuSXZQYXJhbWV0ZXJTcGVj"));
Constructor<?> ff = ee.getConstructor(byte[].class);
Object iv = ff.newInstance(iv0.getBytes());
Class<?> gg = Class.forName(d("amF2YXguY3J5cHRvLkNpcGhlcg=="));
Method hh = gg.getMethod(d("Z2V0SW5zdGFuY2U="), String.class);
Object ddd2 = hh.invoke(null, d("REVTL0NCQy9QS0NTNVBhZGRpbmc="));
try {
Method ii = gg.getMethod(d("aW5pdA=="), Integer.TYPE, Key.class, AlgorithmParameterSpec.class);
ii.invoke(ddd2, 1, key, iv);
Method jj = gg.getMethod(d("ZG9GaW5hbA=="), byte[].class);
bytes2 = (byte[]) jj.invoke(ddd2, str.getBytes());
} catch (Exception e2) {
e = e2;
ddd = ddd2;
e.printStackTrace();
bytes2 = bytes;
ByteString byteString = ByteString.of(bytes2);
return byteString.hex();
}
} catch (Exception e3) {
e = e3;
ddd = null;
}
} catch (Exception e4) {
e = e4;
bytes = null;
}
ByteString byteString2 = ByteString.of(bytes2);
return byteString2.hex();
}

太长不看,让 DeepSeek 帮忙,发现解密缺少 key0iv0。搜索一下 key0,结果 iv0 也出来了:

1
2
private static String key0 = "82305002";
private static String iv0 = "82505002";

DeepSeek 生成的解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from Crypto.Cipher import DES
from Crypto.Hash import MD5
from Crypto.Util.Padding import pad
import binascii

# Given values
key0 = "82305002"
iv0 = "82505002"
plaintext = "whyysqwmstoryhzcontinues" # This is encrypted in the original code

# Step 1: Compute MD5 of key0 (16 bytes)
md5 = MD5.new()
md5.update(key0.encode())
md5_digest = md5.digest()

# Step 2: Take first 8 bytes for DES key
des_key = md5_digest[:8] # DES key must be 8 bytes

# Step 3: Use first 8 bytes of iv0 for IV
iv = iv0.encode()[:8] # IV must be 8 bytes

# Step 4: Encrypt the plaintext (DES/CBC/PKCS5Padding)
cipher = DES.new(des_key, DES.MODE_CBC, iv)
padded_plaintext = pad(plaintext.encode(), 8) # PKCS5 padding
ciphertext = cipher.encrypt(padded_plaintext)

# Step 5: Get hex representation of ciphertext
ciphertext_hex = binascii.hexlify(ciphertext).decode()

# The flag is "flag{" + ciphertext_hex + "}"
flag = f"flag{{{ciphertext_hex}}}"
print(flag)

执行后得到 flag:flag{9a2165f588d8cc5e86103e58bdbbd997cef18cc69cab1136a30147e3bbe6fbfd}


Crypto - signin_rsa

Background

机智的你可以成功 signin 吗

作为一名 Re 新手,我自然是不懂 RSA 的(说得好像你解出来了 encrypt 和 Tree 似的

不过给 DeepSeek 读了发现有戏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from Crypto.Util.number import long_to_bytes # 安装 pycryptodome
from gmpy2 import iroot # 如果安装失败,改用 libnum.nroot

# Given values
n = 63391473280664441274904200586447632651254701180424158427108281651693928841226111518712585495312617412779821462204588428065986005376953783373564926106424797989701648588541831740191002281021705190341030101189048073915563206351313513842348269859235277579119059936460754505010643126116092602365529175094781120103
c1 = 4104695764377959780604501077442202266071223727138597085236828687830804860615462560175600544757443131080359063292189768185543992762611748056543485377257384749425514971210253333516944369981180889484277814771362680422755584410126938813918977971380084864154953125
hint2 = 204105495489668868091538015576189171569873657713403681343766254180158189896024
c2 = 28121558131340982474041738298374018159696174865084105786440297339340785088430587628834203876791752975481488678585552540372105609136359567796041061743937883984968776223417145187563869555955865451402962589066805092577240903705119391097879877855341243248105717369410705538761074189119187539207186087814982669836
hint1 = 189568292591813079898832307494711802345451486761183059811078473781028969917672

# Step 1: Calculate a + b
sum_ab_squared = hint1 + hint2
sum_ab, exact = iroot(sum_ab_squared, 2)
if not exact:
print("Error: hint1 + hint2 is not a perfect square.")
exit()

# Step 2: Calculate a and b
a = hint1 // sum_ab
b = hint2 // sum_ab

# Verify a and b
if a * sum_ab != hint1 or b * sum_ab != hint2:
print("Error: Calculated a and b do not match hints.")
exit()

print(f"[+] Found a = {a}, b = {b}")

# Step 3: Small exponent attack (e=3)
# Check if m1^3 < n (no modulo operation)
m1, exact = iroot(c1, 3)
if not exact:
print("Error: c1 is not a perfect cube.")
exit()

# Step 4: Verification
m2_calculated = a * m1 + b
c2_calculated = pow(m2_calculated, 3, n)
if c2_calculated != c2:
print("Error: Verification failed. m1 may be incorrect.")
exit()

# Step 5: Convert m1 to flag
flag = long_to_bytes(m1)
print(f"[+] Recovered flag: {flag.decode()}")

输出:

1
2
3
Found a = 302132503208700208393770833389777081577, b = 325301786642820377107162010542704820159
Verification successful!
Recovered flag: b'RkNURntXZWxjMG0zX21ZX1JTQTAxISEhfQ=='

RkNURntXZWxjMG0zX21ZX1JTQTAxISEhfQ== 是一个明显的 Base64 密文。用 CyberChef 烹一下,得到 flag:FCTF{Welc0m3_mY_RSA01!!!}


Reverse - Tree

Tip

本题为赛后复现,比赛过程中未解出。其实很简单,不要被吓到了。

Background

土豆哥表示一把梭~
(作者注:贞德是一把梭 TAT)

DIE 查一下:

无壳 32 位,拖 IDA,跳到主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int sub_411C00()
{
int result; // eax
char v1; // [esp+0h] [ebp-22Ch]
char v2; // [esp+0h] [ebp-22Ch]
int i; // [esp+D0h] [ebp-15Ch]
char Str1[108]; // [esp+DCh] [ebp-150h] BYREF
char Str[108]; // [esp+148h] [ebp-E4h] BYREF
char Str2[108]; // [esp+1B4h] [ebp-78h] BYREF
int v7; // [esp+220h] [ebp-Ch]

__CheckForDebuggerJustMyCode(&unk_41C029);
v7 = sub_41127B();
strcpy(Str2, "GAPNkFl^PJGQJ_YHEEx~d");
j_memset(&Str2[22], 0, 0x4Eu);
sub_4110E1("请输入 flag: ", v1);
sub_411037("%s", (char)Str);
sub_411122(Str, v7, (int)Str1);
if ( !j_strcmp(Str1, Str2) )
result = sub_4110E1(asc_417B60, v2);
else
result = sub_4110E1(asc_417B6C, v2);
for ( i = 0; i < 30; ++i )
{
free(*(&Block + i));
result = i + 1;
}
return result;
}

如果只用静态分析结果去问 AI,又是二叉树又是 DFS 的,看不懂啦😢

但其实只需要动态调试,在 flag 判断处下断点,然后将密文 GAPNkFl^PJGQJ_YHEEx~d 作为输入,中断时查看 Str1 的值,即可得到 flag 为 FCTF{WeLCOME_TO_FCtf}

Note

为什么?

如果去看 sub_41127B() 初始化函数,会发现所有编译时数组的数据均为 0!也就是说,程序根本没有构建二叉树。加密算法退化为纯异或的对称加密,本质上和 easy re 相同

官方给的源码中的二叉树编码本身就是对称的。算法主体就是和 easy re 相同的异或加密,只是密钥变成了二叉树的节点值。
由于生成的二叉树固定,因此简单地复用机密函数即可解密。


Reverse - solo

Background

数学中的逻辑游戏 -- 得到的 flag 请加上 FCTF {}

DIE 查一查:

无壳 64 位,拖 IDA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall main(int argc, const char **argv, const char **envp)
{
char Str1[112]; // [rsp+20h] [rbp-80h] BYREF
char flag[16]; // [rsp+90h] [rbp-10h] BYREF

_main();
puts("scanf U answer");
scanf("%s", Str1);
solve_sudoku(board);
extract_flag(board, flag);
if ( !strcmp(Str1, flag) )
puts("Success");
else
puts("No solution exists");
system("pause");
return 0;
}

然后下断点动调,看一下 flag 就行。对,就这么简单,因为这个程序不需要来解这个数独 (sudoku),程序自己已经解好了。extract_flag 也只和已经解出的数独有关。

flag 为:FCTF{I_LK_SDK}


Misc - 溯

Background

Satoru:[嘿,兄弟,听说你卡在这关了?我这儿有个好东西,CTF_ALL_IN_ONE.bat,一键通关,要不要试试?]

Wells 犹豫了一下,但疲惫和求胜心最终战胜了理智。他点击了下载链接。

文件解压后,他双击运行了那个神秘的批处理脚本。屏幕闪烁了一下,命令行窗口飞速滚动,然后 ——

黑屏。

“什么情况?!”Wells 猛地拍了下键盘,但已经晚了。他的电脑突然弹出一堆错误提示,桌面上所有的 CTF 工具包全部被删除,取而代之的是一堆 cmd 弹窗:

“Thanks for the free shell! - Satoru”

他这才意识到 —— 自己被骗了。

愤怒的 Wells 发送了一千封垃圾邮件,誓要爆破 Satoru 的校园邮箱

请你根据数据包取证 Satoru 的恶意批处理脚本,并根据此进一步追踪溯源,发掘两人的交互流程

用 Wireshark(大蓝鲨)打开所给的数据包,然后导出 HTTP 对象(文件 > 导出对象 > HTTP…),查看该分组下的 Hypertext Transfer Protocol 信息,得到 [Full request URI: http://47.120.51.45/files/],通过这个 URI 就可以下载题目所说的 CTF_ALL_IN_ONE.bat 了。

打开这个 bat 文件:

1
2
3
4
5
6
7
8
9
@echo off
git clone https://github.com/Sat0ruG0jo/LetsEnjoyAttack.git
git

:: 无限循环弹出原神官网
:loop
start "" "https://www.bilibil.com"
timeout /t 1 > nul
goto loop

发现这个 bat 会自动 clone 一个 Github 仓库。

这真的是弹出原!神!官网吗,我只看到了一个错版 bilibili 网站,还以为这东西有猫腻呢,然后 WHOIS 一查居然是 2011 年的 ldx(

追踪这个仓库及其所有者。

LetsEnjoyAttack 这个仓库没有什么价值,无论是内容、提交记录还是 Issues 等都干干净净,于是查看所有者的 Github 主页及其他仓库。

SatoruDaily0.0 下有两个 Issues,正是我们的解密关键。

首先看 Daily1 - Issue #1,Satoru 给了一张站台图片,并有提示:请 CTFer 找出这趟地铁的线路,这将作为获取 Flag 必不可少的一环,例如(A1)

图片放大后:

439946802-552f7e75-a1a0-482e-b330-090395b29435

可以辨认出线路站点名。通过搜索关键词,得知这是南京地铁 S8 号线。

然后是最终的 Issue #2,给了 Wells 发来的邮件内容:

Dear Friend , Especially for you - this breath-taking
news ! This is a one time mailing there is no need
to request removal if you won’t want any more . This
mail is being sent in compliance with Senate bill 2116
; Title 3 , Section 303 ! This is a ligitimate business
proposal ! Why work for somebody else when you can
become rich inside 96 WEEKS . Have you ever noticed
nearly every commercial on television has a .com on
in it and people love convenience ! Well, now is your
chance to capitalize on this . We will help you decrease
perceived waiting time by 140% and use credit cards
on your website ! The best thing about our system is
that it is absolutely risk free for you ! But don’t
believe us ! Prof Jones of Indiana tried us and says
“I was skeptical but it worked for me” . We assure
you that we operate within all applicable laws . You
will blame yourself forever if you don’t order now
. Sign up a friend and you’ll get a discount of 10%
! Thank-you for your serious consideration of our offer
!

和 flag 提示:

请 CTFer 开动脑筋,将 LittleWells 的密文解开
陪 Satoru 一起感受 Wells 的愤怒也是计划的一部分:)
FLAG 格式 FCTF{github账户缩写_地铁线路_垃圾邮件解密缩写}
缩写指仅保留大写字母喵

这份邮件是一种典型的垃圾邮件 (SpamEmail)。通过搜索 “垃圾邮件解密缩写”,我找到了一篇文章 CTF Misc 隐写术原理和工具 - 先知社区,里面恰好有提到基于这类垃圾邮件的加密方式,并有一个加解密网站 spammimic

用该网站解密邮件,得到明文 HowDareYou

综合所有信息,得到 flag:FCTF{SG_S8_HDY}


Reverse - jump

Background

简单的 jump -- 得到的 flag 请加上 FCTF {}

对我来说挺难的(

有两处花指令,需要嗯看汇编把花指令搞掉才能让 IDA 反编译出两个加密函数。

怪东西

我要采一朵,送给 REer
REer、REer 你看有~🤓
函数认完了没有😡
啊啊啊啊啊啊,我只是想让你看看这很好看而已啊😭

先用 DIE 查一下

无壳 32 位,拖进 IDA:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
FILE *v3; // eax
char v5; // [esp+0h] [ebp-19Ch]
char v6; // [esp+0h] [ebp-19Ch]
size_t v7; // [esp+10h] [ebp-18Ch]
int v8; // [esp+DCh] [ebp-C0h]
int v9; // [esp+E8h] [ebp-B4h]
char *v10; // [esp+F4h] [ebp-A8h]
size_t Size; // [esp+100h] [ebp-9Ch]
char *Destination; // [esp+10Ch] [ebp-90h]
char Buffer[7]; // [esp+130h] [ebp-6Ch] BYREF
char v14[97]; // [esp+137h] [ebp-65h] BYREF

__CheckForDebuggerJustMyCode(&unk_41C009);
sub_4110DC("LET'S GO!!!\n", v5);
v3 = _acrt_iob_func(0);
fgets(Buffer, 100, v3);
v7 = strcspn(Buffer, "\n");
if ( v7 >= 0x64 )
j____report_rangecheckfailure();
Buffer[v7] = 0;
Destination = (char *)malloc(8u);
strncpy_s(Destination, 8u, Buffer, 7u);
Destination[7] = 0;
Size = j_strlen(Buffer) - 7 + 1;
v10 = (char *)malloc(Size);
strcpy_s(v10, Size, v14);
v9 = sub_411361(Destination, 4);
v8 = sub_4110BE(v10);
if ( v9 == 1 && v8 == 1 )
sub_4110DC("right\n", v6);
else
sub_4110DC("Nooooo\n", v6);
free(Destination);
free(v10);
system("pause");
return 0;
}

主要是两个 flag 校验函数 sub_411361sub_4110BE

但是,sub_411361

1
2
3
4
5
// attributes: thunk
void __cdecl sub_411361(int a1, int a2)
{
JUMPOUT(0x4117E0);
}

sub_4110BE

1
2
3
4
5
// attributes: thunk
void __cdecl sub_4110BE(int a1)
{
JUMPOUT(0x411930);
}

均出现 JUMPOUT,说明有脏东西干扰了 IDA 的识别,此时必须从汇编入手,让 IDA 能够识别出正常函数。

先分析 sub_411361。跳转到 0x4117E0 地址:

旁边的地址都是标红的,说明 IDA 无法判断函数的起始位置,需要我们来指定。

loc_41180A 调用的内存地址在 32 位机器上根本无法使用。对 loc_41180AD 键将其强行视为数据而非代码(也可以直接 nop 掉),然后选择 loc_4117E0loc_411803 这一段按 P 键确定反编译函数范围。

sub_4117E0F5 反编译即可得到赏心悦目的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL __cdecl sub_4117E0(char *Str1, int a2)
{
char v3; // [esp+Fh] [ebp-E5h]
int i; // [esp+E0h] [ebp-14h]

for ( i = 0; Str1[i]; ++i )
{
if ( isalpha(Str1[i]) )
{
if ( islower(Str1[i]) )
v3 = 97;
else
v3 = 65;
Str1[i] = ((a2 + Str1[i] - v3) % 26 + v3) ^ 5;
}
}
return j_strcmp(Str1, Str2) == 0;
}

(复现得到的伪代码比我比赛时瞎搞出来的伪代码质量高了不知道有多少……)

查看 Str2,这是第一部分的 flag 密文:Vq} ht`u

sub_4110BE 如法炮制

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BOOL __cdecl sub_411930(char *Str)
{
char v2; // [esp+D3h] [ebp-29h]
int i; // [esp+DCh] [ebp-20h]
signed int v4; // [esp+E8h] [ebp-14h]

v4 = j_strlen(Str);
for ( i = 0; i < v4 / 2; ++i )
{
v2 = Str[i];
Str[i] = Str[v4 - 1 - i];
Str[v4 - 1 - i] = v2;
}
return j_strcmp(Str, aRpR1muj2) == 0;
}

查看 aRpR1muj2,这是 flag 的第二部分密文:rp_r1muj2_

接下来就可以拿着代码去问 AI 了。

flag 为:FCTF {Op} im`l_2jum1r_pr}

Warning

如果使用 AI,请务必让它正向验证,尤其是 sub_4117E0。在我的复现过程中,虽然伪代码的质量更高,但 DeepSeek 给出的答案却是错误的。

也可以通过动态调试,用任意例子验证并修正算法,在比赛过程中 DeepSeek 有要求这一点。由于复现的伪代码比较完整,它没有要求动调验证。


©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字