Welcome
略(扫二维码关注公众号领取)
总裁四比特,这能玩?
题目名字叫做4比特,特地上网搜了下有没有相关的知识点
知识点:4-bit(半字节/nibble)操作:
4-bit(半字节/nibble):
1个字节(byte)= 8位(bits)= 2个半字节(nibbles)
高4位(高半字节):bit7-bit4
低4位(低半字节):bit3-bit0
十六进制表示:每个半字节正好对应1位十六进制数字(0-F)
#python里的处理方法:(适合新手体质的解读)
#-----------------------------------------------------------
#首先对于|的用法:
#整数:将两个数字的二进制位进行比较,只要有一个为 1,结果位就为 1。
#字典:将两个字典合并成一个新字典。如果键重复,则后一个字典的值会覆盖前一个。
"""
例子:
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 99, 'c': 3} # 键 'b' 重复了
merged_dict = dict1 | dict2
print(merged_dict) # 输出: {'a': 1, 'b': 99, 'c': 3}
"""
#-----------------------------------------------------------------
#第一步:
#这里读取储存二进制数据,'rb'即读取二进制的模式
with open('文件', 'rb') as q:
data = q.read()
#第二步:
#先创建字典K,将每高位节的四个二进制(半字节/nibble)保存成字典中的一个项目直到遍历整个data
K = [b >> 4 for b in data] ### python的”推导式“格式:[核心操作 for 循环]
### b是一个字节(8个二进制),将这个字节二进制右移4位,即只存高位节
#第三步:
#将两个高位节拼在一起并赋值a
a = bytes((K[i] << 4) | K[i+1] ###一个典型的位运算操作,即把两个高位节拼在一起形成一个字节
for i in range(0, len(K)-1, 2))
###tips1:空四个缩进大格是python的特殊表达方式,意味着内容的延续,这样方便观察代码
###tips2:每次操作后产生的字节会储存在bytes()等到所有循环结束,bytes()会根据接收到的数据一次性产生二进制值
首先给了JPG文件,我先尝试了foremost,发现文件末尾有两个89 50 4E 47 0D 0A 1A 0A结尾的PNG,先单独取出来,发现这两个图片是一样的,仅内容和大小,而且也没有盲水印。
后面手动分析了下,发现这个文件的结构是这样的:


JPG
中间有段莫名其妙的字符
PNG
PNG
我尝试将PNG和中间的字符单独提出来,发现两个文件内存大小是一模一样的:


又结合题目又4-bit,说明这是4-bit(半字节/nibble)操作。再分析发现:
中间的数据和png的16进制文件:
低 4 位完全相同
高 4 位不同


关键:将 mid数据的**高 4 位**提取出来,每两个 nibble 组合成一个完整字节,得到一个 ZIP 文件!
先将mid的高四位的16进制数据粗略看下,每两个 nibble 组合成一个完整字节,发现组合起来是是50 4B 03 04,这是zip的文件头
import io
import zipfile
# 读取文件,我的文件名字就叫mid,然后最后的zip名字叫1.zip
with open('mid', 'rb') as q:
data1 = q.read()
# 提取 mid的高 4 位
mid_high_nibbles = [b >> 4 for b in data1]
# 每两个 nibble 组合成一个字节
zip_data = bytes((mid_high_nibbles[i] << 4) | mid_high_nibbles[i+1]
for i in range(0, len(mid_high_nibbles)-1, 2))
with open('1.zip', 'wb') as f:
f.write(zip_data)

分析这个完整的png:

后面还藏了一个zip文件,这个里面带flag.txt
提出来,解压得到:
UniCTF{Y0u_4r3_4_6r347_h4ck3r_!}
工厂应急流量分析
Silent Resolver
看看我藏了什么
拿到题目只给了一个 traffic.pcapng。我第一反应就是先在包里搜一搜关键字(`flag` / `ctf` / `{}` 之类),因为很多 Misc 就藏在明文里。
这份包不大(几十 KB),但直接扫 payload 并没有出现 `flag` / `CTF{` 这种直给的字符串,不过能看到一些 DNS 请求里带了 `unictf` 相关域名。这些是可疑的流量
只看的带unictf的流量
frame contains "exfil"

