本帖最后由 root1605 于 2017-8-25 23:56 编辑
Ⅰ预备
经常看到“加驱”却不知道什么意思?分不清线程与进程?不知道“Hook”是什么?纠结于所谓安软没有“原生”支持64位?本文为科普性质,将对国外区常见论题涉及的知识做一个简要介绍(当然这一贴只涉及很少一部分内容)。为了便于大部分没有基础的童鞋理解,省略了大量细节,若对这方面有兴趣,建议多学习其他纸质资料。 楼主非专业人士,还请大家多多帮助。 为了避免“鸡生蛋,蛋生鸡”的死循环,这里先对本文中涉及的一小部分词汇做一个提前申明,其他的会在合适的地方出现。
伪代码
伪代码是一种描述程序执行步骤的语言,介于自然语言与编程语言之间,它没有固定格式,不能被直接执行,只是程序员用来表达思路的一种工具。
例:(输出一个数的三次方的伪代码)- print "Input a number:"
- let a= input
- print a*a*a
复制代码 本文中某些描述简单逻辑的部分可能会给出少量伪代码。
函数
函数是程序中的一段有特定功能的代码片段,可以看作是一块“小零件”,你可以在程序的其他地方使用这些“零件”,也可以使用别人写好的“零件”而不必自己动手。下面是一个求某数绝对值的函数(伪代码)
- function Abs(x)
- if (x>0)
- return x
- if (x<0 or x=0)
- return -x
- end function
- //“Abs”后的圆括号内的x是参数,一些“零件”需要你告诉它具体的数据它才会工作,
- //比如一个计算正弦值的函数,如果不输入弧度,它便不知道该干什么。
- //return的意思是返回一个值,给使用这个“零件”的人一个结果
- //函数可以没有返回值和参数,具体由函数的用途灵活决定,
- //因为有些功能不需要返回什么值或者得到一些参数
复制代码
实例
这里介绍“实例”的含义为本文中的含义。实例指的是抽象概念的一个具体化的产物,往简单说,一台计算机的配置单不是实例,而根据这个配置单生产出来的一台计算机就是“实例”;一栋楼房的工程图纸不是实例,但根据这个图纸建造出来的一栋实实在在的楼房就是一个“实例”。
Windows API
Windows API,又称作是(Windows)API函数,API为“编程接口”,Microsoft把Windows的很多功能提供成“小零件”(函数,参见上文)的形式供开发者使用?为什么叫“接口”?因为这些函数提供了外部访问系统功能的方法,所以叫“接口”。下面示例调用了一个Windows提供的函数(弹出一个简单的对话框)
- #include <windows.h>
- int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
- {
- ::MessageBox(0, TEXT("This is a Demo"), TEXT("Demo"), MB_OK);
- system("pause");
- return 0;
- }
复制代码
Windows提供的API函数特别多,主要由以下几个方面的:
•基本服务
•组件服务
•用户界面服务
•图形与多媒体协作
•网络
•Web服务
平时编写程序不直接使用Windows API,而是使用一些将这些API封装起来的“库”,比如MFC(微软基础类库),WTL(Windows模板库)等,它们通过一些手段将函数封装起来,更加容易使用,但这些“库”还是建立在Windows API之上的。
有关于Windows API的内容,下文会继续讨论。
*创建MFC工程
Ⅱ 进程与线程
“程序”是一系列静态的指令,它本身不会自动执行。进程通常可以定义为一个程序的“实例”(参看前文“实例”),进程包含了这个程序在运行时所用到的各种资源。
*Windows中的进程
如上图所示,每一个运行的Windows程序都有一个特定的进程,如果同一个程序同时运行两份“实例”,就有两个进程,而这两个进程是互不相干的。就像是根据同一个模子造出来的物品,虽然它们一样,但是是不同的实体。
显然看了这些还是不够清晰,下面讲述几个与进程紧密相关的概念。再讲述线程
虚拟地址空间
让每一个程序都单独地使用一根物理内存条?哈,这是不可能的。所有程序运行时的数据,都在其虚拟地址空间里,为什么是虚拟的?难道数据没有在内存里?
我们先按照大多数人认为的两个运行中的程序使用内存存储和读取数据的过程。
假设内存是由一个个大小相同的方格子组成,1~100代表了有100个“方格子”,也就是100个存储单元。前面都说了,程序不可能各自使用单独的内存条,那么就只好共用了。假设A程序使用了1~50号方格,B程序使用了51~100号方格,但是这并不代表A程序“看不到”B程序在使用51~100号方格,A程序可以任意读取B程序的内存数据,反过来B也可以。如果属于各自程序运行时的数据被修改(不管是恶意的还是无意的),都可能导致“恶意”的效果。你也许看见过小孩抢东西的场景,反正东西就那么多,够的话总觉得别人的好,不够的话更会争夺。
如果采取这种方式使用内存,也太危险了,像现在的Windows系统中运行的程序如此之多,如果各个运行中的程序之间相互影响,一出错是连环崩溃,根本不能满足Windows系统“多任务”的需求。问题似乎回到了原点,那么就每个程序分配一根内存条吧,它们之间是不可见
的。其实,Windows就相当于是这么做的。Windows借助于硬件的支持,实现了一个很大的线性的寻址空间,什么意思呢?(在32位的Windows中)每个运行中的程序都有属于4GB的内存空间,其中有2GB可以自由访问(另外2GB稍后说明),但这4GB是“虚拟的”,你确实可以往里面放东西,但它不一定对应着物理内存中的某一块内存区域。那么系统中这么多进程,每个进程都有4GB,还可以随便放东西,难道Windows这么神通广大吗?
物理内存靠系统是铁定不能增加的,Windows会把一个进程中的某些数据转移到硬盘上,这个数据就在物理内存中不存在了,但是这个运行中的程序并不知道操作系统的这个动作,如果被转移的数据原来在虚拟的地址空间中的地址为0x22222222(16进制数,前面加个前缀“0x”),那么被转移后,程序依然可以通过这个地址来访问数据,此时Windows又会把数据再重新装回到内存中去。
*虚拟地址空间中的地址会映射到物理内存的地址或者硬盘上的数据,这一映射关系由系统控制,不是应用程序
一般,进程运行时产生的运行时的数据肯定不会把物理内存占满,虽然每个进程都有权使用2GB的私有存储空间,但是并不会在加载进程就在物理内存中抢占内存不准其它进程使用。
虚拟地址空间,实际上也是Windows将程序与物理内存隔开加的一个中间层。每个进程都有自己的2GB可以任意使用的空间,且只对当前进程可见,其它进程是看不到的。在A进程中有一个地址为0x1ABCDEEE,B进程中也有一个地址为0x1ABCDEEE,虽然地址相同,但通常它们指向的是不同的数据(为什么是“通常”,比如也可以通过一些方法让某个进程的私有的地址空间的一部分成为共享内存区,但这必须显式确认),这样就很好地实现了进程和进程之间的隔离问题,每个进程之间是相对隔离的,互不可见,即使需要访问,也得借助于系统来完成交互。
图片来自MSDN,虚拟地址与实际物理地址的关系
为什么说在32位系统中,每个进程只有2GB的自由使用的虚拟地址,那么另外2GB呢?这另外2GB的虚拟地址直接映射到核心的系统组件和驱动程序等,它们共用这2GB空间,互相之间是可见的。而且这2GB不能随便访问和修改,原因在下文会简单介绍。
*2GB虚拟地址进程私有,另外2GB关联到系统的核心组件,不能随便使用
线程
按照一般的定义: 线程是指程序的一个指令执行序列,也就是一个指令流的运行单元,不懂没有关系,阅读下面的伪代码:
- 程序开始点:
- 寻找程序运行时目录里的音乐文件hello.mp3
- 如果找到
- 播放
- 否则
- 退出程序
- 寻找程序运行时目录里的音乐文件hello2.mp3
- 如果找到
- 打开
- 否则
- 退出程序
- 程序结束
复制代码
假设这就是一个完整的“指令流”,程序由上执行到下,只有在播放hello.mp3音乐结束后才会播放hello2.mp3,那么如果我想在听hello.mp3的时候同时听到hello2.mp3呢?
可以更改程序:- 程序开始点:
- 创建线程A,将执行以下指令->{寻找程序运行时目录里的音乐文件hello.mp3
- 如果找到
- 播放
- 否则
- 退出程序}
- 创建线程B,将执行以下指令->{寻找程序运行时目录里的音乐文件hello2.mp3
- 如果找到
- 打开
- 否则
- 退出程序}
- 同时运行线程A,线程B
- 程序结束
复制代码
那么,如果hello.mp3和hello2.mp3都存在的话,就可以同时听hello.mp3和hello2.mp3两个音乐了。
进程只相当于是一个程序运行时的容器,不是进程在执行任务,真正执行任务的是线程。
线程在执行任务时会产生各种各样的数据,那么这些数据就存在进程的虚拟地址空间里面。线程不能独立于进程而存在。你可以把想象进程是一个汽车站,而线程使一些执行运输任务的汽车,汽车站里容纳了各种各样的供汽车使用的东西,而真正执行任务的是汽车。所以,所谓的“Windows给进程分配资源,而调度的是线程”就不难理解了。
进程中可以有任意个线程,但总是会有一个,如果没有,那么进程就没有什么意义,会被操作系统回收掉。
另外,不要错误的认为一个“窗口”就是一个进程,或者一个窗口就是一个线程,如图是一个典型的MDI窗体(多文档界面),它都属于一个进程。
关于进程和线程,暂时到这里,不宜深入讨论。
Ⅲ Windows消息处理机制
Windows系统是一个事件(消息)驱动型的系统,消息很好理解,可以认为它是给程序发出的一个“信号”。Windows操作系统为每一个GUI线程维护了一个消息队列(准确的说,任何线程,只要注册了窗口类,肯定会有一个消息队列,来接受用户的操作和系统的消息。不要紧张,这里只介绍最基本的内容。
比如,某一个窗口上有一个按钮,当这个按钮被按下时,首先是系统感知到了这个“事件”,然后判断出这个“事件”是“右键单击”,随后把着一个消息投递给了应用程序,应用程序再根据这个消息,做出相应的动作(怎么做,是编程时提前设定好的)
*如图,当"Button"被按下时,应用程序收到消息,然后根据消息将窗体的标题改为了“被点击了”
消息分为两种,一种是进队,另一种是不进队。所谓进队和不进队,区别就在于“不进队消息是指由Windows直接调用消息处理函数,把消息直接交给其处理。而进队消息是指Windows将消息放入到程序中的消息队列中取,并通过程序中的消息循环,循环把消息取出,经过一定处理,然后由函数DispathMessage函数将消息分发给消息处理函数处理。”,听起来很复杂,但是你把它想简单一点是没有问题的。假设程序就是一个机器人,这个人需要不停的接受外界人的指令它才干活,它会不停搜寻传递给它的信息并作出处理,而有些消息不需要它搜寻,直接由外界遥控它就可以了。当然,实际上消息机制是一个很复杂的东西,这里也没必要理解太多。下面是一个典型的Win32窗体应用程序(C语言,先不要去看代码)
- #include <windows.h>
- #include<tchar.h>
- LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
- int WINAPI _tWinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR szCmdLine, int nCmdShow)
- {
- WNDCLASS wc;
- wc.style = CS_HREDRAW | CS_VREDRAW;
- wc.lpfnWndProc = WindowProc;
- wc.cbClsExtra = 0;
- wc.cbWndExtra = 0;
- wc.hInstance = hInstance;
- wc.hIcon = NULL;
- wc.hCursor = LoadCursor(NULL,IDC_ARROW);
- wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
- wc.lpszMenuName = NULL;
- wc.lpszClassName = _T("MyWindowClass");
- if (!RegisterClass(&wc))
- {
- MessageBox (NULL, _T("窗口注册错误"), _T("错误"), MB_OK);
- return 0 ;
- }
- HWND newWindow = CreateWindow(
- _T(""),
- _T(""),
- WS_OVERLAPPEDWINDOW,
- 0,
- 0,
- CW_USEDEFAULT,
- CW_USEDEFAULT,
- NULL,
- NULL,
- hInstance,
- NULL
- );
- if (NULL == newWindow)
- {
- MessageBox (NULL, _T("无法创建窗体"), _T("错误"), MB_OK);
- return 0;
- }
- ShowWindow(newWindow, nCmdShow);
- UpdateWindow(newWindow);
- MSG msg;
- while(GetMessage(&msg, NULL, 0, 0))
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- }
- LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
- {
- switch (uMsg)
- {
- case WM_DESTROY:
- {
- PostQuitMessage(0);
- break;
- }
- default :
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
- return 0;
- }
复制代码
重点在这个地方:
- while(GetMessage(&msg, NULL, 0, 0))
- {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
复制代码
这是一个循环,意思是,只要操作系统还在发出消息,并程序没有被要求关闭,就处理消息,消息处理函数为
- LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
- {
- switch (uMsg)
- {
- case WM_DESTROY:
-
- PostQuitMessage(0);
- break;
- default :
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
- return 0;
- }
复制代码
这个函数的作用是,根据Windows发出的消息,做出不同动作,比如WM_DESTORY这个消息,就执行PostQuitMessage(0)。
下面这幅图就是Windows消息机制(简化后的)示意图,出处未知:
Ⅳ Hook钩子
在卡饭经常见到“Hook”这个词,总把新人绕的云里雾里,如果不懂上面的知识,就很难明白钩子是干什么的。这里是本文的主干部分,但文字并不多,重点在于理解上文。
Hook,是Windows消息处理机制中的一个监视点,在Windows将消息传递到目标程序之前,Hook会先接收到消息,它可以过滤监听消息流。当它接受到消息时,可以什么也不干,继续将消息传递下去,给目标程序,也可以将消息改变,比如:明明Windows发出了WM_DESTROY消息,而被Hook一篡改,成了WM_PAINT,当然目标程序就不会执行该执行的动作;甚至可以拦截掉消息(吞掉消息),不再向目标程序送达,目标程序还“以为”没有发生这件事。
有些恶意程序就喜欢这么干,即使处于非恶意目的,随意安装钩子会增加Windows消息处理量,会拖慢系统,另外也可能干扰其它程序的正常运行。
Hook API
Hook API指的是截取特定的进程或者系统对某个Windows API的调用,让指令流改变,跳转到自定义的代码段。这个代码段可以干完想干的事情后再跳转回原来的指令流,比如,挂钩了User32.dll中的GetDlgItem函数,那么所有调用GetDlgItem的程序(线程)在调用GetDlgItem的时候就会跳转到挂钩者提供的代码里去而不是直接执行本来应该直接执行的GetDlgItem函数,这样,可以干预其它程序的运行流程。假设某程序是这样的(伪代码):
- start:
- playmusic
- if no photo was showed
- show a photo
- end
复制代码
假如说有个Win32 API是playmusic(当然它实际上没有),如果playmusic被挂钩,挂钩者提供的函数为
- function _hook_demo()
- {
- showphoto
- return to original function playmusic
- }
复制代码
很不幸,本来程序应该先放音乐再打开照片,被这么一干扰,就成了先打开照片再放音乐了。
这里再简单说明一下原理:参照上文,Windows下的应用程序有自己的地址空间,别的进程的地址空间一般不能访问,所以线程只能调用本地址空间内的函数。当务之急就是,将一个代替API函数执行的函数插入到目标进程中,这样在同一个地址空间下才方便调用。有一种很常用的方法(当然不止这一种):把注入代码写到DLL(动态链接库)中,然后通过一些手段(省略过程)让目标进程使用这个DLL。因为代码进入了目标进程的地址空间,所以就可以干想干的事情,而没有限制了(线程总是能没有限制地访问进程内的资源)。
如果还想更进一步,可以在目标进程加载DLL的时候创建新的线程,这个线程可以为注入者做事,把它比喻成一个“内鬼”也可以。
下面给出一个实例(伪代码),防止程序自身的进程被终结:
- start:
- (挂钩TerminateProcess函数)
- 当调用TerminateProcess函数时
- //此时跳到以下指令流
- 检查被终结的进程是不是自身的
- 如果是
- Do nothing//Terminate函数不会被执行
- 如果不是
- 回到原来的Terminate函数
- end
复制代码 Ⅴ 内核模式及内核模式驱动
前文说了,在32位Windows系统上每个进程有4GB地址空间,有2GB的属于系统内核的,不能被直接使用。这体现了Windows分层设计的思想,将底层和高层隔开,防止不经意的破坏。在内核模式下运行的组件共享一个地址空间,一旦其中一个组件崩溃掉,很可能整个系统都要崩亏掉,尤其是以前的系统(Windows XP等)尤为严重,而用户模式下的进程就不会,一个播放器崩溃了与画图程序没有什么大关系,而内核模式就不一样了,它们可以互相修改,互相访问,在卡饭通常说的“系统底层”就是指的内核模式相关的东西,但这个词显然已经被滥用和误用了。
为了避免应用程序随意地修改访问关键的操作系统数据,Windows使用两种处理器访问模式,虽然处理器支持4种访问模式,内核模式,也就是通常说的R0,用户模式为R3。用户模式的代码运行于R3,无权直接访问R0的指令,而操作系统关键的代码在R0下受到隔离保护,但Windows认为内核模式下的代码是可信的,它们可以使用所有的CPU指令,而用户模式的就不行,它们只有一组有限的接口来使用系统的底层服务。
*内核模式与用户模式的关系(简图),简单了解即可
既然内核模式的组件权限这么大,内核模式组件是哪些呢?
内核模式的组件有:
①Windows执行体
②Windows内核
③设备驱动程序
④窗口和图形系统
⑤硬件抽象层
②Windows内核也叫做“微内核”,与整个“Windows内核”区分开来。
Windows执行体:基本的操作系统服务:内存管理,进程线程管理,输入输出,跨进程通信等
Windows内核:此处不介绍
窗口图形系统:图形用户界面的功能
那么这里的重点就是:设备驱动程序和硬件抽象层了。
硬件抽象层:每种硬件都不有不同之处,因此操控它们的方式也各不相同,所以提出了硬件抽象层这一概念,在其上面的部分因为有抽象层的存在,把所有的硬件都提供了一个抽象模型。举个例子,全世界各地都在说不同的语言,但是如果把英语比喻成提取出来的语言,这种语言绝大部分人都能接受,我们只需要面向英语交流就可以应付交流工作了。硬件抽象层的作用与此相同。
设备驱动程序:不要被这个名称忽悠了,它可不一定是一个物理设备的驱动程序,比如QQ浏览器会加载内核模式驱动,它用来驱动哪个硬件?有的时候,编写设备驱动程序,只是一种访问系统内部数据结构的方式。当然病毒也最希望自己能够加载内核模式驱动。像Windows执行体,Windows(微)内核等都是微软写好的系统组件,没法或很难修改,那么就得使用设备驱动程序了,设备驱动程序与这些组件“地位”是平等的,但不代表它是独立的,它不会单独和硬件交流,而是通过硬件抽象层。一般地,要想向系统内核中加入自行编写的代码,安装驱动程序是唯一的办法。
如今很多应用软件都加载内核模式驱动以获得极高的权限,在用户模式下代码崩溃了一般只影响当前进程,而内核模式就很可能影响的是系统内核(整个内核,不是微内核),因为内核模式组件之间没有隔离起来。要谨慎对待安全软件关于软件加驱的提示。
本文到这里结束。
|