# 逆向签到题

直接查看反汇编代码就能看到 flag

逆向签到

# re2

弄清楚加密过程 找到密钥 ,本题采用 RC4 加密,可以参考如下流程

img

讲解博客

将样本放进 IDA 中分析,在左侧找到_main 函数:

按下 tab,查看反汇编代码:

主函数

进入 main_0:

代码如下:

main_0

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+0h] [ebp-44Ch]
  char v5; // [esp+0h] [ebp-44Ch]
  char v6; // [esp+0h] [ebp-44Ch]
  char v7; // [esp+0h] [ebp-44Ch]
  char v8; // [esp+0h] [ebp-44Ch]
  char v9; // [esp+0h] [ebp-44Ch]
  char v10[4]; // [esp+194h] [ebp-2B8h] BYREF
  int v11; // [esp+1A0h] [ebp-2ACh]
  FILE *v12; // [esp+1ACh] [ebp-2A0h]
  FILE *v13; // [esp+1B8h] [ebp-294h]
  _BYTE v14[264]; // [esp+1C4h] [ebp-288h] BYREF
  _BYTE v15[264]; // [esp+2CCh] [ebp-180h] BYREF
  char Str1[60]; // [esp+3D4h] [ebp-78h] BYREF
  char Str[56]; // [esp+410h] [ebp-3Ch] BYREF
  __CheckForDebuggerJustMyCode(&unk_40B027);
  memset(Str, 0, 50);
  memset(Str1, 0, 50);
  memset(v15, 0, 0x100u);
  memset(v14, 0, 0x100u);
  v11 = 1;
  do
  {
    print(asc_406BA0, v4);
    print((char *)&byte_406BFC, v5);
    print(asc_406C48, v6);
    print((char *)&byte_406CA8, v7);
    print((char *)&byte_406D0C, v8);
    print(a1, v9);
    sub_401073("%d", (char)v10);
    if ( *(_DWORD *)v10 == 1 )
    {
      v13 = fopen("flag.txt", "r");
      if ( !v13 )
      {
        print((char *)&byte_406D48, v4);
        getchar();
        exit(0);
      }
      v12 = fopen("enflag.txt", "w");
      if ( !v12 )
      {
        print((char *)&byte_406D70, v4);
        getchar();
        exit(0);
      }
      print(asc_406D84, v4);
      sub_401073("%s", (char)Str);
      sub_401069(Str, Str1);
      sub_401028(Str, (int)v15, (int)v14, v13, v12);
    }
    else if ( *(_DWORD *)v10 == 2 )
    {
      v11 = 0;
    }
    else
    {
      print(asc_406D98, v4);
    }
  }
  while ( v11 );
  return 0;
}
  1. v10 是我们输入的选项,为 1 的时候执行以下代码:
  • 以只读的方式打开 flag.txt , 打开失败则退出
  • 以写的方式打开 enflag.txt , 打开失败则退出
  • 接下来输入一个字符串 Str
  • 最后执行两个函数 sub_401069sub_401028
  1. 分析这两个函数
  • 针对 sub_401069 :

    // attributes: thunk
    int __cdecl sub_401069(char *Str, char *Str1)
    {
      return sub_401A70(Str, Str1);
    }

    进入 sub_401A70:

    char __cdecl sub_401A70(char *Str, char *Str1)
    {
      char v3; // [esp+0h] [ebp-E4h]
      signed int i; // [esp+D0h] [ebp-14h]
      signed int v5; // [esp+DCh] [ebp-8h]
      __CheckForDebuggerJustMyCode(&unk_40B027);
      v5 = strlen(Str);
      for ( i = 0; i < v5; ++i )
        Str1[i] += Str[i] ^ 0x1F;
      if ( !strcmp(Str1, "DH~mqqvqxB^||zll@Jq~jkwpmvez{") )
        print((char *)&byte_406B80, v3);
      else
        print("Error!\n", v3);
      return *Str1;
    }

    这里接受两个参数 Str,Str1,其中 Str 是我们输入的参数,Str1 是主函数创建的长度为 50 字节的值为 0 的数组。程序流程如下:

    • 将 Str1 中的每个字符与 0x1F 异或
    • 得到的新字符串与 "DH~mqqvqxB^||zll@Jq~jkwpmvez {" 进行比较
    • 如果成功则返回字符串 Str1

    这里可以先算一下 Str1:

    cmp_str1 = "DH~mqqvqxB^||ll@Jq~jkwpmvez{"
    plain_text = ""
    for decrypted_char in cmp_str1:
        plain_char = ord(decrypted_char) ^ 0x1f
        plain_text += chr(plain_char)
    print(plain_text)

    可知 Str1 == [Warnning]Accss_Unauthorized ,这也是我们需要输入的字符串。

    接下来点击 X 返回主函数:

  • 针对 sub_401028:

    接收的参数有 sub_401028(Str, (int)v15, (int)v14, v13, v12) , 跟进查看

    这里的 a2 = v15,a3 = v14,Stream = v13,a5 = v12 ,继续跟进

    继续跟进看到的四个函数

    Sub_4010F0:

    这里要注意的是 a1 是上一个函数里的 a2 也就是主函数的 v15,a2 是我们输入的字符串 ([Warnning] Accss_Unauthorized),a3 是我们输入的字符串的长度。

    Sub_401800:

    int __cdecl sub_401800(int a1, int a2, int a3)
    {
      int result; // eax
      int j; // [esp+D0h] [ebp-14h]
      int i; // [esp+DCh] [ebp-8h]
      result = __CheckForDebuggerJustMyCode(&unk_40B027);
      if ( a3 <= 256 )
      {
        for ( i = 0; i < 256; ++i )
        {
          *(_BYTE *)(i + a1) = *(_BYTE *)(a2 + i % a3);
          result = i + 1;
        }
      }
      if ( a3 > 256 )
      {
        for ( j = 0; j < 256; ++j )
        {
          *(_BYTE *)(j + a1) = *(_BYTE *)(j + a2);
          result = j + 1;
        }
      }
      return result;
    }

    接下来判断我们输入的字符串长度是否大于 256 ,如果不足 256,则复制 a2 ([Warnning] Accss_Unauthorized) 直到长度大于 256。如果超过 256,则截断。所以这个函数主要是改变了主函数里的 v15。

    sub_4010c8:

    这里的 a1 是主函数里的 v14。只是初始化了一个数组 并把这个数组每个值赋值为索引。

    sub_40116D:

    接收的参数分别为 a3,a2, 即主函数里的 v14,v15。由于 v14 之前提到过则记录一下 他是一个索引为值的数组,v15 是第一个函数 256 位处理的那个

    继续跟进得到下面的函数

    int __cdecl sub_4018E0(int a1, int a2)
    {
      int result; // eax
      int i; // [esp+D0h] [ebp-2Ch]
      char v4; // [esp+EBh] [ebp-11h]
      int v5; // [esp+F4h] [ebp-8h]
      result = __CheckForDebuggerJustMyCode(&unk_40B027);
      v5 = 0;
      for ( i = 0; i < 256; ++i )
      {
        v5 = (*(unsigned __int8 *)(i + a2) + v5 + *(unsigned __int8 *)(i + a1)) % 256;
        v4 = *(_BYTE *)(i + a1);
        *(_BYTE *)(i + a1) = *(_BYTE *)(v5 + a1);
        *(_BYTE *)(v5 + a1) = v4;
        result = i + 1;
      }
      return result;
    }

    这里需要把 a1 认为是 v14,a2 认为是 v15

    这里是把两个数组的每一位分别做计算之后通过 v5 这个变量累加起来。v4 就是 a1 这个数组的第 i 位,之后将 a1 这个数组的 v5 这个位置的值放入 v4,将 v4 放入 v5 的这个位置。也就是将 a1 的第 i 个位置与第 v5 个位置的值进行置换。

    这个函数改变的就是 a1 就是主函数的 v14,类似于做了个初始置换.

    看最后一个函数:

    4010EB:

    接收的参数为 sub_4010EB (a3, Stream, a5); 也就是主函数的 v14,v13,v12(这两是和 flag 有关的文件)

    跟进:

    这里的 a1 就是 v14,stream 就是 v13,a3 就是 v12

    代码可知:

    • 循环读取 v13 (flag.txt) 中的字符,v7 很显然代表的是读取字符的位置(在文件中的第几位),v6 就是 v14 文件中对应位置的字符并与 v6 累加。
    • 接下来三行代码是将 v14 文件中的 v6 位置的字符与 v7 位置的字符进行替换。
    • 最后将 v14 文件中的 v6 位置的字符与 v7 位置的字符转成数字后累加得到的值作为索引 t,拿到 v14 文件中的第 t 个字符并与源码中的 i (这个 i 是原本的 flag 哈) 进行异或之后写入 v2 (enflag.txt), 即加密后的文件
  1. 综上,整个过程就是 RC4 的实现方式
    1. 不难看出我们输入的 Str 实际就是密钥,密钥为 [Warnning]Accss_Unauthorized
    2. v14 对应的就是 RC4 中的 S 盒,V15 对应的就是 T,我们通过输入的密钥不断填充 V15。
    3. 之后就是 V14 与 V15 直接的运算包括 S 的初始置换以及最后密钥流的生成。
  2. 之后编写解密代码 ,我们可以拿到加密后 flag 的值 ,通过分析也得到了密钥 [Warnning]Accss_Unauthorized 之后我们就需要按照 RC4 的方式同样生成密钥流,最后对密文进行解密。
