逆向

butterfly

一共给出有三个文件、butterfly、encode.dat、encode.dat.key

image-20251022161400149

首先是butterfly的逻辑,将其中的符号恢复部分后,可以得到

__int64 __fastcall sub_4018D0(int n3, const char **a2)
{
  const char *input_file; // r14
  const char *output_file; // r13
  __int64 fd; // rax
  __int64 fd_1; // r12
  __int64 len; // rbx
  __int64 chunk; // rax
  __m64 *_m64_chunk; // rbp
  __int64 read_len; // r15
  __m64 *v11; // rax
  __m64 v12; // mm0
  __m64 v13; // mm2
  _OWORD v14[2]; // [rsp+0h] [rbp-158h] BYREF
  unsigned int v15[2]; // [rsp+20h] [rbp-138h] BYREF

  if ( n3 != 3 )
  {
    print(a1: 2, a2: (__int64)"Usage: %s <input_file> <output_file>\n", *a2);
    print(a1: 2, a2: (__int64)"Example: %s plaintext.txt encoded.dat\n", *a2);
    return 1;
  }
  input_file = a2[1];
  output_file = a2[2];
  fd = open(input_file, rb: "rb");
  fd_1 = fd;
  if ( !fd )
  {
    print(a1: 2, a2: (__int64)"Error: Cannot open file %s\n", input_file);
    return 1;
  }
  sub_407480(fd, 0, n2: 2);
  len = sub_405640(fd_1);
  sub_407480(fd_1, 0, n2: 0);
  chunk = malloc(len + 8);
  _m64_chunk = (__m64 *)chunk;
  if ( !chunk )
  {
    close(fd_1);
    eprint(Error:_Memory_allocation_failed: "Error: Memory allocation failed");
    return 1;
  }
  read_len = readfile(chunk, len + 8, 1, n7: len, fd_1);
  close(fd_1);
  if ( len != read_len )
  {
    free(_m64_chunk);
    eprint(Error:_Memory_allocation_failed: "Error: File read failed");
    return 1;
  }
  *(__int16 *)((char *)_m64_chunk->m64_i16 + len) = len;
  v14[0] = _mm_loadu_si128((const __m128i *)"MMXEncode2024");
  v14[1] = _mm_loadu_si128((const __m128i *)"coding file: %s\n");
  print(a1: 2, a2: (__int64)"Encoding file: %s\n", input_file);
  print(a1: 2, a2: (__int64)"Original size: %zu bytes\n", len);
  *(_QWORD *)v15 = *(_QWORD *)&v14[0];
  if ( (int)len > 7 )
  {
    v11 = _m64_chunk;
    do
    {
      v12 = _m_pxor((__m64)v11->m64_u64, *(__m64 *)v15);
      v13 = _m_por(_m_psrlwi(v12, 8u), _m_psllwi(v12, 8u));
      v11->m64_u64 = (unsigned __int64)_m_paddb(_m_por(_m_psllqi(v13, 1u), _m_psrlqi(v13, 0x3Fu)), *(__m64 *)v15);
      if ( &_m64_chunk[(unsigned int)(len - 1) >> 3] == v11 )
        break;
      ++v11;
    }
    while ( &_m64_chunk[((unsigned int)(len - 8) >> 3) + 1] != v11 );
  }
  _m_empty();
  if ( (unsigned int)sub_401CA0(output_file, _m64_chunk, n32: len) )
  {
    print(a1: 2, a2: (__int64)"Successfully encoded to: %s\n", output_file);
    print(a1: 2, a2: (__int64)"Encoded size: %zu bytes\n", len);
    sub_4777A0((unsigned int)v15, n256: 256, n2: 2, n256: 256, %s.key: (unsigned int)"%s.key", (_DWORD)output_file, v14[0]);
    if ( (unsigned int)sub_401CA0(v15, v14, n32: 32) )
      print(a1: 2, a2: (__int64)"Key saved to: %s\n", (const char *)v15);
  }
  free(_m64_chunk);
  return 0;
}

