windows 的 shim engine分析

转自: http://bbs.zndev.com/htm_data/87/0501/84733.html

先说什么是shim engine....
最直接的方式就是下载这个附件...运行test目录下面的quake3.exe ....
呃...实际上她是windows自己的winver.exe的一个copy而已...
我只是rename成了quake3.exe.....
运行出来界面了..仔细动动..看看有什么不同的地方....
呵呵..你会发现alt+tab 不能用了...或许你要认为我作了手脚...
其实没有..你随便选一个(其实并不是所有的exe都能正常的运行)exe文件...
放到test目录下面..rename成quake3.exe...
然后运行你会发现alt tab还是不能用

是不是很惊奇...如果还不放心的话...自己写一个程序..
仍然放到test目录下面rename成quake3.exe...再运行..

现在总算是相信不是我作手脚了吧...
既然不是我,那是谁呢....当然只有windows了..

都知道xp,2000 sp4,2003以及更高版本的程序右键对话筐里面
有个兼容性的选项...下面的文字就是描述这个东西了...
主要的内容就是描述windows是怎么实现这一套东西的....

呃补充一句...附件里的程序只能在nt平台下运行...
9x系统就别try了...
一来9x下面没有这个东西...
二来我本来就是用unicode编译的...嘿.
请不要用multi byte code 编译源代码...
虽然不会有编译错误..
但是你看不到正确的执行结果...
嗯..附件里面的源代码是一个sdb文件的查看工具.什么是sdb文件..往下看

windows 现在的几个主流操作系统都提供一种兼容模式..
方便早期的程序能比较正常的运行..
那这个功能是怎么作到的呢
概括的讲...windows保存了一份有问题的程序列表..
同时保存着修正这些问题的方式..
在程序开始运行的时候(其实并不只是这个时刻)进行修正也就是打补丁
windows保存的信息放在以sdb为后缀的文件里面
他们都分布在windows下的apppatch目录下面
其中apphelp.sdb是显示help信息用的
drvmain.sdb是用于驱动程序的..他紧跟着hal.dll跟ntoskrnl.exe
被osloader加载到内容...很特别吧...他只是一个data file而不是一个image
sysmain.sdb是最主要的文件了..放的是应用程序的信息
msimain.sdb放的是msi安装包的一些信息..
这里主要的是讨论的是sysmain.sdb..其他的暂时无视..
那么sdb文件是一个什么样子的结构呢..他又起一个什么样的作用呢..
一个一个的解释...
sdb是一个tree一样的结构...在sdbreader目录下面有个table.txt的文件..里面有说明
结构先放放.有我画的图..加上源代码..加上你自己的调试跟踪..相信很容易能了解....
先看功能....

如果你attach一个debugger到test目录下的quake3.exe...
你就会发现有几个很不寻常的dll存在...
Shimeng.dll AcGenral.dll AcXtrnal.dll
就是他们一起完成了令你吃惊的功能....

说了半天..你不要头晕了啊..哈哈

前面说了...windows实现是靠patch完成的..
最主要的方式就是修改exe和dll的import table..
大部分的程序兼容性都是因为不正确使用api所造成的..
所以windows的shim engine也是在这个上面下功夫..

windows把有问题的程序收集起来...
在创建新系统的时候检查将要运行的程序是不是会有问题
如果有..那么就打上补丁..主要就是加载一个额外的辅助dll(他们都放在apppatch目录下面)
额外的辅助dll准备好一份要修改的函数信息...由shimeng.dll完成import table的修改工作

如果你还对刚刚的alt tab问题耿耿于怀的话....那我解释下...
其实是AcGenral.dll创建了一个辅助的线程...并且安装了一个low level的keyboard hook
过滤了那几个按键而已...你可以用ctrl+shift+esc调出任务管理器来切换进程

从上来来看...windows要完成两个方面的工作
第一是判断将要运行的程序是否需要打补丁
第二是要给当前将要运行的程序打上补丁(其实在程序运行过程中也会有可能发生同样的事情)

我们也分两步来看...
首先是第一个问题....

刚刚说了..windows保存有一份有问题的程序列表..你可能觉得只是判断一下就行了..
但是不会那么简单...细心的同学已经注意到了..我的test目录下面有个Extras目录
下面还有help目录..还有两个文件...你改改他们试试...会发现alt+tab又能用了..
嗯...只是希望你明白..windows判断一个程序是否有问题的条件是很丰富的..
基本看来是比较有效的...