#
# 根据 IDA 逆向分析结果编写的 RC4 解密脚本
#
# ========================== 用户配置区 ==========================
# ❗️ 在这里填入加密时使用的密钥
YOUR_SECRET_KEY = "[Warnning]Access_Unauthorized"
# ❗️ 填入加密后的文件名 (即 enflag.txt 的路径)
ENCRYPTED_FILE_PATH = "enflag.txt"
# ❗️ 填入保存解密后文件的名字
DECRYPTED_FILE_PATH = "flag_decrypted.txt"
# =============================================================
class RC4:
    def __init__(self, key: str):
        """
        使用密钥初始化RC4状态,完成密钥调度算法(KSA)。
        这个构造函数完整复刻了 sub_401780, sub_401800, 和 sub_4018E0 的功能。
        """
        key_bytes = key.encode('ascii')  # 将字符串密钥转为字节
        key_length = len(key_bytes)
        # 对应 sub_401780: 初始化 S-box, S [i] = i
        self.S = list(range(256))
        # 对应 sub_401800: 根据密钥初始化 T-vector
        T = [key_bytes[i % key_length] for i in range(256)]
        # 对应 sub_4018E0: 使用 T-vector 对 S-box 进行初始置换
        j = 0
        for i in range(256):
            j = (j + self.S[i] + T[i]) % 256
            # 交换 S [i] 和 S [j]
            self.S[i], self.S[j] = self.S[j], self.S[i]
        print("✅ RC4 状态初始化完成 (KSA complete).")
    def crypt(self, data: bytes) -> bytes:
        """
        对数据进行加密或解密。
        这个方法完整复刻了 sub_4015E0 (PRGA) 的功能。
        """
        i = 0
        j = 0
        output = bytearray()
        for byte in data:
            i = (i + 1) % 256
            j = (j + self.S[i]) % 256
            # 交换 S [i] 和 S [j]
            self.S[i], self.S[j] = self.S[j], self.S[i]
            # 计算 t 并生成密钥流字节 k
            t = (self.S[i] + self.S[j]) % 256
            keystream_byte = self.S[t]
            # 将数据字节与密钥流字节进行 XOR
            output.append(byte ^ keystream_byte)
        return bytes(output)