我们来分布解析这几个部分,首先是读取信息部分

这里仅解密完整的8字节块;尾部不足8字节按原样保留(加密端未处理尾部)将其中的数据读取,并将字符串的长度替换末尾0字符串

image-20251022174747365

接下来是确定加密的key

image-20251022175134174

这里可以看见之后被作为key的v15是由v14的一部分,由于v15的产犊位qword,而原先类型位_m128i,所以最后的v15指向的值未MMXEncod

最关键的加密部分,首先我们可以看结束循环的条件,第一个条件,其中(len-1)>>3 我们可以转化成

(len - 1) // 8

为什么是这个值呢?因为chunk v11的长度为8,而如果输入的长度正好是8的倍数时,len应该是输入长度+1的,此时运算得到的结果正好是下标加1,此地址也恰好是储存长度处(结束地址),与当前地址比较,如果一致则结束。

而下面while里同理.

image-20251022175628115

而加密的过程,其实就是simd的部分了

# 根据IDA Pro分析的加密逻辑,逆向操作:
# 加密过程:
# 1. 与密钥XOR: v17 = _m_pxor(data, key)
# 2. 字节交换: v18 = _m_por(_m_psrlwi(v17, 8), _m_psllwi(v17, 8))
# 3. 循环左移1位: _m_por(_m_psllqi(v18, 1), _m_psrlqi(v18, 63))
# 4. 与密钥相加: _m_paddb(result, key)

注意,simd的操作不是把当前的_mx当做一个整体来操作的,而是根据你的定义,把他们作为你设置的类型依次操作。在1,4的时候两者是没有区别的,但在2,中就不同了。这里详细来说一下。

假设有这样的两个块

image-20251022182040694

_m_paddb(, )处理后得到的是

image-20251022182502114

这种情况下我们可以发现,是否把_mx看作某种类型的组合与结果无关,但如果是左移右移则不同了,这里以第2个向量块举例_m_psrlwi

如果不看做拥有类型,结果是这样的

image-20251022183011820

但是这是错误的,因为_m_psrlwi是以i16作为块,所以结果应该是这样的

image-20251022183234420

也就是我们之前说的,根据类型一次进行那个操作。

ok,讲完这些细节我们再去看上面描述的加密其实就很简单了,就无非是把原先单个加密改为了把多个数值捆绑在一起,一起处理加密,大幅度提高速度。

