L3HCTF RE wp
冠军!这次真的起飞了,正式打CTF一年零两个月终于拿下了第一个大赛的冠军
RE7道题出了道,不过有俩题都是队友给的思路我把解flag脚本搓出来了,其中TemporaPardox拿下1血,AWayOut2拿下2血,Good!
其他方向队友均很给力,每个方向都有血,其中Web甚至6道拿下4道1血,SU战队也是在包队带领下起飞一次了,希望以后愈战愈勇!
TemporalParadox
main开头有花指令,nop掉跳转jmp即可反编译。动态调试跑一轮就知道各个函数的功能
__int64 __fastcall sub_140001D05(__int64 a1, __int64 a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
sub_140002180(a1, a2);
sub_14000A510(a1, a2, v2, (unsigned int)v53, v3, v4);
v58 = get_time(a1, a2, v5, 0, v6, v7);
if ( v58 > 1751990400 && v58 <= 1752052051 )
{
gen_query(a1, a2, v8, (unsigned int)v49, v9, v10);
v57 = &v54;
v12 = std::string::c_str(a1, a2, v11, v49);
v15 = md5(a1, a2, v12, (unsigned int)v53, v13, v14, v41);
sub_14000A820(a1, a2, v15, (unsigned int)v50, (unsigned int)&v54, v16, v42, v47);
sub_14000A6E0(a1, a2, v17, (unsigned int)&v54, v18, v19, v43);
v20 = std::operator<<<std::char_traits<char>>(a1, a2, "query: ", &std::cout);
v21 = std::operator<<<char>(a1, a2, v49, v20);
std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v21);
v22 = std::operator<<<char>(a1, a2, v50, &std::cout);
std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v22);
std::string::~string(a1, a2, v23, v50);
std::string::~string(a1, a2, v24, v49);
}
std::string::basic_string(a1, a2, v8, v52);
v25 = std::operator<<<std::char_traits<char>>(a1, a2, "Please input the right query string I used:", &std::cout);
std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v25);
std::operator>><char>(a1, a2, v52, &std::cin);
v56 = &v55;
v27 = std::string::c_str(a1, a2, v26, v52);
v30 = md5(a1, a2, v27, (unsigned int)v53, v28, v29, v41);
sub_14000A820(a1, a2, v30, (unsigned int)v51, (unsigned int)&v55, v31, v44, v47);
sub_14000A6E0(a1, a2, v32, (unsigned int)&v55, v33, v34, v45);
if ( (unsigned __int8)sub_14000A8E0(
a1,
a2,
(unsigned int)"8a2fc1e9e2830c37f8a7f51572a640aa",
(unsigned int)v51,
v35,
v36,
v46,
v48) )
v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Congratulations!", &std::cout);
else
v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Wrong!", &std::cout);
std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v37);
std::string::~string(a1, a2, v38, v51);
std::string::~string(a1, a2, v39, v52);
return 0LL;
}
可以看出我们需要得到正确的query并满足query md5加密后的值等于8a2fc1e9e2830c37f8a7f51572a640aa;if里是对时间的判断显然是告诉我们要爆破的话时间范围是(1751990400,1752052051)
进入gen_query可以看到各个参数的生成,可以看到两种query,一种是满足pow_like函数的判断则没有a、b、x、y参数,但多了cipher参数;get_rand是模拟生成随机数
__int64 __fastcall sub_140001963(
__time64_t *a1,
__int64 a2,
int a3,
__int64 a4,
int a5,
int a6,
double a7,
double a8,
double a9,
double a10)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
sub_140001518((_DWORD)a1, a2, a3, (unsigned int)v51, a5, a6);
time = get_time(a1);
sub_1400014B5((_DWORD)a1, a2, v10, time, v11, v12);
v58 = 0;
v57 = 0;
v56 = 0;
v55 = 0;
for ( i = 0; i < (int)gen_rand(); ++i )
{
v58 = gen_rand();
v57 = gen_rand();
v56 = gen_rand();
v55 = gen_rand();
}
v52 = gen_rand();
std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::basic_stringstream(a1, a2, v13, v49);
v14 = (double)dword_14000B0E0;
v15 = (double)(int)(v58 | v56);
v18 = v14 * pow_like(v15, 2.0, v15, a10, v16, v17, (double)dword_14000B0E0);
v19 = (double)dword_14000B0E4;
if ( v18 == pow_like((double)(int)(v57 | v55), 2.0, v15, (double)(int)(v57 | v55), v20, v21, v18) * v19 )
{
v22 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50);
v23 = std::operator<<<char>(a1, a2, v51, v22);
v24 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v23);
v25 = std::ostream::operator<<(a1, a2, time, v24);
v26 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v25);
v27 = std::ostream::operator<<(a1, a2, v52, v26);
v28 = std::operator<<<std::char_traits<char>>(a1, a2, "&cipher=", v27);
v31 = sub_14000184D((_DWORD)a1, a2, time, v52, v29, v30, v48);
std::ostream::operator<<(a1, a2, v31, v28);
}
else
{
v32 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50);
v33 = std::operator<<<char>(a1, a2, v51, v32);
v34 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v33);
v35 = std::ostream::operator<<(a1, a2, time, v34);
v36 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v35);
v37 = std::ostream::operator<<(a1, a2, v52, v36);
v38 = std::operator<<<std::char_traits<char>>(a1, a2, "&a=", v37);
v39 = std::ostream::operator<<(a1, a2, v58, v38);
v40 = std::operator<<<std::char_traits<char>>(a1, a2, "&b=", v39);
v41 = std::ostream::operator<<(a1, a2, v57, v40);
v42 = std::operator<<<std::char_traits<char>>(a1, a2, "&x=", v41);
v43 = std::ostream::operator<<(a1, a2, v56, v42);
v44 = std::operator<<<std::char_traits<char>>(a1, a2, "&y=", v43);
std::ostream::operator<<(a1, a2, v55, v44);
}
std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::str(a1, a2, v49, a4);
std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::~basic_stringstream(a1, a2, v45, v49);
std::string::~string(a1, a2, v46, v51);
return a4;
}
__int64 gen_rand()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
v1 = (((dword_14000B040 << 13) ^ (unsigned int)dword_14000B040) >> 17) ^ (dword_14000B040 << 13) ^ dword_14000B040;
dword_14000B040 = (32 * v1) ^ v1;
return dword_14000B040 & 0x7FFFFFFF;
}
__int64 __fastcall sub_1400014B5(_DWORD a1, _DWORD a2, _DWORD a3, unsigned int a4)
{
__int64 result; // rax
unsigned int v5; // [rsp+10h] [rbp+10h]
v5 = a4;
if ( !a4 )
v5 = 1;
result = v5;
dword_14000B040 = v5;
return result;
}
调试可以发现dword_14000B040初始值为get_time返回的time,此外salt值固定为tlkyeueq7fej8vtzitt26yl24kswrgm5,因此a、b、x、y实际上都和t相关
因此首先我写了个python脚本来爆破(c不擅长,部分函数如sub_14000184D直接让gemini分析生成模拟代码,但事后发现其实没用到)
import math
from hashlib import md5, sha1
def gen(dword):
v1 = ((((dword << 13)&0xffffffff) ^ dword) >> 17) ^ ((dword << 13)&0xffffffff) ^ dword
dword = (((32 * v1)&0xffffffff) ^ v1) &0xffffffff
return dword, dword & 0x7FFFFFFF
S_BOX_TABLE_7FF65E2BC020 = [0x0000000E, 0x00000004, 0x0000000D, 0x00000001, 0x00000002, 0x0000000F, 0x0000000B, 0x00000008, 0x00000003, 0x0000000A, 0x00000006, 0x0000000C, 0x00000005, 0x00000009, 0x00000000, 0x00000007]
P_BOX_TABLE_7FF65E2BC0A0 = [0x00000001, 0x00000005, 0x00000009, 0x0000000D, 0x00000002, 0x00000006, 0x0000000A, 0x0000000E, 0x00000003, 0x00000007, 0x0000000B, 0x0000000F, 0x00000004, 0x00000008, 0x0000000C, 0x00000010]
def to_u32(n):
"""将一个数转换为32位无符号整数"""
return n & 0xFFFFFFFF
def to_s32(n):
"""将一个数转换为32位有符号整数"""
n = n & 0xFFFFFFFF
if n & 0x80000000:
return n - 0x100000000
return n
def generate_salt(dword_array):
"""
对应 C++ 函数 sub_7FF65E2B1518
根据硬编码的 dword 数组生成一个32字符的 salt 字符串。
"""
if not dword_array:
raise ValueError("错误: dword_7FF65E2BB060 数组为空,请填写数据。")
s = []
for i in range(32):
v9 = dword_array[i]
v10 = 0
# C++ int 是32位的,Python int 是无限精度的,需要模拟32位行为
v9_s32 = to_s32(v9)
if v9_s32 >= 0:
v10 = v9_s32 / 3 + 48
elif v9_s32 >= -728:
# ~v9 在C++中是对32位整数按位取反
v10 = ~v9_s32 & 0xFFFFFFFF
else:
# 这里的 sub_7FF65E2B31D0 / 1.0986... 被我们分析为 log3
# math.log(x) 是 ln(x),math.log(3) 是 ln(3)
# log3(x) = ln(x) / ln(3)
try:
log_val = math.log(-v9_s32) / math.log(3)
v10 = log_val - 6.0 + 48.0
except ValueError:
# 如果 -v9_s32 <= 0,log会出错,这里设置一个默认值
v10 = 48 # '0'
# 将计算结果转换为字符
s.append(chr(int(v10) & 0xFF))
return "".join(s)
def s_box_transform(state, s_box_table):
"""
对应 C++ 函数 sub_7FF65E2B16C1 (S-盒替换)
"""
if not s_box_table:
raise ValueError("错误: S_BOX_TABLE_7FF65E2BC020 数组为空,请填写数据。")
s = to_u32(state)
for _ in range(4):
# 提取高4位作为索引
index = (s >> 12) & 0xF
sbox_val = s_box_table[index]
# (16 * s) 等价于 (s << 4)
s = sbox_val | (s << 4)
return to_u32(s)
def p_box_transform(state, p_box_table):
"""
对应 C++ 函数 sub_7FF65E2B1785 (P-盒置换)
"""
if not p_box_table:
raise ValueError("错误: P_BOX_TABLE_7FF65E2BC0A0 数组为空,请填写数据。")
s = to_u32(state)
new_state = 0
for i in range(16):
# 获取源比特的位置 (C数组是1-based, Python是0-based)
source_bit_pos = p_box_table[i] - 1
# 检查源比特是否为1
if (s >> source_bit_pos) & 1:
# 如果是1,则在目标位置i设置比特
new_state |= (1 << i)
return new_state
def round_function(state, s_box_table, p_box_table):
"""
对应 C++ 函数 sub_7FF65E2B17F7 (轮函数)
"""
state = s_box_transform(state, s_box_table)
state = p_box_transform(state, p_box_table)
return state
def generate_round_key(key, round_num):
"""
对应 C++ 函数 sub_7FF65E2B16A0 (轮密钥生成)
"""
key_u32 = to_u32(key)
shift_amount = 4 * (round_num - 1)
# C++ 代码中 (unsigned int) >> 是逻辑右移
shifted_key = key_u32 << shift_amount
return to_u32(shifted_key) >> 16
def encrypt_token(timestamp, r_key, s_box_table, p_box_table):
"""
对应 C++ 函数 sub_7FF65E2B184D (加密主函数)
"""
state = to_u32(timestamp)
# 循环 3 轮
for i in range(1, 4):
round_key = generate_round_key(r_key, i)
state ^= round_key
state = round_function(state, s_box_table, p_box_table)
# 循环后的第4步
round_key_4 = generate_round_key(r_key, 4)
state ^= round_key_4
state = s_box_transform(state, s_box_table)
# 最终返回前的第5步
round_key_5 = generate_round_key(r_key, 5)
final_state = state ^ round_key_5
return to_u32(final_state)
for t in range(1751990400, 1752052052):
dword = t
dword, ret = gen(dword)
cnt = ret
i = 0
while i < cnt:
dword, ret = gen(dword)
a = ret
dword, ret = gen(dword)
b = ret
dword, ret = gen(dword)
x = ret
dword, ret = gen(dword)
y = ret
dword, ret = gen(dword)
cnt = ret
i+=1
dword, ret = gen(dword)
r = ret
# pow(a | x, 2)
val1 = math.pow(float(to_s32(a) | to_s32(x)), 2.0)
# pow(b | y, 2)
val2 = math.pow(float(to_s32(b) | to_s32(y)), 2.0)
if math.isclose(0x61 * val1, 0xb * val2):
cipher = encrypt_token(
t, r,
S_BOX_TABLE_7FF65E2BC020,
P_BOX_TABLE_7FF65E2BC0A0
)
query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&cipher={cipher}"
else:
query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&a={a}&b={b}&x={x}&y={y}"
print(t, query)
if md5(query.encode()).hexdigest() == "8a2fc1e9e2830c37f8a7f51572a640aa":
print(sha1(query.encode()).hexdigest())
但python爆破速度非常慢,直接让gemini转为c语言脚本
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
// 引入 OpenSSL 库头文件
#include <openssl/md5.h>
#include <openssl/sha.h>
// 伪随机数生成器,对应 python 的 gen 函数
// 使用指针来返回两个值
void gen(uint32_t* dword, uint32_t* ret) {
uint32_t v1 = ((((*dword << 13) ^ *dword) >> 17) ^ ((*dword << 13) ^ *dword));
*dword = (32 * v1) ^ v1;
*ret = *dword & 0x7FFFFFFF;
}
// 辅助函数:将二进制哈希值转换为十六进制字符串
void bytes_to_hex(const unsigned char* bytes, char* hex_string, size_t len) {
for (size_t i = 0; i < len; ++i) {
sprintf(hex_string + (i * 2), "%02x", bytes[i]);
}
hex_string[len * 2] = '\0';
}
int main() {
const char* target_md5 = "8a2fc1e9e2830c37f8a7f51572a640aa";
for (uint32_t t = 1751990400; t < 1752052052; ++t) {
uint32_t dword = t;
uint32_t ret;
// 初始 gen 调用
gen(&dword, &ret);
uint32_t cnt = ret;
uint32_t a = 0, b = 0, x = 0, y = 0;
int i = 0;
while (i < cnt) {
gen(&dword, &a);
gen(&dword, &b);
gen(&dword, &x);
gen(&dword, &y);
gen(&dword, &cnt);
i++;
}
uint32_t r;
gen(&dword, &r);
// C中需要更大的缓冲区来格式化字符串
char query[512];
snprintf(query, sizeof(query), "salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u", t, r, a, b, x, y);
printf("%u %s\n", t, query);
fflush(stdout); // 强制刷新输出缓冲区,确保立即看到打印
// 计算 MD5
unsigned char md5_result[MD5_DIGEST_LENGTH];
MD5((unsigned char*)query, strlen(query), md5_result);
char md5_hex[MD5_DIGEST_LENGTH * 2 + 1];
bytes_to_hex(md5_result, md5_hex, MD5_DIGEST_LENGTH);
// 比较 MD5
if (strcmp(md5_hex, target_md5) == 0) {
printf("Found MD5 match!\n");
// 计算并打印 SHA1
unsigned char sha1_result[SHA_DIGEST_LENGTH];
SHA1((unsigned char*)query, strlen(query), sha1_result);
char sha1_hex[SHA_DIGEST_LENGTH * 2 + 1];
bytes_to_hex(sha1_result, sha1_hex, SHA_DIGEST_LENGTH);
printf("SHA1: %s\n", sha1_hex);
break; // 找到结果,退出循环
}
}
return 0;
}
gcc ./paradox_solve.c -o solve -lssl -lcrypto -lm
得到正确query的sha1结果
题目很简单后面就爆了
终焉之门
动调发现出现了代码字符串,后续分析代码发现有处地方做了异或解密
__int64 sub_140091CC0()
{
sub_140001450(byte_140093A60, 2318LL);
return sub_140001450(byte_140093380, 1760LL);
}
__int64 __fastcall sub_140001450(_BYTE *a1, __int64 a2)
{
unsigned __int64 v3; // rcx
__int64 result; // rax
if ( a2 != 1 )
{
*a1 ^= 0x56u;
v3 = 1LL;
if ( a2 != 2 )
{
do
{
result = (unsigned __int8)aVm0xd1ntuxlwa1[v3 % 0x1CC];
a1[v3++] ^= result;
}
while ( v3 != a2 - 1 );
}
}
return result;
}
提取出来解密得到vm代码
#version 430 core
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout(std430, binding = 0) buffer OpCodes { int opcodes[]; };
layout(std430, binding = 2) buffer CoConsts { int co_consts[]; };
layout(std430, binding = 3) buffer Cipher { int cipher[16]; };
layout(std430, binding = 4) buffer Stack { int stack_data[256]; };
layout(std430, binding = 5) buffer Out { int verdict; };
const int MaxInstructionCount = 1000;
void main()
{
if (gl_GlobalInvocationID.x > 0) return;
uint ip = 0u;
int sp = 0;
verdict = -233;
while (ip < uint(MaxInstructionCount))
{
int opcode = opcodes[int(ip)];
int arg = opcodes[int(ip)+1];
switch (opcode)
{
case 2:
stack_data[sp++] = co_consts[arg];
break;
case 7:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a + b;
break;
}
case 8:
{
int a = stack_data[--sp];
int b = stack_data[--sp];
stack_data[sp++] = a - b;
break;
}
case 14:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = a ^ b;
break;
}
case 15:
{
int b = stack_data[--sp];
int a = stack_data[--sp];
stack_data[sp++] = int(a == b);
break;
}
case 16:
{
bool ok = true;
for (int i = 0; i < 16; i++)
{
if (stack_data[i] != (cipher[i] - 20))
{
ok = false;
break;
}
}
verdict = ok ? 1 : -1;
return;
}
case 18:
{
int c = stack_data[--sp];
if (c == 0) ip = uint(arg);
break;
}
default:
verdict = 500;
return;
}
ip+=2;
}
verdict = 501;
}
需要去寻找opcode等参数,来到main发现开头出现很多数组初始化,且大小正好对应vm函数的传参
__int64 sub_140091CF0()
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v0 = 0;
sub_14008E370();
sub_140023480(8256LL);
sub_14001F730(1280LL, 800LL, "Password Checker");
sub_140021100(&v26, 0LL, byte_140093380);
v1 = _mm_loadu_si128(&v26);
v2 = sub_14000E700(byte_140093A60, 37305LL);
v20 = sub_14000EEE0(v2);
v21 = sub_14000EFF0(672LL, &opcodes, 35050LL);
v3 = sub_14000EFF0(128LL, &co_consts, 35050LL);
v22 = sub_14000EFF0(64LL, &cipher, 35050LL);
v23 = sub_14000EFF0(1024LL, &stack_data, 35050LL);
v4 = sub_14000EFF0(4LL, &verdict, 35050LL);
v33[0] = 0LL;
v24 = v4;
*(_QWORD *)Str = 0LL;
v29 = 0LL;
v30 = 0LL;
v31 = 0LL;
memset(v32, 0, sizeof(v32));
*(_QWORD *)((char *)v33 + 5) = 0LL;
sub_1400231A0(60LL);
while ( !(unsigned __int8)sub_14001CAC0() )
{
v5 = sub_140025A40();
if ( v5 > 0 && v0 <= 99 )
{
v6 = v0 + 1;
do
{
Str[v6 - 1] = v5;
v0 = v6;
v5 = sub_140025A40();
v7 = (int)v6++ <= 99;
}
while ( v7 && v5 > 0 );
}
v8 = sub_1400258E0(259LL);
if ( v0 > 0 && v8 )
Str[--v0] = 0;
if ( (unsigned __int8)sub_1400258E0(257LL)
&& strlen(Str) == 40
&& !strncmp(Str, "L3HCTF{", 7uLL)
&& HIBYTE(v32[0]) == '}' )
{
v25 = v0;
v12 = &Str[7];
v13 = 0;
do
{
v17 = *v12;
v18 = v12[1];
if ( v17 > 96 )
v14 = v17 - 87;
else
v14 = v17 - 48;
v19 = 16 * v14;
v15 = v13;
v16 = v18 - 48;
if ( v18 >= 97 )
v16 = v18 - 87;
v12 += 2;
v13 += 4;
v27 = v16 + v19;
sub_14000F0B0(v3, &v27, 4LL, v15);
}
while ( (char *)v32 + 7 != v12 );
v0 = v25;
sub_14000C100(v20);
sub_14000F180(v21, 0LL);
sub_14000F180(v3, 2LL);
sub_14000F180(v22, 3LL);
sub_14000F180(v23, 4LL);
sub_14000F180(v24, 5LL);
sub_14000EFE0(1LL, 1LL, 1LL);
sub_14000F140(v24, &verdict, 4LL, 0LL);
sub_14000C110();
}
sub_14001FC90();
v26 = v1;
sub_140020650(&v26);
v9 = sub_14001E170();
v26 = v1;
*(float *)&v9 = v9;
v27 = LODWORD(v9);
v10 = sub_140021440(&v26, "time");
v26 = v1;
sub_140021460(&v26, v10, &v27, 0LL);
sub_14003B9D0(0, 0, 1280, 800, -1);
sub_140020690();
sub_14004DA20((unsigned int)Str, 100, 200, 40, -16777216);
if ( verdict == 1 )
sub_14004DA20((unsigned int)"success", 100, 300, 40, -13863680);
else
sub_14004DA20((unsigned int)"wrong password", 100, 300, 40, -13162010);
sub_14004DA20((unsigned int)"Type password and press [Enter] to check!", 100, 100, 20, -8224126);
sub_14004DA20((unsigned int)"Press [Backspace] to delete characters.", 100, 130, 20, -8224126);
sub_140025CE0();
}
sub_14001FAA0();
return 0LL;
}
可以看到要求flag格式,且内容为16进制字符串32位;此外可以看到v3即co_consts存储了16个十六进制字节
可以模拟vm跑一遍流程了
KNOWN_CIPHER = [243, 130, 6, 509, 336, 56, 178, 222, 346, 407, 156, 471, 110, 40, 326, 151]
class VM:
def __init__(self):
self.opcodes = [0x00000002, 0x00000000, 0x00000002, 0x00000001, 0x00000002, 0x00000000, 0x0000000E, 0x00000000, 0x00000002, 0x00000010, 0x00000008, 0x00000000, 0x00000002, 0x00000002, 0x00000002, 0x00000001, 0x0000000E, 0x00000000, 0x00000002, 0x00000011, 0x00000008, 0x00000000, 0x00000002, 0x00000003, 0x00000002, 0x00000002, 0x0000000E, 0x00000000, 0x00000002, 0x00000012, 0x00000007, 0x00000000, 0x00000002, 0x00000004, 0x00000002, 0x00000003, 0x0000000E, 0x00000000, 0x00000002, 0x00000013, 0x00000007, 0x00000000, 0x00000002, 0x00000005, 0x00000002, 0x00000004, 0x0000000E, 0x00000000, 0x00000002, 0x00000014, 0x00000008, 0x00000000, 0x00000002, 0x00000006, 0x00000002, 0x00000005, 0x0000000E, 0x00000000, 0x00000002, 0x00000015, 0x00000007, 0x00000000, 0x00000002, 0x00000007, 0x00000002, 0x00000006, 0x0000000E, 0x00000000, 0x00000002, 0x00000016, 0x00000007, 0x00000000, 0x00000002, 0x00000008, 0x00000002, 0x00000007, 0x0000000E, 0x00000000, 0x00000002, 0x00000017, 0x00000007, 0x00000000, 0x00000002, 0x00000009, 0x00000002, 0x00000008, 0x0000000E, 0x00000000, 0x00000002, 0x00000018, 0x00000007, 0x00000000, 0x00000002, 0x0000000A, 0x00000002, 0x00000009, 0x0000000E, 0x00000000, 0x00000002, 0x00000019, 0x00000007, 0x00000000, 0x00000002, 0x0000000B, 0x00000002, 0x0000000A, 0x0000000E, 0x00000000, 0x00000002, 0x0000001A, 0x00000007, 0x00000000, 0x00000002, 0x0000000C, 0x00000002, 0x0000000B, 0x0000000E, 0x00000000, 0x00000002, 0x0000001B, 0x00000008, 0x00000000, 0x00000002, 0x0000000D, 0x00000002, 0x0000000C, 0x0000000E, 0x00000000, 0x00000002, 0x0000001C, 0x00000008, 0x00000000, 0x00000002, 0x0000000E, 0x00000002, 0x0000000D, 0x0000000E, 0x00000000, 0x00000002, 0x0000001D, 0x00000007, 0x00000000, 0x00000002, 0x0000000F, 0x00000002, 0x0000000E, 0x0000000E, 0x00000000, 0x00000002, 0x0000001E, 0x00000008, 0x00000000, 0x00000010, 0x00000000, 0x00000002, 0x00000010, 0x00000002, 0x00000011, 0x0000000F, 0x00000000, 0x00000012, 0x00000054, 0x00000002, 0x0000001F, 0x00000001, 0x00000000, 0x00000003, 0x00000001]
self.co_consts = [0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x000000B0, 0x000000C8, 0x000000FA, 0x00000086, 0x0000006E, 0x0000008F, 0x000000AF, 0x000000BF, 0x000000C9, 0x00000064, 0x000000D7, 0x000000C3, 0x000000E3, 0x000000EF, 0x00000087, 0x00000000]
self.co_consts[:16] = list(range(16))
self.stack_data = [0] * 256
self.ip = 0
self.sp = 0
self.max_instructions = 1000
def run(self):
while self.ip < len(self.opcodes) and self.ip < self.max_instructions * 2:
opcode = self.opcodes[self.ip]
arg = self.opcodes[self.ip + 1]
print(opcode, end=" ")
if opcode == 2: # LOAD_CONST
self.stack_data[self.sp] = self.co_consts[arg]
self.sp += 1
print(f"push {hex(self.co_consts[arg])}({arg})")
elif opcode == 7: # BINARY_ADD
b = self.stack_data[self.sp - 1]
a = self.stack_data[self.sp - 2]
self.sp -= 1
self.stack_data[self.sp - 1] = a + b
print(f"add {b}, {a}")
elif opcode == 8: # BINARY_SUBTRACT
b = self.stack_data[self.sp - 1]
a = self.stack_data[self.sp - 2]
self.sp -= 1
self.stack_data[self.sp - 1] = b - a
print(f"sub {b}, {a}")
elif opcode == 14: # BINARY_XOR
b = self.stack_data[self.sp - 1]
a = self.stack_data[self.sp - 2]
self.sp -= 1
self.stack_data[self.sp - 1] = a ^ b
print(f"xor {b}, {a}")
elif opcode == 15: # COMPARE_OP (==)
b = self.stack_data[self.sp - 1]
a = self.stack_data[self.sp - 2]
self.sp -= 1
self.stack_data[self.sp - 1] = 1 if a == b else 0
print(f"cmp {b}, {a}")
elif opcode == 18: # POP_JUMP_IF_FALSE
c = self.stack_data[self.sp - 1]
self.sp -= 1
if c == 0:
self.ip = arg
continue
print("pop")
else:
print("cmp")
return
self.ip += 2
if __name__ == "__main__":
vm = VM()
input = [val for val in vm.co_consts[:16]]
print(input)
vm.run()
correct_cipher = [val for val in vm.stack_data[:16]]
print(correct_cipher)
print(f"CIPHER (16个整数): {[i-20 for i in KNOWN_CIPHER]}")
分析下日志,可以看到每次先异或下一个字节,然后结果一个常数加或减异或结果
2 push 0x0(0)
2 push 0x1(1)
2 push 0x0(0)
14 xor 0, 1
2 push 0xb0(16)
8 sub 176, 1
2 push 0x2(2)
2 push 0x1(1)
14 xor 1, 2
2 push 0xc8(17)
8 sub 200, 3
2 push 0x3(3)
2 push 0x2(2)
14 xor 2, 3
2 push 0xfa(18)
7 add 250, 1
可以逆回去
cipher = [0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE, 0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146,
0x97]
CoConsts = [0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00]
opcodes = [0x00000002, 0x00000000, 0x00000002, 0x00000001, 0x00000002, 0x00000000, 0x0000000E, 0x00000000,
0x00000002, 0x00000010, 0x00000008, 0x00000000, 0x00000002, 0x00000002, 0x00000002, 0x00000001,
0x0000000E, 0x00000000, 0x00000002, 0x00000011, 0x00000008, 0x00000000, 0x00000002, 0x00000003,
0x00000002, 0x00000002, 0x0000000E, 0x00000000, 0x00000002, 0x00000012, 0x00000007, 0x00000000,
0x00000002, 0x00000004, 0x00000002, 0x00000003, 0x0000000E, 0x00000000, 0x00000002, 0x00000013,
0x00000007, 0x00000000, 0x00000002, 0x00000005, 0x00000002, 0x00000004, 0x0000000E, 0x00000000,
0x00000002, 0x00000014, 0x00000008, 0x00000000, 0x00000002, 0x00000006, 0x00000002, 0x00000005,
0x0000000E, 0x00000000, 0x00000002, 0x00000015, 0x00000007, 0x00000000, 0x00000002, 0x00000007,
0x00000002, 0x00000006, 0x0000000E, 0x00000000, 0x00000002, 0x00000016, 0x00000007, 0x00000000,
0x00000002, 0x00000008, 0x00000002, 0x00000007, 0x0000000E, 0x00000000, 0x00000002, 0x00000017,
0x00000007, 0x00000000, 0x00000002, 0x00000009, 0x00000002, 0x00000008, 0x0000000E, 0x00000000,
0x00000002, 0x00000018, 0x00000007, 0x00000000, 0x00000002, 0x0000000A, 0x00000002, 0x00000009,
0x0000000E, 0x00000000, 0x00000002, 0x00000019, 0x00000007, 0x00000000, 0x00000002, 0x0000000B,
0x00000002, 0x0000000A, 0x0000000E, 0x00000000, 0x00000002, 0x0000001A, 0x00000007, 0x00000000,
0x00000002, 0x0000000C, 0x00000002, 0x0000000B, 0x0000000E, 0x00000000, 0x00000002, 0x0000001B,
0x00000008, 0x00000000, 0x00000002, 0x0000000D, 0x00000002, 0x0000000C, 0x0000000E, 0x00000000,
0x00000002, 0x0000001C, 0x00000008, 0x00000000, 0x00000002, 0x0000000E, 0x00000002, 0x0000000D,
0x0000000E, 0x00000000, 0x00000002, 0x0000001D, 0x00000007, 0x00000000, 0x00000002, 0x0000000F,
0x00000002, 0x0000000E, 0x0000000E, 0x00000000, 0x00000002, 0x0000001E, 0x00000008, 0x00000000,
0x00000010, 0x00000000, 0x00000002, 0x00000010, 0x00000002, 0x00000011, 0x0000000F, 0x00000000,
0x00000012, 0x00000054, 0x00000002, 0x0000001F, 0x00000001, 0x00000000, 0x00000003, 0x00000001]
op_pattern = [i for i in opcodes[10:160:10]]
print(op_pattern)
targets = [c - 20 for c in cipher]
inputs = [0] * 16
inputs[0] = targets[0]
for i in range(1, 16):
prev_input = inputs[i - 1]
target = targets[i]
const = CoConsts[i - 1]
op_type = op_pattern[i - 1]
if op_type == 7: # ADD
intermediate = (target - const)&0xff
elif op_type == 8: # SUB
intermediate = (const - target)&0xff
inputs[i] = intermediate ^ prev_input
print(bytes(inputs).hex())
# df9d4ba41258574ccb7155b9d01f5c58
obfuscate
ida反编译错误,main有爆红,不用管直接分析其他函数,分别找到如下函数
首先是3处有效反调试
ptrace
__int64 sub_7E20() { __int64 result; // rax result = ptrace(PTRACE_TRACEME, 0LL, 0LL); if ( result == -1 ) _exit(1); return result; }
读取文件Tracerid值(动调跟进解密后的字符串可以看到Tracerid),可以发现读取了"/proc/self/status"并寻找"TracerPid:"
__int64 sub_55555555BE50() { __int64 result; // rax int v1; // [rsp+0h] [rbp-10h] BYREF __int64 v2; // [rsp+5h] [rbp-Bh] BYREF __int16 v3; // [rsp+Dh] [rbp-3h] char v4; // [rsp+Fh] [rbp-1h] v2 = 0xFBF31E8058E31D9LL; v3 = 31209; v4 = -17; v1 = 1726956429; sub_55555555C1D0((__int64)&v2, (__int64)&v1, 11LL, 4LL); result = sub_55555555BEC0((char *)&v2); if ( (int)result > 0 ) _exit(1); return result; } __int64 __fastcall sub_55555555BEC0(char *a1) { char s[140]; // [rsp+10h] [rbp-D0h] BYREF unsigned int v3; // [rsp+9Ch] [rbp-44h] BYREF FILE *stream; // [rsp+A0h] [rbp-40h] char *needle; // [rsp+A8h] [rbp-38h] int v7; // [rsp+B4h] [rbp-2Ch] BYREF char filename[18]; // [rsp+BAh] [rbp-26h] BYREF int v9; // [rsp+CCh] [rbp-14h] BYREF char modes[2]; // [rsp+D2h] [rbp-Eh] BYREF int v11; // [rsp+D4h] [rbp-Ch] BYREF int v12; // [rsp+DAh] [rbp-6h] BYREF __int16 v13; // [rsp+DEh] [rbp-2h] v12 = 1053795514; v13 = -10245; v11 = 468703135; sub_55555555C1D0((__int64)&v12, (__int64)&v11, 6LL, 4LL); *(_WORD *)modes = 29201; v9 = 233665123; sub_55555555C1D0((__int64)modes, (__int64)&v9, 2LL, 4LL); qmemcpy(filename, "uU \\9\n!V6C}@.D&F)%", sizeof(filename)); v7 = 861021530; sub_55555555C1D0((__int64)filename, (__int64)&v7, 18LL, 4LL); needle = a1; stream = fopen(filename, modes); if ( stream ) { v3 = -1; while ( fgets(s, 128, stream) && (!(unsigned int)__isoc99_sscanf(s, &v12, s, &v3) || !strstr(s, needle)) ) // "TracerPid:" ; fclose(stream); return v3; } else { return (unsigned int)-1; } }
getpid
unsigned __int64 sub_8030() { unsigned __int64 v0; // rax unsigned __int64 v1; // rax unsigned __int64 result; // rax __int64 v3; // [rsp+8h] [rbp-8h] v0 = __rdtsc(); v3 = v0; getpid(); v1 = __rdtsc(); result = v1 - v3; if ( result > 0x186A0 ) _exit(1); return result; }
后来发现还有个不怎么常见的
const char **sub_55555555C070()
{
const char **result; // rax
const char **i; // [rsp+8h] [rbp-28h]
_QWORD v2[4]; // [rsp+10h] [rbp-20h] BYREF
v2[0] = "LD_PRELOAD";
v2[1] = "gdb";
v2[2] = "TERM=linux";
v2[3] = qword_55555555EDD8;
for ( i = (const char **)v2; ; ++i )
{
result = i;
if ( !*i )
break;
if ( getenv(*i) )
_exit(1);
}
return result;
}
通过检测LD_PRELOAD和gdb等环境变量来反调试
一个个force jmp或者nop后patch即可动调
比较函数如下
int __fastcall sub_6180(__int64 a1)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
*(_QWORD *)format = 0x61C477DB26D672BDLL;
v11 = 0x41BD3F9C2FD86CACLL;
v9 = 1102520059;
xor_dec(format, &v9, 16LL, 4LL);
memcpy(dest, &unk_9031, sizeof(dest));
v7 = 1350490027;
xor_dec(dest, &v7, 38LL, 4LL);
memcpy(src, &cmp, 0x21uLL);
v5 = 1189641421;
xor_dec(src, &v5, 33LL, 4LL);
v4 = a1;
memcpy(v3, src, 0x21uLL);
for ( i = 0; i < 32; ++i )
{
if ( *(unsigned __int8 *)(v4 + i) != (unsigned __int8)v3[i] )
{
printf(format);
exit(1);
}
}
return printf(dest);
}
以及两个混淆比较厉害的加密函数(分析可知是rc5加密),可以借助ida9自带的goomba插件解除部分混淆,右键
De-obfuscate即可,但是得到的函数依然存在很多逻辑混淆
比如恒真恒假跳转
if ( unk_B1C8 < 10 && unk_B1C8 >= 10 ) // 恒假
goto LABEL_26;
if ( unk_B218 >= 10 || unk_B218 < 10 ) // 恒真
break;
减1
*v37 - 1067854539 + 1067854538 // 等价于*v37 - 1
异或
*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48 // 等价于*v24^0x51BF6B48
偶数次异或值不变
(*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48) ^ (*v23 & 0xAE4094B7 | ~*v23 & 0x51BF6B48) // 等价于*v24^*v23
或
v9 ^ v8 | v9 & v8 // 等价于v9|v8
最后可以得到两份干净简洁的伪代码交给gemini分析下
__int64 __fastcall sub_555555555250(__int64 a1, __int64 a2)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v41 = a1;
v42 = a2;
v2 = HIDWORD(v42);
v32 = &v27 - 2;
v33 = &v27 - 2;
v34 = &v27 - 2;
v35 = &v27 - 2;
v36 = &v27 - 2;
v37 = &v27 - 2;
v38 = &v27 - 2;
v39 = &v27 - 2;
v40 = &v27 - 2;
*(&v27 - 2) = v41;
HIDWORD(v27) = v2;
LODWORD(v27) = 0;
LABEL_3:
v31 = *v37 < 4u;
if ( v31 )
{
v3 = v38;
*(v34 + *v37) = 0;
*v3 = 0;
while ( 1 )
{
if ( *v38 >= 4u )
{
++*v37;
goto LABEL_3;
}
*(v34 + *v37) = *(*v33 + (*v38 + 4 * *v37)) + (*(v34 + *v37) << 8);
++*v38;
}
}
v4 = v37;
**v32 = 0xB7E15163;
*v4 = 1;
while ( 1 )
{
if ( *v37 >= 0x1Au )
break;
*(*v32 + 4LL * *v37) = *(*v32 + 4LL * (*v37 - 1)) - 0x61C88647;
++*v37;
}
v7 = v35;
v8 = v36;
v9 = v37;
*v38 = 0;
*v9 = 0;
*v8 = 0;
*v7 = 0;
*v39 = 0;
while ( 2 )
{
if ( *v39 < 78 )
{
v10 = v38;
v11 = v36;
v12 = v34;
v13 = v35;
v15 = ((*v35 + *(*v32 + 4LL * *v37) + *v36) >> 29) | ((*v35 + *(*v32 + 4LL * *v37) + *v36) << 3);
*(*v32 + 4LL * *v37) = v15;
*v13 = v15;
v18 = v37;
v19 = ((*v11 + *v35 + *(v12 + *v10)) >> ((*v11 + *v35) + 32)) | ((*v13 + *(v12 + *v10) + *v11) << (*v13 + *v11));
*(v12 + *v10) = v19;
*v11 = v19;
v20 = v38;
*v18 = (*v18 + 1) % 0x1A;
*v20 = (*v20 + 1) & 3;
++*v39;
}
break;
}
result = 1;
return result;
}
int *__fastcall sub_555555555E80(_DWORD *a1, __int64 a2, _DWORD *a3)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND]
v26 = a1;
v27 = a2;
v28 = a3;
v3 = v28;
v22 = &v21 - 2;
v23 = (&v21 - 2);
v24 = &v21 - 2;
v25 = &v21 - 2;
*(&v21 - 2) = v26;
v21 = v3;
LODWORD(v21) = **(&v21 - 2) + *v3;
*(&v21 - 4) = (*(&v21 - 2))[1] + v21[1];
*(&v21 - 4) = 1;
while ( *v25 <= 0xCu )
{
*v23 = *(*v21 + 4LL * (2 * *v25)) + (((*v24 ^ *v23) << *v24) | ((*v24 ^ *v23) >> (32 - *v24)));
*v24 = (((*v23 ^ *v24) >> (32 - *v23)) | ((*v23 ^ *v24) << *v23)) + *(*v21 + 4LL * (2 * *v25 + 1));
*v23 = *v24 ^ *v23;
++*v25;
}
v11 = v22;
v12 = v24;
**v22 = *v23;
result = *v11;
result[1] = *v12;
return result;
}
分析可知是RC5的密钥扩展和加密函数,其中加密函数地方做了魔改,对在轮加密种A多异或了B,解密脚本如下
还有个很坑的点卡了很久,密钥是"cleWtemoH3Lo!FTC",而不是WelcometoL3HCTF!,正好是后者小端存储形式的字符串(需要动调去密钥扩展里看从内存里读取的到底是什么值)
import struct
class RC5:
def __init__(self, key: bytes):
self.w = 32 # 字长(比特)
self.r = 12 # 轮数
self.b = len(key) # 密钥长度
self.t = 2 * (self.r + 1) # 密钥表大小
self.mod = 1 << self.w # 模数
self.S = self._expand_key(key)
def _expand_key(self, key: bytes) -> list:
# 初始化常量
P, Q = 0xB7E15163, 0x61C88647
# 初始化密钥表
S = [P]
for i in range(1, self.t):
S.append((S[i - 1] - Q) % self.mod)
# 将密钥转换为字列表
c = max(len(key) // 4, 1)
L = [0] * c
for i in range(len(key)):
idx = i // 4
shift = 8 * (i % 4)
L[idx] = (L[idx] + (key[i] << shift)) % self.mod
# 混合密钥
i = j = 0
A = B = 0
for _ in range(3 * max(self.t, c)):
A = S[i] = self.rotl((S[i] + A + B) % self.mod, 3)
B = L[j] = self.rotl((L[j] + A + B) % self.mod, (A + B) % self.w)
i = (i + 1) % self.t
j = (j + 1) % c
return S
def rotl(self, x: int, n: int) -> int:
n %= self.w
return ((x << n) | (x >> (self.w - n))) % self.mod
def rotr(self, x: int, n: int) -> int:
n %= self.w
return ((x >> n) | (x << (self.w - n))) % self.mod
def decrypt_block(self, data: bytes) -> bytes:
# 解析输入块
A = struct.unpack('<I', data[:4])[0]
B = struct.unpack('<I', data[4:8])[0]
# 解密过程
for i in range(self.r, 0, -1):
A = A ^ B
B = self.rotr((B - self.S[2 * i + 1]) % self.mod, A) ^ A
A = self.rotr((A - self.S[2 * i]) % self.mod, B) ^ B
B = (B - self.S[1]) % self.mod
A = (A - self.S[0]) % self.mod
# 打包输出
return struct.pack('<II', A, B)
def encrypt_block(self, data: bytes) -> bytes:
"""加密一个64位数据块"""
A = struct.unpack('<I', data[:4])[0]
B = struct.unpack('<I', data[4:8])[0]
# 初始白化
A = (A + self.S[0]) % self.mod
B = (B + self.S[1]) % self.mod
# 轮函数
for i in range(1, self.r + 1):
A = (self.rotl((A ^ B), B) + self.S[2 * i]) % self.mod
B = (self.rotl((B ^ A), A) + self.S[2 * i + 1]) % self.mod
A ^= B
return struct.pack('<II', A, B)
def encrypt(self, plaintext: bytes) -> bytes:
"""加密任意长度数据"""
# 分块加密
blocks = [plaintext[i:i+8] for i in range(0, len(plaintext), 8)]
ciphertext = b''
for block in blocks:
ciphertext += self.encrypt_block(block)
return ciphertext
def decrypt(self, ciphertext: bytes) -> bytes:
# 处理填充(示例使用PKCS#7)
blocks = [ciphertext[i:i + 8] for i in range(0, len(ciphertext), 8)]
plaintext = b''
for block in blocks:
plaintext += self.decrypt_block(block)
return plaintext
if __name__ == "__main__":
key = b"cleWtemoH3Lo!FTC"
rc5 = RC5(key)
plainttext = b"flag{11111222222333333333333334}"
ciphertext = rc5.encrypt(plainttext)
print(ciphertext.hex())
ciphertext = bytes([0x1B, 0xBB, 0xA1, 0xF2, 0xE9, 0x7C, 0x87, 0x21, 0x8A, 0x37, 0xFD, 0x0A, 0x94, 0x1A, 0x81, 0xBC, 0x40, 0x1E, 0xE3, 0xAA, 0x73, 0x2E, 0xD8, 0x3F, 0x84, 0xB8, 0x71, 0x42, 0xCC, 0x35, 0x8B, 0x39])
plaintext = rc5.decrypt(ciphertext)
print(f"Decrypted: {plaintext}")
easyvm
调试可以发现是类tea加密,8字节一组变化
最开始做复杂了一点点去分析VM里每个指令的作用并试图模拟,最后才想到直接ida下条件断点在重要运算指令上即可,把两个操作数打印下就知道每一步计算都做了什么
找到vm计算指令的位置(tea中主要为add、sub、xor、shl、shr),简单写下idapython脚本,以此来模拟trace获得加密log
import idc, idaapi
op1_val = idc.get_reg_value("EDX")
op2_val = idc.get_reg_value("ECX") & 0xFF
result_val = idc.get_reg_value("EDX")
print(f"shl {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val<<op2_val)&0xffffffff)}")
import idc, idaapi
op1_val = idc.get_reg_value("EDX")
op2_val = idc.get_reg_value("ECX") & 0xFF
print(f"shr {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val>>op2_val)&0xffffffff)}")
import idc, idaapi
op1_val = idc.get_reg_value("EAX")
rbp_val = idc.get_reg_value("RBP")
mem_addr = rbp_val + 0x4C
op2_val = idc.get_wide_dword(mem_addr)
print(f"xor {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val^op2_val)&0xffffffff)}")
import idc, idaapi
op1_val = idc.get_reg_value("EAX")
rbp_val = idc.get_reg_value("RBP")
mem_addr = rbp_val + 0x1C
op2_val = idc.get_wide_dword(mem_addr)
print(f"sub {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val-op2_val)&0xffffffff)}")
import idc, idaapi
op1_val = idc.get_reg_value("EAX")
op2_val = idc.get_reg_value("EDX")
print(f"add {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val+op2_val)&0xffffffff)}")
log取一轮加密来分析
shl 0x32323232, 0x3 = 0x91919190
add 0xa56babcd, 0x91919190 = 0x36fd3d5d
add 0x0, 0x32323232 = 0x32323232
add 0x0, 0x32323232 = 0x32323232
xor 0x32323232, 0x36fd3d5d = 0x4cf0f6f
shr 0x32323232, 0x4 = 0x3232323
add 0xffffffff, 0x3232323 = 0x3232322
xor 0x4cf0f6f, 0x3232322 = 0x7ec2c4d
add 0x31313131, 0x7ec2c4d = 0x391d5d7e
add 0x11223344, 0x0 = 0x11223344
shl 0x391d5d7e, 0x2 = 0xe47575f8
add 0xffffffff, 0xe47575f8 = 0xe47575f7
add 0x11223344, 0x391d5d7e = 0x4a3f90c2
add 0xabcdef01, 0x4a3f90c2 = 0xf60d7fc3
xor 0xf60d7fc3, 0xe47575f7 = 0x12780a34
shr 0x391d5d7e, 0x5 = 0x1c8eaeb
add 0xa56babcd, 0x1c8eaeb = 0xa73496b8
xor 0x12780a34, 0xa73496b8 = 0xb54c9c8c
add 0x32323232, 0xb54c9c8c = 0xe77ecebe
sub 0x40, 0x1 = 0x3f
可以看到三个密钥0xa56babcd、0xffffffff、0xabcdef01以及delta=0x11223344
往后分析看所有轮加密完total做了什么,可以发现下一组加密8字节用的total值是上一组结束后的total值
add 0x11223344, 0x488cd100 = 0x59af0444 // 上一组total
shl 0x6bc23e4e, 0x2 = 0xaf08f938
add 0xffffffff, 0xaf08f938 = 0xaf08f937
add 0x59af0444, 0x6bc23e4e = 0xc5714292
add 0xabcdef01, 0xc5714292 = 0x713f3193
xor 0x713f3193, 0xaf08f937 = 0xde37c8a4
shr 0x6bc23e4e, 0x5 = 0x35e11f2
add 0xa56babcd, 0x35e11f2 = 0xa8c9bdbf
xor 0xde37c8a4, 0xa8c9bdbf = 0x76fe751b
add 0x34343434, 0x76fe751b = 0xab32a94f
sub 0x40, 0x1 = 0x3f
shl 0xab32a94f, 0x3 = 0x59954a78
add 0xa56babcd, 0x59954a78 = 0xff00f645
add 0x59af0444, 0xab32a94f = 0x4e1ad93
add 0x0, 0x4e1ad93 = 0x4e1ad93
xor 0x4e1ad93, 0xff00f645 = 0xfbe15bd6
shr 0xab32a94f, 0x4 = 0xab32a94
add 0xffffffff, 0xab32a94 = 0xab32a93
xor 0xfbe15bd6, 0xab32a93 = 0xf1527145
add 0x6bc23e4e, 0xf1527145 = 0x5d14af93
add 0x11223344, 0x59af0444 = 0x6ad13788 // 新一组total
搞懂加密逻辑直接开逆
from ctypes import c_uint32
def tea_encrypt(r, v, key, delta):
v0, v1 = c_uint32(v[0]), c_uint32(v[1])
total = c_uint32(0)
for i in range(r):
v0.value += ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1])
total.value += delta
v1.value += ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0])
return v0.value, v1.value
def tea_decrypt(r, v, key, delta, id):
v0, v1 = c_uint32(v[0]), c_uint32(v[1])
total = c_uint32(delta * r * (id//2+1))
for i in range(r):
v1.value -= ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0])
total.value -= delta
v0.value -= ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1])
return v0.value, v1.value
v = [2272944806, 1784017395, 2920892487, 2984657895, 2840586369, 2613617290, 3301943967, 4053798049]
k = [0xa56babcd, 0xffffffff, 0xabcdef01]
delta = 0x11223344
for i in range(0, len(v), 2):
v[i:i+2] = tea_decrypt(64, v[i:i+2], k, delta, i)
print(list(map(hex, v)))
v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))])
print(v)
AWayOut2
这个题感觉现出的,一股纯纯为难人的味道
我整体的思路很取巧,就是侧信道爆破,大部分人应该没想到
最开始看了眼很明显代码根本读不懂,动调还会跑飞,一开始目标就挺明确,就是侧信道爆破。借助的是pintool,在以往好多比较难分析逻辑的题发挥了一定作用。因此这次比赛我也是毫不犹豫直接上来爆破分析,爆破第一位字母发现hjk指令数和其他异常,后续逐渐发现hjkl为输入范围
a 419744191
k 419730508
h 419730512
j 419730514
l 419744180 # x
la 640207084
lk 640193401
ll 640207073 # x
lj 640193407
lla 860727831
llk 860714148
lll 860714144
llj 860727830 # x
可以发现输入正确的指令数会和输入不是hjkl指令数相差较小,而其他的指令数绝对值差都大于1000,因此可以逐位进行爆破,使用DFS遍历所有可能
由于输入后返回结果时间较长,不使用多线程爆破时间太长了,因此我写了份基本单线程脚本
import os
import collections
import time
from pwn import *
# --- 配置 ---
context.log_level = 'error'
PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"]
# 字符集顺序现在变得更重要。将常见字符放在前面可能会提高效率。
CHARSET = "{hjkl"
FLAG_LENGTH = 118
ANOMALY_THRESHOLD = 9000 # 显著差异的阈值,可以微调
# --- 辅助函数 (与之前相同) ---
def get_instruction_count(test_flag):
"""
运行 PIN 工具并获取给定 flag 的指令数。
"""
try:
p = process(PIN_COMMAND)
p.recvuntil(b'try:\n', timeout=5)
p.sendline(test_flag.encode())
output = p.recvall(timeout=20)
p.close()
lines = output.strip().splitlines()
if lines and lines[-1].isdigit():
return int(lines[-1])
else:
return -1
except Exception as e:
return -1
# --- 新的主逻辑 (激进 DFS) ---
def solve_aggressive_dfs(known_prefix=""):
"""
使用激进的深度优先搜索策略。
一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。
"""
# 基本情况:如果长度达到目标,说明成功了
if len(known_prefix) == FLAG_LENGTH:
print("\n" + "=" * 60)
print(f"[*] 成功!找到完整 Flag: {known_prefix}")
print("=" * 60)
return True
current_pos = len(known_prefix)
print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...")
# 用于存储当前层级已知的最小指令数
min_count_for_this_level = 1000000
total_chars = len(CHARSET)
for i, char in enumerate(CHARSET):
test_flag = known_prefix + char
progress_text = f" -> 进度: {i + 1}/{total_chars} | 测试: '{test_flag}'"
print(progress_text, end=' ')
count = get_instruction_count(test_flag)
print(count)
if count == -1:
continue # 跳过执行失败的尝试
if i == 0:
tmp = count
continue
# 核心逻辑:检查当前字符的指令数是否比“已知最小”还要显著降低
# ANOMALY_THRESHOLD 是你说的 "10000左右"
if abs(tmp - count) > ANOMALY_THRESHOLD:
# continue
print(f"\n [*] 发现显著更优字符 '{char}'...")
print(f" [*] 立即深入 DFS 搜索 '{test_flag}'...")
# 立即递归,不再测试当前层级的其他字符
if solve_aggressive_dfs(test_flag):
return True # 如果这条路成功了,直接返回 True
else:
# 如果深入后发现是死胡同,打印回溯信息并继续在当前层级搜索
print(f"\n[!] 路径 '{test_flag}' 是死胡同, 回溯到第 {current_pos + 1} 位, 继续搜索...")
# 即使是死胡同,这个 count 也是一个新的有效最小值,需要更新
min_count_for_this_level = count
# 如果不是显著降低,就只更新当前层级的最小指令数
elif count < min_count_for_this_level:
min_count_for_this_level = count
# 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的
print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...")
return False
# --- 程序入口 ---
if __name__ == "__main__":
print("=" * 60)
print("开始使用“激进”DFS 策略进行全长度 Flag 爆破")
print("=" * 60)
# 假设 'h' 仍然是正确的第一个字符
if not solve_aggressive_dfs(""):
# 如果不确定第一个字符,使用下面这行:
# if not solve_aggressive_dfs(""):
print("\n" + "=" * 60)
print("[!] 未能找到完整的 Flag。")
print("=" * 60)
上面的是最基础的脚本,后面多线程发现了更多bug
- 要限制方向,不能跑反方向,比如你之前向右走了你下一步不能再向左了,中午跑的时候还没发现,下午才发现结果里不停的jkjkjk
- timeout加得大大的,越往后越慢,出现了好几次获取指令数为-1,直接把我dfs搞乱了,跑了一下午才发现出错了,赶紧把跑出来的路径打印出来,果然出现了一些很不合理的路径如下,还好前面的都没问题不至于再从头开始跑
- threshold指令差值应该看绝对值,最开始爆破时候没发现l方向完指令数增大,导致做差出现负数,所以一直没有l方向
- 要有一个错误基准,因此设置了一个{输入,后续输入4个方向和输入
{
指令数进行比较,绝对值差在1000以内是可以走的方向
爆破脚本如下
#!/usr/bin/env python3
import os
from pwn import *
from concurrent.futures import ThreadPoolExecutor, as_completed
PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"]
CHARSET = "jkhl{"
FLAG_LENGTH = 118
MAX_THREADS = 16
def get_instruction_count(test_flag):
"""
运行 PIN 工具并获取给定 flag 的指令数。
"""
try:
p = process(PIN_COMMAND)
p.recvline(timeout=200)
p.sendline(test_flag.encode())
output = p.recvall(timeout=200)
p.close()
lines = output.strip().splitlines()
if lines and lines[-1].isdigit():
return int(lines[-1])
else:
return -1
except Exception as e:
return -1
# --- 新的主逻辑 (激进 DFS) ---
def solve_aggressive_dfs(known_prefix=""):
"""
使用激进的深度优先搜索策略。
一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。
"""
# 基本情况:如果长度达到目标,说明成功了
if len(known_prefix) == FLAG_LENGTH:
print("\n" + "="*60)
print(f"[*] 成功!找到完整 Flag: {known_prefix}")
print("="*60)
return True
current_pos = len(known_prefix)
print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...")
# 1. 获取基准字符 '{' 的指令数 (此步骤仍然串行)
print(f" -> 获取基准指令数 (字符: '{{')...", end='')
ref_flag = known_prefix + "{"
ref_count = get_instruction_count(ref_flag)
print(f" 指令数: {ref_count}")
# 2. 准备所有要并行测试的任务
if known_prefix:
if known_prefix[-1] == "l":
chars_to_test = ["l", "j", "k"]
elif known_prefix[-1] == "h":
chars_to_test = ["h", "j", "k"]
elif known_prefix[-1] == "j":
chars_to_test = ["l", "j", "h"]
elif known_prefix[-1] == "k":
chars_to_test = ["l", "h", "k"]
else:
chars_to_test = ["h", "l", "j", "k"]
flags_to_test = [known_prefix + char for char in chars_to_test]
candidates = []
# 3. 使用线程池并行爆破所有字符
with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
# executor.map 会将 get_instruction_count 函数应用到 flags_to_test 中的每一项
# 它会按顺序返回结果,这非常方便
all_counts = executor.map(get_instruction_count, flags_to_test)
# 4. 收集并处理结果
print(" -> 所有并行任务已完成,正在分析结果...")
for char, count in zip(chars_to_test, all_counts):
if count != -1:
diff = abs(ref_count - count)
print(f"{known_prefix+char} (指令数: {count}, 差值: {diff})")
if diff < 1000:
candidates.append((char, count))
# 对所有候选路径并行发起 DFS 递归
if candidates:
with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor:
# 提交所有递归任务
futures = [executor.submit(solve_aggressive_dfs, known_prefix + char) for char, _ in candidates]
# 等待第一个成功返回的结果
for future in as_completed(futures):
if future.result(): # 如果某个子任务返回 True
return True # 立刻将成功信号向上传递
# 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的
print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...")
return False
# --- 程序入口 ---
if __name__ == "__main__":
context.log_level = 'error'
print("="*60)
print("开始使用“激进”DFS 策略进行全长度 Flag 爆破")
print("="*60)
if not solve_aggressive_dfs("lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjl"):
print("\n" + "="*60)
print("[!] 未能找到完整的 Flag。")
print("="*60)
跑到最后基本就看出来路径没问题了,
正确路径为lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjljjllljjhhjjljjlj
md5后即为flag
应该有更好的思路,但已经没心思研究这个史题了