WEEK1
逆向工程入门指北

打开压缩包得到一个attachment附件
用file命令查看

看到是64位的ELF文件
运行看一下

一个猜数字程序
扔进IDA查看
进入main函数查看关键判断逻辑分支

可以看到成功的分支会输出flag

点击查看flag字符串,得到flag

UPX

打开附件是一个upx.exe程序
题目提示有壳,用die工具(https://www.52pojie.cn/thread-1736240-1-1.html)查壳

可以看到有upx壳
未脱壳的情况下,用IDA打开看到这样的情况

用upx工具(https://github.com/upx/upx/releases)脱壳

脱壳后再用IDA打开

就可以看到各种函数了
没有main函数,一开始定位到了start函数
打开查看程序行为

有一个输入功能,返回IDA,shift+f12查看字符串

点击转到该字符串位置

选中后ctrl+x查看交叉引用

点击转入sub_1400118A0(),tab键反汇编
__int64 sub_1400118A0()
{
char *v0; // rdi
__int64 i; // rcx
Stream *Stream; // rax
_BYTE v4[32]; // [rsp+0h] [rbp-20h] BYREF
char v5; // [rsp+20h] [rbp+0h] BYREF
_DWORD v6[44]; // [rsp+30h] [rbp+10h]
char Buffer[132]; // [rsp+E0h] [rbp+C0h] BYREF
int j_1; // [rsp+164h] [rbp+144h]
_BYTE v9[60]; // [rsp+188h] [rbp+168h]
int j; // [rsp+1C4h] [rbp+1A4h]
int v11; // [rsp+1E4h] [rbp+1C4h]
int k; // [rsp+204h] [rbp+1E4h]
v0 = &v5;
for ( i = 130LL; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
sub_140011375(&unk_14002200E);
v6[0] = 35;
v6[1] = 43;
v6[2] = 39;
v6[3] = 54;
v6[4] = 51;
v6[5] = 60;
v6[6] = 3;
v6[7] = 72;
v6[8] = 100;
v6[9] = 11;
v6[10] = 29;
v6[11] = 118;
v6[12] = 123;
v6[13] = 16;
v6[14] = 11;
v6[15] = 58;
v6[16] = 63;
v6[17] = 101;
v6[18] = 118;
v6[19] = 41;
v6[20] = 21;
v6[21] = 55;
v6[22] = 28;
v6[23] = 10;
v6[24] = 8;
v6[25] = 33;
v6[26] = 62;
v6[27] = 60;
v6[28] = 61;
v6[29] = 22;
v6[30] = 11;
v6[31] = 36;
v6[32] = 41;
v6[33] = 36;
v6[34] = 86;
sub_14001119F("please input your flag: ");
Stream = _acrt_iob_func(0);
fgets(Buffer, 100, Stream);
j_1 = j_strlen(Buffer);
for ( j = 0; j < j_1; ++j )
{
v11 = Buffer[j] ^ 0x21;
if ( j < j_1 - 1 )
v11 ^= Buffer[j + 1];
v9[j] = v11;
}
for ( k = 0; k < 35; ++k )
{
if ( (char)v9[k] != v6[k] )
{
sub_14001119F("you will never get the flag!!!!\n");
break;
}
}
sub_140011311(v4, &unk_14001AD00);
return 0LL;
}
可以看到这应该就是输入行为的函数
分析加密逻辑
if (j < j_1 - 1) {
v9[j] = (Buffer[j] ^ 0x21) ^ Buffer[j+1];
} else {
v9[j] = Buffer[j] ^ 0x21;
}
- 遍历原始输入
Buffer,如果不是最后一个字符,那么当前字符异或0x21再异或下一个字符 - 如果是最后一个字符,那么就当前字符异或
0x21
那么解密逻辑就是:最后一个字符异或0x21,然后倒数第二个字符异或最后一个字符,再异或0x21,以此类推…
尝试的exp
#include <stdio.h>
#include <string.h>
int main() {
int v6[] = {
35, 43, 39, 54, 51, 60, 3, 72, 100, 11,
29, 118, 123, 16, 11, 58, 63, 101, 118, 41,
21, 55, 28, 10, 8, 33, 62, 60, 61, 22,
11, 36, 41, 36, 86
};
char Buffer[36] = { 0 };
int j;
int v11;
for (j = 34; j >= 0; --j) {
if (j == 34) {
v11 = v6[j];
Buffer[j] = v11 ^ 0x21;
}
else {
v11 = v6[j];
Buffer[j] = (v11 ^ Buffer[j + 1]) ^ 0x21;
}
}
printf("%s",Buffer);
return 0;
}
这个exp打出来一直是乱码,和罗神讨论了很久,最后请J1NXEM师傅帮忙解决的
复盘后发现,出错的关键点是没有正确理解明文和密文的对应关系。在之前的处理中,是直接把v6数组作为Buffer数组的加密版进行处理解密,但v6数组和Buffer数组并非一一对应的关系。
v6的本质是:它定义了 “正确 flag 经过计算后必须满足的条件”,但它本身并不是 flag。就像考试答案的哈希值不是答案本身,只是用来验证答案正确性的标准。
整个解密流程是从最后一个字符开始的,而程序中对于j = 34(第35个验证字符):
-
若
j_1 > 35(即输入长度超过 35,包含Buffer[35]),则j=34 < j_1-1成立,此时:
v6[34] = (Buffer[34] ^ 0x21) ^ Buffer[35] -
若
j_1 = 35(输入恰好 35个字符,无Buffer[35]),则j=34 < j_1-1不成立,此时:
v6[34] = Buffer[34] ^ 0x21
由于Buffer[35]的值(是否存在、具体是什么)是未知的,导致Buffer[34]的值无法直接唯一确定 。
所以只要在ASCII码范围内,枚举最后一个字符(buf[-1]即Buffer[34]),进行同样的解密流程,以计算后所得数值和v6对应位置数值是否相等为依据,验证前 34 个字符是否合理,列出可能的flag,在结果中筛选出正确的flag就行了。
最终exp
v6 = [
35, 43, 39, 54, 51, 60, 3, 72, 100, 11,
29, 118, 123, 16, 11, 58, 63, 101, 118, 41,
21, 55, 28, 10, 8, 33, 62, 60, 61, 22,
11, 36, 41, 36, 86
]
flag = [0] * len(v6)
for last_char in range(32, 127):
buf = [0] * len(v6)
buf[-1] = last_char
ok = True
for i in range(len(v6) - 2, -1, -1):
buf[i] = (v6[i] ^ buf[i + 1]) ^ 0x21
if not (32 <= buf[i] <= 126):
ok = False
break
if ok:
print("Possible flag:", ''.join(map(chr, buf)))
筛选得到flag

upx_revenge

魔改upx壳

扔进010打开看到UPX!标志位被删去了

010手动添加标志位后保存

这里注意要插入而不是直接修改

成功脱壳

进ida发现主要和这个字符串比较
(注:C++ 字符串中\表示单个\,\t表示制表符\t,实际字符串为"lY7bW=\ck?eyjX7]TZ}CVbh\tOyTH6>jH7XmFifG]H7")

这个字符串看上去没什么特别的加密特征
继续分析程序,找到了加密的核心是这里,同时在这里打断点

看此时i_1的值

原来是换表base64,异或了0xE
对正常base64表异或0xE后再解密即得flag

ez3

查看附件

64位的elf
放入ida,进入main函数
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v3; // bl
bool v4; // r12
__int64 v5; // rbx
__int64 v6; // rax
char v8; // [rsp+Fh] [rbp-71h] BYREF
__int64 v9; // [rsp+10h] [rbp-70h] BYREF
__int64 v10; // [rsp+18h] [rbp-68h] BYREF
_BYTE v11[32]; // [rsp+20h] [rbp-60h] BYREF
_BYTE v12[40]; // [rsp+40h] [rbp-40h] BYREF
unsigned __int64 v13; // [rsp+68h] [rbp-18h]
v13 = __readfsqword(0x28u);
printf("Input your flag:\n> ", argv, envp);
fflush(stdout);
std::string::basic_string(v11);
std::operator>><char>(&std::cin, v11);
if ( std::string::length(v11) == 42 )
{
v3 = 0;
v4 = 1;
if ( (unsigned __int64)std::string::length(v11) > 7 )
{
std::string::substr(v12, v11, 0LL, 7LL);
v3 = 1;
if ( !(unsigned __int8)std::operator!=<char>(v12, "moectf{") && *(_BYTE *)std::string::back(v11) == 125 )
v4 = 0;
}
if ( v3 )
std::string::~string(v12);
if ( v4 )
{
puts("FORMAT ERROR!");
}
else
{
std::allocator<char>::allocator(&v8);
v10 = std::string::end(v11);
v5 = __gnu_cxx::__normal_iterator<char *,std::string>::operator-(&v10, 1LL);
v9 = std::string::begin(v11);
v6 = __gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v9, 7LL);
std::string::basic_string<__gnu_cxx::__normal_iterator<char *,std::string>,void>(v12, v6, v5, &v8);
std::string::operator=(v11, v12);
std::string::~string(v12);
std::allocator<char>::~allocator(&v8);
std::string::basic_string(v12, v11);
LOBYTE(v5) = check(v12);
std::string::~string(v12);
if ( (_BYTE)v5 )
{
puts("OK");
puts("But I don't know what the true flag is");
}
else
{
puts("try again~");
}
}
}
else
{
puts("Length error!");
}
std::string::~string(v11);
return 0;
}
main函数就是确定输入的flag是由moectf{}包裹的,然后有一些判断和输出的逻辑。对于真正的flag的内容,调用了check函数做校验,进入check函数
__int64 __fastcall check(__int64 a1)
{
int i; // [rsp+1Ch] [rbp-4h]
for ( i = 0; i <= 33; ++i )
{
check(std::string)::b[i] = 47806 * (*(char *)std::string::operator[](a1, i) + i);
if ( i )
check(std::string)::b[i] ^= check(std::string)::b[i - 1] ^ 0x114514;
check(std::string)::b[i] %= 51966;
if ( check(std::string)::b[i] != a[i] )
return 0LL;
}
return 1LL;
}
分析加密逻辑
对于每个字符串中的第i个字符,执行以下步骤:
- 先计算一个临时值:
temp = 47806 * (输出字符的ASCII值 + i) - 当
i>=1时,对temp进行异或处理:temp ^= (b[i-1] ^ 0x114514),其中b[i-1]是上一轮(i-1)计算得到的结果 - 将处理后的
temp对51966取模,得到本轮结果b[i]:b[i] = temp % 51966 - 检查
b[i]是否与预设的a[i](基准值数组)相等
逆向解密逻辑
当i=0时,没有前置异或,直接解同余方程:
47806 x (c0 + 0) = a[0] (mod 51966)
(c0是第0个字符的ASCII值)
当i≥1时:
已知上一轮结果b[i-1]和a[i],先反推异或前的temp:
temp = (k×51966 + a[i]) ^ (b[i-1] ^ 0x114514)(k为整数,使temp为合理值)
再通过temp = 47806 × (c[i] + i)求解c[i](需满足c[i]为可打印字符的 ASCII 值)。
利用z3-solver写exp
from z3 import *
a = [
0xB1B0, 0x5678, 0x7FF2, 0xA332, 0xA0E8, 0x364C, 0x2BD4,
0xC8FE, 0x4A7C, 0x18, 0x2BE4, 0x4144, 0x3BA6, 0xBE8C, 0x8F7E,
0x35F8, 0x61AA, 0x2B4A, 0x6828, 0xB39E, 0xB542, 0x33EC, 0xC7D8,
0x448C, 0x9310, 0x8808, 0xADD4, 0x3CC2, 0x796, 0xC940, 0x4E32,
0x4E2E, 0x924A, 0x5B5C
]
# 定义位宽(64位确保计算不溢出)
BIT_WIDTH = 64
solver = Solver()
# 定义34个字符变量(BitVec类型,确保位宽一致)
chars = [BitVec(f"c{i}", BIT_WIDTH) for i in range(34)]
for i in range(34):
solver.add(chars[i] >= 32)
solver.add(chars[i] <= 126)
b = [BitVec(f"b{i}", BIT_WIDTH) for i in range(34)]
const_114514 = BitVecVal(0x114514, BIT_WIDTH)
mod_51966 = BitVecVal(51966, BIT_WIDTH)
for i in range(34):
# 计算 temp = 47806 * (chars[i] + i)
i_bv = BitVecVal(i, BIT_WIDTH)
temp = 47806 * (chars[i] + i_bv)
# i>0时应用异或:temp ^= (b[i-1] ^ 0x114514)
if i > 0:
temp = temp ^ (b[i - 1] ^ const_114514)
# 取模51966,结果存入b[i]
solver.add(b[i] == temp % mod_51966)
# 约束b[i]等于a[i](a[i]转换为BitVec)
solver.add(b[i] == BitVecVal(a[i], BIT_WIDTH))
if solver.check() == sat:
model = solver.model()
middle_part = [chr(model[chars[i]].as_long()) for i in range(34)]
flag = "moectf{" + "".join(middle_part) + "}"
print(f"Flag: {flag}")
print(f"长度校验: {len(flag)} (应为42)")
else:
print("未找到符合条件的解")
得到flag

flower

还是64位的elf
ida打开进入main函数

可以注意到solver(v8);这条语句是关键逻辑

但是尝试进入的时候报错,显示无法反编译
查看solver()的汇编代码

发现这里爆红了
分析附近代码逻辑
这里就有明显的花指令混淆
.text:00000000004048E5 loc_4048E5:
.text:00000000004048E5 jz short Label ; 若ZF=1(零标志位为1),跳转到Label
.text:00000000004048E7 jnz short Label ; 若ZF=0(零标志位为0),跳转到Label
.text:00000000004048E9 call near ptr Label+1 ; 调用Label+1地址
jz(零标志位为 1 时跳转)和jnz(零标志位为 0 时跳转)是互斥条件,但两条指令都跳向同一个Label。这意味着无论零标志位是什么状态,程序一定会执行Label处的代码,这两条跳转指令实际上等效于一条无条件跳转(jmp short Label)
call near ptr Label+1 调用Label+1(即地址004048EF),但Label从004048EE开始,Label+1是004048EF。结合后续代码,004048EF处没有实质性指令,调用后会立即返回,仅用于压栈返回地址(干扰栈分析)
选中这块花指令区域,全部NOP掉

(选中后按c强制反编译,按d返回原来的汇编)
修改完成

再反编译就可以成功了
分析逻辑
unsigned __int64 __fastcall solve(__int64 a1)
{
char v1; // bl
unsigned __int64 n125; // rax
bool v3; // r12
__int64 v4; // rax
__int64 v5; // rbx
__int64 v6; // rax
char *v7; // rax
__int64 v8; // rax
char v10; // [rsp+17h] [rbp-59h] BYREF
int i; // [rsp+18h] [rbp-58h]
int i_1; // [rsp+1Ch] [rbp-54h]
__int64 v13; // [rsp+20h] [rbp-50h] BYREF
__int64 v14; // [rsp+28h] [rbp-48h] BYREF
_BYTE v15[40]; // [rsp+30h] [rbp-40h] BYREF
unsigned __int64 v16; // [rsp+58h] [rbp-18h]
v16 = __readfsqword(0x28u);
v1 = 0;
n125 = std::string::length(a1);
v3 = 1;
if ( n125 > 7 )
{
std::string::substr(v15, a1, 0LL, 7LL);
v1 = 1;
n125 = std::operator!=<char>(v15, "moectf{");
if ( !(_BYTE)n125 )
{
n125 = *(unsigned __int8 *)std::string::back(a1);
if ( (_BYTE)n125 == 125 )
v3 = 0;
}
}
if ( v1 )
n125 = std::string::~string(v15);
if ( v3 )
{
v4 = std::operator<<<std::char_traits<char>>((std::ostream *)&std::cout);
std::ostream::operator<<(v4, std::endl<char,std::char_traits<char>>);
}
else
{
std::allocator<char>::allocator(n125);
v14 = std::string::end(a1);
v5 = __gnu_cxx::__normal_iterator<char *,std::string>::operator-(&v14, 1LL);
v13 = std::string::begin(a1);
v6 = __gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v13, 7LL);
std::string::basic_string<__gnu_cxx::__normal_iterator<char *,std::string>,void>(v15, v6, v5, &v10);
std::string::operator=(a1, v15);
std::string::~string(v15);
std::allocator<char>::~allocator(&v10);
i_1 = std::string::length(a1);
if ( i_1 == 32 )
{
for ( i = 0; i < i_1; ++i )
{
v7 = (char *)std::string::operator[](a1, i);
if ( (unsigned int)encode(*v7) != enc[i] )
break;
}
}
v8 = std::operator<<<std::char_traits<char>>((std::ostream *)&std::cout);
std::ostream::operator<<(v8, std::endl<char,std::char_traits<char>>);
}
return v16 - __readfsqword(0x28u);
}
查看密文数组enc