poc

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MMXEncode2024 解密函数
根据IDA Pro反编译的加密算法实现对应的解密逻辑
"""

import struct
import os
import sys


def decrypt_data(encrypted_data, key):
    """
    解密数据

    Args:
        encrypted_data: 加密的数据
        key: 解密密钥 (8字节)

    Returns:
        解密后的数据
    """
    if len(key) != 8:
        raise ValueError("密钥长度必须为8字节")

    # 仅解密完整的8字节块;尾部不足8字节按原样保留(加密端未处理尾部)
    data_len = len(encrypted_data)
    full_len = (data_len // 8) * 8

    decrypted_data = bytearray()

    # 按8字节块处理
    for i in range(0, full_len, 8):
        block = encrypted_data[i : i + 8]

        # 解密单个8字节块
        decrypted_block = decrypt_block(block, key)
        decrypted_data.extend(decrypted_block)

    # 追加尾部未加密的剩余字节
    if full_len < data_len:
        decrypted_data.extend(encrypted_data[full_len:])

    return bytes(decrypted_data)


def decrypt_block(encrypted_block, key):
    """
    解密单个8字节块

    # 根据IDA Pro分析的加密逻辑,逆向操作:
    # 加密过程:
    # 1. 与密钥XOR: v17 = _m_pxor(data, key)
    # 2. 字节交换: v18 = _m_por(_m_psrlwi(v17, 8), _m_psllwi(v17, 8))
    # 3. 循环左移1位: _m_por(_m_psllqi(v18, 1), _m_psrlqi(v18, 63))
    # 4. 与密钥相加: _m_paddb(result, key)

    解密过程(逆向):
    1. 与密钥相减(逆向相加)
    2. 循环右移1位(逆向左移)
    3. 字节交换(逆向)
    4. 与密钥XOR(逆向)
    """
    # 将8字节转换为64位整数
    encrypted_value = struct.unpack("<Q", encrypted_block)[0]
    key_value = struct.unpack("<Q", key)[0]

    # 步骤1: 与密钥相减(逆向的_m_paddb)
    # _m_paddb是字节级加法,需要逐字节处理
    temp = 0
    for i in range(8):
        encrypted_byte = (encrypted_value >> (i * 8)) & 0xFF
        key_byte = (key_value >> (i * 8)) & 0xFF
        # 逆向字节加法(模256减法)
        decrypted_byte = (encrypted_byte - key_byte) & 0xFF
        temp |= decrypted_byte << (i * 8)

    # 步骤2: 循环右移1位(逆向的循环左移1位)
    temp = ((temp >> 1) | (temp << 63)) & 0xFFFFFFFFFFFFFFFF

    # 步骤3: 字节交换(按16位lane内交换高低字节)
    # 加密使用 _m_psrlwi/_m_psllwi 8 位并或,等价于对每个16位内交换字节;其逆操作同样是自身
    temp = (
        ((temp & 0x00FF00FF00FF00FF) << 8) | ((temp & 0xFF00FF00FF00FF00) >> 8)
    ) & 0xFFFFFFFFFFFFFFFF

    # 步骤4: 与密钥XOR(逆向)
    decrypted_value = temp ^ key_value

    # 转换回字节
    return struct.pack("<Q", decrypted_value)


def decrypt_file(encrypted_file, output_file, key_file=None):
    """
    解密文件

    Args:
        encrypted_file: 加密文件路径
        output_file: 输出文件路径
        key_file: 密钥文件路径(可选)
    """
    # 读取加密文件
    with open(encrypted_file, "rb") as f:
        encrypted_data = f.read()

    # 获取密钥
    if key_file and os.path.exists(key_file):
        with open(key_file, "rb") as f:
            key_data = f.read(8)  # 只读取前8字节
    else:
        # 使用默认密钥
        key_data = b"MMXEncode2024"[:8]

    # 解密数据
    decrypted_data = decrypt_data(encrypted_data, key_data)

    # 说明:加密程序写入的输出长度即为原始长度,未包含末尾2字节大小信息

    # 保存解密后的文件
    with open(output_file, "wb") as f:
        f.write(decrypted_data)

    print(f"解密完成: {encrypted_file} -> {output_file}")
    print(f"原始大小: {len(decrypted_data)} 字节")


def main():
    """主函数"""
    if len(sys.argv) < 3:
        print("用法: python decrypt.py <加密文件> <输出文件> [密钥文件]")
        print("示例: python decrypt.py encoded.dat decrypted.txt encoded.key")
        sys.exit(1)

    encrypted_file = sys.argv[1]
    output_file = sys.argv[2]
    key_file = sys.argv[3] if len(sys.argv) > 3 else None

    if not os.path.exists(encrypted_file):
        print(f"错误: 加密文件 '{encrypted_file}' 不存在")
        sys.exit(1)

    try:
        decrypt_file(encrypted_file, output_file, key_file)
    except Exception as e:
        print(f"解密失败: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()

tradere

这个是个很好的虚拟机逆向,具体的图如下

┌─────────────────┐    ┌─────────────────┐
│   Main Process  │    │  Child Process  │
│                 │    │                 │
│  ┌───────────┐  │    │  ┌───────────┐  │
│  │  Father   │  │◄──►│  │    Son    │  │
│  │(Debugger) │  │    │  │(Target)   │  │
│  └───────────┘  │    │  └───────────┘  │
│                 │    │                 │
│  ┌───────────┐  │    │  ┌───────────┐  │
│  │    VM     │  │    │  │sub_4009F7 │  │
│  │Interpreter│  │    │  │(Main Prog)│  │
│  └───────────┘  │    │  └───────────┘  │
└─────────────────┘    └─────────────────┘

我们看zu