【wp】2025强网杯记录
逆向
butterfly
一共给出有三个文件、butterfly、encode.dat、encode.dat.key

首先是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字符串

接下来是确定加密的key

这里可以看见之后被作为key的v15是由v14的一部分,由于v15的产犊位qword,而原先类型位_m128i,所以最后的v15指向的值未MMXEncod
最关键的加密部分,首先我们可以看结束循环的条件,第一个条件,其中(len-1)>>3 我们可以转化成
(len - 1) // 8
为什么是这个值呢?因为chunk v11的长度为8,而如果输入的长度正好是8的倍数时,len应该是输入长度+1的,此时运算得到的结果正好是下标加1,此地址也恰好是储存长度处(结束地址),与当前地址比较,如果一致则结束。
而下面while里同理.

而加密的过程,其实就是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,中就不同了。这里详细来说一下。
假设有这样的两个块

_m_paddb(, )处理后得到的是

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

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

也就是我们之前说的,根据类型一次进行那个操作。
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