呵呵...其实..将来你会发现..我用来作演示的这个东西..
是windows为quake3准备的...
具体的讲
windows检查将要运行的程序的名字是否是quake3.exe..同时检查同目录下面是否有个
Extras目录...在它下面是否有个Help目录..下面是否有BotCommands.htm跟
Dedicated Server.htm两个问题存在...
如果条件都满足..那么windows就会加载AcGenral.dll...它会关上alt tab
windows还会加载AcXtrnal.dll(如果你的显卡没有装opengl驱动的话)...
如果你dumpbin一下它的exports就会发现
它其实是一个opengl的替换品..里面有一个用d3d8包装的opengl
(ps.这个实现其实很次,如果有debug版的dx..可以看到有无数的内存泄漏..呵呵)
上面是希望你认识到..windows用来判断一个程序是否有问题..并不局限于大家所了解的
比如文件名字..文件大小..文件版本..checksum等等这些数据..
将来你会看到判断的条件其实是有无数多的....
单一文件的条件大概有20多个...但是你可以设置好多的文件..
就像上面的例子一样..它会检查3个文件..

好了...现在你了解了windows有一份列表...
并且有丰富的手段来判断程序是否存在兼容性问题...
那么实际上这个工作是怎么完成的呢...

其实是很多个模块一起完成的..
首先是kernel32.dll...
在你创建一个新进程的时候..kernel32.dll在调用ntdll的zwcreateprocess之前..
就会检查将要运行的程序是否有兼容性问题
从win32的createprocess api开始一段代码以后会
调用CreateProcessInternalW函数

在我的机器上是在这个地址
[code]
.text:77E22043 ; __stdcall CreateProcessInternalW(x,x,x,x,x,x,x,x,x,x,x,x)
.text:77E22043 public _CreateProcessInternalW@48
.text:77E22043 _CreateProcessInternalW@48 proc near
[/code]

往下走走走...
[code]
.text:77E22556 call _BasepCheckBadapp@36
[/code]

参数的部分先无视了....进入这个CheckBadapp函数里面
[code]
.text:77E23CB0 ; __stdcall BasepCheckBadapp(x,x,x,x,x,x,x,x,x)
.text:77E23CB0 _BasepCheckBadapp@36 proc near
[/code]

继续往下
先判断shim是否disable了....如果没有就继续
[code]
.text:77E23CBE call _IsShimInfrastructureDisabled@0
.text:77E23CC3 test eax, eax
.text:77E23CC5 jnz loc_77E23BA9
[/code]

然后check shim的cache
[code]
.text:77E23D3F call _BaseCheckAppcompatCache@16
[/code]

如果都跳过了就进入到这里
[code]
.text:77E23C92 call _BaseCheckRunApp@40
[/code]

[code]
.text:77E3C3BB ; __stdcall BaseCheckRunApp(x,x,x,x,x,x,x,x,x,x)
.text:77E3C3BB _BaseCheckRunApp@40 proc near
[/code]

继续往下
[code]
.text:77E3C52E call ds:__imp__CsrClientCallServer@16
[/code]

CsrClientCallServer...这个调用会在另外的一个进程里面完成
(关于这个client server部分...等有机会了我再写一个文章解释解释csrss进程的东西)

暂时的讲...真正实现这个功能检查的在csrss这个进程...由basesrv.dll接受这次调用
要调试的话...用softice一类的才可以哟..因为csrss是一个很关键的进程..
如果冻结了的话..整个ring3系统都没有响应了...

废话少说....最终来到basesrv.dll的这里
[code]
.text:7596A7FD ; __stdcall BaseSrvCheckApplicationCompatibility(x,x)
.text:7596A7FD _BaseSrvCheckApplicationCompatibility@8 proc near
[/code]

继续跟踪...你又发现..这个家伙又是调用别人的...
[code]
.text:7596A907 call _pfnCheckRunApp
[/code]

这个_pfnCheckRunApp指向什么地方呢...它其实是apphelp.dll的一个export的函数
[code]
.text:75D637E8 ; __stdcall ApphelpCheckRunApp(x,x,x,x,x,x,x,x,x,x)
.text:75D637E8 public _ApphelpCheckRunApp@40
.text:75D637E8 _ApphelpCheckRunApp@40 proc near
[/code]

终于到关键的函数了...
[code]
.text:75D63812 call _InternalCheckRunApp@68
[/code]
转到internal的函数

这个函数比较大...而且不知道ms用的个什么版的编译器...
代码很乱...到处分布得都是...跟踪调试起来非常麻烦..

它得大致得步骤这样
1.open sdb
2.check exe match
3.parse check result
4.close sdb
5.return result

暂时知道如果apphelp.dll在sdb文件里面找到了一份match的数据..就会生成一个长度为
188h字节的结构返回..再由basesrv.dll返回..最终由kernel32.dll获取到这份数据

然后kernel32.dll开始正常的进程创建工作...
等到进程创建出来了...kernel32.dll把这个数据copy到新进程的process heap里面
同时在新进程的peb里面保存这个数据指针...
types一下peb看看在+1e8h的offset的地方是一个void* pShimData的指针...
它放的就是刚刚由apphelp生成的数据...

新进程创建出来了..开始运行了..进入到新进程的context...
进入ntdll.dll的
[code]
.text:77F493C1 ; __stdcall LdrpInitializeProcess(x,x)
.text:77F493C1 _LdrpInitializeProcess@8 proc near
[/code]

