MoeCTF2025_逆向AK详解

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

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)计算得到的结果
  • 将处理后的temp51966取模,得到本轮结果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),但Label004048EE开始,Label+1004048EF。结合后续代码,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),应该:

  1. 把常量 hex 字符串当作 SM4-ECB 的密文(48 字节)。

  2. 用密钥 b" moectf2025!!!"(前 16 字节)进行 SM4-ECB 解密。

  3. 对解密结果作 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

在这里插入图片描述

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值