# --- 主执行逻辑 ---
if __name__ == "__main__":
    print("--- RC4解密程序启动 ---")
    # 检查密钥是否已填写
    try:
        # 1. 读取加密后的文件内容
        print(f"📄 正在读取加密文件: {ENCRYPTED_FILE_PATH}")
        with open(ENCRYPTED_FILE_PATH, 'rb') as f:
            encrypted_data = f.read()
        print(f"   读取了 {len(encrypted_data)} 字节数据。")
        # 2. 使用密钥初始化 RC4 加密器
        print(f"🔑 使用密钥 '{YOUR_SECRET_KEY}' 初始化RC4...")
        cipher = RC4(YOUR_SECRET_KEY)
        # 3. 执行解密 (与加密过程完全相同)
        print("⚙️ 正在执行解密操作...")
        decrypted_data = cipher.crypt(encrypted_data)
        print("   解密完成。")
        # 4. 将解密后的数据写入新文件
        print(f"💾 正在将解密结果写入文件: {DECRYPTED_FILE_PATH}")
        with open(DECRYPTED_FILE_PATH, 'wb') as f:
            f.write(decrypted_data)
        print("\n" + "=" * 50)
        print(f"🎉 解密成功!结果已保存至 {DECRYPTED_FILE_PATH}")
        print("=" * 50)
    except FileNotFoundError:
        print(f"❌ 错误: 找不到文件 '{ENCRYPTED_FILE_PATH}'。请检查文件名和路径是否正确。")
    except Exception as e:
        print(f"❌ 发生未知错误: {e}")

# Re3