0000.<很长一串>.a1b2c3d4.exfil.unictf.local
0001.<很长一串>.a1b2c3d4.exfil.unictf.local
...
ffff.1818be0b.a1b2c3d4.exfil.unictf.local
几个明显的信号:
- `exfil` 这种前缀基本就是“我在外带数据”;
- 第一段是 `0000 / 0001 / ...`,像分片序号;
- 中间那坨字符只包含 `a-z` 和 `2-7`(夹杂少量数字),尝试复制一组进cyberchef,提示是Base32;
- 最后一个 `ffff.1818be0b` 看着像结束标记 + 校验值(十六进制 8 位)。
把这些很长的字符全部拿到一个文件里
Sign in
拿到附件是个很小的 zip(200B)
解压出来的文件很小,没有啥用,看看压缩包的hex编码,发现在结尾标识后还有一串ascll代码
U2VjcmV0S2V5

查找资料发现这是zip comment(压缩包注释)
用7z打开验证下,发现确实是注释

Base64解码:
SecretKey
接下来关于压缩包里面的Serpent.dat 是什么
是 **Serpent 分组密码**(128-bit block)的 ECB 两个 block。
密码学知识点:
Serpent算法加密一个128位数据块的过程,可以简化为32轮重复操作,每轮做三件事:
混入密钥:把数据跟这一轮的密钥做异或
S盒替换:把数据分成32个小段,每段4位,用固定的替换表(S盒)换掉
线性变换:对数据做移位、异或等操作,让数据充分混合
最后再加一轮密钥,输出密文。
更形象的比喻:就像把数据块(128位)放进一个32层的加工流水线,每层都加一次密钥、换一次零件(S盒)、搅匀一次(线性变换),最后出来就是加密后的密文。
坑
- 注释里给的只是 `SecretKey`(9 字节),但实际加密通常会喂给 cipher 一个固定长度 key(比如 16 字节)。
- 这题的正确做法是:`SecretKey` **右侧用 `\x00` 补到 16 字节** 作为输入 key。
解出来的明文末尾还有几个 `\x00`,直接 `rstrip(b"\x00")` 就行。
解出来
UniCTF{Serpentine_Secrets}
ai生成的解码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import base64
import struct
import zipfile
# ---------------- Serpent primitive ----------------
SBOX = [
[3, 8, 15, 1, 10, 6, 5, 11, 14, 13, 4, 2, 7, 0, 9, 12],
[15, 12, 2, 7, 9, 0, 5, 10, 1, 11, 14, 8, 6, 13, 3, 4],
[8, 6, 7, 9, 3, 12, 10, 15, 13, 1, 14, 4, 0, 11, 5, 2],
[0, 15, 11, 8, 12, 9, 6, 3, 13, 1, 2, 4, 10, 7, 5, 14],
[1, 15, 8, 3, 12, 0, 11, 6, 2, 5, 4, 10, 9, 14, 7, 13],
[15, 5, 2, 11, 4, 10, 9, 12, 0, 3, 14, 8, 13, 6, 7, 1],
[7, 2, 12, 5, 8, 4, 6, 11, 14, 9, 1, 15, 13, 3, 10, 0],
[1, 13, 15, 0, 14, 8, 2, 11, 7, 4, 12, 10, 9, 3, 5, 6],
]
INV_SBOX = []
for sb in SBOX:
inv = [0] * 16
for i, v in enumerate(sb):
inv[v] = i
INV_SBOX.append(inv)
MASK32 = 0xFFFFFFFF
PHI = 0x9E3779B9
def rotl32(x: int, r: int) -> int:
x &= MASK32
return ((x << r) | (x >> (32 - r))) & MASK32
def rotr32(x: int, r: int) -> int:
x &= MASK32
return ((x >> r) | (x << (32 - r))) & MASK32
def sbox_bitslice(sb, w0: int, w1: int, w2: int, w3: int):
"""Apply a 4-bit S-box in bitslice form on 4x32-bit words."""
y0 = y1 = y2 = y3 = 0
for b in range(32):
nib = ((w0 >> b) & 1) | (((w1 >> b) & 1) << 1) | (((w2 >> b) & 1) << 2) | (((w3 >> b) & 1) << 3)
out = sb[nib]
if out & 1:
y0 |= 1 << b
if out & 2:
y1 |= 1 << b
if out & 4:
y2 |= 1 << b
if out & 8:
y3 |= 1 << b
return y0 & MASK32, y1 & MASK32, y2 & MASK32, y3 & MASK32
def lt(w0: int, w1: int, w2: int, w3: int):
w0 = rotl32(w0, 13)
w2 = rotl32(w2, 3)
w1 = (w1 ^ w0 ^ w2) & MASK32
w3 = (w3 ^ w2 ^ ((w0 << 3) & MASK32)) & MASK32
w1 = rotl32(w1, 1)
w3 = rotl32(w3, 7)
w0 = (w0 ^ w1 ^ w3) & MASK32
w2 = (w2 ^ w3 ^ ((w1 << 7) & MASK32)) & MASK32
w0 = rotl32(w0, 5)
w2 = rotl32(w2, 22)
return w0, w1, w2, w3
def inv_lt(w0: int, w1: int, w2: int, w3: int):
w2 = rotr32(w2, 22)
w0 = rotr32(w0, 5)
w2 = (w2 ^ w3 ^ ((w1 << 7) & MASK32)) & MASK32
w0 = (w0 ^ w1 ^ w3) & MASK32
w3 = rotr32(w3, 7)
w1 = rotr32(w1, 1)
w3 = (w3 ^ w2 ^ ((w0 << 3) & MASK32)) & MASK32
w1 = (w1 ^ w0 ^ w2) & MASK32
w2 = rotr32(w2, 3)
w0 = rotr32(w0, 13)
return w0, w1, w2, w3
def key_schedule(user_key: bytes):
"""Serpent key schedule (like common reference impl):
- accepts 16/24/32 bytes key, then pads with a single 1-bit (implemented as byte 0x01) and zeros to 32 bytes.
"""
if len(user_key) > 32:
raise ValueError("key too long")
if len(user_key) < 32:
user_key = user_key + b"\x01" + b"\x00" * (31 - len(user_key))
w = list(struct.unpack("<8I", user_key))
for i in range(8, 140):
t = w[i - 8] ^ w[i - 5] ^ w[i - 3] ^ w[i - 1] ^ PHI ^ (i - 8)
w.append(rotl32(t, 11))
keys = []
for i in range(33):
a, b, c, d = w[4 * i + 8], w[4 * i + 9], w[4 * i + 10], w[4 * i + 11]
# key schedule uses S_(3-i)
a, b, c, d = sbox_bitslice(SBOX[(3 - i) % 8], a, b, c, d)
keys.append((a, b, c, d))
return keys
def decrypt_block(block16: bytes, round_keys):
w0, w1, w2, w3 = struct.unpack("<4I", block16)
# last round: xor K32, invS7, xor K31
k32 = round_keys[32]
w0 ^= k32[0]
w1 ^= k32[1]
w2 ^= k32[2]
w3 ^= k32[3]
w0, w1, w2, w3 = sbox_bitslice(INV_SBOX[7], w0, w1, w2, w3)
k31 = round_keys[31]
w0 ^= k31[0]
w1 ^= k31[1]
w2 ^= k31[2]
w3 ^= k31[3]
for r in range(30, -1, -1):
w0, w1, w2, w3 = inv_lt(w0, w1, w2, w3)
w0, w1, w2, w3 = sbox_bitslice(INV_SBOX[r % 8], w0, w1, w2, w3)
kr = round_keys[r]
w0 ^= kr[0]
w1 ^= kr[1]
w2 ^= kr[2]
w3 ^= kr[3]
return struct.pack("<4I", w0 & MASK32, w1 & MASK32, w2 & MASK32, w3 & MASK32)
# ---------------- challenge logic ----------------
def main(zip_path: str):
with zipfile.ZipFile(zip_path, "r") as z:
# zip comment -> base64 -> b"SecretKey"
raw = z.comment.strip()
key_str = base64.b64decode(raw)
data = z.read("Serpent.dat")
# 关键点:题目这里是把 key 补到 16 字节再喂给 cipher
user_key = key_str.ljust(16, b"\x00")
rk = key_schedule(user_key)
# ECB (2 blocks)
pt = decrypt_block(data[:16], rk) + decrypt_block(data[16:32], rk)
pt = pt.rstrip(b"\x00")
print(pt.decode())
if __name__ == "__main__":
import sys
zip_path = sys.argv[1] if len(sys.argv) > 1 else "attachment (1).zip"
main(zip_path)
最终输入nc端口,拿到最终的flag
截取的线索