查看encode函数
__int64 __fastcall encode(int a1)
{
int v1; // eax
v1 = key++;
return a1 ^ (unsigned int)v1;
}
查看key

是一个先自增再异或的加密逻辑
所以理论上只要逐个异或解密就行,但是,不对!
不知道为什么,反正最后要爆破key的值
exp
enc = [
0x4F, 0x1A, 0x59, 0x1F, 0x5B, 0x1D, 0x5D, 0x6F,
0x7B, 0x47, 0x7E, 0x44, 0x6A, 0x07, 0x59, 0x67,
0x0E, 0x52, 0x08, 0x63, 0x5C, 0x1A, 0x52, 0x1F,
0x20, 0x7B, 0x21, 0x77, 0x70, 0x25, 0x74, 0x2B
]
def decode_with_key(start_key):
return "".join(chr(enc[i] ^ (start_key + i)) for i in range(len(enc)))
for k in range(256):
candidate = decode_with_key(k)
if all(32 <= ord(c) <= 126 for c in candidate):
print(f"key={k:02X} => moectf{{{candidate}}}")

当key = 0x29时输出了正确的flag
好了我发现了
对key使用ctrl +x 查看交叉引用,发现这个值还在main函数里使用过
找到对应代码

key在被solve函数调用前偷偷和0xA进行了一次异或
0x23 ^ 0xA = 0x29
A cup of tea