本题比较简单,没有跨函数分析:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  size_t v3; // rax
  int v5; // [rsp+Ch] [rbp-134h] BYREF
  unsigned int i; // [rsp+10h] [rbp-130h]
  int v7; // [rsp+14h] [rbp-12Ch]
  int v8; // [rsp+18h] [rbp-128h]
  int v9; // [rsp+1Ch] [rbp-124h]
  int v10; // [rsp+20h] [rbp-120h]
  int v11; // [rsp+24h] [rbp-11Ch]
  int v12; // [rsp+28h] [rbp-118h]
  int v13; // [rsp+2Ch] [rbp-114h]
  int v14; // [rsp+30h] [rbp-110h]
  int v15; // [rsp+34h] [rbp-10Ch]
  unsigned __int64 v16; // [rsp+38h] [rbp-108h]
  _DWORD v17[8]; // [rsp+40h] [rbp-100h]
  char s[5]; // [rsp+60h] [rbp-E0h] BYREF
  char v19[107]; // [rsp+65h] [rbp-DBh] BYREF
  char dest[104]; // [rsp+D0h] [rbp-70h] BYREF
  unsigned __int64 v21; // [rsp+138h] [rbp-8h]
  v21 = __readfsqword(0x28u);
  v7 = 80;
  v8 = 64227;
  v9 = 226312059;
  v10 = -1540056586;
  v11 = 5;
  v12 = 16;
  v13 = 3833;
  v5 = 0;
  puts("plz input the key:");
  __isoc99_scanf("%s", s);
  v3 = strlen(s);                               // 判断输入字符的长度
  strncpy(dest, v19, v3 - 6);
  dest[strlen(s) - 6] = 0;
  __isoc99_sscanf(dest, "%x", &v5);
  v17[0] = v7;
  v17[1] = v8;
  v17[2] = v9;
  v17[3] = v10;
  v17[4] = (v11 << 12) + v12;
  v17[5] = v13;
  v17[6] = v5;
  v16 = 0;
  for ( i = 0; i <= 6; ++i )
  {
    for ( v16 += (unsigned int)v17[i]; v16 > 0xFFFF; v16 = v15 + (unsigned int)(unsigned __int16)v16 )
    {
      v14 = (unsigned __int16)v16;
      v15 = v16 >> 16;
    }
  }
  if ( v16 == 0xFFFF )
    puts("OK");
  else
    puts("Error");
  return 0;
}
  1. 整体流程:

    1. 初始化变量后,需要输入一个字符串
    2. 从第六位开始截取这个字符串并 copy 给 dest,注意这里输入的时候是给的 s 但是由于 s 的长度只有 5 而且紧接着 s 下面就是 v19,所以我们输入的字符串超过 5 位后的字符都会被存在 v19
    3. 将 dest 中的值以十六进制的形式读给 v5
    4. 初始化 v17 数组
    5. 之后进行六次循环
      1. 将 v16 加上 v17 对应位置的值
      2. 如果超过了 ffff 那么将 v16 保存到 v14,之后右移 16 位并与自己累加
    6. 循环完成之后,如果得到 v16 恰好是 ffff 则输出 OK。
  2. 写脚本求输入的字符串,使得 v16 恰好为 ffff

    分析算法过程:

    根据分析过程编写如下代码:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def calculate_hex_key():
    """
    根据已知的校验和算法和 "%x" 格式,直接计算出最终的key。
    """
    # 1. 初始化常量
    s_fixed_values = [
        0x50,  # s[0] = x
        0xfae3,  # s[1] = c
        0xd7d3f7b,  # s[2] = v
        0xa43499f6,  # s[3] = n
        0x10 + 5 * 0x1000,  # s[4] = m + b * 0x1000
        0xef9  # s[5] = h
    ]
    # 2. 计算已知数值的总和
    fixed_sum = sum(s_fixed_values)
    # 3. 模拟 C 代码中的校验和折叠算法
    def checksum_fold(value):
        while value > 0xffff:
            value = (value & 0xffff) + (value >> 16)
        return value
    # 4. 计算部分校验和
    partial_checksum = checksum_fold(fixed_sum)
    # 5. 根据目标 0xffff 计算出 j 的值
    target = 0xffff
    j = target - partial_checksum
    # 6. 构造最终的 key
    # 格式为 "% x",所以使用 j 的十六进制字符串表示
    # 末尾附加任意 6 个字符以满足程序的长度要求
    final_key = f"{j:x}" + "A" * 6
    print("=" * 40)
    print("已确认 sscanf 格式为 '%x'.")
    print(f"计算出的 j 的值: 0x{j:x}")
    print(f"因此, 正确的 Key 是: {final_key}")
    print("=" * 40)
if __name__ == "__main__":
    calculate_hex_key()

得到输入的值应该是 0x1a9f:

# re4

查看 main 函数

v4 是我们需要输入的值,直接跳到 sub_1400010E0 函数:

这里的 a1 就是 v4(我们输入的字符串)

先要看懂 v3 + -26 * (v3 / 26) 实际上就是 v3%26

由于循环中 v5 = a4890572163qwe[v3 + -26 * (v3 / 26)]; 在不停除以 26,则实际上是将 v3 转换成一个 26 进制的数,这个数的每一位作为 a4890572163qwe 字符串的索引取值放进 v4 这个字符串中,所以我们得先求 v4, 由于目前不知道我们需要输入的字符串我们接着往下看。

v2 代表的是转换为 26 进制之后的位数

下面的循环:

由于 v8 和 v9 都是 char 类型,所以 v8+1 就是 v9,所以 v7 = *(&v8 + v6--);

v7 为 v3 的逆序的每一个字符

查看字符串 a4890572163qwe:

接下来看 sub_1400011E0:

sub_1400011E0 (v7 ^ 7); 该函数先将每个字符与 7 进行异或作为函数的参数

_QWORD *__fastcall sub_1400011E0(char a1)
{
  _QWORD *result; // rax
  __int64 v3; // rdx
  result = malloc(0x10u);
  v3 = qword_140004618;
  qword_140004618 = (__int64)result;
  *(_QWORD *)(v3 + 8) = result;
  *(_BYTE *)v3 = a1;
  result[1] = 0;
  return result;
}

这里就是把与 7 异或完的字符放进数组里组成完整的字符串。