7文件里面是:
RinDSA|W6dlkbXsob
下面的PNG是

这个不像是摩斯密码,有长短也有中
按 **白(255)=1,黑(0)=0** 转成 bit 流,然后每 8 个 bit 组一个 byte。
def decode_scanline_png(png_path: str) -> str:
img = Image.open(png_path).convert("L")
w, h = img.size
assert h == 1, f"expected 1px height, got {img.size}"
pixels = list(img.getdata())
# 白(255)=1,黑(0)=0
bits = [1 if p > 127 else 0 for p in pixels]
assert len(bits) % 8 == 0, "bit length should be multiple of 8"
out = []
for i in range(0, len(bits), 8):
b = 0
for j in range(8):
b = (b << 1) | bits[i + j]
out.append(b)
return bytes(out).decode("ascii")
得出来:
_Great_to01}
而第一个文件我思考了很久
因为png给了个flag后半部分,所以这边尽量往前半部分靠
重点:文件名:7!!!!!!!!!!!!!

解出来
UniCTF{P1ckle_the_Great_to01}
BlueBreath(哥斯拉webshell)
巨兽并不会凭空消失,它只是换了一种频率在呼吸
先初步看一遍,大量对 `172.30.96.1:8000` 的请求都在 404(像是目录扫描),但其中有少数返回 200
http.response.code == 200

