吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3009|回复: 18
上一主题 下一主题
收起左侧

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

  [复制链接]
跳转到指定楼层
楼主
爱飞的猫 发表于 2025-5-11 21:36 回帖奖励

前因

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

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

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

下面是对应的 SHA256 校验值:

9d139494eba8891846a0ce961c1f3235c4eefb1d9d04018da2a585e681af7744 *ebook.exe

观察文件

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

文件末尾

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

文件偏移: 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」的内容:

14F:D33E  78 01 6D 94                                      x.m”

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

# 安装依赖: 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) 查看

可以看到这样的数据:

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

                            ... 省略 ...

000007d0  dc 00 00 0d 00 73 72 63  5c 69 6d 67 5c 63 2e 6a  |.....src\img\c.j|
000007e0  70 67                                             |pg|
000007e2

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

文件清单

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

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

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

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

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

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

025:2C81  05 00 00 00 96 DA 9D EE 4D F8 A8 80 B0 B6 6E B9  ....–Ú.îMø¨?°¶n¹
025:2C91  BE A4 C9 A5 05 3A 6F 2C 29 78 33 F1 9C 38 4B 0D  ¾¤É¥.:o,)x3ñœ8K.
025:2CA1  DA 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 利用 FLIRT 签名识别出来了,反编译的代码还是相对比较好理解的。

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

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

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 函数后会产生解密后的数据:

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

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

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

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

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 实例:

using Classes::TComponent::TComponent;

void __fastcall CipherInit_492520(
  TCipher *cipher,
  const void *key
) {
  uint8_t digest[20]; // [esp+0h] [ebp-20h] 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 算法的密钥进行初始化。

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

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 这么成熟的库,与其它语言的实现一定能兼容的对吧?对吧??

可执行文件内置了一些符号,可以看出程序中的 SHA1Blowfish 实现来自 DCPcrypt

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

procedure TDCP_blockcipher64.DecryptCFBblock(const Indata; var Outdata; Size: longword);
var
  i: longword;
  p1, p2: PByte;
  Temp: array[0..7] 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;

(来源:SnakeDoctor/DCPcrypt: DCPblockciphers.pas;正确做法是使用 pkcs#1 等填充方案)

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

DCPcrypt 的 IV 生成部分也是非标行为。

有兴趣的同学可以自行尝试逆向(或瞄一眼我的实现),这里就不展开了。

解密流程

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

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

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

然后就是文件清单。

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

文件清单的格式如下:

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

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

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

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

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

参考实现

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

github.com/FlyingRainyCats/age_unpack

免费评分

参与人数 9威望 +2 吾爱币 +120 热心值 +9 收起 理由
flybird2007 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
MowChan + 1 鼓励转贴优秀软件安全工具和文档!
Hmily + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
lpxx + 1 + 1 我很赞同!
烟99 + 10 + 1 用心讨论,共获提升!
qqycra + 2 + 1 用心讨论,共获提升!
wszjf + 4 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
chinawolf2000 + 1 + 1 热心回复!
Bob5230 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| 爱飞的猫 发表于 2025-5-24 00:00 |楼主
fys2008 发表于 2025-5-23 15:48
@爱飞的猫 @爱飞的猫

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

最后,对处理有意见请发到 『投诉举报区』 。如果你还有问题请在该板块发帖询问,发帖前请阅读对应版规。继续在无关贴(包括本帖)进行回复将会被忽略。
推荐
 楼主| 爱飞的猫 发表于 2025-5-13 04:50 |楼主
本帖最后由 爱飞的猫 于 2025-5-13 04:52 编辑
烟99 发表于 2025-5-12 23:54
现在还是不太明白,我想用其他语言写,有密钥后,还需不需要IV,另外还需不需要指定解密模式

你需要自己实现 CFB,参考下方的流程图:

此外 CFB 是需要 IV 的。DCPcrypt 的实现是在不提供 IV 的时候,帮你生成一个(用空数据 ECB 加密一次),大概是这样:

iv = blowfish_ecb_encrypt(key, "\x00\x00\x00\x00\x00\x00\x00\x00")
沙发
小众资源 发表于 2025-5-11 21:56
3#
wsck63304521 发表于 2025-5-11 22:32
逻辑清晰合理
4#
烟99 发表于 2025-5-12 09:44
未知6好像是用hex字节记录的配置信息,然后exe的数据区的首个数据也是zlib,大小是第一个资源文件的偏移减exe数据区首个数据的偏移,解压后是一个ocx格式的flash播放插件

免费评分

参与人数 1热心值 +1 收起 理由
爱飞的猫 + 1 热心回复!

查看全部评分

5#
6833 发表于 2025-5-12 10:07
看不懂,但是很高级的样子
6#
fix9527 发表于 2025-5-12 11:17
看不懂,但是还是喜欢看
7#
358059103 发表于 2025-5-12 13:01
虽然看不太明白,菜鸟一个,感谢分享。
8#
dling89 发表于 2025-5-12 13:49
小白努力学习中。。
9#
zjcs5210 发表于 2025-5-12 14:26
真好,很好用
10#
zlh94001 发表于 2025-5-12 18:35
感谢分享 受益匪浅!!!!!!!
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-5-29 07:55

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表