## LearnRag
在此感谢谷歌Colab和哈机密ai的大力赞助
flag有些乱 最后团队里大伙一起拼好的。
所谓拼好题就是xxxxx后面不能说了,exp和之前某次比赛差不多,图中就是所有的exp了。
L3HCTF{wowthisisembedding}
TemporalParadox
根据f5报错nop掉一些东西。可以很轻松地找到关键主逻辑 本题tag:时间戳爆破 rand seed 相关知识点23年春秋杯考过了 不过感觉本题是想科普什么md5加盐之类的东西,没懂。 然后动调改跳转把salt搞出来 salt貌似是个什么函数生成 没管( 写个脚本爆破一下 依旧感谢 开虚拟机linux跑一下
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <openssl/md5.h>
#define TMIN 1751990400u
#define TMAX 1752052051u
#define TARGET "8a2fc1e9e2830c37f8a7f51572a640aa"
static uint32_t rng_state;
/* ------------- 线性反馈伪随机,与原算法等价 ------------- */
static inline uint32_t prng_next(void)
{
uint32_t x = rng_state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
rng_state = x;
return x & 0x7fffffffU;
}
/* ------------- 16-byte MD5 → 32-byte lower-hex ------------- */
static void md5_to_hex(const unsigned char *in, char *out)
{
static const char lut[] = "0123456789abcdef";
for (int i = 0; i < 16; ++i) {
uint8_t v = in[i];
out[i * 2] = lut[v >> 4];
out[i * 2 + 1] = lut[v & 0x0f];
}
out[32] = '\\\\0';
}
/* ------------- 主程序 ------------- */
int main(void)
{
unsigned char digest[16];
char hex[33];
char query[256];
for (uint32_t t = TMIN; t <= TMAX; ++t) {
/* 初始化 PRNG */
rng_state = t;
uint32_t a = 0, b = 0, x = 0, y = 0;
uint32_t loop = prng_next();
/* 每轮消耗 4 个随机数,完全对齐原逻辑 */
while (loop--) {
a = prng_next();
b = prng_next();
x = prng_next();
y = prng_next();
}
uint32_t r = prng_next();
int qlen = snprintf(query, sizeof(query),
"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5"
"&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u",
t, r, a, b, x, y);
MD5((unsigned char *)query, qlen, digest);
md5_to_hex(digest, hex);
if (strcmp(hex, TARGET) == 0) {
puts(">>> MATCH FOUND! <<<");
puts(query);
return 0;
}
}
puts("扫描完毕,未命中目标。");
return 0;
}
## obfuscate
手动过掉一些反调试 然后跟着调试尝试寻找输入 然后发现一个类似key的东西 WelcometoL3HCTF!,但是函数中key做了复杂的计算 但不涉及输入 同时可以看到% 那么大概率可以猜到这是个KSA结构。那么很显然我们可以直接提取S盒就好了。 s盒:
sbox = [0x122F2C9C, 0xE3BCCAE7, 0xD0FFC0F2, 0xD9A12544, 0x8A27992F,
0x55B1B935, 0x9110B161, 0x92811564, 0x5CE9B359, 0x77C79A51,
0x4265527A, 0x8AB57C4B, 0x11529FA4, 0x9D9F63FF, 0xA970B936,
0xC8EABA0D, 0x9A0EB4AA, 0xB0BC6E7F, 0x9784B100, 0x70DCD3AE,
0x6057A44E, 0x89187658, 0xE00098A8, 0x45773540, 0xF9374F1A,
0x913FA548]
跟踪sbox找到加密程序,但是不是RC4,讨论时感觉像RC5/6(把这个信息喂给哈机密ai),哈机密ai梭哈了一下,出来了(赛后队友告诉我是 RC5)
1️⃣ 单块测试
明文: b'09876543'
明文(hex): 3039383736353433
密文(hex): 78f2ca6e7e5c0762
解密结果: bytearray(b'09876543')
解密(hex): 3039383736353433
解密成功: True
2️⃣ 已知密文解密
密文(hex): 1BBBA1F2E97C87218A37FD0A941A81BC401EE3AA732ED83F84B87142CC358B39
解密结果: b'5fd277be39046905ef6348ba89131922'
解密(hex): 3566643237376265333930343639303565663633343862613839313331393232
ASCII解析: '5fd277be39046905ef6348ba89131922'
3️⃣ 长文本测试
原文: b'Hello, this is a test message for Feistel cipher!'
加密后(hex): cbeb26cd874fd2c37e5ded66b52197bbcf2836d3660add7522c77367881ad305148edfe7bf21913a3028888faaf90ff691f1ab74f81d9481
解密后: b'Hello, this is a test message for Feistel cipher!'
长文本解密成功: True
🔬 S盒分析
S盒长度: 26
S盒前8个值: ['0x122f2c9c', '0xe3bccae7', '0xd0ffc0f2', '0xd9a12544', '0x8a27992f', '0x55b1b935', '0x9110b161', '0x92811564']
唯一值数量: 26/26
平均值: 2366628618 (0x8d0fe309)
超高校级密码冠军小课堂
以下是哈机密ai的exp
def a32small(byte_array):
"""字节数组转32位小端整数数组"""
return [int.from_bytes(byte_array[i:i + 4], byteorder='little', signed=False) for i in range(0, len(byte_array), 4)]
def b32small(dword_array):
"""32位整数数组转字节数组"""
byte_list = bytearray()
for dword in dword_array:
byte_list.extend(dword.to_bytes(4, byteorder='little'))
return byte_list
# S盒 - 密钥材料
sbox = [0x122F2C9C, 0xE3BCCAE7, 0xD0FFC0F2, 0xD9A12544, 0x8A27992F,
0x55B1B935, 0x9110B161, 0x92811564, 0x5CE9B359, 0x77C79A51,
0x4265527A, 0x8AB57C4B, 0x11529FA4, 0x9D9F63FF, 0xA970B936,
0xC8EABA0D, 0x9A0EB4AA, 0xB0BC6E7F, 0x9784B100, 0x70DCD3AE,
0x6057A44E, 0x89187658, 0xE00098A8, 0x45773540, 0xF9374F1A,
0x913FA548]
def ROL32(x, shift):
"""32位循环左移"""
shift = shift & 31 # 确保shift在0-31范围内
return ((x << shift) | (x >> (32 - shift))) & 0xFFFFFFFF
def ROR32(x, shift):
"""32位循环右移"""
shift = shift & 31 # 确保shift在0-31范围内
return ((x >> shift) | (x << (32 - shift))) & 0xFFFFFFFF
def feistel_encrypt(plaintext_bytes):
"""
Feistel加密函数
输入: 8字节明文
输出: 8字节密文
"""
# 转换为32位整数对
m = a32small(plaintext_bytes)
# 初始化: 加上S盒的前两个值
num = [(m[0] + sbox[0]) & 0xFFFFFFFF, (m[1] + sbox[1]) & 0xFFFFFFFF]
# 12轮Feistel变换
for i in range(1, 13):
# 第一步: 变换左半部分
num[0] = (sbox[2 * i] + ROL32(num[0] ^ num[1], num[1] & 31)) & 0xFFFFFFFF
# 第二步: 变换右半部分
num[1] = (sbox[2 * i + 1] + ROL32(num[0] ^ num[1], num[0] & 31)) & 0xFFFFFFFF
# 第三步: 异或交换
num[0] = (num[0] ^ num[1]) & 0xFFFFFFFF
# 转换回字节数组
return b32small(num)
def feistel_decrypt(ciphertext_bytes):
"""
Feistel解密函数
输入: 8字节密文
输出: 8字节明文
"""
# 转换为32位整数对
num = a32small(ciphertext_bytes)
# 12轮逆向Feistel变换
for i in range(12, 0, -1):
# 逆向第三步: 异或交换的逆操作
num[0] = (num[0] ^ num[1]) & 0xFFFFFFFF
# 逆向第二步: 右半部分的逆变换
# num[1] = sbox[2*i+1] + ROL32(num[0] ^ num[1], num[0] & 31)
# 所以: ROL32(num[0] ^ num[1], num[0] & 31) = num[1] - sbox[2*i+1]
# 因此: num[0] ^ num[1] = ROR32(num[1] - sbox[2*i+1], num[0] & 31)
temp = (num[1] - sbox[2 * i + 1]) & 0xFFFFFFFF
num[1] = (ROR32(temp, num[0] & 31) ^ num[0]) & 0xFFFFFFFF
# 逆向第一步: 左半部分的逆变换
temp = (num[0] - sbox[2 * i]) & 0xFFFFFFFF
num[0] = (ROR32(temp, num[1] & 31) ^ num[1]) & 0xFFFFFFFF
# 逆向初始化: 减去S盒的前两个值
num = [(num[0] - sbox[0]) & 0xFFFFFFFF, (num[1] - sbox[1]) & 0xFFFFFFFF]
# 转换回字节数组
return b32small(num)
def encrypt_multiple_blocks(plaintext_bytes):
"""
加密多个8字节块
"""
if len(plaintext_bytes) % 8 != 0:
# 如果不是8的倍数,进行PKCS7填充
padding_len = 8 - (len(plaintext_bytes) % 8)
plaintext_bytes += bytes([padding_len] * padding_len)
result = bytearray()
for i in range(0, len(plaintext_bytes), 8):
block = plaintext_bytes[i:i + 8]
encrypted_block = feistel_encrypt(block)
result.extend(encrypted_block)
return bytes(result)
def decrypt_multiple_blocks(ciphertext_bytes):
"""
解密多个8字节块
"""
result = bytearray()
for i in range(0, len(ciphertext_bytes), 8):
block = ciphertext_bytes[i:i + 8]
decrypted_block = feistel_decrypt(block)
result.extend(decrypted_block)
# 移除PKCS7填充
if result:
padding_len = result[-1]
if padding_len <= 8 and all(b == padding_len for b in result[-padding_len:]):
result = result[:-padding_len]
return bytes(result)
def main():
"""主函数 - 演示加密解密过程"""
print("🔐 Feistel密码 完整EXP")
print("=" * 50)
# 测试1: 单个8字节块
print("\\\\n1️⃣ 单块测试")
test_input = b"09876543"
print(f"明文: {test_input}")
print(f"明文(hex): {test_input.hex()}")
# 加密
encrypted = feistel_encrypt(test_input)
print(f"密文(hex): {encrypted.hex()}")
# 解密
decrypted = feistel_decrypt(encrypted)
print(f"解密结果: {decrypted}")
print(f"解密(hex): {decrypted.hex()}")
print(f"解密成功: {test_input == decrypted}")
# 测试2: 给定的密文解密
print("\\\\n2️⃣ 已知密文解密")
cipher_hex = "1BBBA1F2E97C87218A37FD0A941A81BC401EE3AA732ED83F84B87142CC358B39"
cipher_bytes = bytearray.fromhex(cipher_hex)
print(f"密文(hex): {cipher_hex}")
# 解密多个块
decrypted_result = decrypt_multiple_blocks(cipher_bytes)
print(f"解密结果: {decrypted_result}")
print(f"解密(hex): {decrypted_result.hex()}")
# 尝试解析为ASCII
try:
ascii_result = decrypted_result.decode('ascii', errors='ignore')
print(f"ASCII解析: '{ascii_result}'")
except:
print("无法解析为ASCII")
# 测试3: 长文本加密解密
print("\\\\n3️⃣ 长文本测试")
long_text = b"Hello, this is a test message for Feistel cipher!"
print(f"原文: {long_text}")
# 加密
encrypted_long = encrypt_multiple_blocks(long_text)
print(f"加密后(hex): {encrypted_long.hex()}")
# 解密
decrypted_long = decrypt_multiple_blocks(encrypted_long)
print(f"解密后: {decrypted_long}")
print(f"长文本解密成功: {long_text == decrypted_long}")
def crack_known_plaintext(known_pairs):
"""
已知明文攻击 - 如果有明文密文对,可以验证S盒
"""
print("\\\\n🎯 已知明文攻击验证")
for plaintext, ciphertext in known_pairs:
calculated_cipher = feistel_encrypt(plaintext)
match = calculated_cipher == ciphertext
print(f"明文: {plaintext.hex()}")
print(f"期望密文: {ciphertext.hex()}")
print(f"计算密文: {calculated_cipher.hex()}")
print(f"匹配: {match}")
print("-" * 30)
def analyze_sbox():
"""分析S盒特性"""
print("\\\\n🔬 S盒分析")
print(f"S盒长度: {len(sbox)}")
print(f"S盒前8个值: {[hex(x) for x in sbox[:8]]}")
# 检查重复值
unique_count = len(set(sbox))
print(f"唯一值数量: {unique_count}/{len(sbox)}")
# 检查值的分布
avg_val = sum(sbox) / len(sbox)
print(f"平均值: {avg_val:.0f} (0x{int(avg_val):08x})")
if __name__ == "__main__":
main()
analyze_sbox()
# 如果有已知的明文密文对,可以用这个函数验证
# known_pairs = [(b"testdata", bytearray.fromhex("..."))]
# crack_known_plaintext(known_pairs)
## SNAKE
游戏题 同样加了很多反调试 在此感谢xxx赞助的超绝IDA9.1和xxx赞助的超绝反调试插件
讲个笑话 有人一开始反调试加多了跑不起来,以为要用titanhide。
队友用厉害的方法剖析了这道题,但是因为时间问题所以这道题的wp依旧由本菜鸡撰写一个偷鸡解法。

通过调试和静态分析找到主要流程 将食物部分的逻辑改掉即可
2个jz改jnz
好的,我们来分析这段来自CTF逆向工程(Reverse Engineering)挑战的伪代码。这看起来像是用IDA Pro之类的工具从一个用Go语言编写的贪吃蛇游戏中反编译出来的C伪代码。
代码的核心逻辑是,flag并不是在满足某个特定条件(比如分数达到100)时才显示,而是在每次吃到食物时,都会被“解密”一次。 你需要正确地进行游戏,才能在最后得到完全解密的flag。
下面我为你分步解析关键部分:
1. 关键数据结构
首先,要理解几个重要的变量,它们很可能是指向游戏核心数据结构的指针:
v11 (或 v79): 指向一个主游戏状态的结构体。这个结构体里包含了游戏板的尺寸、加密密钥、加密的flag等信息。
v25 (或 v80): 指向代表蛇身体的结构体(在Go中通常是“切片”或slice)。这个结构体里包含了蛇的坐标、长度,以及我们最关心的分数。
2. Score(分数)的关键项
我们来定位分数是如何增加的。请看这部分代码:
复制
// ...
v51 = *(_QWORD **)(v11 + 8);
if ( *v51 == v49 && v51[1] == n3 )
{
++*(_QWORD *)(v25 + 32); // <--- 分数增加在这里!
// ...
}
// ...
分析:
if ( *v51 == v49 && v51[1] == n3 ): 这是一个条件判断。v51 指向的是蛇头的坐标,而 v49 和 n3 是食物的坐标。这个 if 语句的作用就是判断蛇头是否碰到了食物。
++*(_QWORD *)(v25 + 32);: 如果蛇吃到了食物,这行代码就会执行。它将 v25 指针偏移 32 字节位置的值加一。
结论: 分数(Score)就存储在 v25 所指向的蛇结构体偏移 32 字节的位置。每次吃到食物,这个值就会 +1。
笑点1 patch思路是从这里来的 笑点2 flag恰好是100分显示
然后玩通关,截图找不到了 写wp的时候又玩了一次
发表回复