打开是个exe程序
扔进die看一下,没有壳

进入ida找不到main函数
查看程序行为

一个输入字符串的操作
在ida的String视图也没找到这个字符串
那怎么办呢
想了下去输出函数那里找交叉引用
好好好输出函数也没找到,不过看到了strlen(),猜测和验证flag的逻辑有关
点击strlen()的汇编视图,ctrl + x查看交叉引用,一层层查看上层函数的逻辑,找到了这个函数
__int64 sub_1400162E0()
{
char *v0; // rdi
__int64 i; // rcx
_BYTE v3[32]; // [rsp+0h] [rbp-20h] BYREF
char v4; // [rsp+20h] [rbp+0h] BYREF
_DWORD v5[12]; // [rsp+28h] [rbp+8h] BYREF
_DWORD v6[20]; // [rsp+58h] [rbp+38h]
_DWORD buf[20]; // [rsp+A8h] [rbp+88h] BYREF
_DWORD buf_1[20]; // [rsp+F8h] [rbp+D8h] BYREF
char Str[64]; // [rsp+148h] [rbp+128h] BYREF
size_t Size; // [rsp+188h] [rbp+168h]
int j; // [rsp+1A4h] [rbp+184h]
int v12; // [rsp+1C8h] [rbp+1A8h] BYREF
int v13; // [rsp+1CCh] [rbp+1ACh]
int v14; // [rsp+1E4h] [rbp+1C4h]
int k; // [rsp+204h] [rbp+1E4h]
v0 = &v4;
for ( i = 130LL; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 += 4;
}
sub_140011384(&unk_140023015);
v5[0] = 289739801;
v5[1] = 427884820;
v5[2] = 1363251608;
v5[3] = 269567252;
v6[0] = 2026214571;
v6[1] = 578894681;
v6[2] = 1193947460;
v6[3] = -229306230;
v6[4] = 73202484;
v6[5] = 961145356;
v6[6] = -881456792;
v6[7] = 358205817;
v6[8] = -554069347;
v6[9] = 119347883;
v6[10] = 0;
memset(buf, 0, 0x2CuLL);
memset(buf_1, 0, 0x2CuLL);
sub_1400111A4(&You_are_wrong___);
sub_1400113ED(&unk_14001AEE4, Str);
Size = j_strlen(Str);
j_memcpy(buf, Str, Size);
for ( j = 0; j < 5; ++j )
{
v12 = buf[2 * j];
v13 = buf[2 * j + 1];
sub_14001109B(&v12, v5);
buf_1[2 * j] = v12;
buf_1[2 * j + 1] = v13;
}
v14 = 1;
for ( k = 0; k < 11; ++k )
{
if ( buf_1[k] != v6[k] )
{
v14 = 0;
sub_1400111A4("You are wrong!!");
break;
}
}
if ( v14 == 1 )
sub_1400111A4("Congratulations!!!!");
sub_140011320(v3, &unk_14001AE60);
return 0LL;
}
是对输入字符串的处理和验证
找到加密函数
__int64 __fastcall sub_1400117E0(unsigned int *a1, _DWORD *a2)
{
__int64 n4; // rax
int v3; // [rsp+24h] [rbp+4h]
unsigned int v4; // [rsp+44h] [rbp+24h]
unsigned int v5; // [rsp+64h] [rbp+44h]
int i; // [rsp+A4h] [rbp+84h]
sub_140011384(&unk_140023015);
v3 = 0;
v4 = *a1;
v5 = a1[1];
for ( i = 0; i < 32; ++i )
{
v3 += 1131796;
v4 += (a2[1] + (v5 >> 5)) ^ (v3 + v5) ^ (*a2 + 16 * v5);
v5 += (a2[3] + (v4 >> 5)) ^ (v3 + v4) ^ (a2[2] + 16 * v4);
}
*a1 = v4;
n4 = 4LL;
a1[1] = v5;
return n4;
}
写python脚本逆向得到flag
# Decrypt TEA-like pairs and recover Str (standalone script)
from struct import pack
key = [289739801, 427884820, 1363251608, 269567252]
# target ciphertext array v6 (from decompiled code); negative values converted to unsigned 32-bit
v6 = [
2026214571,
578894681,
1193947460,
(-229306230) & 0xFFFFFFFF,
73202484,
961145356,
(-881456792) & 0xFFFFFFFF,
358205817,
(-554069347) & 0xFFFFFFFF,
119347883,
0
]
def decrypt_pair(cipher0, cipher1, key):
v4 = cipher0 & 0xFFFFFFFF
v5 = cipher1 & 0xFFFFFFFF
a2 = [k & 0xFFFFFFFF for k in key]
delta = 1131796
rounds = 32
v3 = delta * rounds
for _ in range(rounds):
sub1 = ( (a2[3] + (v4 >> 5)) ^ ( (v3 + v4) & 0xFFFFFFFF ) ^ ( (a2[2] + ((v4 * 16) & 0xFFFFFFFF)) & 0xFFFFFFFF ) ) & 0xFFFFFFFF
v5 = (v5 - sub1) & 0xFFFFFFFF
sub2 = ( (a2[1] + (v5 >> 5)) ^ ( (v3 + v5) & 0xFFFFFFFF ) ^ ( (a2[0] + ((v5 * 16) & 0xFFFFFFFF)) & 0xFFFFFFFF ) ) & 0xFFFFFFFF
v4 = (v4 - sub2) & 0xFFFFFFFF
v3 -= delta
return v4, v5
decrypted_words = []
for j in range(5):
c0 = v6[2*j]
c1 = v6[2*j + 1]
p0, p1 = decrypt_pair(c0, c1, key)
decrypted_words.append(p0)
decrypted_words.append(p1)
# 拼回字节流(小端)
all_bytes = b''.join(pack('<I', w) for w in decrypted_words)
str_bytes = all_bytes.split(b'\x00', 1)[0]
print("Recovered bytes:", str_bytes)
print("Recovered string:", str_bytes.decode('utf-8', errors='replace'))
得到flag