这里要留意下数组的地址是 qword_140004618,接着返回主函数,继续分析下一个函数

void __noreturn sub_140001220()
{
  __int64 v0; // r9
  int v1; // ecx
  __int64 v2; // rdx
  char v3; // al
  int v4; // r8d
  __int64 v5; // r9
  char v6; // cl
  int v7; // eax
  v0 = qword_140004620;
  v1 = 0;
  v2 = 0;
  while ( 1 )
  {
    v3 = *(_BYTE *)v0;
    v4 = v1 + 1;
    v5 = *(_QWORD *)(v0 + 8);
    if ( v3 != aV4pY59[v2] )
      v4 = v1;
    qword_140004620 = v5;
    if ( !v5 )
      break;
    v6 = *(_BYTE *)v5;
    v7 = v4 + 1;
    v0 = *(_QWORD *)(v5 + 8);
    if ( v6 != aV4pY59[v2 + 1] )
      v7 = v4;
    qword_140004620 = v0;
    if ( v0 )
    {
      v2 += 2;
      v1 = v7;
      if ( v2 < 14 )
        continue;
    }
    goto LABEL_11;
  }
  v7 = v4;
LABEL_11:
  if ( v7 == 14 )
    sub_1400012E0();
  sub_1400012B0();
}

这里就是从我们刚才的字符串数组开始与字符串 aV4pY59 进行比较,我们先获得该字符串:

最终就是逐位比较这 14 位的字符串如果比较成功那么 v7 刚好等于 14 则执行 12E0 函数会给出 flag。

接下来编写脚本,获取我们要输入的字符串:

  1. 首先根据密文 /…v4p$$!>Y59-,每一位与 7 异或回退到进入函数前:

    target_ciphertext = "/..v4p$$!>Y59-"
    decrypted_list = [chr(ord(c) ^ 7) for c in target_ciphertext]
    decrypted_string = "".join(decrypted_list)
    print(f"目标密文是: {target_ciphertext}")
    print(f"解密后的字符串是: {decrypted_string}")

  2. 根据得到的解密后的字符串 ()) q3w##&9^2>* 继续逆推:

    1. ())q3w##&9^2>*
    2. 找到 ()) q3w##&9^2> 每个字符在原本字符 (")(&^%489$!057@#><:2163qwe") 的索引
    3. 最后将获取到的索引值转换成十进制
    alphabet = ")(*&^%489$!057@#><:2163qwe"
    base26_string = "())q3w##&9^2>*"
    final_number = 0
    for char in base26_string:
      # 找到字符在字母表中的位置(索引)
      digit_value = alphabet.find(char)
      if digit_value == -1:
        print(f"错误: 字符 '{char}' 不在字母表中!")
        break
      # 核心转换算法
      final_number = final_number * 26 + digit_value
    print(f"最终的密码 (输入数字) 是: {final_number}")

    输入的密钥

# Re5

先直接说结果:

把这里原本的 jnz 语句改成两个 nop 即可;

还是分析 main 函数:

sub_4020B0 () 是做一些静态初始化

sub_4015BD () 函数

这里会比较 Str 这个字符串是不是 1,如果是的话执行下面的函数 不是的话就退出

我们先看一下 Str 的值,如下图,很显然第二位不是 1。

Sub_401520 函数这里会调用一个 1.dll 中的 H 函数,逐位进行加 / 解密

最后进行输出

所以 有两种方法:

一个是修改字符串的值,另一个是修改汇编语句

CMP 指令之后的那条条件跳转指令,例如 JNZ short loc_XXXXXX (如果不相等就跳转)。

用 NOP 替换指令: NOP (No Operation, 空操作) 是一个 CPU 指令,它什么也不做,只会让程序执行下一条指令。我们可以用它来 “抹掉” 不想要的指令。

由于一个 jnz 指令的长度是两个字节,而 NOP 指令的长度只有一个字节,必须使用两个 1 字节的指令去填满原来两字节的空间,最后得到如下

变成

覆盖之后,patch 进文件即可 之后打开终端运行软件命令可以查看到结果

修改后的4015BD函数

运行结果

# 武穆遗书

  1. 使用 die 工具检测软件为 upx 打包方式(加壳),需要去壳

  1. 使用 x64dbg 打开软件,先执行 F9 进入 ep,但这里只是壳的 ep 我们需要找到 oep;

  1. 由于是 upx 打包,可以看到第一句是 pushad,则找到对应 popad 就能找到源程序的 oep
    • Ctrl f 搜索 popad

  1. 找到 popad 之后,早第一个 jmp 语句,即是源程序的 oep,打断点之后执行到 oep

  • 进入 403d2c 并选中

  • 使用 scylla 进行脱壳,得到脱壳后的文件

之后使用 IDA 分析脱壳后的文件,前面做了一些反调试和虚拟机检测的操作我们可以直接 nop 掉,使用 x32dbg。

