本篇涉及一点逆向工程。
就此我先询问了 CTP 官方交流群,官方给出的解答是,需要链接新的信息采集的库 WinDataCollect。
(顺便介绍一下 Windows 版本 CTP 库的组成文件:)
我看了一下对应的 WinDataCollect.h 头文件,表示此库中只有一个函数 CTP_GetSystemInfo。
进一步询问得知,只有中继模式才需要手动调用此函数(CTP_GetSystemInfo),直连模式不需要手动调用此函数。而我是直连模式。既然不用手动调用,干嘛要链接它?
我将信将疑,按官方的指示做了尝试,结果果然还是失败。
然后官方给了我一套 demo,执行 demo,结果 demo 是可以正确上报信息的。官方建议我采用和 demo 相同的架构。
demo 的程序架构和我现有的程序架构相去甚远,改起来是一个大工程。
既然是官方,我再一次选择相信他,改。
不过改成同样的架构后,执行还是失败。
不再迷信官方,我还是选择相信自己。
回到问题本身。既然 demo 能用,那 demo 发的网络数据包是怎样的?我自己的程序发的网络数据包又是怎样的?用工具抓包即可。
再补充介绍一下,从程序开始执行到期货公司收到采集信息的数据包,一共经历了四个步骤:
那么具体是哪一步将采集的信息通过网络发送了出去呢?于是用 wireshark 抓包得到:
对比 demo 和我自己的程序的抓包,可知 FMTP 450(数据包大小为 450 字节。其实不是 FMTP 协议,而是 CTP 自己内部的数据包协议) 这个截图中被黑色高亮的包,正是包含采集信息数据包。(具体如何推断得到的?此处省略1千字)
然后调试单步执行程序,发现当执行完 ReqUserLogin 函数后,此消息 FMTP 450 被发送出去。至此得到本文第一个重要结论:
结论1:CTP 库函数 ReqUserLogin 执行时,将采集到的系统信息发送了出去。
这个数据包中的系统信息明显是经过加密的,并不能直接看出其中包含的内容的含义。
那么 ReqUserLogin 这个函数里面到底做了什么?信息采集是否也是在这个函数里面完成的?它又是如何对采集的信息加密的?
因此,想要弄明白上面的问题,让我们回到 Linux。
(顺便介绍一下 Linux 版本 CTP 库的组成文件:)
引论:我虽然没有 CTP 库函数的源码,但是库函数本身就是代码,被写在 .dll/.so 文件中,只不过是机器码。再加上符号表,可谓汇编代码。想知道 ReqUserLogin 这个库函数在做什么,那读其汇编代码就可以了。
于是在 Linux 中,执行 gdb,在 ReqUserLogin 函数上打断点。stepi 进入,突然眼前一亮:
上图显示 ReqUserLogin 调用了 CTP_GetSystemInfoUnAesEncode,那么基本上可以确定是系统信息采集的工作是由 ReqUserLogin 这个函数完成的了。
继续跟进,此函数进一步调用了 CTP_GetRealSystemInfo 和 RSA_EncodeCollectedData(buf_in, in_size, buf_out, out_size),这也回答了另外一个问题,即采集信息发送时加密格式为 RSA-256。
至此得到本文第二个重要结论:
结论2:CTP 库函数 ReqUserLogin 采集系统信息,并将采集到的信息经过 RSA 加密后通过网络数据包发送给了期货公司。
加密前的原文为:
"(操作系统类型)@(信息采集时间)@(私网IP1)@(私网IP2)@(网卡MAC1)@(网卡MAC2)@(设备名)@(操作系统版本)@(Disk_ID)@(CPU_ID)@(BIOS_ID)"
那为什么我自己的 Windows 版本程序就采集不到系统信息呢?
查看 CTP 官方文档,发现 CTP 在 Windows 上采集系统信息用到的手动执行的命令,是 cmd 命令 "wmic path win32_physicalmedia get SerialNumber"。那么可以猜想 ReqUserLogin 实现也是调用了这个命令。
那么要执行这条指令,可以先创建一个管道 Pipe,再创建一个进程 S,进程 S 收集系统信息,并写 Pipe,然后我的程序读 Pipe,拿到 S 收集到的信息。对应的几个系统函数为:
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
BOOL CreateProcessA(
//程序名,可以是 null
LPCSTR lpApplicationName,
//关键字段:要执行的命令,这个必须有
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
//这是另外一个结构体,其中指定了 stdin,
//stdout, stderr 文件描述符,stdout 对应 Pipe
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
上面这几个函数和其具体实现的名字略有不同。(顺便提一句,我是 64 系统,但程序编译的版本是 32 位的,位数不同具体实现的名字可能也不同)
打开 VS 的汇编级别和库函数的调试选项,在对应的系统函数 _CreatePipeStub@16
, _CreateProcessA@40
, _ReadFileImplementation@20
上打断点。
重点关注 CreateProcessA,断点停在下面的位置:
这段汇编代码是在做什么?先不用管,先了解一下 Windows 系统函数的 Calling Convention:
0xfffffffc
(栈顶,单位是 Byte),规定初始位置不能放任何东西。并且每次如果要往栈里放入一个元素(大小一般是 4 Bytes),需要先 esp = esp - 4 (得到 0xfffffff8
),然后向这个位置写入数据。这个数据的内存地址也就是 0xfffffff8
,大小为 4 Bytes,覆盖 0xfffffff8
, 0xfffffff9
, 0xfffffffa
, 0xfffffffb
这四个 Bytes。这个过程也称为"push/或称为压栈/进栈/入栈"call
,真正调用函数,同时将返回地址压栈。因此当断点停在 CreateProcessA@40
时,表示已经进入了此函数体,此时 esp 中的值为返回地址,esp + 4 中的值为 lpApplicationName
,esp + 8 中的值为 lpCommandLine
,以此类推。CreateProcessA
的 10 个参数,从右往左依次入栈。也就是说,lpProcessInformation
是第一个入栈的,在内存地址的最高处,大小为 4 Bytes;lpApplicationName
是最后一个入栈的,在最低处,大小为 4 Bytes。回到刚才的话题。此时断点停在了 CreateProcessA@40
位置。此时查看寄存器 ESP 中的值:026ADCDC,那么进一步查看从 026ADCDC 开始的一段连续内存的内容:
注意:
0x5e531eb5
,因为我的 CPU 是 Little Endian(小端格式) 的,所以对于指针,低位 b5 在最前端。lpApplicationName
,值为空,说明这个参数没有用到lpCommandLine
,值为 0x5e6528ec,这是一个 char* 指针,因此需要继续查看对应的内存段内容:那么就可以确认此命令确实被执行过了。
但是此命令的执行并不成功,最后读到的内容是空的。(为了严谨,要确认 CreatePipe
得到的 Pipe 文件描述符和 CreateProcessA.lpStartupInfo
中的文件描述符是一致的,还要确认 ReadFile
读取的 Pipe 文件描述符也是同一个。最后再查看 ReadFile.lpBuffer
对应的内存。此处省略1千字)
那一定是这个命令执行遇到了什么问题。
在 Windows 中,如果一个系统函数执行发生了问题,一般可以通过 GetLastError
获得错误信息。
那么猜测如果运气好的话,CTP 也是调用过这个函数,只是没有把错误信息打印出来。
那么在 _GetLastErrorStub@0
上面打断点,在 _CreateProcessA@40
函数执行之后,紧接着果然截获到了此断点:
惊喜万分之下,先了解一下 GetLastError
这个系统函数:
DWORD GetLastError();
此函数无参数,仅返回一个数值,来表示上一条命令执行时遇到的错误代码。
根据 Win32 系统调用规范,函数的返回值是放在寄存器 EAX 中的。
因此,在 VS 中按 Shift+F11,Step Out,再查看 eax 的值:结果是 2。
查阅 Windows 帮助中心,根据 System Error Codes 章节可知:
ERROR_FILE_NOT_FOUND
2 (0x2)
The system cannot find the file specified.
2 表示执行 system("wmic path win32_physicalmedia get SerialNumber") 时发生了错误 "file not found"。
那是什么 file 没有找到呢?只能是 wmic.exe 这个可执行程序没有找到。
于是在我的电脑中搜索到了 wmic.exe
这个文件的位置,把此位置加入系统环境变量 PATH
中,再次执行我的程序,期货公司表示这次成功收到了采集的的系统信息!
于是可以得到结论:
结论3:如果是直连模式,是可以完全忽略 WinDataCollect 这个库的,不用 #include,也不用链接它。
结论4:原来正常工作的程序的架构不需要修改,直接编译链接新的穿透式监管的库即可,只需要在 Windows 上运行时为系统环境变量 PATH 增加 wmic.exe 所在的路径。
大问题是解决了,还有还有一个小问题还是没有解决:为什么 demo 的执行不需要手动设置 PATH 就可以正确找到 wmic.exe
的位置,为什么我的程序必须手动设置 PATH 才可以?
我仅仅做了一些初步的观察:发现我的程序在执行时,有一条非常可疑的 log:(Win32): Unloaded 'C:\Windows\SysWOW64\apphelp.dll'
,而在 demo 的执行中,没有发生这个情况。
而 demo 在执行信息采集时,恰好先 Load 了 apphelp.dll
,进一步 Load 了 WMIC.exe
,因此猜想是因为这个 Unload 导致了最终需要手动设置 PATH。
至于为什么 apphelp.dll
会被 Unload,我没有继续调试。如果以后能有幸知道,再做分享。