Two cups of tea

核心加密函数是这两个

第一个是xtea,第二个是xxtea
先用 XTEA 解密初始 key 的前两 DWORD(enc_bytes),然后补充 [0x12345678, 0x9ABCDEF0] 得到完整 XXTEA key,再用 XXTEA 解密真正的密文 buffer。
(regadgets库还是好用的)
exp:
from regadgets import *
enc_bytes = bytes([0xAF, 0x1C, 0x94, 0xB8, 0xA7, 0xC6, 0xFC, 0xD0])
k1 = [2, 0, 2, 5]
buff = [1566723124, 2250899117, 2635151259, 4241830417, 1175413710,
3313593960, 4266852525, 167777774, 2550586299, 4014614088]
decrypted_enc_bytes = xtea_decrypt(enc_bytes, k1, rounds=32)
k = byte2dword(decrypted_enc_bytes[:8]) + [0x12345678, 0x9ABCDEF0]
buff_bytes = dword2byte(buff)
flag_bytes = xxtea_decrypt(buff_bytes, k, shift_func=xxtea_std_shift)
print("flag:", flag_bytes.decode('utf-8'))
print("XXTEA key:", [hex(x) for x in k])

base

扔进die看一下

再扔到ida
看main函数
int __fastcall main(int argc, const char **argv, const char **envp)
{
Stream *Stream; // rax
__int64 v4; // rdx
__int64 v5; // rax
unsigned __int64 n0x64; // rax
char *Str1; // rbx
int v8; // eax
char *Format; // rcx
char v11[16]; // [rsp+20h] [rbp-98h] BYREF
char Buffer[112]; // [rsp+30h] [rbp-88h] BYREF
sub_140001010(Format);
sub_140001010((char *)&Format_);
Stream = _acrt_iob_func(0);
fgets(Buffer, 100, Stream);
v4 = -1LL;
v5 = -1LL;
do
++v5;
while ( Buffer[v5] );
if ( v5 && v11[v5 + 15] == 10 )
{
n0x64 = v5 - 1;
if ( n0x64 >= 0x64 )
sub_140001448(Buffer);
Buffer[n0x64] = 0;
}
do
++v4;
while ( Buffer[v4] );
Str1 = (char *)sub_140001070(Buffer, v4, v11);
v8 = strcmp(Str1, "bW9lY3Rme1kwdV9DNG5fRzAwZF9BdF9CNDVlNjQhIX0=");
Format = (char *)&unk_140003300;
if ( v8 )
Format = (char *)&byte_140003318;
sub_140001010(Format);
free(Str1);
return 0;
}
将输入的字符串和预设的base64字符串作比对
解码该字符串即可