首先通过 IDA 得到这四个初始函数的位置,使用 x32dbg 通过 ctrl g 跳转到这个地址,将这四个函数全部 nop 掉:选中 cll=> 指令点击 Space=> 改成 nop(下面选择剩余字节用 nop 填充)

在这些 nop 的地方放置断点,使用 f9 运行到这里后一步一步调试,接下来是 je 指令,默认不会跳转,我们需要修改 zf 标志位改成 1 跳转:

注意:一定是跳转 不能直接把 je 指令 nop 掉除非你把下面的三条指令全部 nop 掉直接执行跳转的位置

跳转之后,继续执行,直到执行完 401390 函数,就可以直接看到 flag,之后继续执行就是输入和 flag 相同的字符即可。

# Re7

下载文件后是个 jar 包,先提取 jar 包中的文件

jar -xf EzJar.jar

得到两个文件,其中一个 Ez.java

按照上面的思路,发现 flag 不对

之后尝试解析这个下载出来的 jar 包,解析脚本如下:

import zipfile
import os
import sys
import datetime
import argparse
def analyze_jar(filepath):
    """
    Analyzes the structure of a JAR/ZIP file, showing offsets and appended data.
    """
    if not os.path.exists(filepath):
        print(f"错误: 文件不存在 '{filepath}'")
        return
    print(f"--- 开始分析: {os.path.basename(filepath)} ---")
    print(f"文件总大小: {os.path.getsize(filepath):,} 字节")
    print("-" * 40)
    try:
        with zipfile.ZipFile(filepath, 'r') as zf:
            print("[+] 文件列表和内部结构:\n")
            infolist = zf.infolist()
            if not infolist:
                print("  -> 此JAR包为空或无法识别内部文件。")
            for info in infolist:
                # ZipInfo 对象包含了丰富的元数据
                header_offset = info.header_offset
                filename = info.filename
                # 计算压缩数据的真实起始偏移量
                # 公式:数据偏移量 = 文件头偏移量 + 30 (固定头大小) + 文件名长度 + 额外字段长度
                data_offset = header_offset + 30 + len(filename.encode('utf-8')) + len(info.extra)
                print(f"  文件名: {filename}")
                print(f"    ├─ 修改时间: {datetime.datetime(*info.date_time)}")
                print(f"    ├─ 文件头偏移量 (Header Offset): {header_offset:<10} (0x{header_offset:08X})")
                print(f"    ├─ 数据偏移量 (Data Offset):   {data_offset:<10} (0x{data_offset:08X})")
                print(f"    ├─ 压缩后大小: {info.compress_size:,} 字节")
                print(f"    ├─ 原始大小:   {info.file_size:,} 字节")
                print(f"    └─ CRC校验码:  0x{info.CRC:08X}\n")
    except zipfile.BadZipFile as e:
        print(f"[!] 严重错误: {e}")
        print("    -> 文件不是一个有效的JAR/ZIP文件,或者已经严重损坏。")
    except Exception as e:
        print(f"[!] 发生未知错误: {e}")
    # --- 手动检查文件末尾的附加数据 ---
    print("[+] 附加数据检查:")
    with open(filepath, 'rb') as f:
        # ZIP 文件的结尾记录(EOCD)标志是 PK\x05\x06
        eocd_signature = b'\x50\x4B\x05\x06'
        # 从文件末尾开始搜索,效率更高
        f.seek(0, os.SEEK_END)
        filesize = f.tell()
        # 我们只在文件末尾一小块区域内搜索,以防万一
        read_buffer = max(0, filesize - 65536)
        f.seek(read_buffer)
        data = f.read()
        eocd_pos_in_data = data.rfind(eocd_signature)
        if eocd_pos_in_data == -1:
            print("  -> 未找到标准的ZIP文件结尾记录 (EOCD)。")
            print("     这证实了 'zip END header not found' 错误。文件可能不完整。")
        else:
            eocd_abs_pos = read_buffer + eocd_pos_in_data
            # EOCD 记录中,注释长度字段在偏移量 20 处,占 2 字节
            comment_len_bytes = data[eocd_pos_in_data + 20: eocd_pos_in_data + 22]
            comment_len = int.from_bytes(comment_len_bytes, 'little')
            # ZIP 文件的理论结束位置
            expected_end = eocd_abs_pos + 22 + comment_len
            print(f"  -> 找到文件结尾记录 (EOCD) 在偏移量: {eocd_abs_pos}")
            print(f"  -> ZIP结构理论结束位置: {expected_end}")
            if filesize > expected_end:
                appended_size = filesize - expected_end
                print(f"  -> [!] 警告: 文件末尾发现 {appended_size:,} 字节的附加数据!")
            else:
                print("  -> 正常: 未发现附加数据。")
    print("-" * 40)
    print("--- 分析结束 ---")
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="分析一个JAR/ZIP文件的内部结构、偏移量和附加数据。")
    parser.add_argument("jarfile", help="需要分析的JAR文件路径")
    args = parser.parse_args()
    analyze_jar(args.jarfile)

