跳转至

钩住 RTMPS 的 encode() 函数

我发现用于加密出站数据包数据的 encode() 函数是一个非虚的 __thiscall,意味着它由近调用调用。此外,调用发生在循环内部。整个循环看起来像清单 8-12,直接取自 OllyDbg 反汇编窗格。

loop:
    MOV EAX, [ESI+3C58]
    SUB EAX,EDI
  LEA EAX, [ESI+EDI+1C58]
    PUSH EAX
    MOV ECX,ESI
  CALL encode
    CMP EAX,-1
  JE SHORT endLoop
    ADD EDI,EAX
  CMP EDI, [ESI+3C58]
    JL loop
endLoop:

清单 8-12:encode() 循环

通过一些分析和 airlog 的指导,我确定在①处调用的 encode() 函数把字节数组和缓冲区长度(分别叫它们 buffer 和 size)作为参数。函数失败时返回 -1,否则返回 size。函数以 4,096 字节的块为单位操作,这就是为什么发生在循环中。

转成更易读的伪代码,调用 encode() 的循环看起来像这样(数字指清单 8-12 中相关的汇编指令):

for (EDI = 0; EDI < [ESI+3C58]; ) {
    EAX =  encode( &[ESI+EDI+1C58], [ESI+3C58] - 
EDI);
    if (EAX == -1)  break;
    EDI += EAX;
}

我对 encode() 做什么不感兴趣,但我需要它循环遍历的整个缓冲区,而钩住 encode() 是我获取该缓冲区的手段。看看清单 8-12 中的真实循环,我知道调用对象实例的完整缓冲区存储在 ESI+0x1C58,完整大小存储在 ESI+0x3C58,EDI 包含循环计数器。我考虑到这些设计了钩子,最终创建了一个两部分钩子。

钩子的第一部分是 reportEncode() 函数,它在第一次循环迭代时记录整个缓冲区。以下是 reportEncode() 函数的完整内容:

DWORD __stdcall reportEncode(
    const unsigned char* buffer,
    unsigned int loopCounter)
{
    if (loopCounter == 0)
        printBuffer(buffer, size);
    return origEncodeFunc;
}

这个函数把 buffer、size 和 loopCounter 作为参数,返回我称为 encode() 的函数的地址。在获取该地址之前,我的钩子的第二部分 myEncode() 函数做了所有脏活,获取 buffer、size 和 loopCounter,如下所示:

void __declspec(naked) myEncode()
{
    __asm {
        MOV EAX, DWORD PTR SS:[ESP + 0x4] // 获取 

buffer

        MOV EDX, DWORD PTR DS:[ESI + 0x3C58] // 获取

完整大小

        PUSH ECX // 存储 ecx
        PUSH EDI // 压入当前位置(循环计数器)
        PUSH EDX // 压入 size
        PUSH EAX // 压入 buffer
        CALL reportEncode // 报告编码调用
        POP ECX // 恢复 ecx
        JMP EAX // 跳转到 encode
    }
}

myEncode() 函数是一个纯汇编函数,它用近调用钩子替换原始 encode() 函数调用。把 ECX 存储在栈上后,myEncode() 获取 buffer、size 和 loopCounter,并把它们传给 reportEncode() 函数。调用 reportEncode() 函数后,myEncode() 恢复 ECX,直接跳入 encode(),使原始函数执行并优雅地返回循环。

由于 myEncode() 清理它在栈上使用的一切,myEncode() 运行后栈仍然在正确位置包含原始参数和返回地址。这就是为什么 myEncode() 直接跳入 encode() 而不是使用函数调用:那个栈已经设置了正确的返回地址和参数,所以 encode() 函数会认为一切正常。