ebx = peb
[code]
.text:77F493CC mov eax, large fs:18h ; teb
.text:77F493D2 push ebx
.text:77F493D3 mov ebx, [eax+30h] ; ebx = peb
[/code]

ecx = pShimData,保存到[ebp-14h]的地方
[code]
.text:77F49462 lea eax, [ebx+1E8h] ; [peb+1e8h] = pShimData
.text:77F49468 mov ecx, [eax]
.....................................
.text:77F49487 mov [ebp-14h], ecx ; [ebp-14h] = pShimData
[/code]

检查[ebp-14h]的值不是0就跳转
[code]
.text:77F49CA7 loc_77F49CA7: ; CODE XREF: LdrpInitializeProcess(x,x)+93Cj
.text:77F49CA7 mov edi, [ebp-14h]
.text:77F49CAA test edi, edi
.text:77F49CAC lea eax, [ebp+40h]
.text:77F49CAF jnz loc_77F492BD
[/code]

到loc_77F492BD..然后加载shim engine
[code]
.text:77F492BD and dword ptr [ebx+1ECh], 0
.text:77F492C4 push edi
.text:77F492C5 push eax
.text:77F492C6 push edi
.text:77F492C7 call _LdrpLoadShimEngine@12
[/code]

顺便提一下..从这里开始可以在ring3下面设置新进程的断点了
[code]
.text:77F492A7 loc_77F492A7: ; CODE XREF: LdrpInitializeProcess(x,x)+8D7j
.text:77F492A7 call _DbgBreakPoint@0 ; DbgBreakPoint()
.text:77F492AC mov eax, [ebx+68h]
[/code]
那个_DbgBreakPoint@0调用就是通知debugger可以调试新进程了...

进入
[code]
.text:77F57269 ; __stdcall LdrpLoadShimEngine(x,x,x)
.text:77F57269 _LdrpLoadShimEngine@12 proc near
[/code]

往下...加载shimeng.dll
[code]
.text:77F5727A push 0
.text:77F5727C push offset _g_pShimEngineModule
.text:77F57281 lea eax, [ebp+var_8]
.text:77F57284 push eax
.text:77F57285 push 0
.text:77F57287 push 0
.text:77F57289 push 0
.text:77F5728B call _LdrpLoadDll@24
[/code]

获取shimeng.dll的导出函数..有一个叫SE_InstallBeforeInit的
调用它
[code]
.text:77F57294 call _LdrpGetShimEngineInterface@0 ; LdrpGetShimEngineInterface()
.text:77F57299 mov eax, _g_pfnSE_InstallBeforeInit
.text:77F5729E test eax, eax
.text:77F572A0 jz short locret_77F572AA
.text:77F572A2 push [ebp+arg_8]
.text:77F572A5 push [ebp+arg_4]
.text:77F572A8 call eax
[/code]

进去
[code]
.text:71A5CA10 ; __stdcall SE_InstallBeforeInit(x,x)
.text:71A5CA10 public _SE_InstallBeforeInit@8
.text:71A5CA10 _SE_InstallBeforeInit@8 proc near
[/code]

这个函数很简单

1.获取shim data...从peb里面直接读出来
[code]
.text:71A5CA47 call _SeiGetShimData@16
[/code]

2.调用下面这个函数
[code]
.text:71A5CAA7 call _SeiInit@24
[/code]

3.free掉shim data的buffer

关键的函数在SeiInit里面...也很复杂
简单的讲
1.初始化全局的数据
2.建立将要用到的几个重要数据结构
3.准备好要patch的dll...获取要patch的函数名字地址..等等
4.按照一定的条件把当前已经加载的模块打上补丁调用PatchNewModules
5.完成..

以后如果又有新的module加载到进程地址空间
ntdll总会调用shimeng.dll的SE_DllLoaded的导出函数
这个函数调用PatchNewModules(其实是jmp)..给新的module打补丁...

可以看到两个操作分别由两个函数完成..
因为这两个函数大量依赖sdb文件格式..所以我就没有列出他们的代码来..

题外话
如果你要问...我的那些代码里面的函数名字啊什么的怎么来的...
嗯.windows有提供所有的操作系统的文件的pdb下载...就是program database
里面有函数的名字参数信息等等...比较推荐的是下载checked build版的pdb
信息丰富些...有部分pdb还带有type info...那种简直就跟完整的源代码一样了
下载工具嘛..当然是softice带的Symbol Retriever了...
反汇编的工具...ida...4.6以后的版本都能非常顺利的加载pdb文件了..
4.7的ida还能自动提示你下载呢...嗯嗯...广告时间...

好了...你现在也大概的了解了整个的流程...现在来具体的看看..
先说sdb的文件格式..

sdb文件里面存放了有问题的app的列表已经修改更正的方法..
如果你不习惯我写的程序的话呢
用ms自己的吧...xp的用户在你的安装盘下面的support目录的tools下面有个act20.exe的
运行它..完成安装...然后运行compatadmin.exe在开始菜单里面就有...
2003的用户.....很不幸...我没有找到合适的下载安装...xp的那个不能安装..
但是compatadmin.exe却能在2003下面运行...我的附件里面有一个check build xp下的
compatadmin....不过也很不幸....ms没有准备这个程序的pdb...哈哈