have_fun

点开是一个交互验证flag的逻辑

die查看文件情况

没什么异常,拖进ida逆向
找到WinMain函数
为什么要找WinMain函数:点进去程序很容易就能看出它不是命令行形式的,而是一个Windows桌面应用程序,而VS打包的Windows桌面应用程序的默认入口点就在WinMain函数

选中的代码表明:该窗口类所创建的窗口,其所有消息都会由 sub_140001210 函数处理,即窗口的具体行为由 sub_140001210 实现
跟进sub_140001210

简单分析以后定位到了这里,这里有两个点击某个按钮后就会弹出对话框的过程,窗口过程分别由sub_140001420和DialogFunc处理
分别跟进后找到DialogFunc是验证flag的函数
INT_PTR __fastcall DialogFunc(HWND hDlg, int a2, unsigned __int16 n1003)
{
HWND hDlg_1; // rbx
int v4; // edx
__int64 n32; // rax
int n32_2; // r8d
__m128 si128; // xmm2
__int64 v8; // rdx
unsigned __int64 v9; // rcx
__m128 v10; // xmm0
__m128 v11; // xmm1
__m128 v12; // xmm0
__m128 v13; // xmm1
__int64 n32_1; // rdx
unsigned __int16 *v15; // rdx
int v16; // r8d
int v17; // r9d
const WCHAR *v18; // r8
_OWORD v20[2]; // [rsp+0h] [rbp-1D8h] BYREF
WCHAR String[8]; // [rsp+20h] [rbp-1B8h] BYREF
_OWORD v22[12]; // [rsp+30h] [rbp-1A8h] BYREF
_WORD v23[104]; // [rsp+F0h] [rbp-E8h] BYREF
hDlg_1 = hDlg;
v4 = a2 - 272;
if ( !v4 )
{
v18 = &word_140003368;
LABEL_22:
SetDlgItemTextW(hDlg, 1002, v18);
return 1LL;
}
if ( v4 != 1 )
return 0LL;
if ( (unsigned __int16)(n1003 - 1) > 1u )
{
if ( n1003 == 1003 )
{
GetDlgItemTextW(hDlg, 1001, String, 100);
n32 = -1LL;
do
++n32;
while ( String[n32] );
n32_2 = 0;
if ( (int)n32 >= 32 )
{
si128 = (__m128)_mm_load_si128((const __m128i *)&xmmword_1400033F0);
v8 = 0LL;
v9 = 0LL;
do
{
v10 = (__m128)_mm_loadu_si128((const __m128i *)&String[v9 / 2]);
n32_2 += 32;
v8 += 32LL;
v11 = (__m128)_mm_loadu_si128((const __m128i *)&v22[v9 / 0x10]);
v9 += 64LL;
v22[v9 / 0x10 + 8] = _mm_xor_ps(v10, si128);
v12 = (__m128)_mm_loadu_si128((const __m128i *)&v20[v9 / 0x10]);
v22[v9 / 0x10 + 9] = _mm_xor_ps(v11, si128);
v13 = (__m128)_mm_loadu_si128((const __m128i *)&v20[v9 / 0x10 + 1]);
v22[v9 / 0x10 + 10] = _mm_xor_ps(v12, si128);
v22[v9 / 0x10 + 11] = _mm_xor_ps(v13, si128);
}
while ( v8 < (int)(n32 - (n32 & 0x1F)) );
}
n32_1 = n32_2;
if ( n32_2 < (__int64)(int)n32 )
{
do
{
v23[n32_1] = String[n32_1] ^ 0x2A;
++n32_1;
}
while ( n32_1 < (int)n32 );
}
v23[(int)n32] = 0;
v15 = v23;
do
{
v16 = *(unsigned __int16 *)((char *)v15 + (char *)aGeoiLqbj - (char *)v23);
v17 = *v15 - v16;
if ( v17 )
break;
++v15;
}
while ( v16 );
if ( v17 || (v18 = (const WCHAR *)&unk_140003388, (_DWORD)n32 != 16) )
v18 = (const WCHAR *)&unk_1400033A0;
hDlg = hDlg_1;
goto LABEL_22;
}
return 0LL;
}
EndDialog(hDlg, n1003);
return 1LL;
}
分析得知核心验证逻辑是将输入的字符串逐字节异或后和预设字符串aGeoiLqbj

提取预设字符串内容的有效位,直接异或0x2A即得flag

A simple program

打开是验证flag的程序,丢入ida
简单分析可以知道用户输入flag与程序定义的str2作了比较

同时可以看到str2的值为"moectf{this_is_a_flag}"
但是回到平台验证并不通过,那么继续分析
既然输入flag是与str2比较验证,那么就ctrl+x看str2的交叉引用

可以看到还有一个函数引用了它
分析可知这个函数才是真正的加密和验证函数

exp:
byte_4031A4 = [
0x4E, 0x4C, 0x46, 0x40, 0x57,
0x45, 0x58, 0x7A, 0x13, 0x56,
0x7C, 0x73, 0x17, 0x50, 0x50,
0x66, 0x47, 0x02, 0x02, 0x5E
]
key = 0x23
flag = ''.join([chr(byte ^ key) for byte in byte_4031A4])
print("Flag:", flag)

再回顾分析发现是在TlsCallback中修改了strncmp的函数逻辑,变成了一个简单异或

2048_master_re

扔进die看一下

运行看到是一个2048的小游戏

扔进ida查看
哇,函数超多

那就看看字符串表,找找输出flag相关的信息
shift+f12打开
注意到flag.txt文件名

进入对应存放位置后,ctrl+x查看交叉引用

找到了验证flag的函数 sub_401C83()
__int64 sub_401C83()
{
size_t v1; // rax
char Str[136]; // [rsp+20h] [rbp-D0h] BYREF
unsigned __int64 i_1; // [rsp+A8h] [rbp-48h] BYREF
char _2048master2048ma[32]; // [rsp+B0h] [rbp-40h] BYREF
void *Block; // [rsp+D0h] [rbp-20h]
FILE *Stream; // [rsp+D8h] [rbp-18h]
unsigned __int64 i; // [rsp+E0h] [rbp-10h]
unsigned int v8; // [rsp+ECh] [rbp-4h]
Stream = fopen("flag.txt", "r");
sub_428CB0(Stream, "%100s", Str);
fclose(Stream);
if ( strlen(Str) != 37 )
return 1LL;
strcpy(_2048master2048ma, "2048master2048ma");
v1 = strlen(Str);
Block = (void *)sub_401A81(Str, v1, _2048master2048ma, &i_1);
if ( Block )
{
v8 = 0;
for ( i = 0LL; i < i_1; ++i )
{
if ( *((_BYTE *)Block + i) != byte_495280[i] )
v8 = 1;
}
free(Block);
return v8;
}
else
{
sub_428D00("Encryption failed\n");
return 1LL;
}
}
进一步查看加密函数,可以发现是XXTEA加密
__int64 __fastcall sub_401A81(__int64 a1, __int64 a2, _QWORD *a3, __int64 a4)
{
__int64 v5; // rdx
_QWORD v6[3]; // [rsp+20h] [rbp-30h] BYREF
__int64 v7; // [rsp+38h] [rbp-18h] BYREF
__int64 v8; // [rsp+40h] [rbp-10h]
void *Block; // [rsp+48h] [rbp-8h]
Block = (void *)sub_401898(a1, a2, &v7);
if ( !Block )
return 0LL;
v5 = a3[1];
v6[0] = *a3;
v6[1] = v5;
sub_401530(Block, (unsigned int)v7, v6);
v8 = sub_40195B(Block, v7, a4);
free(Block);
return v8;
}
查看byte_495280数组的内容