找到这些异常的http回复OK的流
流5774:

其中 /uploads/shell.php更离谱:请求是 POST,Content-Type: application/octet-stream,body 不是表单而是一坨二进制,看着就像 WebShell 管理工具的通信数据。
另外还能看到一次 OST /index.php`的 multipart 上传(文件名 `hint.zip`),里面有个hint.png。强行提取出来利用png文件头进行明文攻击
bkcrack.exe -C 1.zip -c hint.png -p png_header
得到zip加密密钥:
24cc690a 59f6451b e4fbbab0
再用密钥继续开出png:
./bkcrack.exe -C 1.zip -c hint.png -k 24cc690a 59f6451b e4fbbab0 -d hint.png
再观察这个图片,基本的注释,文件杂糅的隐写都没有,尝试LSB隐写

(注意到这里,图片人眼看到的只有黑色白色,而这些像素点又有别的颜色,而127是255的二进制储存第一位由1改为0.说明是LSB隐写的通道7:)
用stegsolve看,在通道7的时候左上角会出现黑点点

将Ahiz_2026进行md5处理,得到key的MD5前16位

2dc3ef5ff0c67015
对/uploads/shell.php` 的连接做 Follow TCP Stream,会发现:
第一次连接里有一个超大的 POST(几万字节),服务端回包里下发 `Set-Cookie: PHPSESSID=...
后续几次连接都带着相同的 `PHPSESSID`,POST 的 body 较短
知识点:关于哥斯拉那套逻辑(刚好印证题目)
第一次把 payload 注入到 session 里,之后都靠 cookie 维持状态。
麻烦在于:body 是加密/混淆后的,需要把它还原出来
得到了key,再用xor进行解密
第一个post前面几行:
`$parameters=array();
$_SES=array();
function run($pms)
{
...`
说XOR 之后的明文还被 gzip 压缩过,再 gzip.decompress() 才能看到真正内容。
解出来的请求就很直白了,例如:
- methodName ... test
- methodName ... getBasicsInfo ; 回系统信息(当前目录、uname、php 版本等)
- methodName ... execCommand + `cmdLine ...` ; 直接执行系统命令
其中还有一条:
sh -c "cd "/var/www/html/uploads/";cat flag.txt"
对应响应解 gzip 后直接得到 flag

UniCTF{w1reSha3k_easy_or_hard}

Comments NOTHING