运行看看界面吧..大致了解下sdb里面都保存有什么样子的信息先...

我们还是继续..
sdb文件的数据存放是一块一块的..每一块有一个头..描述这个块的数据类型
如果这个块是一个容器类型的..那么它就可以跟其他的块构成一种继承的体系结构
也就是我在table.txt里面描述的那样..

ms定义有9种块类型..每个块的类型用占有了4个bits..保存到了块的头里面
[code]
union _tag_header
{
struct _small
{
WORD m_wTag;
};

struct _big
{
WORD m_wTag;
DWORD m_dwSize;
};
};
[/code]

m_wTag & 0xf000得到的就是块的类型
ms定义的块类型有如下几种
0x1000 = Indicator; 0x2000 = BYTE; 0x3000 = WORD;0x4000 = DWORD;0x5000 = QWORD
0x6000 = String Index to String Table;
0x7000 = List or Table(String Table and Index Table);
0x8000 = String ; 0x9000 = Binary Data Buffer

前面6种都使用_small格式的header..因为他们的数据大小是固定的.分别是0,1,2,4,8,4字节
后面三种使用_big格式的header..大小在header里面给出..

先看简单的
0x1000的没有数据跟在头的后面.它主要用来作一个标记的作用

0x2000后面有一个字节的数据
0x3000是2个字节
0x4000是4个,0x5000也8个
这三个是最主要的数据类型..用来记录程序的信息.比如大小(DWORD),文件时间(QWORD).等等

0x6000也是4个..他用作string table的偏移量

0x8000是一个单独的String.在header后面紧跟着unicode的字符串...他主要是当作string
table的一个表项来处理..ms把文件里面使用的string 收集起来放到一个表里面..
然后用一个偏移量来表示一个string

0x9000是二进制的数据...在header后面紧跟着2进制的数据.主要用来放patch的code
(msi部分也有用他来放文件数据的)

而0x7000的就是文件结构的构架者了...他们把sdb文件形成一个tree一样的继承结构
他们本身并不保存数据..只是当作一个容器使用..比如下面这样
wTag dwSize wTag wValue
[0x7001][0x0000000c][0x4001][0x00012345][0x3002][0x1234][0x1001]
[0x7002][0x00000112][0x7003][0x00000030].....

0x7001描述的list大小有12个字节..也就是说[0x4001]跟[0x3002]跟[0x1001]
这些都属于他的范围(加起来刚刚好12个字节)..也就是说后面三个都是他的child
而0x7002已经不属于他的范围了..是他的兄弟..而0x7003还在0x7002的范围里面
所以是0x7002的child...画成tree图
[code]

+----7001
| +----4001
| +----3002
| +----1001
|
+----7002
+----7003
[/code]

一定要明白这种结构..
如果觉得有些困难的话...参考我的源代码...那么函数的名字都参考了ms的名字
Tag表示0x7001这样的东西..TagID其实就是一个文件的offset..

sdb的tree结构的整体可能情况我已经准备到table.txt里面了.同时运行我的程序
选一个sdb文件.如果不选..默认是c盘windows下apppatch下的sysmain.sdb
然后出来的tree or app选yes就能看到sdb的结构了

这里作个简单的描述
root node是一个假想的node.他有3个children..排头的是index table..
这个用来加速搜索的..暂时先放放..接着是database...主要的数据都放在这个的下面
然后是string table

database下面放的就几乎是全部的信息了...sysmain.sdb里面database下面出现的内容
主要是library layer 跟exe...排头的几个数据描述的是关于这个sdb文件的信息..

强烈建议你运行我的程序对比table.txt跟我的描述来看.很多你一看就明白是什么东西了..
程序运行方法如上..

先看exe的部分...这个个list 保存的是所以记录在案的有问题的exe文件
*n表示有很多个这样的list
里面放有基本的比如文件名字..通配的文件名字(比如你想凡是叫*_MP.exe的都进行某种操作)
应用程序的名字(这个是一个文本的名字.前面那个是比如xxx.exe的名字)
vendor的名字..guid(这个用来读取注册表的..你可以在注册表里面设定某些flags
ms用这个guid作为key的名字去获取这些flags)
等等等的基本信息...在table.txt里面..我写着有check的就表示..
在检查exe匹配的时候(也就是上面的由apphelp.dll完成的第一步)要进行检查的项目
没有标记check的就表示不需要检查匹配的.
除了一些基本的信息以外..
还有几个很重要的信息..
其中0x7008是一个匹配的列表..里面放了所有必须要进行匹配的项目,同样用check标记出来
这些都很简单..都是些标准的数据类型..一看就明白
只是注意一个0x1003的表示是使用not操作..就是说要不匹配列举出来的项目才通过
File Match List也可以有很多个..这样你就可以指定某个exe文件的匹配程度
比如quake3的那个就必须匹配3个文件..而他只是要求这3个文件存在就行
也就是每个list下面只有一个0x6001的string ,描述文件的名字(相对路径)
这一点你可以看ms的compatadmin来了解..或者用我的程序打开sysmain.sdb
tree or app的message box选no..然后左边的list里面按q..就会看到Quake III
然后看右边列举的...很简单.看看就明白...
右边的tree ctrl里面...如果是要check的..我就用[V]标记了