写脚本解密
from regadgets import *
data = bytes([0x35, 0x79, 0x77, 0xCC, 0x1B, 0x13, 0x41, 0x34, 0xF9, 0xFF, 0x9F, 0x91, 0xFF, 0x5B, 0x94, 0x78, 0x86, 0x2A, 0xAF, 0xAE, 0xD7, 0x9E, 0x31, 0x4D, 0x7A, 0xC4, 0xA5, 0x51, 0xD1, 0xD9, 0x6E, 0x44, 0x18, 0x52, 0x86, 0x1B, 0x42, 0x8A, 0xC9, 0x63])
flag = xxtea_decrypt(data, b'2048master2048ma', 0x3E9779B9)
print(flag)
解密得到flag

ezandroid

也是通过这个题入手一下安卓逆向
题目给了一个apk文件
夜神模拟器打开,看到是一个验证flag的功能

扔到jadx反编译apk
看核心配置文件AndroidManifest.xml

在 Android 应用中,AndroidManifest.xml 文件是至关重要的,它定义了应用的所有组件以及组件之间的关系,包括应用的入口点。打开反编译后的 APK 中的 AndroidManifest.xml 文件,查找 标签,它们通常定义了应用的各个 Activity(包括启动 Activity)。
入口点通常由以下两个标记表示:
<action android:name="android.intent.action.MAIN" />:标记这是应用的主入口。
<category android:name="android.intent.category.LAUNCHER" />:表示该 Activity 会出现在应用启动器中(即桌面)。
所以得出程序入口点在com.example.ezandroid.MainActivity

一个Activity的生命周期是:onCreate()->onStart()->onResume()->onPause()->onStop()->onDestroy(),所以可以先锁定onCreate函数,锁定app加载的主要逻辑!
全局搜索com.example.ezandroid.MainActivity字段找到onCreate函数

分析可知主要逻辑是将用户输入的字符串和程序预设的字符串bW9lY3Rme2FuZHJvaWRfUmV2ZXJzZV9JNV9lYXN5fQ==做比对

base64解码得到flag

ezandroid.pro

稍微进阶一点的安卓逆向,涉及到了native层
其实就是程序文件和库函数的关系,native层的函数会放在apk文件里lib文件夹的对应架构文件夹里的.so文件
可以通过解压apk提取
1.

2.

3.

将文件用ida打开即可分析
对apk文件的分析还是用jadx,去配置文件里找入口函数


可以在主逻辑里找到
native方法声明
public native boolean check(String str);
加载native库
static {
System.loadLibrary("ezandroidpro");
}
native层函数要严格遵守JNI命名规则:
JNIEXPORT jboolean JNICALL
Java_com_example_ezandroidpro_MainActivity_check(JNIEnv *env, jobject thiz, jstring input)
去ida里面找到这个函数