可以看到一个异常 EzJar.class/ 这个文件没被解压出来而且大小是 6000 字节,很显然有问题,可尝试分离 jar 包:

import zipfile
import argparse
import os
def separate_jar_components_v2(source_jar_path):
    OBFUSCATED_SOURCE_NAME = 'EzJar.class/EzJar.java'
    OBFUSCATED_BYTECODE_NAME = 'EzJar.class/'
    OUTPUT_SOURCE_JAR = 'source_code.jar'
    OUTPUT_BYTECODE_JAR = 'compiled_bytecode.jar'
    CLEAN_SOURCE_NAME = 'EzJar.java'
    CLEAN_BYTECODE_NAME = 'EzJar.class'
    # 标准清单文件的内容
    # MANIFEST_CONTENT = "Manifest-Version: 1.0\n\n"
    MANIFEST_CONTENT = "Manifest-Version: 1.0\nMain-Class: EzJar\n\n"
    MANIFEST_PATH = "META-INF/MANIFEST.MF"
    if not os.path.exists(source_jar_path):
        print(f"错误: 源文件不存在 '{source_jar_path}'")
        return
    source_data = None
    bytecode_data = None
    print(f"--- 正在分析 '{source_jar_path}' ---")
    try:
        with zipfile.ZipFile(source_jar_path, 'r') as zf:
            for info in zf.infolist():
                if info.filename == OBFUSCATED_SOURCE_NAME:
                    source_data = zf.read(info)
                elif info.filename == OBFUSCATED_BYTECODE_NAME:
                    bytecode_data = zf.read(info)
        # 创建源代码 JAR
        if source_data:
            print(f"正在创建标准源代码包 -> '{OUTPUT_SOURCE_JAR}'...")
            with zipfile.ZipFile(OUTPUT_SOURCE_JAR, 'w', compression=zipfile.ZIP_DEFLATED) as zf_source:
                # 添加清单文件
                zf_source.writestr(MANIFEST_PATH, MANIFEST_CONTENT)
                # 添加源代码文件
                zf_source.writestr(CLEAN_SOURCE_NAME, source_data)
            print(f" -> 成功创建 '{OUTPUT_SOURCE_JAR}'")
        # 创建字节码 JAR
        if bytecode_data:
            print(f"正在创建标准字节码包 -> '{OUTPUT_BYTECODE_JAR}'...")
            with zipfile.ZipFile(OUTPUT_BYTECODE_JAR, 'w', compression=zipfile.ZIP_DEFLATED) as zf_bytecode:
                # 添加清单文件
                zf_bytecode.writestr(MANIFEST_PATH, MANIFEST_CONTENT)
                # 添加字节码文件
                zf_bytecode.writestr(CLEAN_BYTECODE_NAME, bytecode_data)
            print(f" -> 成功创建 '{OUTPUT_BYTECODE_JAR}'")
        print("\n--- 分离完成 ---")
    except Exception as e:
        print(f"[!] 处理过程中发生错误: {e}")
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="将一个混淆的JAR文件分离成两个包含标准清单的、干净的JAR文件。")
    parser.add_argument("source_jar", help="原始的、被混淆的JAR文件路径。")
    args = parser.parse_args()
    separate_jar_components_v2(args.source_jar)

compiled_bytecode.jar 是我们分离的 jar 包,接下来使用 jd 或者直接在 idea 反编译这个 jar 包,可以直接读到伪代码

根据代码逻辑写一个复现脚本,找到密文,密钥之后解密:

import base64
import hashlib
from Crypto.Cipher import DES, Blowfish
from Crypto.Util.Padding import unpad
# 全局变量,用于存储 Java 代码中的 l 和 I 数组
l = [0] * 18
I = [""] * 17
def lII():
    """复刻Java的lII()方法,计算出l[]数组的值"""
    global l
    l[0] = (156 ^ 136) & ~(127 ^ 107)
    l[1] = 1
    l[2] = 3
    l[3] = 2
    l[4] = 185 ^ 197 ^ 32 ^ 88
    l[5] = 1 ^ 28 ^ 24
    l[6] = 16 ^ 22
    l[7] = 69 ^ 77 ^ 70 ^ 73
    l[8] = (106 + 107 - 66 + 32) ^ (33 + 90 - 98 + 162)
    l[9] = (154 + 21 - 48 + 38) ^ (78 + 146 - 163 + 111)
    l[10] = 29 ^ 23
    l[11] = 64 ^ 75
    l[12] = 105 ^ 101
    l[13] = 2 ^ 106 ^ 101
    l[14] = 72 ^ 70
    l[15] = 144 ^ 159
    l[16] = 160 ^ 176
    l[17] = 177 ^ 160
def decrypt_l(b64_str, key_str):
    """复刻Java的l()方法 (XOR解密)"""
    decoded_bytes = base64.b64decode(b64_str)
    key_bytes = key_str.encode('utf-8')
    result = bytearray()
    for i in range(len(decoded_bytes)):
        result.append(decoded_bytes[i] ^ key_bytes[i % len(key_bytes)])
    return result.decode('utf-8')
