FCTF2025 Personal Writeup
- 1 CCB 2026 Half-Final Guangzhou
- 2 2025 强网杯S9 Reverse 复现
- 3 LitCTF2025 Personal WriteUP
- 4 FCTF2025 Personal Writeup 本文
打 CTF 一定要用 Windows!✍️
ROIS 的恩情还不完 ✋😭🤚
Writer: W4t3rm310nabc from Hacklz Team
banner 来自 bilibili@Sth_Sunly
小鲸鱼我爱你喵 😍
DeepSeek 得了 MVP!我是躺赢狗!
话说今年 Web 这么难吗,我队两位 Webers 两天竟只做出一题
Web - 签到?
Wells写了个签到系统,人都到了但人都没到
请协助Wells排查一下系统,作为奖励只要你签到上了就可以拿到flag了
原始解者与思路口述:SZ_KC
F12 开启开发者工具,会看到 HTML 里一个很长的注释:
// 如果是处理点击坐标的请求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;}根据给出的坐标写模拟点击代码:
// 获取图片元素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
题目描述:欢迎来到FCTF,速来拿分!
手速题,基本没有什么加密的地方。
DIE 查一下:

无壳,拖到 IDA 反编译:
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 类型数组:
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};写出解密函数(甚至伪代码可以直接用):
#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,会显示:

我勒个豆!竟然要点 20220422 下才有 flag,还有退出清零!我才不干。
打开 jadx,选择 click.apk,打开“文本搜索”窗口(大放大镜图标),搜索 20220422,得到:

只有一处代码用到了这个数字,点击跳转:
if (this$0.ccccccccc() == 20220422) { tv.setText("flag{" + this$0.decrypt("whyysqwmstoryhzcontinues") + "}"); }下面就是我们要的 flag 了(加密后的)。
点击 decrypt 跳转:
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 帮忙,发现解密缺少 key0 和 iv0。搜索一下 key0,结果 iv0 也出来了:
private static String key0 = "82305002";private static String iv0 = "82505002";DeepSeek 生成的解密脚本:
from Crypto.Cipher import DESfrom Crypto.Hash import MD5from Crypto.Util.Padding import padimport binascii
# Given valueskey0 = "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 keydes_key = md5_digest[:8] # DES key must be 8 bytes
# Step 3: Use first 8 bytes of iv0 for IViv = 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 paddingciphertext = cipher.encrypt(padded_plaintext)
# Step 5: Get hex representation of ciphertextciphertext_hex = binascii.hexlify(ciphertext).decode()
# The flag is "flag{" + ciphertext_hex + "}"flag = f"flag{{{ciphertext_hex}}}"print(flag)执行后得到 flag:flag{9a2165f588d8cc5e86103e58bdbbd997cef18cc69cab1136a30147e3bbe6fbfd}
Crypto - signin_rsa
机智的你可以成功signin吗
作为一名 Re 新手,我自然是不懂 RSA 的(说得好像你解出来了 encrypt 和 Tree 似的)
不过给 DeepSeek 读了发现有戏:
from Crypto.Util.number import long_to_bytes # 安装 pycryptodomefrom gmpy2 import iroot # 如果安装失败,改用 libnum.nroot
# Given valuesn = 63391473280664441274904200586447632651254701180424158427108281651693928841226111518712585495312617412779821462204588428065986005376953783373564926106424797989701648588541831740191002281021705190341030101189048073915563206351313513842348269859235277579119059936460754505010643126116092602365529175094781120103c1 = 4104695764377959780604501077442202266071223727138597085236828687830804860615462560175600544757443131080359063292189768185543992762611748056543485377257384749425514971210253333516944369981180889484277814771362680422755584410126938813918977971380084864154953125hint2 = 204105495489668868091538015576189171569873657713403681343766254180158189896024c2 = 28121558131340982474041738298374018159696174865084105786440297339340785088430587628834203876791752975481488678585552540372105609136359567796041061743937883984968776223417145187563869555955865451402962589066805092577240903705119391097879877855341243248105717369410705538761074189119187539207186087814982669836hint1 = 189568292591813079898832307494711802345451486761183059811078473781028969917672
# Step 1: Calculate a + bsum_ab_squared = hint1 + hint2sum_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 ba = hint1 // sum_abb = hint2 // sum_ab
# Verify a and bif 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: Verificationm2_calculated = a * m1 + bc2_calculated = pow(m2_calculated, 3, n)if c2_calculated != c2: print("Error: Verification failed. m1 may be incorrect.") exit()
# Step 5: Convert m1 to flagflag = long_to_bytes(m1)print(f"[+] Recovered flag: {flag.decode()}")输出:
Found a = 302132503208700208393770833389777081577, b = 325301786642820377107162010542704820159Verification successful!Recovered flag: b'RkNURntXZWxjMG0zX21ZX1JTQTAxISEhfQ=='RkNURntXZWxjMG0zX21ZX1JTQTAxISEhfQ== 是一个明显的 Base64 密文。用 CyberChef 烹一下,得到 flag:FCTF{Welc0m3_mY_RSA01!!!}
Reverse - Tree
本题为赛后复现,比赛过程中未解出。其实很简单,不要被吓到了。
土豆哥表示一把梭~ (作者注:贞德是一把梭 TAT)
DIE 查一下:

