作者: James

  • 原创带后台苹果CMSv10全屏高端模板[首涂第二十一套]

    原创带后台苹果CMSv10全屏高端模板[首涂第二十一套]

    模板介绍

    这是一款带“主题管理系统”的模板,它是可以完全自主配置主题的一套系统,脱离cms主程序独立针对主题界面管理,体积小巧、功能强大、免安装,当前版本具备了50多个模块500多个设置项,默认参数基本满足常用配置不用每个手动修改,也许很多功能并不常用但等你用到的时候才能体会它的精妙,颠覆以往的无管理界面、文件找不到、不会改代码的模板通病,有了这套系统完全可以自主配置几乎你能用到的所有东西,例如常见的导航、推荐、菜单、广告、颜色、样式等等,这些也只是基础功能,更多更强大的功能欢迎下载体验。

    首涂21套模板依然是DIY系列样式,优化重构了前期同系列的不足,添加了更流行的元素和设计风格,这是我们完全自主开发没用任何css框架且只针对影视站设计的一个UI库,体积小代码质量高加载速度快对搜索引擎更友好,对懂技术的站长阅读起来更易扩展更方便,同时方便站长自行修改扩展主题样式我们未对前端任何代码(含JS)加密或压缩打乱。

    模板特点

    1,让不懂代码的小白,像大佬一样做好影视站
    2,增加主题后台管理设置开关 前台实现效果
    3,4种模板颜色风格 前台任意切换
    4,3种播放器样式后台任意切换
    5,预留多个多样式广告位 让你更方便快捷的添加广告。
    6,包含了会员、专题、资讯、明星、评论、下载、留言页面等样式及全部功能。
    7,3个播放列表样式后台任意切换
    8,3种幻灯样式,后台任意切换
    9,自定义导航栏
    10,自定义手机底部导航菜单

    模板截图

    图片[4] - [首涂21套模板] 原创带后台苹果CMSv10全屏高端模板- 长江技术博客
    图片[1] - [首涂21套模板] 原创带后台苹果CMSv10全屏高端模板- 长江技术博客
    图片[2] - [首涂21套模板] 原创带后台苹果CMSv10全屏高端模板- 长江技术博客
    图片[3] - [首涂21套模板] 原创带后台苹果CMSv10全屏高端模板- 长江技术博客
  • 苹果cmsV10 MXPro自适应影视站主题模板[首涂二十九套]

    苹果cmsV10 MXPro自适应影视站主题模板[首涂二十九套]

    MXPro模板说明

    • MXPro 模板主题(又名:mxonepro)是一款基于苹果 cms程序的一款全新的简洁好看 UI 的影视站模板,类似于西瓜视频,模板简洁且有周更记录样式等多功能后台设置,类似mxone 魔改版的预告片功能,用来做影视站模板也是极好的,之前的作者不再进行更新就卖给了首涂模板需要授权才能使用,但是首涂拿到之后基本上是不会再次更新了,目前名称是:首途二十九套模板;本模板是在之前的版本基础上添加更多新功能机修复 BUG 以及进行调整优化且模板无需授权,其他等功能可往下看详细介绍。
    • 演示站:猪泡泡影院

    MXPro模板截图

    图片[1] - [首涂29套模板]2022苹果cms MXPro自适应影视站模板 - 长江技术博客
    模板首页
    图片[2] - [首涂29套模板]2022苹果cms MXPro自适应影视站模板 - 长江技术博客
    模板热榜
    图片[3] - [首涂29套模板]2022苹果cms MXPro自适应影视站模板 - 长江技术博客
    模板详情页

    MxPro 功能更新记录

    Mxpro V4.3 更新日志:2022-09-03

    1. 新增-模板 SEO 代码核心优化,全局代码 SEO 已优化,更多详情可自行安装后查看源代码
    2. 新增-视频详情中视频简介展开/收起按钮
    3. 新增-视频剧集集数倒序排序功能,解决集数较多的情况方便查看最新剧集
    4. 新增-视频详情页当中添加打赏功能,打赏二维码可后台自行设置
    5. 新增-取消之前单独的豆瓣跳转功能,添加豆瓣、抖音、快手、百度等查看功能
    6. 新增-全站添加简繁体切换功能,提高观影体验,如需进入后台开启
    7. 新增-视频播放页面,相关推荐下方添加正在热映模板推荐的视频
    8. 新增-首页侧边栏添加明星库,资讯导航按钮,可后台自行开关控制
    9. 优化文章资讯页推荐阅读的文章样式
    10. 优化专题模板的展示方式,及修复专题过多文字溢出的问题
    11. 修复手机端中播放页视频标题过长的情况下会遮挡其他功能按钮,已修正为文字居中溢出文字自动下一行
    12. 修复模板中 msg 提示不会自动跳转的问题
    13. 模板全代码减少冗余,优化加载速度
    14. 老用户更新包添加

    MXPro模板安装教程说明:

    1. 苹果 cms 运行环境 PHP 版本 7.4 以下 数据库 mysql 5.6 无需 SG11
    2. 将模板安装压缩包上传到苹果 cms 程序根目录下解压覆盖即可 【或者将 mxtheme 放置根目录然后将 mxpro 目录文件放置 template 文件中】
    3. 苹果 cms 后台-系统-网站参数配置-网站模板-选择 mxpro 模板目录填写 html
    4. 网站模板选择好之后一定要先访问前台,然后再进入后台设置
    5. 主题后台地址:mxpro 主题,/admin.php/admin/mxpro/mxproset
      admin.php 改成你登录后台的 xxx.php 名称
    6. 首页幻灯片设置视频推荐 9 后台可自定义设置
    7. 其余功能进入后台自行设置
    8. 追剧周表在视频数据中,节目周期添加周一至周日自行添加,格式:一,二,三,四,五,六,日
    9. 部分功能我设置的都是默认关闭的,根据自己喜好后台设置开启
    10. 更多安装教程,下载源码包打开README.md文件查看

  • Dumping PHP Opcodes Protected by SourceGuardian

    Dumping PHP Opcodes Protected by SourceGuardian

    Intro

    In this article, we’ll walk through my process for revealing SourceGuardian-protected PHP bytecode. We’ll get into some PHP 5.4 internals since this is the version Nagios XI was built on. Also we’ll perform some static and dynamic analysis of the SourceGuardian loader extension. Finally, the end result is a modified version of the Vulcan Logic Dumper (VLD). Many thanks to Derick Rethans and all who contributed to VLD!

    Here is a brief outline of the topics to be covered:

    • PHP Bytecode
    • The SourceGuardian Loader
    • Vulcan Logic Dumper
    • Hooking zend_execute
    • Challenges encountered
    • Opcode Handlers
    • Analyzing Custom Handlers
    • My Solution

    Below is a protected file. The goal is to decode this into something we can analyze.

    Do you read SourceGuardian?

    Before we move onto analysis, let’s see a description of the SourceGuardian product. Their website says, “Our PHP encoder protects your PHP code by compiling the PHP source code into a binary bytecode format, which is then supplemented with an encryption layer.“

    PHP Bytecode

    Similar to other interpreted programming languages, PHP source code is compiled into bytecode. For example, the following PHP code:

    <?php
    echo "hello world";
    ?>

    Would be compiled into the below. Although, the below graphic is a visual representation of a zend_op_array. The Vulcan Logic Dumper (VLD) can be used to dump bytecode in this format. The output shows individual opcodes and their associated fields.

    Source: https://www.php.net/manual/en/internals2.opcodes.echo.php

    Here is another short example:

    <?php
    for($i=0; $i<3; $i++){
    echo "hi";
    }
    ?>

    Would be compiled into:

    Source: https://www.php.net/manual/en/internals2.opcodes.jmpnz.php

    As we go, keep in mind that source code is compiled into operations. I may call them instructions as well.

    sg_load()

    From now on, I’ll refer to SourceGuardian-protected files simply as “encoded” files, and SourceGuardian will be abbreviated as “SG”. When an encoded file is launched by the PHP interpreter, it is decoded by an SG “loader,” which is implemented as a PHP extension.

    Given that the encoder compiles the source code and encrypts the bytecode, the loader must decrypt and execute the compiled bytecode. The loader implements a key function called sg_load(), which does this. In all encoded files, you’ll find a call to this function at the end of the file.

    sg_load() is called in an encoded file

    My goal was to simply dump the original bytecode instructions with VLD.

    VLD

    Let’s check out how VLD works. We’ll start with an unencoded “hello world” example:

    <?php
    echo "Hello world!\n";
    ?>

    If we were to dump this with VLD, it would show:

    The catch is that VLD hooks zend_compile_file(), and this output is coming from there. After zend_compile_file() is called to compile the source code into a zend_op_array, the op array is dumped using the vld_dump_oparray() function. This is all handled in vld_compile_file().

    Source: https://github.com/derickr/vld/blob/483716c1626d05edb01ef9bc9a70046c327c5218/vld.c#L374

    If we were to run VLD as-is against an encoded file, the results would not give us what we want. It was not designed to decode protected files. Instead, we would see opcodes for the SG wrapper code along with a call to sg_load(). The input to sg_load(), containing encrypted bytecode, would not be dumped because it does not need to be compiled.

    Note: The VLD project description explicitly states it “can not be used to un-encode PHP code that has been encoded with any encoder.”

    SG wrapper code dumped. Notice the call to sg_load() at the top.

    Dumping Opcodes in zend_execute()

    An encoded file must be executed, right? The bytecode is decrypted then executed by zend_execute(). This is where I started to get my hands dirty.

    VLD already has a hook built in for zend_execute(), so if we modify VLD to dump the zend_op_array passed to zend_execute(), we can see the opcodes being executed. Note that VLD renames the function to vld_execute().

    static void vld_execute(zend_op_array *op_array TSRMLS_DC)
    #endif
    {
    php_printf("\nexecute()\n");
    vld_dump_oparray (op_array TSRMLS_CC);
    old_execute(op_array);
    }

    Modified Vld.c

    In order to have a controlled test environment, I created some sample files and encoded them. I started with hello.php from before.

    Here is the result of running the modified VLD against hello.php.

    It clearly executed just fine. But why are there no opcodes shown?

    Empty Opcode Dump

    I needed to start debugging to see what was going on under the hood. First off, a zend_op_array is passed as an argument here.

    static void vld_execute(zend_op_array *op_array TSRMLS_DC)

    Let’s see what the structure looks like:

    Source: https://github.com/php/php-src/blob/09d2b01f384dee54f0348c865a6b2e3c85d26ebd/Zend/zend_compile.h#L53

    Source: https://github.com/php/php-src/blob/09d2b01f384dee54f0348c865a6b2e3c85d26ebd/Zend/zend_compile.h#L255

    Now, what does vld_dump_oparray() do with it? This is defined in srm_oparray.c. Quite a bit happens, in fact. It analyzes the branches, formats the output and dumps the opcodes in the array. There is a loop that iterates over each zend_op in the opcodes member and calls vld_dump_op().

    Here is the zend_op structure.

    Source: https://github.com/php/php-src/blob/09d2b01f384dee54f0348c865a6b2e3c85d26ebd/Zend/zend_compile.h#L54

    Source: https://github.com/php/php-src/blob/09d2b01f384dee54f0348c865a6b2e3c85d26ebd/Zend/zend_compile.h#L106

    Okay, so what does vld_dump_op() do? Essentially, it inspects the specified zend_op and outputs the relevant pieces. One unusual thing is this: the lineno is always 0.

    In comes the debugger!

    All debugging was performed in the GNU Debugger (GDB). I set a breakpoint on execute() so we can inspect the op_array and opcodes contained within. I’ve left out the SG wrapper code dump and excessive debugger output. Something to note is that execute() must be hit twice because the first call to execute is for the wrapper code, and the second call executes the bytecode we’re after.

    $ gdb php
    Reading symbols from php...
    (gdb) b execute
    Breakpoint 1 at 0x36f760: file php-src/Zend/zend_vm_execute.h, line 343.
    (gdb) r -dvld.dump_paths=0 -dvld.execute=0 hello.php
    Starting program: /usr/local/bin/php -dvld.dump_paths=0 -dvld.execute=0 hello.php
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    . . . <snip> . . .Breakpoint 1, execute (op_array=0x7ffff5b7e918) at php-src/Zend/zend_vm_execute.h:343
    343 {
    (gdb) c
    Continuing.execute()
    filename: hello.php
    function name: (null)
    number of ops: 3
    compiled vars: none
    line #* E I O op fetch ext return operands
    --------------------------------------------------------------------
    Breakpoint 1, execute (op_array=0x7ffff5b85340) at php-src/Zend/zend_vm_execute.h:343
    343 {
    (gdb) p op_array
    $1 = (zend_op_array *) 0x7ffff5b85340
    (gdb) p *op_array
    $2 = {type = 2 '\002', function_name = 0x0, scope = 0x0, fn_flags = 134217728, prototype = 0x0, num_args = 0, required_num_args = 0, arg_info = 0x0, refcount = 0x7ffff5b805f8, opcodes = 0x7ffff5b7ea18, last = 3, vars = 0x0, last_var = 0, T = 0, brk_cont_array = 0x0,
    last_brk_cont = 0, try_catch_array = 0x0, last_try_catch = 0, static_variables = 0x0, this_var = 4294967295, filename = 0x7ffff5b7eab8 "hello.php", line_start = 0, line_end = 0, doc_comment = 0x0, doc_comment_len = 0, early_binding = 4294967295,
    literals = 0x7ffff5b85440, last_literal = 2, run_time_cache = 0x0, last_cache_slot = 0, reserved = {0x555555f4e450, 0x0, 0x0, 0x0}}

    Take note of a few things here in the op_array. Last = 3, which makes sense, there are 3 operations. It’s also weird that line_start and line_end are both 0 though. Let’s look at the individual zend_op’s.

    (gdb) p op_array->opcodes[0]
    $4 = {handler = 0x7ffff4a09280, op1 = {constant = 4122471032, var = 4122471032, num = 4122471032, hash = 140737315859064, opline_num = 4122471032, jmp_addr = 0x7ffff5b7ea78, zv = 0x7ffff5b7ea78, literal = 0x7ffff5b7ea78, ptr = 0x7ffff5b7ea78}, op2 = {constant = 0,
    var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, extended_value = 0, lineno = 0, opcode = 42 '*',
    op1_type = 0 '\000', op2_type = 0 '\000', result_type = 0 '\000'}(gdb) p op_array->opcodes[1]
    $5 = {handler = 0x5555558dfaa0 <ZEND_ECHO_SPEC_CONST_HANDLER>, op1 = {constant = 4122498112, var = 4122498112, num = 4122498112, hash = 140737315886144, opline_num = 4122498112, jmp_addr = 0x7ffff5b85440, zv = 0x7ffff5b85440, literal = 0x7ffff5b85440,
    ptr = 0x7ffff5b85440}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0},
    extended_value = 0, lineno = 0, opcode = 40 '(', op1_type = 1 '\001', op2_type = 0 '\000', result_type = 0 '\000'}(gdb) p op_array->opcodes[2]
    $6 = {handler = 0x5555558cd390 <ZEND_RETURN_SPEC_CONST_HANDLER>, op1 = {constant = 4122498152, var = 4122498152, num = 4122498152, hash = 140737315886184, opline_num = 4122498152, jmp_addr = 0x7ffff5b85468, zv = 0x7ffff5b85468, literal = 0x7ffff5b85468,
    ptr = 0x7ffff5b85468}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0},
    extended_value = 0, lineno = 0, opcode = 62 '>', op1_type = 1 '\001', op2_type = 0 '\000', result_type = 0 '\000'}

    All three instructions have a lineno of 0.

    Looking at vld_dump_op(), it was clear why the ops are not being dumped.

    Source: https://github.com/derickr/vld/blob/483716c1626d05edb01ef9bc9a70046c327c5218/srm_oparray.c#L696

    I commented that if-block out. And this was the new output:

    Encoded

    Comparing this output to the original, unencoded file:

    Not Encoded

    Interesting. So the encoded sample has an additional JMP instruction at the beginning. Oddly, the JMP goes straight to the return though… that can’t be right. This didn’t make sense, so I created more samples.

    Comparing Samples

    Let’s look at a basic example with an if…else.

    <?php$num = rand(0, 1);
    if ($num == 1)
    {
    echo "1\n";
    }
    else
    {
    echo "0\n";
    }?>

    And here is the VLD output.

    Encoded
    Not Encoded

    Very interesting… the encoded sample output has 2 additional instructions, and the JMP is at the beginning again. Also, oddly, if you follow the instructions for the encoded output, it just doesn’t add up. First we jump to instruction 4, and then rand() is called. However, only 1 argument is passed to rand. Instruction 3 is not executed prior to the call to rand. Also you can see that the JMPZ is changed to a JMPZNZ. Either we jump to instruction 11 then instruction 3, which is a SEND_VAL. Or we jump to the ASSIGN instruction. None of it makes sense.

    There was a common trend I saw when analyzing sample after sample:

    • An initial additional JMP instruction
    • Some instructions were completely changed – e.g. JMPZ turned into JMPZNZ
    • Control flow via branching did not match the logic for an unencoded dump

    These observations led me to believe that there was some obfuscation going on.

    Opcode Handlers

    Back to the debugger. If you look at the op handlers, something sticks out. For reference, there are a variety of op handlers that know what to do with a specific opcode.

    (gdb) p op_array->opcodes[0]
    $4 = {handler = 0x7ffff4a09280, op1 = {constant = 4122471032, var = 4122471032, num = 4122471032, hash = 140737315859064, opline_num = 4122471032, jmp_addr = 0x7ffff5b7ea78, zv = 0x7ffff5b7ea78, literal = 0x7ffff5b7ea78, ptr = 0x7ffff5b7ea78}, op2 = {constant = 0,
    var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, extended_value = 0, lineno = 0, opcode = 42 '*',
    op1_type = 0 '\000', op2_type = 0 '\000', result_type = 0 '\000'}(gdb) p op_array->opcodes[1]
    $5 = {handler = 0x5555558dfaa0 <ZEND_ECHO_SPEC_CONST_HANDLER>, op1 = {constant = 4122498112, var = 4122498112, num = 4122498112, hash = 140737315886144, opline_num = 4122498112, jmp_addr = 0x7ffff5b85440, zv = 0x7ffff5b85440, literal = 0x7ffff5b85440,
    ptr = 0x7ffff5b85440}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0},
    extended_value = 0, lineno = 0, opcode = 40 '(', op1_type = 1 '\001', op2_type = 0 '\000', result_type = 0 '\000'}(gdb) p op_array->opcodes[2]
    $6 = {handler = 0x5555558cd390 <ZEND_RETURN_SPEC_CONST_HANDLER>, op1 = {constant = 4122498152, var = 4122498152, num = 4122498152, hash = 140737315886184, opline_num = 4122498152, jmp_addr = 0x7ffff5b85468, zv = 0x7ffff5b85468, literal = 0x7ffff5b85468,
    ptr = 0x7ffff5b85468}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0},
    extended_value = 0, lineno = 0, opcode = 62 '>', op1_type = 1 '\001', op2_type = 0 '\000', result_type = 0 '\000'}

    Notice in opcode 0 that the handler address is in a different address space than the other two opcode handlers. 0x7ffff4a09280 vs 0x5555558dfaa0 or 0x5555558cd390. Also, opcode 0 doesn’t seem to have a symbol associated with the address. On the other hand, opcodes 1 and 2 have handlers that point to ZEND_ECHO_SPEC_CONST_HANDLER and ZEND_RETURN_SPEC_CONST_HANDLER.

    Let’s take a look at the address ranges for loaded libraries:

    0x7ffff4a09280 belongs to ixed.5.4.lin, which is the SG loader extension.

    The other two handlers are mapped within the PHP executable. This is quite curious. The first jump instruction handler points to a function contained in the SG loader extension. We’ll set a breakpoint in there and let execution continue.

    (gdb) b *0x7ffff4a09280
    Breakpoint 2 at 0x7ffff4a09280
    (gdb) c
    Continuing.

    Breakpoint 2, 0x00007ffff4a09280 in ?? () from /usr/local/lib/php/extensions/no-debug-non-zts-20100525/ixed.5.4.lin

    I prefer intel over at&t syntax. So we set the flavor.

    (gdb) set disassembly-flavor intel
    (gdb) disas
    No function contains program counter for selected frame.

    Weird. Let’s try disassembling a range. No need to read this. More on that later.

    (gdb) disas $rip,$rip+128
    Dump of assembler code from 0x7ffff4a09280 to 0x7ffff4a09300:
    => 0x00007ffff4a09280: push rbp
    0x00007ffff4a09281: movabs rsi,0xaaaaaaaaaaaaaaab
    0x00007ffff4a0928b: push rbx
    0x00007ffff4a0928c: sub rsp,0x8
    0x00007ffff4a09290: mov rdx,QWORD PTR [rip+0x210ff9]
    0x00007ffff4a09297: mov rbx,QWORD PTR [rdi]
    0x00007ffff4a0929a: mov rax,QWORD PTR [rdi+0x28]
    0x00007ffff4a0929e: movsxd rdx,DWORD PTR [rdx]
    0x00007ffff4a092a1: mov rbp,QWORD PTR [rbx+0x8]
    0x00007ffff4a092a5: mov rcx,rbp
    0x00007ffff4a092a8: mov rdx,QWORD PTR [rax+rdx*8+0xd0]
    0x00007ffff4a092b0: mov rax,QWORD PTR [rax+0x40]
    0x00007ffff4a092b4: sub rcx,rax
    0x00007ffff4a092b7: mov rdx,QWORD PTR [rdx]
    0x00007ffff4a092ba: sar rcx,0x4
    0x00007ffff4a092be: imul rcx,rsi
    0x00007ffff4a092c2: shl rcx,0x4
    0x00007ffff4a092c6: mov ecx,DWORD PTR [rcx+rdx*1]
    0x00007ffff4a092c9: lea rcx,[rcx+rcx*2]
    0x00007ffff4a092cd: shl rcx,0x4
    0x00007ffff4a092d1: lea rcx,[rax+rcx*1]
    0x00007ffff4a092d5: mov QWORD PTR [rbx+0x8],rcx
    0x00007ffff4a092d9: mov rcx,rbx
    0x00007ffff4a092dc: sub rcx,rax
    0x00007ffff4a092df: mov rax,rcx
    0x00007ffff4a092e2: sar rax,0x4
    0x00007ffff4a092e6: imul rax,rsi
    0x00007ffff4a092ea: shl rax,0x4
    0x00007ffff4a092ee: call QWORD PTR [rdx+rax*1+0x8]
    0x00007ffff4a092f2: mov QWORD PTR [rbx+0x8],rbp
    0x00007ffff4a092f6: add rsp,0x8
    0x00007ffff4a092fa: pop rbx
    0x00007ffff4a092fb: pop rbp
    0x00007ffff4a092fc: ret

    I stepped through this, and ultimately landed at the CALL instruction:

    0x00007ffff4a092ee: call   QWORD PTR [rdx+rax*1+0x8]

    Next, I stepped into this function call.

    (gdb) si
    ZEND_JMP_SPEC_HANDLER (execute_data=0x7ffff5b4c9e0) at php-src/Zend/zend_vm_execute.h:430
    430 {

    My, oh my. The SG custom JMP handler eventually called the ZEND_JMP_SPEC_HANDLER. There is a zend_execute_data structure passed as an argument as well. After a bit of fumbling around – starting and restarting the debugger – and scratching my head, I noticed something about the data structure passed to the Zend handler.

    Operand 1 to the current PHP operation (opline.. which points inside op_array->opcodes), had changed!

    Before entering the SG jmp handler
    After entering the zend jmp handler

    The jmp_addr is different! This explains why the control flow logic in the VLD opcode dumps don’t make sense. The JMP operands have been tampered with.

    At this point, I felt I needed to do some in depth analysis of the SG jmp handler.

    Source Guardian JMP Handler Analysis

    I opened ixed.5.4.lin in Hopper Disassembler. The JMP handler function is at offset 0x9280 in the file, and a cursory glance around revealed that there are 4 additional functions composed of similar logic. The usage of constant 0xaaaaaaaaaaaaaaab in each of them was a dead giveaway.

    I then realized that these were probably additional custom opcode handlers, and I would need to analyze each of them. My next task was to figure out which opcodes map up to which handlers. I did this by modifying the vld_dump_op() function to compare the current opcode structure’s handler address to the handler supplied by the Zend engine. If the handler’s address didn’t match up with the Zend handler’s address, it would print some output prior to dumping the operation’s fields.

    Added some debug statements

    This allowed me to determine some of the offsets of custom handlers and their corresponding opcodes. For example, here is a JMPZNZ:

    and a JMP:

    These offsets (0x280 and 0x3f0) correspond to the handlers in the Hopper disassembly. This was confirmation that the nearby functions were almost all surely custom handlers.

    At this point I knew I had to accomplish a couple things:

    • Map all custom handler functions to opcode values in the SG loader extension
    • Figure out how to “fix” the opcode structures so that vld_dump_op() would display the correct operands. This would make the control flow logic make sense.

    I decided to go with option 2 first. I wanted to prove that I could doctor up a basic JMP instruction before I moved on to other instructions. I’m going to run through the JMP handler, and we’ll talk about what’s happening. Once we’ve gone through this handler, the others are quite similar.

    Dynamic Analysis of the JMP Handler

    As we’ve seen, a JMP is placed at the beginning of each op_array. At the second invocation of execute(), we can print the first opcode to get the address of the JMP handler. It should look familiar.

    (gdb) p op_array->opcodes[0]
    $1 = {handler = 0x7ffff4a09280, op1 = {constant = 4122470936, var = 4122470936, num = 4122470936, hash = 140737315858968, opline_num = 4122470936, jmp_addr = 0x7ffff5b7ea18, zv = 0x7ffff5b7ea18, literal = 0x7ffff5b7ea18,
    ptr = 0x7ffff5b7ea18}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0,
    zv = 0x0, literal = 0x0, ptr = 0x0}, extended_value = 0, lineno = 0, opcode = 42 '*', op1_type = 0 '\000', op2_type = 0 '\000', result_type = 0 '\000'}
    (gdb) b *0x7ffff4a09280
    Breakpoint 2 at 0x7ffff4a09280

    Next, we’ll continue into the handler function.

    (gdb) c
    Continuing.
    Breakpoint 2, 0x00007ffff4a09280 in ?? () from /usr/local/lib/php/extensions/no-debug-non-zts-20100525/ixed.5.4.lin

    Next, I dumped the registers to see what’s pointing where. My research was conducted on an x86_64 architecture – System V. This is important to know for recognizing function arguments.

    (gdb) info registers
    rax 0x7ffff5b7ea18 140737315858968
    rbx 0x7ffff5b4c9e0 140737315654112
    rcx 0x7ffff5b4ca70 140737315654256
    rdx 0x555555dbaa68 93825001040488
    rsi 0x0 0
    rdi 0x7ffff5b4c9e0 140737315654112
    rbp 0x7ffff5b809f8 0x7ffff5b809f8
    rsp 0x7fffffff93a8 0x7fffffff93a8
    r8 0x555555f53ec0 93825002716864
    r9 0x7ffff5b85770 140737315886960
    r10 0xfffffffffffff6bf -2369
    r11 0x55555594a9b0 93824996387248
    r12 0x1 1
    r13 0x3ff0 16368
    r14 0x7ffff5b4ca70 140737315654256
    r15 0x0 0
    rip 0x7ffff4a09280 0x7ffff4a09280
    eflags 0x246 [ PF ZF IF ]
    cs 0x33 51
    ss 0x2b 43
    ds 0x0 0
    es 0x0 0
    fs 0x0 0
    gs 0x0 0

    So the rdi register is pointing to 0x7ffff5b4c9e0. This is the first function argument for System V calling convention. If you look at zend_vm_execute.h, you’ll see that a handler takes an argument of type ZEND_OPCODE_HANDLER_ARGS.

    static int ZEND_FASTCALL  ZEND_JMP_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

    Really, it’s just a macro for a pointer to a zend_execute_data structure.

    #define ZEND_OPCODE_HANDLER_ARGS zend_execute_data *execute_data TSRMLS_DC

    Let’s print out the structure contents in GDB.

    (gdb) p *((zend_execute_data *)0x7ffff5b4c9e0)$4 = {opline = 0x7ffff5b7ea18, function_state = {function = 0x7ffff5b809f8, arguments = 0x0}, fbc = 0x0, called_scope = 0x0, op_array = 0x7ffff5b809f8, object = 0x0, Ts = 0x7ffff5b4ca70, CVs = 0x7ffff5b4ca70,symbol_table = 0x555555dbaa68 <executor_globals+392>, prev_execute_data = 0x7ffff5b4b060, old_error_reporting = 0x0, nested = 0 '\000', original_return_value = 0x7ffff5b4c438, current_scope = 0x7ffff5b4c458,current_called_scope = 0x7ffff5b4c478, current_this = 0x7ffff5b4c498, current_object = 0x7ffff5b4c4b8}

    This makes sense because the op_array has the same address as the argument to execute(). Here’s a look back at when we hit that break point.

    Breakpoint 1, execute (op_array=0x7ffff5b809f8) at php-src/Zend/zend_vm_execute.h:343

    Now that we know the argument is zend_execute_data, allow me to show you the important functionality in the function. For reference, here is the disassembly again:

       0x00007ffff4a09280: push   rbp
    0x00007ffff4a09281: movabs rsi,0xaaaaaaaaaaaaaaab
    0x00007ffff4a0928b: push rbx
    0x00007ffff4a0928c: sub rsp,0x8
    0x00007ffff4a09290: mov rdx,QWORD PTR [rip+0x210ff9]
    0x00007ffff4a09297: mov rbx,QWORD PTR [rdi]
    0x00007ffff4a0929a: mov rax,QWORD PTR [rdi+0x28]
    0x00007ffff4a0929e: movsxd rdx,DWORD PTR [rdx]
    0x00007ffff4a092a1: mov rbp,QWORD PTR [rbx+0x8]
    0x00007ffff4a092a5: mov rcx,rbp
    0x00007ffff4a092a8: mov rdx,QWORD PTR [rax+rdx*8+0xd0]
    0x00007ffff4a092b0: mov rax,QWORD PTR [rax+0x40]
    0x00007ffff4a092b4: sub rcx,rax
    0x00007ffff4a092b7: mov rdx,QWORD PTR [rdx]
    0x00007ffff4a092ba: sar rcx,0x4
    0x00007ffff4a092be: imul rcx,rsi
    0x00007ffff4a092c2: shl rcx,0x4
    0x00007ffff4a092c6: mov ecx,DWORD PTR [rcx+rdx*1]
    0x00007ffff4a092c9: lea rcx,[rcx+rcx*2]
    0x00007ffff4a092cd: shl rcx,0x4
    0x00007ffff4a092d1: lea rcx,[rax+rcx*1]
    0x00007ffff4a092d5: mov QWORD PTR [rbx+0x8],rcx
    0x00007ffff4a092d9: mov rcx,rbx
    0x00007ffff4a092dc: sub rcx,rax
    0x00007ffff4a092df: mov rax,rcx
    0x00007ffff4a092e2: sar rax,0x4
    0x00007ffff4a092e6: imul rax,rsi
    0x00007ffff4a092ea: shl rax,0x4
    0x00007ffff4a092ee: call QWORD PTR [rdx+rax*1+0x8]
    0x00007ffff4a092f2: mov QWORD PTR [rbx+0x8],rbp
    0x00007ffff4a092f6: add rsp,0x8
    0x00007ffff4a092fa: pop rbx
    0x00007ffff4a092fb: pop rbp
    0x00007ffff4a092fc: ret

    The Important Parts

    0x00007ffff4a09290: mov rdx,QWORD PTR [rip+0x210ff9]

    What happens is a pointer is dereferenced and the value is stored into rdx. Notice that the pointer address is calculated as a relative offset from the instruction pointer, rip.

    (gdb) p/x $rdx
    $1 = 0x7ffff4c1a640

    And it points into the SG loader … so it’s dipping into the loader to grab another pointer.

    (gdb) info proc mappings
    ...0x7ffff4c1a000 0x7ffff4c1b000 0x1000 0x1a000 ixed.5.4.lin...

    Prior to this instruction:

    0x00007ffff4a092d5: mov QWORD PTR [rbx+0x8],rcx

    Rbx points to opline (current operation), so this means the instruction sets opline->op1 to the value at rcx.

    (gdb) p *(zend_op *)$rbx
    $35 = {handler = 0x7ffff4a09280, op1 = {constant = 4122470936, var = 4122470936, num = 4122470936, hash = 140737315858968, opline_num = 4122470936, jmp_addr = 0x7ffff5b7ea18, zv = 0x7ffff5b7ea18, literal = 0x7ffff5b7ea18,
    ptr = 0x7ffff5b7ea18}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0,
    zv = 0x0, literal = 0x0, ptr = 0x0}, extended_value = 0, lineno = 0, opcode = 42 '*', op1_type = 0 '\000', op2_type = 0 '\000', result_type = 0 '\000'}

    After the instruction executes, notice that op1 has changed, and the jmp_addr is a different address.

    (gdb) p *(zend_op *)$rbx
    $39 = {handler = 0x7ffff4a09280, op1 = {constant = 4122470984, var = 4122470984, num = 4122470984, hash = 140737315859016, opline_num = 4122470984, jmp_addr = 0x7ffff5b7ea48, zv = 0x7ffff5b7ea48, literal = 0x7ffff5b7ea48,
    ptr = 0x7ffff5b7ea48}, op2 = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0, zv = 0x0, literal = 0x0, ptr = 0x0}, result = {constant = 0, var = 0, num = 0, hash = 0, opline_num = 0, jmp_addr = 0x0,
    zv = 0x0, literal = 0x0, ptr = 0x0}, extended_value = 0, lineno = 0, opcode = 42 '*', op1_type = 0 '\000', op2_type = 0 '\000', result_type = 0 '\000'}

    At the point when the zend vm opcode handler is called, the operands have been de-obfuscated. The actual JMP handler is called, and control flow can occur as it was originally intended to work.

    0x00007ffff4a092ee: call QWORD PTR [rdx+rax*1+0x8]

    Finally, the opline->op1 is restored back to its obfuscated value before the function returns.

    0x00007ffff4a092f2: mov QWORD PTR [rbx+0x8],rbp 

    So basically,

    1. The current op is de-obfuscated with its original operands.
    2. Then the zend vm opcode handler is called.
    3. And finally, the op is restored back into an obfuscated state.

    My strategy

    Now that we’ve seen how the most basic SG opcode handler (JMP) is implemented, I’d like to talk about my process for “fixing” the zend_op structures prior to dumping them with vld_dump_op(). Remember that the control flow logic doesn’t add up as of now. It took me a while to figure out a solid strategy for this.

    What I ended up doing was creating functions matching up to each of the SG handlers. I copied all of the assembly instructions, and modified the functions slightly. The modifications include the following:

    • construct a zend_execute_data object and pass it in as argument 1 (rdi)
    • dynamically calculate the address for this: mov rdx,QWORD PTR [rip+0x210ff9] … and pass it in as argument 2 (rsi)
    • instead of calling the zend vm handler, store that address as the handler in the opline (current instruction). This would cause the zend vm handler to be called instead of the SG handler.
    • don’t restore the operands! they’ve already been modified to reflect the correct ones. e.g. jmp destination will make sense

    Here is my function for fixing JMP operations. The instructions I’ve added or edited are bold:

    fix_jmp:
    mov rdx, QWORD PTR [rsi] # set rdx to point to some structure containing other pointers
    push rbp
    movabs rsi, 0xaaaaaaaaaaaaaaab
    push rbx
    sub rsp, 0x8
    mov rbx, qword ptr [rdi] # rdi points to opline
    mov rax, qword ptr [rdi+0x28]
    movsxd rdx, dword ptr [rdx]
    mov rbp, qword ptr [rbx+8]
    mov rcx, rbp
    mov rdx, qword ptr [rax+rdx*8+0xd0]
    mov rax, qword ptr [rax+0x40]
    sub rcx, rax
    mov rdx, qword ptr [rdx]
    sar rcx, 0x4
    imul rcx, rsi
    shl rcx, 0x4
    mov ecx, dword ptr [rcx+rdx]
    lea rcx, qword ptr [rcx+rcx*2]
    shl rcx, 0x4
    lea rcx, qword ptr [rax+rcx]
    mov qword ptr [rbx+8], rcx
    mov rcx, rbx
    sub rcx, rax
    mov rax, rcx
    sar rax, 0x4
    imul rax, rsi
    shl rax, 0x4
    # originally this would call ZEND_SPEC_JMP_HANDLER
    # but now, we'll just set the opline->handler to the real one
    mov rcx, qword PTR [rdx+rax+8]
    mov qword PTR [rbx], rcx
    # removed
    # this would reset op1 values to original "obfuscated" values
    # mov qword [rbx+8], rbp

    add rsp, 0x8
    pop rbx
    pop rbp
    ret

    This process was repeated for all of the custom operation handlers. A new function was created to fix various instruction types.

    Once I was able to fix all instruction types that SG seemed to have mangled, there was one final (or two, really) hurdle to jump over. The problem was that, since I was hooking zend_execute, I was only dumping opcodes that were actually being executed. So for example, the “main” part of a PHP file would be dumped because it was the logic that had to run. But as we’ll see, this leaves out some key components.

    Functions and Classes

    Any functions that were defined but were never executed would not be dumped. This was true for classes and their methods as well.

    We’ll look at an example with classes, since it tests both.

    <?phpclass ClassOne
    {
    function func_one()
    {
    echo "one";
    }function notused_one()
    {
    return 1;
    }
    }class ClassTwo
    {
    function func_two()
    {
    echo "two";
    }function notused_two()
    {
    return 2;
    }
    }$a = rand(1, 2);if ($a == 1)
    {
    $b = new ClassOne();
    $b->func_one();
    }else
    {
    $b = new ClassTwo();
    $b->func_two();
    }?>

    There are two classes, each with a method that could be used and a “notused” method that will absolutely not be called. Depending on whether rand() returns a 1 or 2, either ClassOne->func_one() or ClassTwo->func_two() will be executed. The output will indicate which method was called.

    As you can see in this output, ClassOne->func_one() was called. The main logic of the script is dumped along with func_one(). However, notused_one() is missing from the output as well as all of ClassTwo’s methods.

    The key to dumping the unused classes and functions is to access the compiler globals function table and class table. The only trick is that these tables need to be “fixed” prior to dumping, just like we’ve done before. Every function entry is a zend_op_array, so we can apply the same “fixing” logic to functions and class methods.

    Wrapping Up

    All in all, the main opcode dumping logic, handled in vld_execute, looks like the below snippet. First the main op_array is dumped. After this, any functions are dumped that exist in the function_table, and finally, the class_table is searched for methods, and these methods are dumped as well.

    // first, fix opcodes not contained in a function or class
    if (op_array->function_name == NULL || strlen(op_array->function_name) == 0) {
    fix_op_array(op_array);
    vld_dump_oparray (op_array TSRMLS_CC);
    }// now fix defined functions
    zend_hash_apply(CG(function_table), (apply_func_t) vld_fix_fe TSRMLS_CC);
    zend_hash_apply_with_arguments (CG(function_table) APPLY_TSRMLS_CC, (apply_func_args_t) vld_dump_fe, 0);// now fix defined classes and class funcs
    zend_hash_apply (CG(class_table), (apply_func_t) vld_fix_cle TSRMLS_CC);
    zend_hash_apply (CG(class_table), (apply_func_t) vld_dump_cle TSRMLS_CC);

    The “fix_op_array” function is responsible for “fixing” all of the op_arrays, and it is used inside vld_fix_fe as well. This function performs several tasks including calculating offsets within the SG loader extension, determining which opcodes to fix, and ultimately, calling the functions that were implemented to “fix” the op_arrays. Here is a switch case showing the opcode numbers that are handled. Notice that several opcodes can map to the same fix function.

    switch (execute_data->op_array->opcodes[i].opcode)
    {
    // 42
    case ZEND_JMP:
    // 100
    case ZEND_GOTO:
    fix_jmp(execute_data, sg_offset);
    break;
    // 46
    case ZEND_JMPZ_EX:
    // 47
    case ZEND_JMPNZ_EX:
    // 152
    case ZEND_JMP_SET:
    // 158
    case ZEND_JMP_SET_VAR:
    fix_jmpnz_ex(execute_data, sg_offset);
    break;
    // 45
    case ZEND_JMPZNZ:
    fix_jmpznz(execute_data, sg_offset);
    break;
    // 68
    case ZEND_NEW:
    // 78
    case ZEND_FE_FETCH:
    // 77
    case ZEND_FE_RESET:
    fix_new(execute_data, sg_offset);
    break;
    // 107
    case ZEND_CATCH:
    fix_catch(execute_data, sg_offset);
    break;
    default:
    break;
    }

    If you’re interested in viewing all of the code, take a look at the project on GitHub. The “fix” functions are all defined in fix_sg.S. Keep in mind that this is all tailored to the SG 5.4 Linux x86_64 loader extension. Additionally, to limit the length of output, I’ve coded things up so that no includes will be dumped.

    Before you leave, let’s see a fully decoded class.php. I’ve had to split the output up into multiple images due to the size.

    “main” function
    ClassOne
    ClassTwo and the output (“Two”)

    There you have it. By hooking zend_execute() and fixing opcodes using SourceGuardian’s own decoder logic, we can dump an encoded file with VLD’s functionality. As I said before, the decoder was implemented to target encoded PHP 5.4 files on an x86_64 Linux environment. If you find any bugs or see improvement opportunities, please feel free to reach out

  • PHP:vld扩展的安装与使用

    PHP:vld扩展的安装与使用

    一、安装

    1、下载官方插件安装压缩包

    官方网址:http://pecl.php.net/package/vld

    下载命令:

    wget http://pecl.php.net/get/vld-0.17.0.tgz
    

    注:下载的URL是在相对的版本链接上,点击右键,复制链接即可

    2、解包

    解包命令:

    tar zxvf vld-0.17.0.tgz 
    

    3、编译和安装

    进入解压后的vld目录:

    cd vld-0.17.0/
    

    扩展php扩展模块:

    phpize
    

    使用locate找php-config路径:

    locate php-config

    注:locate命令没有的话可以使用命令:【# yum -y install mlocate 】 安装后使用 【#  updatedb】 更新数据后可以直接使用

    配置编译vld的php-config路径(替换?): 

    ./configure --with-php-config=? --enable-vld

    编译安装:

    make && make install
    

    编辑php.ini,添加vld.so新扩展:

    extension=vld.so
    

    4.重启php配置生效

    二、使用

    注意:当有多个PHP版本时,运行php命令,需要指定装有vld扩展的php版本路径命令!

    1.linux多PHP版本下指定PHP版本执行命令?

    以php7.4版本为例,该版本执行文件命令路径为:

    /www/server/php/74/bin/php
    

    进入命令行的配置文件.bashrc,添加:

    alias php74=/www/server/php/74/bin/php

    就可以用php74 执行命令了!

    2.vld命令,显示opcode

    ①显示opcode,并显示运行结果

    php74 -dextension=vld.so -dvld.active=1 test.php

    ②只显示opcode

    php74 -dextension=vld.so -dvld.active=1 -dvld.execute=0 test.php
    

  • PHP:利用vld扩展SG11解密基础学习

    PHP:利用vld扩展SG11解密基础学习

    什么是SG11?

    • Source Guardian,一种PHP加密器,可以说是目前最好的加密方式了,多用于保护源代码不被盗取倒卖。

    • 它的代码特征是文件中包含:sg_load(

    • 搜索后,发现这类SG11解密方面的教程非常少,几乎没有。但也能看到有一些Decoder提供解密服务,价格基本在100-200元/文件。价格之贵,足以说明它的保密性了。

    解密原理

    通过安装PHP vld扩展,用操作码OP对PHP文件进行逆向解密。

    [content_hide]

    vld.c 文件
    
    //if (!VLD_G(execute)) {			
    //}
    
    vld_dump_oparray(&execute_data->func->op_array);
    return old_execute_ex(execute_data TSRMLS_DC);
    ----------------------------------------------------------------
    srm_oparray.c 文件
    
    #include "zend_smart_str.h"
    #include "ext/standard/php_var.h"
    
    static inline int vld_dump_zval_double(ZVAL_VALUE_TYPE value)
    {
    	return vld_printf (stderr, "%f", value.dval);
    }
    
    
    static inline int vld_dump_zval_array(zval* value)
    {
    	smart_str buf = {0};
    	php_var_export_ex(value,1,&buf);
    	smart_str_0 (&buf);
    	ZVAL_VALUE_STRING_TYPE *new_str;
    	new_str = php_url_encode(ZSTRING_VALUE(buf.s), buf.s->len);
    	int ret = vld_printf(stderr,"%s",ZSTRING_VALUE(new_str));
    	efree(new_str);
    	smart_str_free(&buf);
    	return ret;
    }
    
    case IS_ARRAY:          return vld_dump_zval_array (&val);

    [/content_hide]

  • 通过宝塔面板实现多端口建站与SG11解密

    通过宝塔面板实现多端口建站与SG11解密

    实践过程

    通过宝塔面板搭建网站

    说明 

    因为篇幅原因,在这里只写几个主要的小问题,也是我在之前第一次搭建过程中遇到的问题,具体搭建教程网上都有。

    • 在安装宝塔面板之前,请注意将云服务器的操作系统更换为 CentOS操作系统,因为我在初始配置中选的默认系统,结果不知何种原因,在安装宝塔面板后,无法安装Web服务器(Nginx)。3
    • 记得一定要在安全组配置里开放宝塔面板的端口(8888),否则打不开,也可以把其他常用端口打开。4
    • 小技巧:如何在没有域名及二级域名的情况下,通过不同的IP端口来实现访问不同的网站内容?
      1. 添加站点,随便输入一个域名(例:aliyun.com),创建数据库,提交。5
      2. 在设置-域名管理,添加域名(格式:IP:端口号)(ps:端口号尽量避开常用端口),然后添加,如下图。6添加成功,可以把之前的那个删掉。7
      3. 在安全组/防火墙中,开放上一步添加的端口号,只有配置了端口号,我们才能打开网站。8
      4. 在浏览器输入地址,网站创建成功。按照这样的步骤,我们就可以创建不同的IP端口地址进入不同的网站了。9

    SG11解密

    What?

    • Source Guardian,一种PHP加密器,可以说是目前最好的加密方式了,多用于保护源代码不被盗取倒卖。
    • 它的代码特征是文件中包含:sg_load(
    • 搜索后,发现这类SG11解密方面的教程非常少,几乎没有。但也能看到有一些Decoder提供解密服务,价格基本在100-200元/文件。价格之贵,足以说明它的保密性了。

    Why?

    因为最近买了一个源码,部分文件就是用SG11加密的,很想尝试给它破解了。(仅用于学习)

    How?

    结果国内SG11解密教程非常少,找了许久,才找到一个只有四小节的视频课程,这里就不放视频了,还有在外国网站上看到的 SG11解密教程,之后都会在无错源码上单独发出来。

    那么具体是怎么做的呢?

    1. 首先需要下载vld(PHP的扩展),然后把它上传到ECS服务器中,并解压。说明 什么是vld?是一个PHP扩展,它可以查看PHP程序的opcode,也就是操作码。10
    2. 通过一系列配置(配置较多,就不放在这了),安装成功。11
    3. 简单操作:将一个简单加密文件解密。
      1. 先写一个php文件,例如:Helloworld!12
      2. 输入命令php -dvld.avtive=1 index.php,然后就能看到它的操作码op。13
      3. 再来给index.php文件进行SG11加密。结果如下图。14
      4. 再次输入命令php -dvld.avtive=1 index.php ,如下图。15

    以上是我使用vld对SG11加密文件解码的一个基本操作,具体解密还需要一定的操作码知识和PHP知识,利用操作码对PHP文件进行逆向解密。

  • 杰奇1.80开源源码

    杰奇1.80开源源码

    虽然杰奇2.0,3.0,3.1出来很久了,但是杰奇1.7,1.8仍然是使用得最多的。一个是因为更加稳定,另外也是因为模板比较多。

    2.0,3.0以及后续的版本在性能上并没有什么突破,只是加了一些原创的功能,这对于很多人来说是没有必要的。

    1.8和1.7的安装方式一致,这里不再累述。

    无错源码提供的是非编译过的源码文件,可以完美无错运行且不需要Zend环境。

  • 一个好玩的小说采集程序For杰奇

    又找到一个好玩的杰奇的小说采集程序,支持目标站登录采集。界面很友好,好看又简单。

    看截图:

    该源码站长没有测试过,所以放出来积分下载就可以了。

    有精力的可以试一下。很好玩的一个系统。

  • 7月18日1中午12点开启:夏季冰点价 Megalayer高防服务器低至299元

    Megalayer推出夏季冰点特价活动,多款服务器等您来!

    活动一:香港双E5-2450L/16G/1T HDD 399元/月

    香港2*E5-2450L/16G/1T HDD特价返场,仅需399元/月,续费同价。

    地区机型内存硬盘可选带宽原价促销价格下单链接
    香港2*E5-2450L16G1T HDD10M 优化带宽15M 全向带宽20M 国际带宽900元/月399元/月立即购买

    活动二:美国I3-4130/4G/180G SSD 99元/月

    美国I3/4G/180G SSD,99元/月,续费同价。此配置仅有少量库存,先到先得!

    地区机型内存硬盘可选带宽原价促销价格下单链接
    美国I3-41304G180G SSD30M 优化带宽100M 全向带宽200M 国际带宽399元/月99元/月立即购买

    活动三:香港高防服务器

    以下机型,服务器中含1个管理IP+1个20G高防IP,香港防御接入香港本地清洗,低延时高性能,能有效抵御DDoS攻击。

    地区机型内存硬盘可选带宽防御等级原价促销价格下单链接
    香港E3-12308G1T HDD10M 优化带宽15M 全向带宽20M 国际带宽20G1569元/月299元/月立即购买
    香港双E5-2450L16G1T HDD10M 优化带宽15M 全向带宽20M 国际带宽20G1970元/月499元/月立即购买

    香港高防服务器均为香港本地清洗,提供中国电信、中国联通本地清洗,延时和质量接近优化带宽;中国移动攻击会送到美国进行清洗,大陆地区延时会增加,并有少量丢包情况。

    测试IP:154.218.0.1

    活动四:美国高防服务器

    地区机型内存硬盘可选带宽防御等级原价促销价格下单链接
    圣何塞E3-12308G1T HDD30M 优化带宽100M 全向带宽200M 国际带宽 100G1499元/月299元/月立即购买

    美国高防服务器洗均为国际清洗,质量接近全向带宽,大陆地区延时会增加,并有少量丢包情况。

    测试IP:154.31.19.254

    活动五:大带宽服务器

    本次大带宽服务器,精选两款经典产品,支持G口以上带宽,每个大带宽均搭配3个独立IP,均支持硬件升级。

    地区机型内存可选硬盘带宽配置原价促销价格下单链接
    圣何塞E3-12308G240G SSD1T HDD1G 全向带宽 3469元/月999元/月立即购买
    圣何塞双E5-266032G240G SSD1T HDD10G 全向带宽 33770元/月9499元/月立即购买

    全向带宽测试IP:154.64.8.254、45.201.245.254

    活动六:年付VPS下单立减10元

    除1C1G年付VPS外,本次新增了2C2G、2C4G年付特价VPS,下单使用下方优惠码,立减10元。

    SUMMERVPS10

    地区核心/内存IP数量带宽价格促销价格订购链接
    香港1核1G1个普通IP优化带宽 2M199元/年189元/年立即购买
    香港2核2G1个普通IP优化带宽 2M299元/年289元/年立即购买
    香港2核4G1个普通IP优化带宽 2M399元/年389元/年立即购买
    圣何塞1核1G1个普通IP优化带宽 10M199元/年189元/年立即购买
    圣何塞2核2G1个普通IP优化带宽 10M299元/年289元/年立即购买
    圣何塞2核4G1个普通IP优化带宽 10M399元/年389元/年立即购买
    圣何塞1核1G1个普通IP全向带宽 20M199元/年189元/年立即购买
    圣何塞2核2G1个普通IP全向带宽 20M299元/年289元/年立即购买
    圣何塞2核4G1个普通IP全向带宽 20M399元/年389元/年立即购买
    新加坡1核1G1个普通IP优化带宽 3M199元/年189元/年立即购买
    新加坡2核2G1个普通IP优化带宽 3M299元/年289元/年立即购买
    新加坡2核4G1个普通IP优化带宽 3M399元/年389元/年立即购买
    新加坡1核1G1个原生IP优化带宽 3M249元/年239元/年立即购买
    新加坡2核2G1个原生IP优化带宽 3M349元/年339元/年立即购买
    新加坡2核4G1个原生IP优化带宽 3M449元/年439元/年立即购买

    活动七:显卡服务器

    香港显卡服务器,搭配1050Ti 4G显存,满足游戏模拟器、视频提升等业务需求,现8折优惠:

    GPUSERVER20%OFF

    地区机型内存硬盘可选带宽显卡原价促销价格下单链接
    香港双E5-266064G480G SSD10M 优化带宽15M 全向带宽20M 国际带宽 1050Ti (4GD5)2350元/月1880元/月立即购买 

    注意事项:

    1.      本次优惠活动为7月18日中午12点起,至8月31日,或至产品售罄为止;

    2.      所有产品均续费同价;

    3.      所有活动机器不支持退款,下单前请务必确认清楚;

    4.      支付成功后,订单将进入审批(pending)状态,机器将按序通过邮件交付,请您耐心等待;

    5.      此活动机器仅指活动套餐产品,升级部分均按原价计费;

    6.      此活动不与季付95折、买五赠一、买十赠二优惠同享;

    7.      若升级产品硬件,交付时间可能会延迟,请以工单的沟通为准;

    8.      活动解释权归Megalayer所有。

  • m3u8的ts文件的PES加解密分析以及示例

    m3u8的ts文件的PES加解密分析以及示例

    一、前言

    最近有朋友问我,某个视频网站也是阿里ts加密方式。恰巧51假期,就拿来分析一番,一看代码与之前某视频网的加密方法几乎完全一样。唯一不同的是 AES解密时逻辑稍有不同。还有一些奇怪的问题,同时发现,自己写过的代码,自己都已经不理解了,之前吾爱发的解密文章,被xx了,综合种种吧,冒出了写此文,算是一个复习,同时把方法分享给大家。此外,前些日子有个朋友在帖子中提到了PES解密的问题,希望此文也可以帮助到他。@VOOV

    二、TS文件结构概述

    1、几个基本概念
    ES流(Elementary Stream) 基本码流,不分段的音频、视频或其他信息的连续码流。
    PES流 把基本流ES分割成段,并加上相应头文件打包成形的打包基本码流。PES是打包过的ES,已经插入PTS和DTS,一般一个PES是一帧图像。
    TS流(Transport Stream) 传输流,将具有共同时间基准或独立时间基准的一个或多个PES组合(复合)而成的单一数据流(用于数据传输)。
    其数据内容可包含视频、音频、字幕等数据。将一个视频切成多个ts文件,实现视频的分段传输。多用于电视媒体。

    2、ts文件格式
    ts文件由ts数据包组成,每个包大小为188字节(或204字节,在188个字节后加上16字节的CRC校验数据,其他格式一样),每个数据包存储的内容可能不同,可能是视频、音频、字幕,或索引表信息,索引表就类似于一本书的目录,通过目录,就可以找到需要的章节,章节就类似于视频或音频等数据。
    注:本文所描述的ts包,均为188字节。

    ts数据包 由 4字节包头、附加数据(一般用来填充,为了满足188字节)、负载数据(即PES的部分数据)如下图:

    一个完整的PES包数据,可能存在于多个ts数据包中,也就是说,一个ts包中,可能含有pes包的包头,也可能仅仅含有pes包的负载数据.
    下图展示了,PES包是如何转为TS包的。

    下面来分析占4字节(32比特)ts包头的结构以及附加域(长度不定)的结构。先上图。

    这里我们仅分析我们用到的字段,其中头中用到4个字段值,附加域只用到长度字段。如下表。

    序号标识位数说明
    0sync_byte8 bits同步字节,固定是0x47
    即每个ts包的首字节都是0x47
    2payload_unit_start_indicator1 bit负载单元开始标识
    用来判断是否是pes包的起始包
    若为0,则表示非起始包。
    非PES起始包,不含有PES包头
    4PID(Packet ID)13 bitsts包的数据类型
    ts包有几种数据类型:
    PAT、PMT、音频、视频、字幕等
    6adaptation_field_control2 bits附加域数据标识,有如下值:
    00:供未来使用,出ISO/IEC所保留
    01:无adaptation field,仅有效载荷
    10:仅有Adaptation field,无有效载荷
    11:Adaptation field后,带有效载荷
    翻译下:
    因为ts包长度固定188字节,因此
    若附加域数据过多,就会无法装载payload
    附加域中的字段
    0adaptation field length8 bits自适应域长度,后面数据长度
    除去本字段外,附加域其余字段的长度

    表中提及的PAT、PMT相当于一本书的目录,PAT相当于目录的目录,通过他们就可以找到某视频的位置。
    PAT的pid为0,首先我们就会分析PAT。
    接下来分析下PES头的数据格式。为我们后面解密做铺垫。先上图。

    字段很多,只分析我们需要的字段。如下表:

    序号标识位数说明
    0pes开始标识24 bitspes包开始标识
    固定值:0x000001
    10PES头中后面数据的长度8 bitspes头后面字段的长度
    pes头的长度就等于:
    本字段以及之前所有字段的长度
    加上本字段的值

    这里其实只要拿到pes头数据的长度。显然通过第10个字段,就可以计算出pes头的长度了。
    以上知识点,就可以支撑我们继续分析ts文件的加解密了。

    三、ts加密分析

    结合代码,我们分析下加密的逻辑。

    为了便于调试,这里我用未解密的video.ts文件作为样例,以及自己写的解密demo,来分析。
    (关于demo以及源代码等,我会放在文末)

    用其他软件(我用SublimeText)以16进制的形式,打开video.ts。
    这个一直开着,用来与代码读取的数据进行对比。看我们代码读的数据是什么。为什么这么读。

    a、首先找到ts文件数据解析函数。这里就是append(…)函数。(关于如何定位此函数,请参考我之前的文章)

    运行demo, 输入key,导入ts。提前在append函数首行打上断点。点击开始解密。会进入我们的断点。

    接下来看下我们传入的ts数据。

    以16进制的形式打印e1的值,与我们的video.ts数据对比,是一致的。
    看下图(此步骤,没啥意义,就是为了找找感觉)

    继续,在587行 C = syncOffset(e); 代码处添加断点,继续执行,程序会停留在此断点。
    此函数是在找 ts包的起始偏移,因为每个ts包都是188字节,
    所以此函数就通过判断连续3个188字节的首字节是否是71(16进制0x47), 若是则确定此索引为起始索引。
    我们这里都是0,也就是ts文件的第一个字节就是0x47,细心的朋友,已经发现了。

    接下来进入循环开始解析ts数据了。注意代码中 bill开头的函数与变量,是用来解密的。暂时忽略。
    在638行,也就是for循环的第一行,加断点,继续执行,会停在这里。我们分析下for循环的条件。先看看图。

    看637行的for循环

    for (o -= (o + C) % 188, a = C; a < o; a += 188)

    这里C是587行同步偏移返回的值,我们这里都是0。所以for循环就等于以下:

    for (o -= o % 188, a = 0; a < o; a += 188)

    这就清晰多了
    这里只有2个变量,a和o,a初始值是0,然后每次循环累加188,看看o是哪里来的。
    在本函数的第三行,也就是568行,看到 o = e.length, e在上一行,就是我们ts数据的uint8数组。
    因此,o就是ts数据的总长度, 那么o -= o % 188,是什么意思?
    先用总长度对188取余,然后总长度再减去余数, 也就是说,是为了保证我们循环总长度为188的整数倍。
    为什么这么做?是为了循环体内,不出现数组越界情况。(循环内部会分析)
    延伸下,这里循环结束后,取余出去的那部分数据不就没有分析到了嘛。
    所以当循环结束后,还得解析取余出去的那部分数据。这样整个ts文件数据就都被解析到了。

    继续,看638行的 if (71 === e[a]) ,显然这是在判断ts包的首字节是否为71(71是十进制,16进制0x47)
    如果首字节是0x47,则分析此包数据。否则直接报错。
    此时a为0,那么我们看看e[0]的值,确实是71。
    去之前打开的video.ts文件,看看第一个字节是不是0x47。一定是的。

    目前,我们是在video.ts文件的第一个字节处,也就是第一个ts包。此时方便我们查看本地的video.ts的数据。
    所以结合ts文件格式和代码,我们分析下一段代码,就是 639->643行间的 if…else…

    先来看 639行:

    if (f = !!(64 & e[a + 1]), c = ((31 & e[a + 1]) << 8) + e[a + 2], (48 & e[a + 3]) >> 4 > 1)

    好家伙,看着就懵逼的感觉。
    可以看到,if条件内,有3个语句,逗号分割,当最后一个语句为真时,就会进入if内部。
    也就是说,前2个语句,就是执行下,跟if条件没啥关系。那也得分析&#128516;

    先来看第一个语句

    if (f = !!(64 & e[a + 1]), c = ((31 & e[a + 1]) << 8) + e[a + 2], (48 & e[a + 3]) >> 4 > 1)

    叹号取反,双叹号就是负负得正。等于没有。所以只看: 64 & e[a + 1]

    我们知道a是0,那么e[a+1],显然就是video.ts的第二个字节的值。
    我们可以看到,e[1]的值也为64 , 然后再 与 64 进行与运算。
    我们把64都转为2进制(1个字节8bits, 所以补足8位)
    64:  0100 0000
    64:  0100 0000

    然后进行与运算。
    可以发现和64进行与运算的目的,就是取 取本字节8位中的左起第二位。
    该bit就是ts头中的第9位(0开始),前面我们分析过 ts头的第9位是payload_unit_start_indicator,
    即负载标志位。判断本ts包的负载数据是否是pes的起始包。
    (不理解的话,可以翻阅ts文件结构概述章节)

    因此我们可以知道
    f 即判断本ts包的数据是否是pes的起始包。(若是起始包,包含pes头)
    若是起始包,则f为1,否则0

    继续看第二个语句:


    直接翻译下:
    把 第二个字节的值 和 31 进行与运算,然后左移8位,再和第三个字节值 相加。
    分析过程省略,大家自行操作。
    上结果,c的值就是 ts头中占有13个比特的pid。
    pid代表了ts包的数据类型,可以是音频,视频、PAT、PMT或其他

    c = ((31 & e[a + 1]) << 8) + e[a + 2]
    

    此时的pid,不用看,一定是0,0代表是PAT。
    这里再介绍下PAT与PMT。
    PMT存储了媒体的目录信息,哪个视频在哪里,哪个是音频等
    PAT则是存储了PMT的信息,PMT在哪之类的。

    因此一开始一定是先解析PAT,通过PAT找到PMT,解析PMT找到我们需要的 音视频数据。

    继续看第三个语句:

    (48 & e[a + 3]) >> 4 > 1
    翻译:
    第四个字节和48进行与运算,右移4位,然后看是否大于1

    分析略,直接上结果:

    给(48 & e[a + 3]) >> 4 起个名字叫k吧,
    k的值就是 ts包头的32位占2bits的 adaptation_field_control,附加区域控制字段。
    该字段的值,用来判断附加区域是否存在,大于1 表示存在 附加域。(具体可看上一章节)

    由此,我们可以知道,只要存在附加域,就会进入if内部。
    若不存在附加域,则执行else,稍后分析。

    先来看if内部,也就是640行:

    if ((d = a + 5 + e[a + 4]) === a + 188)

    因为此时,a=0,所以简化下d的等式:
    d = 5 + e[4]   ===  188

    翻译下: ts的第5个字节值加上5。
    我们知道ts的头是4个字节,并且此时在if内部,即是存在附加域的。
    因此 我们去上一章节 看下附加域的数据格式,可以知道:
    第一个字节(8bits)代表的是adaptation_field_length, 即附加域后面的数据长度。就是此字节后面的数据长度。
    那么再加5,就表示算上 4字节的ts头长度,以及 adaptation_field_length 所占的1字节。
    也就是说 d = 5 + e[4] 的值,就是 ts头长度 和 附加域长度 之和,
    那么和188比较是为什么?  因为ts包的总长度为188,当ts头和附加域的总长度已经达到188时,就不会存在负载数据了,
    所以就不必继续分析此包,直接 continue,继续下一个包解析。

    好,接下来看看else代码,就一行,643行:d = a + 4;
    相信大家应该能猜到了。这里的4就是ts头的长度,d = a + 4,d 即表示ts负载数据的起始索引了。

    综上, 简单总结下这个if … else …

    1、f: 计算ts包的负载数据是否是pes的包的起始包。
    2、c: 计算ts包的pid
    3、判断是否存在附加域,若存在计算附加域和ts头的总长度。得到ts负载数据的起始索引d的值。
    4、若不存在附加域,则 ts负载数据的起始索引 d 的值为:包起始索引 + 4(ts头的长度)。

    结论:f表示是否是pes起始包, c代表pid, d表示ts包负载数据的起始索引。
    f、c、d 后面会一直用。 如下图:

    接下来就是 switch 语句了。

    switch (c) {
        case m:
            f && (E && (l = D(E)) && bill_appendTsData(l,d) && void 0 !== l.pts , E = {
                data: [],
                size: 0,
                bill_dataIdx:[]
     
            }), E && (E.data.push(e.subarray(d, a + 188)), E.bill_dataIdx.push(d), E.size += a + 188 - d);
            break;
        case _:
            f && (T && (l = D(T)) && bill_appendTsData(l,d) && void 0 !== l.pts, T = {
                data: [],
                size: 0,
                bill_dataIdx:[]
     
            }), T && (T.data.push(e.subarray(d, a + 188)), T.bill_dataIdx.push(d), T.size += a + 188 - d);
            break;
        case w:
            f && (A && (l = D(A)) && bill_appendTsData(l,d) && void 0 !== l.pts , A = {
                data: [],
                size: 0,
                bill_dataIdx:[]
     
            }), A && (A.data.push(e.subarray(d, a + 188)), A.bill_dataIdx.push(d), A.size += a + 188 - d);
            break;
        case 0:
            f && (d += e[d] + 1), S = R(e, d);
            break;
        case S:
            f && (d += e[d] + 1);
            var O = k(e, d, true, false);
            m = O.avc, m > 0 , _ = O.audio, _ > 0 , w = O.id3, w > 0 , p && !b && (p = !1, a = C - 188), b = !0;
            break;
        case 17:
        case 8191:
            break;
        default:
            p = !0
    }

    我们前面分析知道 c 就是pid, 因此,switch,就是根据pid来进行解析不同数据包。

    看下 switch的case值:
    case m: , case _: , case w: , case 0:, case S:, case 17:, case 8191: , defalut:
    只有 m 、_ 、w 、 S ,4个变量的未知。

    我们知道此时 c的值是0, 会进入 case 0 分支的代码,
    此处是解析PAT,S = R(e, d); 得到S的值。

    看S分支的代码,我们可以看到其中会给 m,_,w 3个变量赋值,其实S是解析PMT。

    PMT解析完,就得到了 其他3个case 分支的值,我们继续看其他 case m,_,w 分支的代码,
    非常像,只是变量不同。通过分析知道,此3个分支就是解析加密数据的部分。在此不再叙述。

    接下来就分析这3个分支的一个, 就选第一个case m

    直接在case m 分支内部第一行打断点,即646行,其他断点全部过掉,然后继续执行。程序停在了646行。

    分析下变量的值:
    首先分析:f,表示是否是pes起始包。 此时的f的值一定是 1(true),为什么?
    因为我们是第一次进入m分支,说明我们第一次解析pid为m的类型ts包,第一次解析此包,说明它一定是pes的起始包。
    所以 f 一定是1, 结合上一章节pes包在ts包中的装载格式,就会明白,pes的包被分割到不同的ts中,
    那么切割到第一个ts 包中的pes数据,一定包含pes的包头,所以该ts的 f 值一定是1 。如下图:

    f 是1 ,就会继续执行f后面的代码。

    接下来一行一行分析下 case m 的代码。bill_开头的代码,暂时过滤,是解密用的。

    case m:
        f && (E && (l = D(E)) && bill_appendTsData(l,d) && void 0 !== l.pts , E = {
            data: [],
            size: 0,
            bill_dataIdx:[]
     
        }), E && (E.data.push(e.subarray(d, a + 188)), E.bill_dataIdx.push(d), E.size += a + 188 - d);
        break;

    有两个语句以逗号分割,两个语句之间是依次执行。

    分析语句1:
    f && (E && (l = D(E)) && bill_appendTsData(l,d) && void 0 !== l.pts , E = {
            data: [],
            size: 0,
            bill_dataIdx:[]

        })

    翻译以下:
    当 f 为真时, 若E 有值,则执行 (l = D(E)) && bill_appendTsData(l,d) && void 0 !== l.pts,并给E重新赋值
                            若E 为空,则直接给E赋值
    当 f 为假时, 后面代码不会执行,语句1结束

    这里 l = D(E), 此代码将加密的PES数据解密,返回给l

    分析语句2:
    无论语句1如何执行,语句2都会执行。

    E && (E.data.push(e.subarray(d, a + 188)), E.bill_dataIdx.push(d), E.size += a + 188 – d);

    若E 为真,则给E的data添加 e的索引d到a+188之间的数据, 给E的size累加值: a + 188 -d ,这是刚才添加数据的长度。
    若E 为空, 则结束

    我们知道 d是 ts包负载数据的起始索引,d > a, a是ts包的起始索引。所以 e.subarray(d, a + 188),这个数据,就是ts包的负载数据。

    因此语句2的目的就是:将ts包的负载数据添加到 E.data中,同时记录下添加的数据的总大小。

    我们将语句1和2一起翻译下:

    当f为真时,即ts包负载是pes的起始包,若E为存在值,则直接去解密E的数据,返回给l,
    接下来则给E重新赋值,然后将此时ts的负载数据,添加到E.data中,并记录总大小size

    当f为假时,即ts包负载不是pes的起始包,将此时ts的负载数据,添加到E.data中,并记录总大小size

    我们可以发现规律,只有当 f 为真时且E数据存在,会去解密pes数据,且解密的数据是 f为假时, 添加到E.data中的数据。

    由此,我们可以得出,加密的数据是一个完整的PES数据,(PES头未加密,需要在pes解析中分析才能知道)。且这些PES数据存在于多个ts包中。

    接下来分析PES解密函数:l = D(E)

    在此函数的第一行,即:457行,打断点,删除其他断点,继续执行。会停留在此处。

    查看下传进来的参数t的值,其实就是上个函数的E的值,发现有size与data。
    其中data即pes的数据,data是个数组,数组内的元素其实是 存在于各个ts包中的pes数据。看图:

    直接断点到493行,在这里我们分析下 c 的值,这个比较重要。

    在476行, c = a + 9, a = r[8] , r = u[0], u其实就是我们的传进来的t.data
    我们观察下u[0]的数据,发现开头的三个值是 0 0 1, 这3个值是 0x 00 00 01,表示PES包的开始。
    所以u[0], 就是第一个ts包的负载,也就是包含pes包头的负载数据。

    也就是说,r = u[0]的数据中是有pes头数据的。
    结合我们上一章节的PES头数据格式,分析下a = r[8], 可以知道r[8]就是PES中占8bits的,PES头中后
    面数据长度的字段。也就是说,r[8]的值就是PES头中,此字段后面的数据的长度。

    那么 c = a + 9, 其实就是 PES头的总长度。此处c的值为31。

    因为r[8]字段的值代表PES头后面剩余数据的长度,
    加上本字节以及之前字节的长度,所以就是PES头的总长度了。

    接下来继续分析:
    将断点设在518行,继续执行,程序停留在518行。
    查看下o的值、长度,以及t.data的第一个的值,对比下。看图:

    可以看到o的值比t.data的总长度少了 31,就是c的值。
    再看o的值与t.data[0]的数据从第31个索引开始,是不是完全相同了。

    说明上面497行的for循环做的事就是:将PES的数据合并到一起,并去除PES头的数据。o便是结果。

    for (var b = 0, g = u.length; b < g; b++) {
        r = u[b];
       [/b] var v = r.byteLength;
        if (c) {
            if (c > v) {
                c -= v;
                continue
            }
            r = r.subarray(c), v -= c, c = 0
        }
        o.set(r, e), e += v
    }

    再看518行:o = startAES(o);
    此代码就是将 去除PES头的数据进行解密。得到解密后的数据。

    本函数将解密后的PES数据返回。进行下一步处理。

    由此我们知道,此ts的加密方式是对每个pes的负载数据(去除pes头)进行加密的。

    至此,ts的加密逻辑分析完成。
    总结下:

    1、程序首先加载ts数据
    2、每188个字节的循环,解析ts包
    3、根据包的数据类型(pid判断),去进行不同的解析。
    4、先解析PAT、得到PMT、得到其他媒体数据音视频等
    5、将存在于多个ts包中的pes包的数据以及总大小,保存至变量。
    6、将取得的PES包的数据和大小,传递给pes解析函数
    7、解析函数将所有pes数据组装到一起并去除PES头
    8、将组装的后的 pes数据,传给AES解密函数进行解密
    9、得到解密后的PES数据,返回给播放器

    我们现在知道了ts的数据是如何解析的,数据是在哪里解密的,以什么形式加密的。
    那么接下来就来分析下,我们如何对ts文件进行解密。

    四、如何进行解密

    聪明的你,估计已经想到了。既然我们在上一章节拿到了解密数据,那么把解密数据,替换掉加密数据,然后重新保存ts,不就ok了吗

    我只能说,聪明!!!

    先分析下思路:
    我们已知道 加密数据存在于多个ts包中,将多个ts中的数据提取,然后整和,再去解密,得到解密的整和数据。

    所以,我们就要将 解密后的数据 进行拆分 到多个ts中。

    得到解密的数据: 多个ts包 –> 得到待解密的pes –> 得到解密的数据

    将解密数据还原: 解密的数据 –> 拆分到解密数据 –> 复原到多个ts包中

    如何拆分解密的数据?
    根据解密时,传递进来的整和的pes数据的size来进行拆分。

    如何复原到ts包中?
    记录解密时,获取pes数据时,pes数据所在的索引。
    根据索引将相应的数据替换ts中的数据。

    下面来具体操作:
    1、首先在ts中提取pes数据时,记录下提取数据的索引。
    因为此时记录的索引是包含PES的头的长度。实际的解密数据是不包含PES头的。
    所以我们要把索引传递到pes解析函数中,因为只有在pes解析函数中,才能拿到pes头的长度。
    拿到pes头的长度后,把有pes包头的 的数据的索引值去掉pes头的长度。

    上代码,在所有提取pes数据的地方,添加索引数组,并记录提取pes数据的索引。看图:

    2、接下来在解析pes的函数中,对得到的pes解密数据进行拆分。

    其实拆分与组合是类似,方向相反。根据传进来的pes数据的大小,以及ts包的数量来拆分。

    拿到解密的数据,拆分后,将数据保存,同时将第一个含有pes头的索引加上pes头的长度。

    将索引和拆分的数据,一同随其他数据返回。

    每解析一个pes,我们就替换一个原始的未解密的pes数据。看图:

    3、在解析ts的append函数中,收到拆分了解密的PES数据以及索引后,开始替换ts的原加密数据。

    先看下解密的数据替换的函数:

    function bill_appendTsData(nd, idx) {
        //idx 没有用到,可忽略
         
        var i = 0,j = 0;
        let dataArr = nd.bill_pd.data;
        let idxArr = nd.bill_pd.dataIdx;
        let len = dataArr.length;
        if( len != idxArr.length ) {
            console.log('数据索引与数据数量不同');
            return;
        }
         
        for( i = 0; i < len; i++ ) {
            let darr = dataArr[i];
            let didx = idxArr[i];
             
            for ( j = 0; j < darr.length; j++) {
                bill_d[didx+j] = darr[j];
            }
        }
    }

    其实很简单,根据拿到的解密的数据和数据在ts文件的索引,替换相应的数据。
    这里打了个断点,看下接收到的拆分后的解密数据以及索引。

    这是替换函数,看下在哪里调用替换函数。在收到解密的pes数据后,紧接着就调用。
    此外,当for循环结束后,还需要对3个类型的ts包的数据,进行解密一次。
    为什么这么做?大家思考啊

    至此,PES解密分析就完成了。

    五、总结以及demo

    demo源码和示例视频,我上传到网盘了,下图为demo示例

    总结
    1、在某代码中,js函数如果不写返回值,竟然不会返回。之前代码正常。
    2、关于ts包和pes包的关系,理解了很久,最后结合代码和文章,才弄清楚最终逻辑,有些文章内容是错的,会带跑偏。
    3、对于代码中ts头和pes头的分析,也思考了很久,有时候半天想不明白。
    4、对于ts数据格式,什么PAT等等各种表,懵逼的狠。也是结合代码,总算梳理明白了。
    5、文章写了3天,梳理ts的知识,梳理代码,准备素材,再整理成文,期望对大家有所帮助。
    6、因本人水平有限,文中若有错误之处,还望各位批评指正,共同进步。