小弟新手报到,提交一篇拙文一篇,供前辈们批评


使用OllyDbg 分析 USB HID 设备接口协议
作者: Tase     tase@163.com
关键字: OllyDbg,USB ,HID,Bus hound,

分析工具: OllyDbg,Bus hound
分析对象: 一个USB 接口飞行模拟器

目的:通过分析使用USB HID设备的软件,从中其主程序中“扣”HID 设备数据协议,再根据这些协议自己实现USB HID的设备,达到仿制原装 HID数据采集设备的目的。

我有幸拿到了一款原装的飞行模拟软件和接口设备,这个模拟器是通过一个USB接口把遥控设备上的数据采集到PC,再通过软件模拟现实飞行,因为原装设备昂贵,因此打算DIY一套接口,便下手分析了一把!

这个模拟器软件有两部分组成,其一是模拟软件,其二是一个USB的数据采集设备,在拿到设备前,我就猜想,它可能是HID(人体学设备)DEVICE,现在很多USB数据采集设备都是采用的HID协议的方法设计,这样的话就可以实现在WINDOWS下免驱动安装,并且可以通过windows自带的接口以客户态的方式访问这些硬件接口。

经过测试,果然是HID设备,使用Bushound5 总线观察器,用它来捕捉总线上的USB设备数据,HID设备很简单,花了两个小时就基本弄清楚了它大部分的数据含义,因此很快就按照我理解的协议把设备在单片机开发板上实现了,插上我的设备,模拟器软件很快就识别了我的接口,但显示各通道数据时,都是0,也就是说,它的系统检测到了一个接口设备,但这个设备采样的数据是错误的。


于是我又回到总线数据分析,找到我遗漏的2个bit,这个2个bit 是我没有理解的,我猜想它是校验位,经过观察,这个2个bit只和几个字节有关,但就是这2个bit花了我4天的时间,这几天让我学会Excel 中很多大多数人不知道的功能,学会了使用VBScript 写宏,我甚至用到了MATLAB,我一直是用统计的方法寻找2bit的规律,但最终我失败了。

山穷水尽,想到了Ollydbg 分析它主程序中的USB接口的协议,没有想到,一个晚上,就解决了我的问题。
在Windows 下访问HID 设备可以有两种方法,一是核态,也就是用驱动程序的方法访问,这个方法非常复杂,我知道原理,但自己还没有写过这样的驱动。二是客户态,即应用程序通过调用Windows提供的Hid 访问函数就可以实现对HID设备的访问。如果我们截获了这些函数的调用,那么,距离我们要解决的目标就不远了。

一般情况下,主程序识别USB HID外设的方法:首先调用HidD_GetHidGuid函数获取HID设备的类标识,调用SetupDiGetClassDevs函数查询所有已安装的HID设备,得到一个指向该HID设备集合的句柄,调用SetupDiEnumDeviceInterface函数查询HID设备集中每一个设备的接口信息,对每一个接口,调用SetupDiGetDeviceInterfaceDetail函数获取其详细的信息,包括设备名称(头四个字节),CreateFile用此设备名打开设备,调用SetupDiDestroyDeviceInfoList函数释放设备信息集合;第二步,打开设备,获取设备的属性值以及设备能力描述,调用CreaterFile函数打开本设备。调用HidD_GetAttributes函数,获取USB设备的有关属性。它包含了设备的厂商ID、产品ID及产品的版本号等。可以根据这些信息判断该设备是否为目标设备,但我们关心的并不是这些,我们关心的是这个设备打开以后的 ReadFile 操作,我们知道,Windows下的设备都是当作文件来读写的。

启动OllyDbg 装入模拟器软件,下断点 HidD_GetAttributes,CreateFile,这个函数是能把HID 的关键信息获取,例如厂商ID,产品ID,版本?等,在调试前,拔掉机器上所有不必要的USB设备,这样枚举到的设备会少些,你断点的次数也会少很多。

因为前面用到了Bus Hound,很容易知道我们要关心的设备的 VID 和 PID,这样在断点在HidD_GetAttributes, 之后,很容易得知哪个设备是我们所关心的,在堆栈中记下设备的字符串路径。在CreateFile函数断点时,确认要打开的设备是否是我们关心的设备?

我们关心的HID设备打开以后,就要关心 ReadFile  和 WriteFile了,下断点 ReadFile ,不幸的是,这样的断点太多了! 想到一个问题,HID虽然是低速设备,但刷新时间也是10ms/帧,那么一般的WINDOWS的定时器最多只能精确到50ms,要想不丢帧读取HID的设备数据,一定会用到高精度的定时器——多媒体定时器。

用到了多媒体定时器,就肯定会用到下面的几个函数 ,timeGetDevCaps,timeBeginPeriod,timeSetEvent ,遂下断点在这函数,经过一番努力,果然找到了关键的ReadFile,读完了HID的设备数据后,干什么? 按照常理,就该分析和校验了,果然,我分析的正确,正是如此,下面是破解当时获取的关键汇编代码,因为这篇文章是几个月后写的,所以有些内容只能靠回忆了。