了解了这个match list就能想象apphelp.dll的工作了..
从exe的名字先找到对应的exe list..然后找到file match list..
然后遍历每个list的每个属性..检查匹配..很容易吧..
至于怎么从xxx找到xxx再找到xxx...呵呵..如果你对sdb的结构很了解了就很容易了
看我的源代码吧...

我们来看看apphelp.dll的工作..
[code]
.text:75D63820 ; __stdcall InternalCheckRunApp(x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x)
.text:75D63820 _InternalCheckRunApp@68 proc near
[/code]

往下..打开sdb文件
[code]
.text:75D63968 call _SdbInitDatabaseEx@12
[/code]

寻找match的exe
[code]
.text:75D63992 call _SdbGetMatchingExe@24
[/code]

分析结果...生成peb->pShimData
[code]
.text:75D639F1 call _ParseSdbQueryResult@20
[/code]

进入
[code]
.text:75D61AC4 ; __stdcall SdbGetMatchingExe(x,x,x,x,x,x)
.text:75D61AC4 _SdbGetMatchingExe@24 proc near

................................

.text:75D61C5B mov eax, 7007h ; EXE_LIST_TAG
.text:75D61C60 push eax
.text:75D61C61 push dword ptr [esi+4]
.text:75D61C64 push esi
.text:75D61C65 call _SdbpSearchDB@32
[/code]
到这里...搜索7007h也就是exe list

搜索部分使用了index table...先不管...最终找到exe list的tagid(也就是文件偏移)
[code]
.text:75D6AB4C call _SdbpCheckExe@44
[/code]

[code]
.text:75D64078 ; __stdcall SdbpCheckExe(x,x,x,x,x,x,x,x,x,x,x)
.text:75D64078 _SdbpCheckExe@44 proc near

..................

检查 4021h...如果存在的话

.text:75D640BB push 4021h ; Runtime Platform ID
.text:75D640C0 push [ebp+ExeListTagID]
.text:75D640C3 push edi
.text:75D640C4 call _SdbFindFirstTag@12

........
同上
.text:75D640D1 push 4022h ; OS SKU
.text:75D640D6 push [ebp+ExeListTagID]
.text:75D640D9 push edi
.text:75D640DA call _SdbFindFirstTag@12

.......
还是同上
.text:75D640EA push 401Fh ; OS Service Pack
.text:75D640EF push [ebp+ExeListTagID]
.text:75D640F2 push edi
.text:75D640F3 call _SdbFindFirstTag@12

.........
如果上面三个条件都通过了
.text:75D64115 call _SdbpCheckForMatch@28

..................
.text:75D64131 ; __stdcall SdbpCheckForMatch(x,x,x,x,x,x,x)
.text:75D64131 _SdbpCheckForMatch@28 proc near

..............
获取match file list
.text:75D64172 push 7008h ; MATCH_FILE_LIST_TAG
.text:75D64177 push [ebp+ExeTagID]
.text:75D6417A push [ebp+ShimDB]
.text:75D6417D call _SdbFindFirstTag@12

...............
检查 logic not 标识
.text:75D6418D push 1003h ; MATCH_LOGIC_NOT_TAG
.text:75D64192 push eax
.text:75D64193 push [ebp+ShimDB]
.text:75D64196 call _SdbFindFirstTag@12 ; SdbFindFirstTag(x,x,x)
.text:75D6419B xor ecx, ecx
.text:75D6419D cmp eax, ebx
.text:75D6419F setnz cl
.text:75D641A2 mov [ebp+UseLogicalNot], ecx
...........
下面有*通配符的处理..就是说*出现在file match里面的话..就用exe的名字替换
.................
然后还有%的处理..用环境变量替换..或者是system目录替换(%system32%)

收集好所有的要进行match的file以后(一直loop 到 .text:75d64172)

然后检查所有的attributes

.text:75D64A8C call _SdbpCheckAllAttributes@16

.................
.text:75D64B0D ; __stdcall SdbpCheckAllAttributes(x,x,x,x)
.text:75D64B0D _SdbpCheckAllAttributes@16 proc near

..................
用eax作为下标遍历_g_rgAttributeTags数组里面的所有tag...
如果存在的话..根据类型读取值出来

.text:75D64B2E mov eax, [ebp+var_4]
.text:75D64B31 xor esi, esi
.text:75D64B33 mov si, _g_rgAttributeTags[eax]
.text:75D64B3A push esi
.text:75D64B3B push [ebp+arg_8]
.text:75D64B3E push edi
.text:75D64B3F call _SdbFindFirstTag@12
.text:75D64B44 mov edx, eax
.text:75D64B46 test edx, edx
.text:75D64B48 jnz short loc_75D64B5D