bool __fastcall Java_com_example_ezandroidpro_MainActivity_check(__int64 a1, __int64 a2, __int64 a3)
{
const char *s; // x0
const char *src; // x21
size_t n0x17; // x0
size_t n; // x22
char *dest; // x23
_BOOL4 v10; // w20
unsigned __int64 v11; // x24
char *_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C; // x19
__int64 *v13; // x10
unsigned __int64 v14; // x9
unsigned __int8 *v15; // x10
char *_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649; // x11
int v17; // w12
int v18; // w13
void *s1_1; // x21
void *v21[2]; // [xsp+0h] [xbp-50h] BYREF
void *s1; // [xsp+10h] [xbp-40h]
void *_moectf2025______[3]; // [xsp+18h] [xbp-38h] BYREF
_QWORD v24[2]; // [xsp+30h] [xbp-20h] BYREF
void *dest_1; // [xsp+40h] [xbp-10h]
__int64 v26; // [xsp+48h] [xbp-8h]
v26 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
s = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0LL);
if ( !s )
return 0;
src = s;
n0x17 = strlen(s);
if ( n0x17 >= 0xFFFFFFFFFFFFFFF0LL )
std::__basic_string_common<true>::__throw_length_error(v24);
n = n0x17;
if ( n0x17 >= 0x17 )
{
v11 = (n0x17 + 16) & 0xFFFFFFFFFFFFFFF0LL;
dest = (char *)operator new(v11);
v24[1] = n;
dest_1 = dest;
v24[0] = v11 | 1;
goto LABEL_8;
}
dest = (char *)v24 + 1;
LOBYTE(v24[0]) = 2 * n0x17;
if ( n0x17 )
LABEL_8:
memcpy(dest, src, n);
dest[n] = 0;
(*(void (__fastcall **)(__int64, __int64, const char *))(*(_QWORD *)a1 + 1360LL))(a1, a3, src);
strcpy((char *)_moectf2025______, " moectf2025!!!!!!");
_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C = (char *)operator new(0x70uLL);
strcpy(
_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C,
"4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C0B971BF2EFBCB160E531A646DF7A6AC0B");
sm4Encrypt(v21, v24, _moectf2025______);
v13 = (__int64 *)v21[1];
v14 = (unsigned __int64)LOBYTE(v21[0]) >> 1;
if ( ((__int64)v21[0] & 1) == 0 )
v13 = (__int64 *)((unsigned __int64)LOBYTE(v21[0]) >> 1);
if ( v13 == &qword_60 )
{
if ( ((__int64)v21[0] & 1) != 0 )
{
s1_1 = s1;
v10 = memcmp(s1, _4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C, 0x60uLL) == 0;
goto LABEL_21;
}
v15 = (unsigned __int8 *)v21 + 1;
_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649 = _4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C;
do
{
v17 = *v15;
v18 = (unsigned __int8)*_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649;
v10 = v17 == v18;
if ( v17 != v18 )
break;
--v14;
++v15;
++_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649;
}
while ( v14 );
}
else
{
v10 = 0;
}
if ( ((__int64)v21[0] & 1) != 0 )
{
s1_1 = s1;
LABEL_21:
operator delete(s1_1);
}
operator delete(_4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C);
if ( ((__int64)_moectf2025______[0] & 1) != 0 )
operator delete(_moectf2025______[2]);
if ( (v24[0] & 1) != 0 )
operator delete(dest_1);
return v10;
}
分析加密逻辑之后,发现要恢复明文(也就是通过比较的那个 32 字节 flag),应该:
-
把常量 hex 字符串当作 SM4-ECB 的密文(48 字节)。
-
用密钥 b" moectf2025!!!"(前 16 字节)进行 SM4-ECB 解密。
-
对解密结果作 PKCS#7 去填充(去掉末尾的 0x10×16),得到 32 字节的原始明文(flag)。
decrypto_exp:
#!/usr/bin/env python3
# sm4_decrypt_with_libs.py
# 使用 gmssl 或 pycryptodome 解密指定 SM4 密文(不包含任何纯 Python 实现)
from binascii import unhexlify, hexlify
import sys
import itertools
import string
CIPHERTEXT_HEX = "4EEB1EEF2914D79BFA8C5006332097ED2EF06C4A59CAE31C827A08D45CC649C0B971BF2EFBCB160E531A646DF7A6AC0B"
BASE_KEY = " moectf2025!!!!!!" # 来自反编译,注意开头空格
# 生成 key 候选(只做合理的变体)
def gen_key_candidates(base):
variants = set()
variants.add(base)
variants.add(base[:16])
variants.add(base[-16:])
variants.add(base.strip())
variants.add(base.replace(" ", ""))
# 小变体:去掉末尾1~3个字符(常见的手误)
for i in range(1, 4):
if len(base) > i:
variants.add(base[:-i])
variants.add(base.upper())
variants.add(base.lower())
variants.add(base + "\x00")
variants.add((base + "\x00")[:16])
# 保证最终都是 16 字节(截断或空字节右填充)
out = []
for v in variants:
vb = v.encode("utf-8", errors="ignore")
if len(vb) >= 16:
out.append(vb[:16])
else:
out.append(vb.ljust(16, b'\x00'))
# 去重
uniq = []
seen = set()
for k in out:
if k not in seen:
seen.add(k)
uniq.append(k)
return uniq
def pkcs7_unpad(data):
if not data:
return None
pad = data[-1]
if pad < 1 or pad > 16:
return None
if data[-pad:] != bytes([pad]) * pad:
return None
return data[:-pad]
def looks_printable(b):
try:
s = b.decode("utf-8", errors="ignore")
except Exception:
return False
printable = sum(1 for ch in s if ch in string.printable)
return printable >= max(1, int(len(s) * 0.8))
# 先尝试 import gmssl,再尝试 pycryptodome;若都不可用则提示
USE_GMSSL = False
USE_PYCRYPTO = False
gmssl_mod = None
pycrypto_mod = None
try:
from gmssl.sm4 import CryptSM4, SM4_DECRYPT
USE_GMSSL = True
gmssl_mod = CryptSM4
except Exception:
try:
from Crypto.Cipher import SM4
from Crypto.Util.Padding import unpad as crypto_unpad
USE_PYCRYPTO = True
pycrypto_mod = SM4
except Exception:
pass
if not (USE_GMSSL or USE_PYCRYPTO):
print("错误:系统中未找到 `gmssl` 或 `pycryptodome`。")
print("建议安装其中一个:")
print(" pip install gmssl")
print("或")
print(" pip install pycryptodome")
sys.exit(1)
def decrypt_with_gmssl(key16, ct_bytes):
sm4 = CryptSM4()
# decrypt expects key bytes
sm4.set_key(key16, SM4_DECRYPT)
out = []
# ECB
try:
pt_ecb = sm4.crypt_ecb(ct_bytes)
out.append(("GMSSL-ECB", pt_ecb))
except Exception:
pass
# CBC with zero IV
try:
pt_cbc = sm4.crypt_cbc(b"\x00"*16, ct_bytes)
out.append(("GMSSL-CBC-iv0", pt_cbc))
except Exception:
pass
return out
def decrypt_with_pycrypto(key16, ct_bytes):
out = []
# ECB
try:
cipher = pycrypto_mod.new(key16, pycrypto_mod.MODE_ECB)
pt = cipher.decrypt(ct_bytes)
out.append(("PyCrypto-ECB", pt))
except Exception:
pass
# CBC with zero IV
try:
cipher = pycrypto_mod.new(key16, pycrypto_mod.MODE_CBC, iv=b"\x00"*16)
pt = cipher.decrypt(ct_bytes)
out.append(("PyCrypto-CBC-iv0", pt))
except Exception:
pass
return out
def main():
ct = unhexlify(CIPHERTEXT_HEX)
print("cipher len bytes:", len(ct))
keys = gen_key_candidates(BASE_KEY)
print("trying %d key candidates..." % len(keys))
results = []
for k in keys:
if USE_GMSSL:
try:
res = decrypt_with_gmssl(k, ct)
for mode, pt in res:
results.append((mode, k, pt))
except Exception:
pass
if USE_PYCRYPTO:
try:
res = decrypt_with_pycrypto(k, ct)
for mode, pt in res:
results.append((mode, k, pt))
except Exception:
pass
# 过滤并显示可读候选,优先标注 len==32
found = []
for mode, k, pt in results:
note = []
unp = pkcs7_unpad(pt)
if unp is not None:
display = unp
note.append("pkcs7_unpadded")
else:
display = pt
if len(display) == 32:
note.append("len32")
if looks_printable(display):
note.append("printable")
if note:
found.append((mode, k, display, note, pt))
if found:
print("\n可能的明文候选(优先带 pad/unpad 信息与 len32 标注):")
for mode, k, disp, note, raw in found:
print("----")
print("mode:", mode)
print("key(hex):", hexlify(k).decode())
try:
key_ascii = k.decode("utf-8", errors="ignore")
except Exception:
key_ascii = "<non-decodable>"
print("key(ascii approx):", repr(key_ascii))
print("notes:", ",".join(note))
print("plaintext (hex):", hexlify(disp).decode())
print("plaintext (utf8 replace):", disp.decode("utf-8", errors="replace"))
# also show raw block decrypt (hex)
if raw != disp:
print("raw decrypted (hex):", hexlify(raw).decode())
else:
print("\n未找到明显可读或合规长度的候选。列出前 10 次尝试的原始解密输出(hex/utf8 replace):")
for i, (mode, k, pt) in enumerate(results[:10]):
print("----")
print("mode:", mode)
print("key(hex):", hexlify(k).decode())
print("plaintext(hex):", hexlify(pt).decode())
print("plaintext(decode):", pt.decode("utf-8", errors="replace"))
if __name__ == "__main__":
main()
运行得到flag

mazegame

运行程序,看到以下界面;

