前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CTP 看穿式监管版本,收集信息为什么会失败?

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

作者头像
JIFF
发布2019-08-02 15:26:23
5.9K1
发布2019-08-02 15:26:23
举报
文章被收录于专栏:Toddler的笔记Toddler的笔记

本篇涉及一点逆向工程。

背景介绍

  • 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 个参数:
代码语言:javascript
复制
BOOL CreatePipe(
  PHANDLE               hReadPipe,
  PHANDLE               hWritePipe,
  LPSECURITY_ATTRIBUTES lpPipeAttributes,
  DWORD                 nSize
);
  • 创建进程:CreateProcessA,它一共有 10 个参数:
代码语言:javascript
复制
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
代码语言:javascript
复制
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 这个系统函数:

代码语言:javascript
复制
DWORD GetLastError();

此函数无参数,仅返回一个数值,来表示上一条命令执行时遇到的错误代码。

根据 Win32 系统调用规范,函数的返回值是放在寄存器 EAX 中的。

因此,在 VS 中按 Shift+F11,Step Out,再查看 eax 的值:结果是 2。

查阅 Windows 帮助中心,根据 System Error Codes 章节可知:

代码语言:javascript
复制
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,我没有继续调试。如果以后能有幸知道,再做分享。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Toddler的笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景介绍
  • 好戏开始
    • 第一回 听者谆谆,言者邈邈
      • 第二回 天网恢恢,疏而不漏
        • 第三回 桃李不言,下自成蹊
          • 第四回 顺藤摸瓜,循序渐进
            • Win32 系统调用 Convention
          • 第五回 云开见日,林深见鹿
            • last but not end
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档