无壳 32 位,拖 IDA,跳到主函数:
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}
为什么?
如果去看 。sub_41127B() 初始化函数,会发现所有编译时数组的数据均为 0!也就是说,程序根本没有构建二叉树。加密算法退化为纯异或的对称加密,本质上和 easy re 相同
官方给的源码中的二叉树编码本身就是对称的。算法主体就是和 easy re 相同的异或加密,只是密钥变成了二叉树的节点值。 由于生成的二叉树固定,因此简单地复用机密函数即可解密。
Reverse - solo
数学中的逻辑游戏 —得到的flag请加上FCTF{}
DIE 查一查:

无壳 64 位,拖 IDA:
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 - 溯
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 文件:
@echo offgit clone https://github.com/Sat0ruG0jo/LetsEnjoyAttack.gitgit
:: 无限循环弹出原神官网:loopstart "" "https://www.bilibil.com"timeout /t 1 > nulgoto loop发现这个 bat 会自动 clone 一个 GitHub 仓库。
这真的是弹出原!神!官网吗,我只看到了一个错版 bilibili 网站,还以为这东西有猫腻呢,然后 WHOIS 一查居然是 2011 年的 ldx(
追踪这个仓库及其所有者。
LetsEnjoyAttack 这个仓库没有什么价值,无论是内容、提交记录还是 Issues 等都干干净净,于是查看所有者的 GitHub 主页及其他仓库。
在 SatoruDaily0.0 下有两个 Issues,正是我们的解密关键。
首先看 Daily1 - Issue #1,Satoru 给了一张站台图片,并有提示:请 CTFer 找出这趟地铁的线路,这将作为获取 Flag 必不可少的一环,例如(A1)
图片放大后:

可以辨认出线路站点名。通过搜索关键词,得知这是南京地铁 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
简单的jump —得到的flag请加上FCTF{}
对我来说挺难的(
有两处花指令,需要嗯看汇编把花指令搞掉才能让 IDA 反编译出两个加密函数。
我要采一朵花,送给 REer REer、REer 你看有花~🤓 函数认完了没有😡 啊啊啊啊啊啊,我只是想让你看看这花很好看而已啊😭
先用 DIE 查一下

无壳 32 位,拖进 IDA:
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_411361 和 sub_4110BE。
但是,sub_411361:
// attributes: thunkvoid __cdecl sub_411361(int a1, int a2){ JUMPOUT(0x4117E0);}sub_4110BE:
// attributes: thunkvoid __cdecl sub_4110BE(int a1){ JUMPOUT(0x411930);}均出现 JUMPOUT,说明有脏东西干扰了 IDA 的识别,此时必须从汇编入手,让 IDA 能够识别出正常函数。
先分析 sub_411361。跳转到 0x4117E0 地址:

旁边的地址都是标红的,说明 IDA 无法判断函数的起始位置,需要我们来指定。
loc_41180A 调用的内存地址在 32 位机器上根本无法使用。对 loc_41180A 按 D 键将其强行视为数据而非代码(也可以直接 nop 掉),然后选择 loc_4117E0 到 loc_411803 这一段按 P 键确定反编译函数范围。

实际上,这是指令跳转到了一个误识别的指令内部(偏移值为 2,如果 loc_41180A 对应的指令是完整的,不会有这个偏移值)。
请参考本站的花指令。
对 sub_4117E0 按 F5 反编译即可得到赏心悦目的伪代码:
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 如法炮制

得到:
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 的第二部分密文:rpr1muj2
接下来就可以拿着代码去问 AI 了。
flag 为:FCTF{Op}im`l_2jum1r_pr}
如果使用 AI,请务必让它正向验证,尤其是 sub_4117E0。在我的复现过程中,虽然伪代码的质量更高,但 DeepSeek 给出的答案却是错误的。
也可以通过动态调试,用任意例子验证并修正算法,在比赛过程中 DeepSeek 有要求这一点。由于复现的伪代码比较完整,它没有要求动调验证。
学习了IDA类型修复后,这道题目的逆向结果还可以更加完美。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!