def decrypt_lI(b64_str, key_str):
    """复刻Java的lI()方法 (Blowfish解密)"""
    key = hashlib.md5(key_str.encode('utf-8')).digest()
    cipher = Blowfish.new(key, Blowfish.MODE_ECB)
    decoded_bytes = base64.b64decode(b64_str)
    return unpad(cipher.decrypt(decoded_bytes), Blowfish.block_size).decode('utf-8')
def decrypt_I(b64_str, key_str):
    """复刻Java的I()方法 (DES解密)"""
    # Java 的 Arrays.copyOf (..., 8) 相当于取前 8 个字节
    key = hashlib.md5(key_str.encode('utf-8')).digest()[:8]
    cipher = DES.new(key, DES.MODE_ECB)
    decoded_bytes = base64.b64decode(b64_str)
    return unpad(cipher.decrypt(decoded_bytes), DES.block_size).decode('utf-8')
def ll():
    """复刻Java的ll()方法,解密并填充I[]数组"""
    global I, l
    I[l[0]] = decrypt_I("AQiA0bYffm9HvMlm7RnEMX/tEQAUj4Xb", "FrlAZ")
    I[l[1]] = decrypt_I("hXzZyx8IUHw=", "Esxsh")
    I[l[3]] = decrypt_l("ID8PFlEKM1kKHhIkWRUdBjFD", "gVysq")
    I[l[2]] = decrypt_I("50fO6ARqllg=", "VZbFF")
    I[l[4]] = decrypt_lI("mvXqH+/XIESPZaSG3ZbZlA==", "TuZSw")
    I[l[5]] = decrypt_l("JQ0R", "aHBFu")
    I[l[6]] = decrypt_I(
        "dMKiRQ19iTevvzL7NtVg5+ye5BywL2QaxtVANFLuC5B2/KuC+/5L6BwtCB7zpWK1XBTQr0VWC3Vt/uYEl2xmjskE0dDrCk2C", "dPxYA")
    I[l[7]] = decrypt_lI("B/MVYKSzgq8=", "phiUP")
    I[l[8]] = decrypt_I("ZtBOhuHeK3Y=", "MfnkQ")
    I[l[9]] = decrypt_lI("aPhz+GjGynRlU3Alo00QeQ==", "wWtUj")
    I[l[10]] = decrypt_l("BRQFNiRyBwQrNDcUSw==", "RfjXC")
    I[l[11]] = decrypt_I("pT10j0lChvyrNwYRFdqBzxqFp1ruTgo9", "hGcuT")
    I[l[12]] = decrypt_lI(
        "UhBbCDk5yqaWl1uHJyS/OGmtcfVyvOOsk78/1f0MU8U3UfAf1Xf0FWNbpcKes/0HRz9SU/icRJHswW2xWjHrcFzhpsvwzqUl", "eeMoV")
    I[l[13]] = decrypt_I("s11BihYBzRBpcX9EF43utw==", "RjiXK")
    I[l[14]] = decrypt_I("wk5jH1cyKoA=", "frsxP")
    I[l[15]] = decrypt_I("Nfa0rxB8IRArMq2F4iLlLg==", "ulQWJ")
    I[l[16]] = decrypt_lI("wLcWNd0Xsbw=", "JgPGn")
def final_crack():
    """执行最终的核心破解逻辑"""
    print("[+] 正在复刻Java内部的解密过程...")
    # 步骤一:初始化常量数组 l 和 I
    lII()
    ll()
    print("[+] 已成功解密并重建所有隐藏字符串。")
    # 步骤二:从重建的数组中获取关键信息
    # main 方法中的密钥是 I [l [4]],l [4] 的值是 4
    main_logic_key_str = I[l[4]]
    # main 方法中的目标密文是 I [l [6]],l [6] 的值是 6
    main_logic_ciphertext_b64 = I[l[6]]
    print(f"    -> 破解出的DES密钥: {main_logic_key_str}")
    print(f"    -> 破解出的目标密文: {main_logic_ciphertext_b64[:20]}...")
    # 步骤三:执行最终的 DES 解密
    print("[+] 正在执行最终解密...")
    key_bytes = main_logic_key_str.encode('utf-8')
    ciphertext_bytes = base64.b64decode(main_logic_ciphertext_b64)
    cipher = DES.new(key_bytes, DES.MODE_ECB)
    decrypted_padded_bytes = cipher.decrypt(ciphertext_bytes)
    # 移除填充
    flag = unpad(decrypted_padded_bytes, DES.block_size).decode('utf-8')
    print("-" * 50)
    print(f"[*] 最终Flag: {flag}")
    print("-" * 50)
if __name__ == '__main__':
    final_crack()

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

尘落 微信支付

微信支付

尘落 支付宝

支付宝

尘落 贝宝

贝宝