应该是道迷宫题
那么就要先逆向出移动方式和迷宫图
通过分析程序的汇编代码可以发现

从(1,1)开始

到(32,15)结束
迷宫数据在字符串视图就可以看到

提取出地图数据,正常来讲要写寻路算法,比如BFS,但是现在,迷宫一把梭启动!
https://github.com/LingerJAB/MazeSolver
将(1,1)改为S,(32,15)为E

粘贴进程序并设置好入口和出口

设置好输出

直接求解!得到答案!

可视化:

将路径用moectf{}包裹提交,即为flag

WEEK2
ezpy

借这道题入门一下python逆向
Python是解释型语言,没有严格意义上的编译和汇编过程。但是一般可以认为编写好的python源文件,由python解释器翻译成以.pyc为结尾的字节码文件。pyc文件是二进制文件,可以由python虚拟机直接运行。
Python在执行import语句时,将会到已设定的path中寻找对应的模块。并且把对应的模块编译成相应的PyCodeObject中间结果,然后创建pyc文件,并将中间结果写入该文件。然后,Python会import这个pyc文件,实际上也就是将pyc文件中的PyCodeObject重新复制到内存中。而被直接运行的python代码一般不会生成pyc文件。
加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。
题目给了一个pyc文件
去Github上下载一个python反编译的集成工具PyGlimmer
https://github.com/yoruak1/PyGlimmer
配置好后将pyc文件扔进去反编译

可以看到是一个凯撒加密的逻辑
写python脚本解密
def caesar_cipher_decrypt(text, shift):
# 解密使用负的移位值,相当于反向移位
return caesar_cipher_encrypt(text, -shift)
def caesar_cipher_encrypt(text, shift):
result = []
for char in text:
if char.isalpha():
if char.islower():
new_char = chr((ord(char) - ord("a") + shift) % 26 + ord("a"))
else: # 大写字母
new_char = chr((ord(char) - ord("A") + shift) % 26 + ord("A"))
result.append(new_char)
else:
result.append(char)
return "".join(result)
# 已知的加密文本
encrypted_text = "wyomdp{I0e_Ux0G_zim}"
# 原始移位值(通过模26计算有效移位)
shift = 114514 % 26 # 结果为10
# 解密获取原始文本
original_text = caesar_cipher_decrypt(encrypted_text, shift)
print(f"原始文本: {original_text}")
运行得到flag

speed


die没什么异常,运行程序可以看到有个很快的弹窗一闪而过
拖入ida静态分析,可以看到WinMain里有创建和销毁窗口的操作

x64dbg断到销毁窗口前,然后单步执行,即得到flag

guess


运行看看

猜数字游戏
丢进ida分析,反编译报错

跳转到报错位置看一下

有个永真跳转花指令
直接nop掉

再反编译

C++程序,分析一下代码
粗略看一下可以发现是RC4,再细致分析,在rc4_ksa中看到了问题

有一个+42的偏移,是魔改rc4
同时要找到enc(加密后的flag数据)和key
但是在ida里对应位置直接看不到,那么就往回看如何加载它
先看enc

交叉引用可以看到有三个函数引用了它

先看初始化的函数
看到加载了这样的值

点进去找到了enc

再看key
同样的方法



找到了key是moectf2025
那么就是写脚本解密了
然后遇到了一个问题,之前复制的enc长度不是偶数,导致解密无法成功,最后发现是ida反汇编后显示的数据不全,那么就进入hex view看原始数据
找到了未显示完全的数据

exp:
#!/usr/bin/env python3
import binascii
import re
import argparse
import sys
def rc4_custom_decrypt(enc_bytes: bytes, key: bytes) -> bytes:
# KSA with extra +42
S = list(range(256))
j = 0
klen = len(key)
for i in range(256):
j = (j + S[i] + key[i % klen] + 42) & 0xFF
S[i], S[j] = S[j], S[i]
# PRGA (standard)
i = 0
j = 0
out = bytearray()
for b in enc_bytes:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
t = (S[i] + S[j]) & 0xFF
k = S[t]
out.append(b ^ k)
return bytes(out)
def clean_hex(s: str) -> str:
return re.sub(r'[^0-9a-fA-F]', '', s)
def decode_and_print(pt: bytes):
# Prefer utf-8, fallback to latin1, otherwise show hex
try:
text = pt.decode('utf-8')
print("=> moectf{" + text + "}")
return
except UnicodeDecodeError:
pass
try:
text = pt.decode('latin1')
print("=> moectf{" + text + "}")
return
except Exception:
pass
print("=> decrypted bytes (hex):", pt.hex())
def main():
parser = argparse.ArgumentParser(description="Decrypt custom-RC4 (KSA +42) from hex.")
parser.add_argument('--enc', type=str, help='enc hex string (ASCII hex)', required=False)
parser.add_argument('--key', type=str, help='key string', required=False)
args = parser.parse_args()
# You may hardcode values here if you prefer:
enc_hex = args.enc or "464DCF81DE6F2E16BE203F10565CCDBFF18CCD6A45967D20DC558FB76C0CC3AE07D154"
key_str = args.key or "moectf2025"
enc_hex_clean = clean_hex(enc_hex)
print(f"[+] cleaned enc_hex = {enc_hex_clean}")
L = len(enc_hex_clean)
print(f"[+] length = {L} hex chars ({L//2} bytes if even)")
if L == 0:
print("[-] enc_hex is empty after cleaning. Check the input.")
sys.exit(1)
if L % 2 == 1:
print("[-] Odd-length hex string detected (missing nibble).")
# If you are certain and want to quickly test, uncomment the next line to append '0'
# enc_hex_clean = enc_hex_clean + "0"
print(" Suggestion: re-copy the full quoted string from the binary's .rdata (use Hex view).")
sys.exit(2)
try:
enc_bytes = binascii.unhexlify(enc_hex_clean)
except binascii.Error as e:
print("[-] unhexlify failed:", e)
sys.exit(3)
key = key_str.encode('latin1')
pt = rc4_custom_decrypt(enc_bytes, key)
decode_and_print(pt)
if __name__ == "__main__":
main()

catch

核心执行函数是solve
要先了解C++的异常处理机制,,try里面一定会抛出异常,但是这里的try相当于什么都没干,那么就怀疑是ida反编译错误,把try和clean的部分全部nop掉再重新反编译

分析发现是一个rot13,直接用在线工具解密即可

WEEK3
rusty_sudoku


rust逆向启动!
运行程序

解数独题
ida进入main函数

看到真正的main入口:rusty_sudoku::main
分析反编译程序找到在这里解码内置模板


丢给ai求解后答案为:369184572185327694274956831632879415897541263541632789756213948918465327423798156
继续分析程序可知是对这个答案进行了md5运算

丢给工具运算

用moectf{}包裹后即为flag

5501

被折叠的 条评论
为什么被折叠?



