专栏首页Toddler的笔记CTP 看穿式监管版本,收集信息为什么会失败?

CTP 看穿式监管版本,收集信息为什么会失败?

本篇涉及一点逆向工程。

背景介绍

  • CTP 是国内期货交易程序化下单的库,也就是我现在用的库。
  • 国内期货交易的程序化下单,必须先把订单报给期货公司,期货公司再转发给交易所。
  • 最近 CTP 应监管要求,升级了新版本(v6.3.15)。新版本中会自动采集使用者电脑的一些信息(例如CPU_ID, Disk_ID, BIOS_ID)。然后把采集的信息通过网络报送给期货公司。
  • 按道理说我的代码只需要重新编译,链接新的 CTP 库即可正常运行。
  • 可是当我这样做了以后,期货公司却告诉我,他们收到的报送信息中,我的 CPU_ID, Disk_ID, BIOS_ID 字段都为空。
  • 而且,明明是哪里出了问题,整个过程却没有任何明显的错误提示,因此我一脸懵逼。

好戏开始

第一回 听者谆谆,言者邈邈

就此我先询问了 CTP 官方交流群,官方给出的解答是,需要链接新的信息采集的库 WinDataCollect。

(顺便介绍一下 Windows 版本 CTP 库的组成文件:)

我看了一下对应的 WinDataCollect.h 头文件,表示此库中只有一个函数 CTP_GetSystemInfo。

进一步询问得知,只有中继模式才需要手动调用此函数(CTP_GetSystemInfo),直连模式不需要手动调用此函数。而我是直连模式。既然不用手动调用,干嘛要链接它?

我将信将疑,按官方的指示做了尝试,结果果然还是失败。

然后官方给了我一套 demo,执行 demo,结果 demo 是可以正确上报信息的。官方建议我采用和 demo 相同的架构。

demo 的程序架构和我现有的程序架构相去甚远,改起来是一个大工程。

既然是官方,我再一次选择相信他,改。

不过改成同样的架构后,执行还是失败。

第二回 天网恢恢,疏而不漏

不再迷信官方,我还是选择相信自己。

回到问题本身。既然 demo 能用,那 demo 发的网络数据包是怎样的?我自己的程序发的网络数据包又是怎样的?用工具抓包即可。

再补充介绍一下,从程序开始执行到期货公司收到采集信息的数据包,一共经历了四个步骤:

  • 1.CTP 动态库被加载,调用了动态库的初始化函数
  • 2.程序调用 CTP->Init 函数
  • 3.程序调用 CTP->ReqAuthenticate 函数
  • 4.程序调用 CTP->ReqUserLogin 函数

那么具体是哪一步将采集的信息通过网络发送了出去呢?于是用 wireshark 抓包得到:

对比 demo 和我自己的程序的抓包,可知 FMTP 450(数据包大小为 450 字节。其实不是 FMTP 协议,而是 CTP 自己内部的数据包协议) 这个截图中被黑色高亮的包,正是包含采集信息数据包。(具体如何推断得到的?此处省略1千字)

然后调试单步执行程序,发现当执行完 ReqUserLogin 函数后,此消息 FMTP 450 被发送出去。至此得到本文第一个重要结论:

结论1:CTP 库函数 ReqUserLogin 执行时,将采集到的系统信息发送了出去。

这个数据包中的系统信息明显是经过加密的,并不能直接看出其中包含的内容的含义。

第三回 桃李不言,下自成蹊

那么 ReqUserLogin 这个函数里面到底做了什么?信息采集是否也是在这个函数里面完成的?它又是如何对采集的信息加密的?

  • Windows 中的 CTP 库函数的 dll(即动态链接库) 是不包含符号表的。而 Linux 版本的是包含符号表的,从而可以知道 ReqUserLogin 调用了哪些函数。
  • 我并没有 CTP 库的源码

因此,想要弄明白上面的问题,让我们回到 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 收集到的信息。对应的几个系统函数为:

  • 创建管道:CreatePipe,它有 4 个参数:
BOOL CreatePipe(
  PHANDLE               hReadPipe,
  PHANDLE               hWritePipe,
  LPSECURITY_ATTRIBUTES lpPipeAttributes,
  DWORD                 nSize
);
  • 创建进程:CreateProcessA,它一共有 10 个参数:
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
);
  • 读 Pipe (因为 Pipe 是个"文件",因此就是读文件):ReadFile
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:

Win32 系统调用 Convention

  • 栈:一段连续的内存,用于存放数据。Win32 的栈自顶向下生长。举个夸张的比喻,例如我有一个栈指针寄存器 esp,指向初始位置 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。
    • 在 Windows 中,调用某些系统函数时,函数的参数通过栈传递,也就是参数依次入栈,从而在函数体内可以从栈中读取这些参数。
    • 调用系统函数时,函数的返回地址被压栈,从而让函数退出时可以找到回去的路。
  • 这里介绍到的压栈操作只是真正 Win32 Calling Convention 的一小部分。

回到刚才的话题。此时断点停在了 CreateProcessA@40 位置。此时查看寄存器 ESP 中的值:026ADCDC,那么进一步查看从 026ADCDC 开始的一段连续内存的内容:

注意:

  • esp 中当前所指的内容为函数返回地址,是一个指针,0x5e531eb5,因为我的 CPU 是 Little Endian(小端格式) 的,所以对于指针,低位 b5 在最前端。
  • esp + 4 中的内容为 lpApplicationName,值为空,说明这个参数没有用到
  • esp + 8 中的内容为 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 所在的路径。

last but not end

大问题是解决了,还有还有一个小问题还是没有解决:为什么 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,我没有继续调试。如果以后能有幸知道,再做分享。

本文分享自微信公众号 - Toddler的笔记(gh_5708a01db935),作者:李国诚

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • placement new

    JIFF
  • 地球时间和 C++ 时间库

    GMT 是一个 时区,也指一种 时制。很久以前,科学家通过天文观察,将一个太阳日定义为 86400 秒,以英国 Greenwich 天文台白天平均太阳最高点作为...

    JIFF
  • 智能指针

    刚学编程时,最常听到的一句话是不是“new 的内存用完要记得 delete,不然会造成内存泄漏”?然而事实上是:

    JIFF
  • 基于sklearn的线性回归器理论代码实现

    理论 线性回归器 相比于线性分类器,线性回归器更加自然。回归任务的label是连续的变量(不像分类任务label是离散变量),线性回归器就是直接通过权值与输入对...

    月见樽
  • 学习Javascript之尾调用

    总括: 本文介绍了尾调用,尾递归的概念,结合实例解释了什么是尾调用优化,并阐述了尾调用优化如今的现状。

    Damonare
  • 不仅仅是新的单细胞相关R包层出不穷,旧的R包也会更新用法

    单细胞R包如过江之卿,入门的话我推荐大家学习5个R包,分别是: scater,monocle,Seurat,scran,M3Drop 需要熟练掌握它们的对象,:...

    生信技能树jimmy
  • 《西部世界》与《头号玩家》:哪个才是人类与人工智能相处的正确方式?

    新智元
  • Azure上基于HTTP trigger的Lambda Function

    Azure上通过HTTP方式触发的Lambda Function,函数体直接在浏览器里编写:

    Jerry Wang
  • Python实现小数的二进制与十进制形式转换

    对于十进制小数,乘以2,取整数部分,对剩余的小数部分重复这个过程,直至小数为0,把得到的整数部分依次保存,即为转换结果。例如,十进制小数0.125转换为二进制小...

    Python小屋屋主
  • Lua函数的冒号调用和点调用

    冒号定义函数中的self指向函数所属表对象,即self是table类型,通过self表可以:访问挂载在该表下的所有冒号定义函数 如,有定义A={},A:b()...

    bering

扫码关注云+社区

领取腾讯云代金券