...........
.text:75D64B5D loc_75D64B5D: ; CODE XREF: SdbpCheckAllAttributes(x,x,x,x)+3Bj
.text:75D64B5D mov ecx, esi
.text:75D64B5F and ecx, 0F000h
.text:75D64B65 xor eax, eax
.text:75D64B67 cmp ecx, 4000h
.text:75D64B6D jz loc_75D66591
.text:75D64B73 cmp ecx, 5000h
.text:75D64B79 jnz loc_75D66263
............
下面有诸如这样的函数调用读取值
.text:75D64B85 call _SdbReadQWORDTag@16

............
.text:75D66591 loc_75D66591: ; CODE XREF: SdbpCheckAllAttributes(x,x,x,x)+60j
.text:75D66591 push 0
.text:75D66593 push edx
.text:75D66594 push edi
.text:75D66595 call _SdbReadDWORDTag@12

............
值读出来了就进入到
.text:75D64B9B call _SdbpCheckAttribute@16

...............
.text:75D64BA9 ; __stdcall SdbpCheckAttribute(x,x,x,x)
.text:75D64BA9 _SdbpCheckAttribute@16 proc near

..................
他根据tag表示的意思一一获取每个属性,有些是文件大小(用windows获取文件大小的函数)
有些是文件版本信息..用version.dll提供的函数..
有些是pe文件信息(checksum,pe checksum,linker version等等)..获取pe文件头
然后从里面读...
.text:75D64BF2 call _SdbpGetAttribute@12

分支成3个部分
.text:75D64D02 call _SdbpGetVersionAttributes@8
.................
.text:75D66697 call _SdbpGetHeaderAttributes@8
.................
.text:75D64CAA call _SdbpGetFileDirectoryAttributes@4

获取完了就进行检查
也是3个部分...普通的dword qword直接比较

版本信息检查函数
.text:75D64E2F call _SdbpCheckVersion@16
...................
.text:75D65309 call _SdbpCheckUptoVersion@16
.............
字符串匹配
.text:75D64C68 call _SdbpPatternMatch@8

检查完了就返回一个true or false
然后在SdbpCheckForMatch里面

.text:75D64A94 loc_75D64A94: ; CODE XREF: SdbpCheckForMatch(x,x,x,x,x,x,x)+211Bj
.text:75D64A94 test eax, eax
.text:75D64A96 jz loc_75D64E0C
.text:75D64A9C test esi, esi ; esi = Logical not
.text:75D64A9E jnz loc_75D66251

检查返回值...同时检查logcial not标记...
然后正确的结果...
再出来以后就是把结果打包到一个ShimData里面..里面包含有这个
exe使用的exe list的tagID的值...这个值会在获取exe的patch信息的时候使用..
只是省略些步骤而已...

至于要检查哪些tag呢..看看那个数组就知道了
.data:75D7E060 _g_rgAttributeTags dw 4001h ; DATA XREF: SdbpCheckAllAttributes(x,x,x,x)+26r
.data:75D7E060 ; TagToIndex(x):loc_75D64C8Er
.data:75D7E062 dw 4003h
.data:75D7E064 dw 5002h
.data:75D7E066 dw 5003h
.data:75D7E068 dw 6011h
.data:75D7E06A dw 6012h
.data:75D7E06C dw 6009h
.data:75D7E06E dw 6010h
.data:75D7E070 dw 6013h
.data:75D7E072 dw 6014h
.data:75D7E074 dw 6015h
.data:75D7E076 dw 6016h
.data:75D7E078 dw 4007h
.data:75D7E07A dw 4008h
.data:75D7E07C dw 4009h
.data:75D7E07E dw 400Ah
.data:75D7E080 dw 4006h
.data:75D7E082 dw 400Bh
.data:75D7E084 dw 401Ch
.data:75D7E086 dw 6017h
.data:75D7E088 dw 6020h
.data:75D7E08A dw 500Dh
.data:75D7E08C dw 5006h
.data:75D7E08E dw 401Dh
.data:75D7E090 dw 401Eh
.data:75D7E092 dw 4012h
.data:75D7E094 dw 0
.data:75D7E096 dw 0

别忘了计算在调用checkallattribute之前检查的3个属性...
[/code]

检查的部分就到这里...很简单..你了解了file match的结构以后都能想象出来了已经..

下面看使用的部分...
首先要了解的就是shim ref list
他放在shim info的reference..也就是他只是保存了一个ref而已..
并不是value本身...这就好像是一个指针...间接索引..

ms把一个patch叫作一个shim...shim都有一个名字...
比如quake3使用的两个shim 一个叫IgnoreAltTab一个叫EmulateOpenGL
ref list里面得那个dword就是真正得shim info所在得文件得offset..也就是tagID
这个dword是可选的...如果不存在的话..就得用shim得名字去搜索位于library list下面的
所有shim的list.