读懂了这些代码就完全弄明白了它的协议。

004417B2   . 8B0D D8CD9500  MOV ECX,DWORD PTR DS:[95CDD8]     //取出CH1-CH4 的高位字节
004417B8   . A3 00CE9500    MOV DWORD PTR DS:[95CE00],EAX
004417BD   . 33D2           XOR EDX,EDX
004417BF   . 8BC1           MOV EAX,ECX
004417C1   . 8A15 D7CD9500  MOV DL,BYTE PTR DS:[95CDD7]       //取出CH1通道数据
004417C7   . 25 FF000000    AND EAX,0FF
004417CC   . 8BF2           MOV ESI,EDX                      // 这里要注意,保存CH1的低8位数据,将来用来算校验位
004417CE   . 8BD0           MOV EDX,EAX
004417D0   . 83E2 03        AND EDX,3                         //用0x3 来掩码 高位字节,取出CH1的高2位
004417D3   . 33DB           XOR EBX,EBX 
004417D5   . C1E2 08        SHL EDX,8                         //将CH1 的高2位左移 8位
004417D8   . 8A1D D6CD9500  MOV BL,BYTE PTR DS:[95CDD6]       //取出CH2 的通道数据
004417DE   . 8D9432 DC03000>LEA EDX,DWORD PTR DS:[EDX+ESI+3DC] //计算出 CH1 的最终数据,高2位+CH1的数据 + 常数3DC
004417E5   . 8915 E0CD9500  MOV DWORD PTR DS:[95CDE0],EDX      //保存CH1的数据
004417EB   . 8BD0           MOV EDX,EAX
004417ED   . 83E2 0C        AND EDX,0C                         //取出CH2的高2位掩码
004417F0   . C1E2 06        SHL EDX,6                          //计算出 CH2 的高2位
004417F3   . 8D941A DC03000>LEA EDX,DWORD PTR DS:[EDX+EBX+3DC] //计算出 CH2 的完整数据
004417FA   . 33DB           XOR EBX,EBX
004417FC   . 8915 E4CD9500  MOV DWORD PTR DS:[95CDE4],EDX      //保存CH2 的数据
00441802   . 8BD0           MOV EDX,EAX
00441804   . 83E2 30        AND EDX,30                         //取CH3 高位 的掩码
00441807   . 8ADD           MOV BL,CH                          //取CH3 低8位数据
00441809   . C1E2 04        SHL EDX,4                          //高2位移位
0044180C   . 25 C0000000    AND EAX,0C0                        //取CH4 的高位掩码
00441811   . 8D8C1A DC03000>LEA ECX,DWORD PTR DS:[EDX+EBX+3DC] //计算出CH3 的完整数据
00441818   . 33D2           XOR EDX,EDX
0044181A   . 8A15 D5CD9500  MOV DL,BYTE PTR DS:[95CDD5]        //取CH4 低8位数据
00441820   . 890D E8CD9500  MOV DWORD PTR DS:[95CDE8],ECX      //保存CH3 的数据
00441826   . 8D8482 DC03000>LEA EAX,DWORD PTR DS:[EDX+EAX*4+3DC] //计算CH4 D 数据
0044182D   . A3 ECCD9500    MOV DWORD PTR DS:[95CDEC],EAX       //保存CH4 的数据
00441832   . A1 DCCD9500    MOV EAX,DWORD PTR DS:[95CDDC]       //取出CH5-CH8 的高位数据和标志位数据
00441837   . A8 40          TEST AL,40   // 这个标志的第6bit 是表示奇偶帧的
00441839   . 74 41          JE SHORT REFLEX.0044187C  //如果是0 表示是偶数帧,那么CH5,CH6 有效,GOTO 到0044187C
0044183B   . 25 FF000000    AND EAX,0FF   //否则 是奇数帧
00441840   . 33D2           XOR EDX,EDX
00441842   . 8A15 DBCD9500  MOV DL,BYTE PTR DS:[95CDDB]  //取CH5的低8位数据
00441848   . 8BC8           MOV ECX,EAX
0044184A   . 83E1 0C        AND ECX,0C
0044184D   . C1E1 06        SHL ECX,6
00441850   . 8D8C11 DC03000>LEA ECX,DWORD PTR DS:[ECX+EDX+3DC] //同上,计算出CH5 的完整数据
00441857   . 8BD0           MOV EDX,EAX
00441859   . 83E2 03        AND EDX,3       //取CH6 的高2位
0044185C   . 890D F8CD9500  MOV DWORD PTR DS:[95CDF8],ECX //保存CH5
00441862   . C1E2 08        SHL EDX,8
00441865   . 33C9           XOR ECX,ECX
00441867   . 8A0D DACD9500  MOV CL,BYTE PTR DS:[95CDDA]   //取CH6 的低8位
0044186D   . 8D940A DC03000>LEA EDX,DWORD PTR DS:[EDX+ECX+3DC]  //计算出CH6 数据
00441874   . 8915 FCCD9500  MOV DWORD PTR DS:[95CDFC],EDX // 保存CH6 数据
0044187A   . EB 4A          JMP SHORT REFLEX.004418C6
0044187C   > 25 FF000000    AND EAX,0FF
00441881   . 33D2           XOR EDX,EDX
00441883   . 8A15 DBCD9500  MOV DL,BYTE PTR DS:[95CDDB]
00441889   . 8BC8           MOV ECX,EAX
0044188B   . 83E1 0C        AND ECX,0C
0044188E   . C1E1 06        SHL ECX,6
00441891   . 8D8C11 DC03000>LEA ECX,DWORD PTR DS:[ECX+EDX+3DC]
00441898   . 8BD0           MOV EDX,EAX
0044189A   . 890D F0CD9500  MOV DWORD PTR DS:[95CDF0],ECX
004418A0   . 83E2 03        AND EDX,3
004418A3   . 33C9           XOR ECX,ECX
004418A5   . 8A0D DACD9500  MOV CL,BYTE PTR DS:[95CDDA]
004418AB   . C1E2 08        SHL EDX,8
004418AE   . 8D940A DC03000>LEA EDX,DWORD PTR DS:[EDX+ECX+3DC]
004418B5   . 8BC8           MOV ECX,EAX
004418B7   . C1E9 07        SHR ECX,7                         // 这里也需要注意,这里是取出标志位的最高位,做判断用的
004418BA   . 8915 F4CD9500  MOV DWORD PTR DS:[95CDF4],EDX     // 上面的部分是算出 7- 8 通道的数据并保存
004418C0   . 890D 04CE9500  MOV DWORD PTR DS:[95CE04],ECX
004418C6   > 8BD0           MOV EDX,EAX                        //公共算2BIT 校验位的 部分 FLAG
004418C8   . 83F6 07        XOR ESI,7                          //CH1 和 0X7 XOR ==> A
004418CB   . 83E2 0F        AND EDX,0F                         //标志位取低4位 ==>B
004418CE   . 03D6           ADD EDX,ESI                        //标志位的低4位和A + B==>C
004418D0   . D1E2           SHL EDX,1                          //C << 1 ==> D
004418D2   . 33D0           XOR EDX,EAX                        //D XOR FLAG ==> E
004418D4   . F6C2 30        TEST DL,30                         //E & 0x30 ==>F  如果是 0 就成功了!
004418D7   . 74 3A          JE SHORT REFLEX.00441913
004418D9   . A1 C4CD9500    MOV EAX,DWORD PTR DS:[95CDC4]
004418DE   . 85C0           TEST EAX,EAX
004418E0   . 75 40          JNZ SHORT REFLEX.00441922
004418E2   . A1 08CE9500    MOV EAX,DWORD PTR DS:[95CE08]
004418E7   . C705 C4CD9500 >MOV DWORD PTR DS:[95CDC4],1
004418F1   . 50             PUSH EAX
004418F2   . EB 1D          JMP SHORT REFLEX.00441911
004418F4   > 8B0D 08CE9500  MOV ECX,DWORD PTR DS:[95CE08]
004418FA   . C705 C4CD9500 >MOV DWORD PTR DS:[95CDC4],1
00441904   . 8935 B8CD9500  MOV DWORD PTR DS:[95CDB8],ESI
0044190A   . 8935 BCCD9500  MOV DWORD PTR DS:[95CDBC],ESI
00441910   . 51             PUSH ECX
00441911   > FFD5           CALL EBP
00441913   > A1 C4CD9500    MOV EAX,DWORD PTR DS:[95CDC4]
00441918   . 33F6           XOR ESI,ESI
0044191A   . 3BC6           CMP EAX,ESI
0044191C   .^0F84 61FEFFFF  JE REFLEX.00441783
00441922   > 5F             POP EDI
00441923   . 5E             POP ESI
00441924   . 5D             POP EBP
00441925   . 33C0           XOR EAX,EAX
00441927   . 5B             POP EBX
00441928   . 59             POP ECX
00441929   . C2 0400        RETN 4


2Bit= ((((pPacket->byteCh1 ^ 0x07) + (CH_HID_REPORT[7] & 0x0F))<<1)&0x30)>>4;
 

当时困扰我4天的2BIT校验数据,其实用C语言一行就写完了,兴奋啊! 立刻将这个算法写进单片机的固件,插上我自己DIY的HID模拟器设备,OK,系统已经能正确的从我的设备中采集数据了。
USB 协议破解成功!


这是我第一次使用OllyDbg 解决实际问题,愚昧之处还请各位前辈批评。



总结: 有时我们在解决一个难题的时候,往往在正面不能获取进展时,不如从侧面,后面下手,所谓杀猪捅屁股,各有各的杀法!