爱飞的猫 发表于 2025-5-11 21:36

解包《雅致 Flash打包器 (AGE Flash Packer)》生成的可执行文件

## 前因

起因是在论坛收到一则求助:

> 求助关于雅致Flash播放器(AGE FlashPacker)的数据解密问题
> https://www.52pojie.cn/thread-2030470-1-1.html

文中给出了一个样本文件:

- [蓝奏网盘](https://wwio.lanzoub.com/iPVRq2vvpedc)
- [百度网盘](https://pan.baidu.com/s/1kI9o5n8ARTx2--OXTreJnw?pwd=52pj)(备份)

下面是对应的 SHA256 校验值:

```text
9d139494eba8891846a0ce961c1f3235c4eefb1d9d04018da2a585e681af7744 *ebook.exe
```

## 观察文件

先拿十六进制编辑器看看文件吧。

### 文件末尾

在文件的末尾可以看到类似这样的内容:

```yaml
文件偏移: 0x014fd711

字符串:
长度: 0D 00 # =0x0d, 13
内容: "untitled1.swf"

未知内容: # 未知 1
20 03 00 00 58 02 00 00 00 00 00 00 00 01

签名信息:
"AGE Flash Player"

未知内容:
未知2: 11 D7 4F 01 # =0x014fd711
未知3: 00 20 0C 00
未知4: 80 95 2D 00
未知5: 3E D3 4F 01 # =0x014fd33e
未知6: 43 00 00 00 # =0x43
```

其中,未知内容块有一些看起来像是在引用文件结尾的数据。

- 「未知 2」看起来指向这段元信息开始处
- 「未知 5」看起来是某个数据的偏移

其它未知内容信息不足以判定到底在干什么,因此看看「未知 5」的内容:

```text
14F:D33E78 01 6D 94                                    x.m”
```

这个 `78 01` 看起来就是 zlib 压缩后的数据。虽然不清楚具体数据有多大,保险起见将元数据前的内容全部一起解压看看:

```py
# 安装依赖: pip install hexdump2
from hexdump2 import hexdump
import os
import zlib

with open("ebook.exe", "rb") as f:
    f.seek(0x014FD33E, os.SEEK_SET)
    data = f.read(0x014FD711 - 0x014FD33E) # 不填这个参数读到结尾也可以
data = zlib.decompress(data)
hexdump(data)# 或直接 print(data) 查看
```

可以看到这样的数据:

```text
0000000081 2c 25 00 21 16 66 0000 90 01 00 0d 00 75 6e|.,%.!.f.......un|
0000001074 69 74 6c 65 64 31 2e73 77 66 a2 42 8b 00 66|titled1.swf.B..f|
0000002069 17 00 00 90 01 00 0e00 73 72 63 5c 62 67 6d|i........src\bgm|
000000305c 30 31 2e 6d 70 33 08ac a2 00 42 60 19 00 00|\01.mp3....B`...|

                            ... 省略 ...

000007d0dc 00 00 0d 00 73 72 635c 69 6d 67 5c 63 2e 6a|.....src\img\c.j|
000007e070 67                                             |pg|
000007e2
```

看起来像是「打包」的文件清单以及一些基本信息。

### 文件清单

这个格式相对比较简单,大概如下:

```yaml
地址:81 2c 25 00# =0x00252c81
未知1: 21 16 66 00# =0x00661621
未知2: 00 90 01 00# =0x00019000

字符串:
长度: 0d 00 (=0x0d, 13)
内容: "untitled1.swf"
```

后面的内容都是重复该格式直到结束。

你可能注意到上面有个未知内容,这个稍后会提到。

偏移 `0x00252c81` 处的数据大概是这样:

```text
025:2C8105 00 00 00 96 DA 9D EE 4D F8 A8 80 B0 B6 6E B9....–Ú.îMø¨?°¶n¹
025:2C91BE A4 C9 A5 05 3A 6F 2C 29 78 33 F1 9C 38 4B 0D¾¤É¥.:o,)x3ñœ8K.
025:2CA1DA EB 45 0B 35 8F 27 87 80 C3 70 AA E5 F5 79 5DÚëE.5.'‡?Ãpªåõy]

                            ... 省略 ...

```

这个文件是 `swf` 文件,但是这附近却看不到 Flash 文件的特征(`"CWS"` 或 `"FWS"`)。文件被加密了呢。

### 解密算法?

之前的内容还能全靠猜,但如果数据加密了,光靠猜是猜不出结果的。

原帖提及了一个处理文件解密的函数入口:`00498B80`;以此为切入点,使用 IDA 分析吧。

有些 Delphi 的内置函数被 IDA 利用 识别出来了,反编译的代码还是相对比较好理解的。

: https://docs.hex-rays.com/user-guide/signatures/flirt

但是注意:虚表调用/结构体缺少数据,这部分只能手动分析然后补上。

将 `00498B80` 反编译,然后稍作整理:

```cpp
using Classes::TList;

bool __fastcall ServeRequest_498B80(
TRequest *req,
int uri,
TWriterStream *res
) {
bool handled = FALSE;
if ( uri ) {
    int index = FindFileIndexByURI(req, uri);

    // -1 表示文件不存在
    if ( index >= 0 ) {
      TFileEntry * file = (TFileEntry *)TList::Get(req->files, index);
      req->reader->vtb->Seek(req->reader, file->offset, SEEK_SET);
      if ( file->enc_size <= 0 )
      Send_498954(req, res, file->size, NULL);
      else
      DecryptSend_4989A0(req, &res, file->size, file->enc_size);
      handled = TRUE;
    }
}
return handled;
}
```

读者如果有写过服务器代码,会发现这里的代码与响应请求的结构非常相似。

再结合动态调试,可以发现执行 `DecryptSend_4989A0` 函数后会产生解密后的数据:

```x86asm
loc_00498BDE:
push eax                      ; 参4: 加密大小
lea edx,dword ptr ss:; 参2: 响应
mov ecx,dword ptr ds:; 参3: 大小
mov eax,ebx                   ; 参1: 请求
call ebook.4989A0             ; 调用函数 DecryptSend_4989A0
jmp ebook.498BFD            ; [+4] => 这里是解密后的数据
```

因此继续深入 `DecryptSend_4989A0` 的解密逻辑,可以发现这里又调用了一堆其它函数。

单纯静态分析调用的函数会比较困难,带着调试器跟一下数据会方便很多。

例如 `CipherFactory_493274` 是从已注册的块加密列表中找到对应的值:

```c
TCipher *__fastcall CipherFactory_493274(int cipher_id) {
LinkCipher *cipher; // ebx

for ( cipher = g_Ciphers_4A4EF8; cipher; cipher = cipher->next ) {
    if (cipher_id == cipher->id) {
      return cipher->methods->Create(); // Blowfish: 00493090
    }
}
return NULL;
}
```

在调试器跟踪的话比较容易知道有什么内容:


(查询代表 `0x05` 的 Blowfish 算法,可以在 `ebx+8` 处看到它的 ID)

可以发现它只注册了一个算法,也就是代表 Blowfish 的 `0x05`。

而 `CipherInit_492520` 则是初始化刚才拿到的 Blowfish 实例:

```cpp
using Classes::TComponent::TComponent;

void __fastcall CipherInit_492520(
TCipher *cipher,
const void *key
) {
uint8_t digest; // BYREF

if ( cipher->initialized )
    cipher->vtb->Burn(cipher);

DCP_SHA1 *sha1 = (DCP_SHA1 *)TComponent(&cls_Sha1_TDCP_sha1);
sha1->vtb->Init(sha1);
SHA1::UpdateString_49321C(sha1, key);
sha1->vtb->Final(sha1, digest);
System::TObject::Free((System::TObject *)sha1);

// Blowfish MaxKeySize: 448
if ( cipher->vtb->GetMaxKeySize(cipher) >= 160 ) {
    cipher->vtb->Init(cipher, digest, 160, NULL);
} else {
    int MaxKeySize = cipher->vtb->GetMaxKeySize(cipher);
    cipher->vtb->Init(cipher, digest, MaxKeySize, NULL);
}

System::__linkproc__ FillChar(digest, 20, 0xFF);
}
```

- 合理猜测 Blowfish 的密钥是 `SHA1("AGE Flash Player")`,跟踪发现确实如此。
- 产生的摘要值(digest)顺理成章的作为 Blowfish 算法的密钥进行初始化。

将类型信息整合,加上我们的注解,可以得到下方的代码:

```c
int __fastcall DecryptSend_4989A0(
TRequest *req,
TWriterStream **res,
int full_len,
int enc_len
) {
TFileHeader hdr; // size=12
BufferRead_41B1D8(req->reader, &hdr, sizeof(hdr));

// 初始化解密过程
TCipher *cipher = CipherFactory_493274(hdr.magic);
CipherInit_492520(cipher, AnsiString("AGE_Flash_Player"));

// 如果是块加密算法,设置为 CFB 模式。
if ( IsClass(cipher, &cls_DCPcrypt_TDCP_blockcipher) ){
    cipher->CipherMode = BLOCK_MODE_CFB_BLOCK;
}

// 测试解密,不相等就报错。
cipher->vtb->decrypt(cipher, &hdr.cipher, &hdr.cipher, 12);
if ( hdr.cipher != hdr.plain ) {
    RaiseExcept(ExceptionFactory(error_ctx)); // 抛出错误
}

// 解密文件头
DecryptStream_4925C0(cipher, req->reader, *res, enc_len);

// 如果还有数据,将剩下的数据拷贝过去
if ( enc_len < full_len - 12 ) {
    Send_498954(req, *res, full_len - 12 - enc_len, NULL);
}

// 清理并释放资源
cipher->vtb->Burn(cipher);
System::TObject::Free(cipher);

return NULL;
}
```

看起来并没有什么魔改的地方,剩下的就很简单了… 对吧?

### DCPcrypt

> DCPcrypt 这么成熟的库,与其它语言的实现一定能兼容的对吧?对吧??

可执行文件内置了一些符号,可以看出程序中的 `SHA1` 和 `Blowfish` 实现来自 。

虽然对算法本身的实现(即 ECB 模式)是标准的,但当数据长度未与块大小对齐的情况下…

```pas {linenos=inline, hl_lines=["22-27"]}
procedure TDCP_blockcipher64.DecryptCFBblock(const Indata; var Outdata; Size: longword);
var
i: longword;
p1, p2: PByte;
Temp: array of byte;
begin
if not fInitialized then
    raise EDCP_blockcipher.Create('Cipher not initialized');
p1:= @Indata;
p2:= @Outdata;
FillChar(Temp, SizeOf(Temp), 0);
for i:= 1 to (Size div 8) do
begin
    Move(p1^,Temp,8);
    EncryptECB(CV,CV);
    Move(p1^,p2^,8);
    XorBlock(p2^,CV,8);
    Move(Temp,CV,8);
    p1:= PByte(PByte(p1) + 8);
    p2:= PByte(PByte(p2) + 8);
end;
if (Size mod 8)<> 0 then
begin
    EncryptECB(CV,CV);
    Move(p1^,p2^,Size mod 8);
    XorBlock(p2^,CV,Size mod 8);
end;
end;
```

(来源:;正确做法是使用 `pkcs#1` 等填充方案)

: https://github.com/SnakeDoctor/DCPcrypt/blob/f319817/DCPblockciphers.pas#L319

因此如果需要使用其它语言实现,那么需要利用 `ECB` 模式,来手动实现 DCPcrypt 版的 `CFB` 模式。

> DCPcrypt 的 IV 生成部分也是非标行为。
>
> 有兴趣的同学可以自行尝试逆向(或[瞄一眼我的实现]),这里就不展开了。

: https://wiki.lazarus.freepascal.org/DCPcrypt
: https://github.com/FlyingRainyCats/age_unpack/blob/8fb72d1/age/cipher.py#L14

## 解密流程

> 需要的信息都有了,总结一下流程。

首先是「读取可执行文件信息」,也就是文件末尾的 20 个字节:

```yaml
末尾数据:
元数据偏移: u32 # 例 0x014fd711
未知3:      u32
未知4:      u32
清单偏移:   u32 # 例 0x014fd33e
未知6:      u32
```

然后就是文件清单。

- 读取「清单偏移」与「元数据偏移」之间的数据
- 使用 `zlib.decompress` 解压缩

文件清单的格式如下:

```yaml
数据偏移: u32
完整长度: u32
加密长度: u32

文件名:
长度: u16
内容: Vec<u8> # 文件名长度为 "$.文件名.长度"
```

剩下的就是依次进行提取了:

- 跳到指定数据偏移处
- 读入加密数据长度的内容
- 初始化 Blowfish 算法,并进行解密
- 读入「完整长度 - 加密长度 - 12」字节的数据
- 这部分数据不需要解密,直接写出即可

到此,提取流程就完成了。

## 参考实现

使用 Python 做了个简单的实现,大概只支持某一个版本的 AGE 打包器生成的文件。

⇒ (https://github.com/FlyingRainyCats/age_unpack)

爱飞的猫 发表于 2025-5-24 00:00

fys2008 发表于 2025-5-23 15:48
@爱飞的猫 @爱飞的猫




删帖是因为你的回答与问题无关。如果你有阅读主贴并分析问题,可以发现程序本身运行是没有问题,出问题的是对可执行文件(dll)进行修改后遇到问题。正确思路是程序对文件进行了某种校验,导致加载失败,应当对该机制进行分析。“使用第三方软件修复缺少的dll”既不是该贴楼主询问的情况,也不能解决他的问题。

最后,对处理有意见请发到 『投诉举报区』 。如果你还有问题请在该板块发帖询问,发帖前请阅读对应版规。继续在无关贴(包括本帖)进行回复将会被忽略。

小众资源 发表于 2025-5-11 21:56

分析逻辑清晰合理、透彻,感谢授人以渔。

wsck63304521 发表于 2025-5-11 22:32

逻辑清晰合理

烟99 发表于 2025-5-12 09:44

未知6好像是用hex字节记录的配置信息,然后exe的数据区的首个数据也是zlib,大小是第一个资源文件的偏移减exe数据区首个数据的偏移,解压后是一个ocx格式的flash播放插件

6833 发表于 2025-5-12 10:07

看不懂,但是很高级的样子{:1_921:}{:1_921:}{:1_921:}{:1_921:}

fix9527 发表于 2025-5-12 11:17

看不懂,但是还是喜欢看

358059103 发表于 2025-5-12 13:01

虽然看不太明白,菜鸟一个,感谢分享。

dling89 发表于 2025-5-12 13:49

小白努力学习中。。

zlh94001 发表于 2025-5-12 18:35

感谢分享 受益匪浅!!!!!!!

烟99 发表于 2025-5-12 23:54

现在还是不太明白,我想用其他语言写,有密钥后,还需不需要IV,另外还需不需要指定解密模式
页: [1] 2 3
查看完整版本: 解包《雅致 Flash打包器 (AGE Flash Packer)》生成的可执行文件