而真正的shim也是两个主要的成员..一个是名字...上面的搜索过程就是要比较这个名字
另外一个是完成shim功能的dll的名字..比如quake3的IgnoreAltTab是有AcGenral.dll完成的
还有一个command line..会作为一个参数传递..看下面的解释
同时这里还有include跟exclude list,这个是用来指定哪些文件的import table需要被修改
哪些又不需要...同时注意到library下面也有一个include list..那个是全局的..

然后还有patch ref list..他的使用方法也跟shim 差不多..他是一个binary的buffer
里面放有patch的code...他由若干个变长的结构构成...
[code]
struct _patch_item
{
DWORD m_dwOp; // 0 = nop 2 = patch 4 = compare
DWORD m_dwNextItemOffset;
DWORD m_dwPatchSize;
DWORD m_dwPatchStartAddress;
DWORD m_dwPadding;
TCHAR m_szModuleName[32];
BYTE m_byCode[1];
};
op指定了当前这个item要完成的操作..一般的顺序是4,2,4,2.....0
就是说..对于一个文件..我先检查某一部分的特征数据(op=4)如果匹配了
表示文件是可以作memory patch的 然后跟一个op = 2的进行patch..然后循环..最后用0结尾
因为patch是变长的..所以m_dwNextItemOffset指定了下一个item相对于当前的偏移
m_dwPatchStartAddress是从模块的DosHeader开始计算的
(也就是通常用GetModuleHandle返回的值)...m_szModuleName指定module的名字
如果是数组第一个元素就是0,那么使用当前exe的DosHeader..这个可以从peb里面获取
解释的具体方法可以参考我的源代码..那个数组是用来作compile time assert的
确保m_byCode的offset是0x54..#pragma是用来关闭unreferenced local var的警告的
[/code]

至于flags ref list也同上面两个寻找方式差不多..他主要是用过运行环境的..
设置一些开关...这个部分我也没有怎么跟踪...

而layer ref list稍微不同...他不在library下面..而在database下面..
至于什么是layer...其实就是shim patch flags的组合..
比如一个layer叫win95..那么你就可能会要求有很多shim要使用..而有很多的程序都
运行在这种环境下..你就可以把这些shim整合成一个layer..而直接在exe下面放一个layer
ref list就可以了...

然后要说的就是include 跟 exclude list了...
知道shim是要修改import table的..可能你要求某些dll的import table不去修改.或者
只是修改某个dll的import table而不是默认的修改几乎全部的dll..那么include exclude
list就是一个好的东西....
两种list都有相同的结构..唯一不同的就是
如果在list下面有一个include indicator(0x1001)那么这个就是include..
如果没有..就是一个exclude...
然后有三个特殊的通配符...*是用来清空一个list的...$是用来通配当前的exe文件的
NOSF是表示不检查受windows保护的系统文件的..他只能跟include使用..跟exclude放到
一起就被无视了...

这样一来的检查规则就是
5 = include* 也就是只检查exclude list,记住*是用来情况一个list
如果一个dll出现在了这个list里面...就不对他作patch动作

3 = exclude* 只是检查include list.如果在里面就patch.不在就不patch

4 = include NOSF + exclude* 只是检查include list 只是对里面的作patch

1 = include and exclude 都检查 在include list patch.在exclude里面不patch.
都不在的话..如果是在system32或者SxS目录下面就不patch.否则patch

2 = include and exclude NOSF 同上.只是要额外检查windows的保护

NOSF的含义就是必须要作额外的windows 保护的检查
这个部分大致是这样...我没有作深入的分析..只是走马的看了看

最后了...patch是怎么完成的呢...
首先作memory patch..也就是上面patch ref list里面的工作
然后是修改import table.

对于每个shim list里面的dll..shimeng.dll加载他们..他们会export一个叫
GetHookApis的函数

shimeng.dll调用他们..传递参数
第一个是command line..这个是在shim list里面指定的
第二是shim的名字比如IgnoreAltTab这样的

shim dll返回一个HookApi的结构...里面放有要hook的函数的名字等等信息..
同时返回要hook的函数的个数...

至于修改import table就容易了
dos header + 3c 取出来 加到 dos header上 再偏移80h的地方取到import table的rva
再加到dos header上面...然按需要修改first chunk数组里面的值...当然得作些检查比较...
这个部分的技术都已经是烂到不能再烂的了...不多说了..看看实际的代码吧

关键函数
[code]
.text:71A5BEB0 ; __stdcall SeiInit(x,x,x,x,x,x)
.text:71A5BEB0 _SeiInit@24 proc near
.............
全局初始化
.text:71A5BF70 call _SeiInitGlobals@4

收集使用到的shim的tagid值到数组里面
.................
.text:71A5BFDE call _SeiBuildShimRefArray@24

..................
收集全局的include list....
.text:71A5C0D6 call _SeiBuildGlobalInclList@4

.....................
收集单个shim的include跟exclude list
全局的include list会被copy进来...
当然如果有include *这样的.自然会被清空了
.text:71A5C37B call _SeiBuildInclExclList@16

....................
获取shim dll的path
.text:71A5C3DD call _SdbGetDllPath@16

............
获取shim dll的handle..如果没有加载则加载之
.text:71A5C40C call _LdrGetDllHandle@16
...............
.text:71A5C433 call _LdrLoadDll@16

...............
shim command line
.text:71A5C494 call _SeiGetShimCommandLine@12

............
获取shim dll的GetHookApis函数地址
.text:71A5C509 call _LdrGetProcedureAddress@16

.......
调用他...返回值是一个HookApi结构数组地址..count填充第三个参数指定的地址
.text:71A5C522 lea ecx, [esp+10Ch+var_E4]
.text:71A5C526 push ecx ; hook count
.text:71A5C527 lea edx, [esp+110h+var_B0]
.text:71A5C52B push edx ; shim name
.text:71A5C52C push edi ; command line
.text:71A5C52D mov [esp+118h+var_E4], 0
.text:71A5C535 call eax

...............
load memory patch
.text:71A5C7E9 call _SeiLoadPatches@8

...............
真正开始patch module
.text:71A5C861 call _PatchNewModules@4

.............
解析hook api的真正内存地址
.text:71A5A58D call _SeiResolveAPIs@4

...........
作memory patch
.text:71A5A5B3 call _SeiAttemptPatches@0

...........
然后会检查要patch的module的路径是否是system32,是否是SxS等等
为include exclude检查作准备
最后调用
.text:71A5A710 call _SeiHookImports@24
...........
.text:71A5A200 ; __stdcall SeiHookImports(x,x,x,x,x,x)
.text:71A5A200 _SeiHookImports@24 proc near
.............

.text:71A5A2C9 mov edi, [esp+0B0h] ; edi = NtHeader
.text:71A5A2D0 mov eax, [edi+3Ch] ; eax = PE Header offset
.text:71A5A2D3 mov esi, [eax+edi+80h] ; esi = import table RVA

.........
遍历import table寻找要hook的api的module(由shim dll的GetHookApis返回的结构里面指出)

..............
找到这个module了..
遍历export函数表寻找要修改的函数

调用它先转换序号
.text:71A5A3C1 call _SeiGetOriginalImport@4

然后是它
.text:71A5A3CE call _SeiConstructChain@12
.text:71A5A3D3 mov esi, eax
.text:71A5A3D5 test esi, esi ; esi = pHookApi

来完成寻找工作...
如果找到了..返回值是要hook的api的结构信息(就是由GetHookApis返回的那个)

...........
检查exclude
.text:71A5A3EA push ecx ; ModuleName
.text:71A5A3EB call _SeiIsExcluded@12

如果也通过了
............
修改内存保护属性
.text:71A5A438 mov ebx, ds:__imp__NtProtectVirtualMemory@20
...........
.text:71A5A45D call ebx ; __declspec(dllimport) NtProtectVirtualMemory(x,x,x,x,x)
修改之...edi是要修改FirstChunk的地址...
esi是HookApi结构地址...offset + 8 是新的函数地址
............
.text:71A5A463 mov eax, [esi+8]
.text:71A5A466 mov [edi], eax

..................
改回来保护属性
.text:71A5A482 call ebx ; __declspec(dllimport) NtProtectVirtualMemory(x,x,x,x,x)

.........
往上循环遍历....直到完成
[/code]

以后凡是有新的dll被加载到进程地址空间..
ntdll都会调用shimeng.dll的SE_DllLoaded函数
ntdll会准备好一个加载的dll的链表
为什么是一个链表而不是一个文件?因为在加载一个dll的时候可能它也会import别的dll
如果单个文件的话..dll的reference不解析的话..
first chunk并没有被修改指向函数地址而是指向name 数组的话..
修改了import table也没有用...

[code]
.text:71A5A9F0 ; __stdcall SE_DllLoaded(x)
.text:71A5A9F0 public _SE_DllLoaded@4
.text:71A5A9F0 _SE_DllLoaded@4 proc near
..................

.text:71A5AA2A jmp _PatchNewModules@4
[/code]

终于完了......不知道这么多东西堆积起来有没有人愿意看...呵呵
代码太多了...我也没有办法作完整的全部的分析..也不能深入到每个细节
有些部分我就跳过去了....

最后几个比较有用的环境变量SHIM_DEBUG_LEVEL设置一个比较大得正数..能看到shim的调试信息
至于ShowDebugInfo就不用了..那点信息少得可怜..嗯..它不是环境变量..是一个regedit的
还有几个全局得变量..似乎是编译得时候指定..没有看见shimeng.dll自己有修改
_g_DebugLevel _g_bDbgPrintEnabled都指定一个正数就ok了..这个得用调试器了..

最后..enjoy yourself

你可能感兴趣的:(windows,list,dll,exe,include,patch)