C++黑客编程揭秘与防范(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-16 10:34:36

点击下载

作者:冀云编著

出版社:信息技术第一出版分社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

C++黑客编程揭秘与防范

C++黑客编程揭秘与防范试读:

前言

什么是黑客?摘自百度百科中的一句话,“黑客一词,原指热心于计算机技术,水平高超的电脑专家,尤其是程序设计人员”。其实,黑客并不利用自己已有的技术去对他人的系统进行渗透并破坏。黑客的为人处世也非常低调,不会整天拿着别人写好的工具去入侵网站或“抓肉鸡”,做这么没意义的事。如果是黑客天天做这些事,怎么可能有多余的时间真正地研究技术?

编程、破解、入侵

编程、破解、入侵是黑客所掌握的技能,但是后两者都是以前者的编程为基础的。破解别人的程序是站在写程序的角度去考虑的,而入侵依靠的是系统的漏洞,发掘漏洞同样是需要编程知识、系统底层知识和调试技术。也就是说,想做一名黑客,在自身的知识体系中编程知识是占据很大份额的。也就应了网上的一句话——“不会编程的黑客就不是黑客”。

黑客编程与普通编程的区别

黑客编程,其实也就是利用普通的编程技术编写一些黑客工具,或者是网络安全工具。该方面的知识是一把双刃剑,无论是编写黑客工具,还是编写安全工具,都离不开这些知识。本书的重点是通过简单的编程知识配合良性的实例让大家了解黑客编程,并对漏洞进行防范,希望大家正确对待技术的合理应用。

本书的前置知识

阅读本书需要有C、C++语言的基础知识,本书并不是一门编程语言关于语法知识的教科书。如果读者希望能够顺利阅读此书,至少要有阅读C、C++语言编程的能力。如果没有C、C++语言的基础,而有其他语言的基础,那么也是没有问题的。在掌握了编程思想,或者会使用API函数后,用自己熟悉的语言进行相应的开发也是可以的。但是,为了将来能更好、更深入地学习系统的底层知识,建议学习C、C++和汇编语言。

本书适合的读者

长期研究黑客工具的程序员,且有打算自己开发网络安全工具的人员。

掌握编程基本语法想要进行相关编程实践的读者。

本书的定位

本书并非高深的技术性书籍,市场上较深入的研究系统底层、加解密相关的、网络安全编程相关的书籍非常多。但是,很多并非入门类的书籍。本书旨在,通过简单的语法知识及常用的系统函数完成一些有特定功能的安全工具。在读者有基础的前提下,让读者对系统知识等各方面有一个全面的了解,并且在笔者的带领下一步步地完成书中的实例,也可以增强读者的动手能力,源程序下载地址www.ptpress.com.cn。

需要声明的是:

本书的内容是帮助读者提升防范黑客攻击的能力和技术,普及网络安全知识,绝不是为那些怀有不良动机的人提供支持,也不承担因为技术被滥用所产生的连带责任,请读者自觉遵守国家相关法律。

第1章 黑客编程入门

你是否曾经在用别人开发的工具尝试“入侵”,你是否希望开发出自己的黑器……相信很多人有着这种近似相同的经历。本章将简单介绍黑客编程及工具开发。如果你是初学编程,如果你从来没有接触过黑客软件的开发,如果你急于想了解黑客编程方面的知识……那么就请继续往下阅读。

1.1 编程语言和开发环境的选择

初学者刚开始学习编程语言最头疼的问题就是如何选择编程语言及合适的开发环境,下面就来具体介绍一下。

有人认为学编程就是学编程语言,而VC、VB这样的开发环境只是工具,不需要学。这个想法是错误的,因为开发环境提供了很多开发工具,如VC这个集成开发环境就提供了与之对应的PSDK、MFC等。除了语言以外,要开发特定的软件是需要开发包和开发工具支持的。况且,编程语言也是一种工具,用于和计算机进行交流的工具。所以我们既要学习编程语言,也要学习开发工具。

对于选择哪种编程语言或者开发环境其实也没有特定的标准。有这样一句话,“真正的程序员用VC,聪明的程序员用Delphi,用VB的不是程序员”。笔者却并不这么认为,因为在很多编程的书籍上常常这样提醒并告诫学习者,编程的精髓是“算法”,而语言是用来描述算法的。因此,大家也不必因为无法选择而无从下手。

黑客一般都掌握多种编程语言,他们不但掌握着与底层相关的如汇编、C之类的编程语言,而且还掌握很多脚本语言,如Python、Perl、Ruby……很多黑客在发现0Day以后用Perl或者Python来写POC;MSF使用的是Ruby来进行开发Exploit;有的黑客在反病毒时竟然写个批处理就搞定了……对于黑客来说,一切语言都是服务于自己的思想的,只要能快速实现自己的想法,能完成自己所要完成的功能就行,从不拘泥于任何语言和工具。在网上有很多学习不同编程语言的人们之间经常互相攻击,这其实是一种极端的行为,大家还是理性地对待这些问题比较好。

前面说了这么多,仿佛是在绕圈子,一直没有介绍到底应该选择什么编程语言和开发环境。我们这里选择使用C 语言作为黑客编程的学习语言,选择VC6(VisualC++ 6.0的缩写)来作为我们的开发环境。VS 6 相对于Visual Studio 2005、Visual Studio 2008和Visual Studio 2010之类的开发环境来说要小巧很多,当前是可以满足我们的开发需求的。选择C语言的原因是由于Windows的API都是用C语言定义的,相对于使用其他编程语言会方便很多。笔者认为在VB下使用API就非常不方便,尤其是涉及指针这个概念的时候。除了VC6以外,还需要下载新版的PSDK,因为VC6中包含的PSDK过于旧,有些新的API我们无法通过包含头文件而直接使用,因此这个也是必须的。

1.1.1 何为SDK、API和MFC

既然选择VC作为开发环境,那么先来了解一下VC开发环境中今后会用到的一些工具的概念,这些概念都相对比较简单,常见的概念有SDK、API和MFC。

SDK 是Software Develop Kits 的缩写,即软件开发工具包。SDK 是一个泛指,比如对视频采集卡进行二次开发,那么视频采集卡会提供SDK;再比如对动态令牌进行二次开发,那么动态令牌也会提供SDK。操作系统为了程序员在其平台下开发应用程序也会提供SDK,我们对系统提供的开发包称之为PSDK。PSDK 是Platform SDK 的意思,也就是平台SDK。对于我们来说,平台就是Windows操作系统。Windows下的PSDK包含了进行Windows软件开发的文档和API函数的输入库、头文件等一些与Windows开发相关的工具。PSDK是一个单独的开发包,不过每个不同版本的VC和其他一些开发环境中也已经包含了它。

API 是Application Programming Interface的缩写,即应用程序接口。不同的SDK 提供不同的API。PSDK提供的API就是操作系统提供的开发应用程序的接口,比如Windows系统下删除文件的 API 函数是 DeleteFile();再比如 Windows 系统下移动文件的 API 函数是MoveFile(),而其他一些供二次开发的SDK中附带的API,也是为了进行开发应用程序而提供的接口。

MFC 是Microsoft Foundation Class的缩写,即微软基础类库。它是微软为了简化程序员的开发工作量所提供的基于C++类的一套面向对象的库,它封装了常见的API函数,使得开发较为方便。

我们在书中会用到API进行开发,也会使用MFC进行开发。不过对于MFC的使用,基本上用在与界面相关的部分,一般是简单地带过,不会进行过多的讨论。我们的重点是放在API函数的使用上。关于MFC的相关内容,还请大家自行参考学习。

1.1.2 VC6和SDK的配置

新版的PSDK(Windows Server 2003 SP1 Platform SDK)的下载地址为http://www.microsoft.com/downloads/en/details.aspx?FamilyID=eba0128f-a770-45f1-86f3-7ab010 b398a3。如果此地址过期的话,请大家在网上自行搜索并下载。

SDK 和 VC6 互相是独立的,不需要安装在同一个目录下,根据自己的实际情况安装就可以了。在安装好 VC6 和新版的 SDK 后,需要在 VC6 中进行相应的设置才能使用新版的SDK,否则VC6仍然使用其自带的旧的SDK。SDK和VC6的安装步骤这里就不介绍了(提示:请把VC6安装完整,VC6会提供一些代码,对我们的学习是非常有帮助的),下面介绍新版的SDK如何配置才能在VC6中使用。

启动 VC6,单击菜单“Tools”->“Options”命令,打开“Options”对话框,如图 1-1所示。图1-1 “Options”对话框

选择“Directories”选项卡,在“Show directories for”下拉列表中选择“Include files”,选项并在“Directories”列表框中添加新的PSDK头文件的目录,放在列表的最上面,如图1-2所示。图1-2 头文件的路径

在“Show directories for”下拉列表中选择“Library files”选项,并在“Directories”列表框中添加新的PSDK库文件的目录,放在列表的最上面,如图1-3所示。

切记要把所添加的目录放到列表的最上边,因为在VC编译代码的时候会搜索这些目录里的文件,如果随便放,编译器会因找不到相关API函数定义而报函数未定义的错误。

另外,还必须下载一个MSDN。MSDN 即Microsoft Developer Network,它是微软开发的联机帮助文档,可以帮助我们在使用API的时候进行快速的查阅,以方便我们对API的使用和理解。但是MSDN里的内容全部都是英文的,如果你英文不太好可以借助搜索引擎来学习API的使用。本书只对所提到的API函数常用的参数进行介绍,其他参数需要大家自行进行学习。图1-3 库文件的路径

1.2 应用程序的调试

在开发程序的过程中,除了编码以外还需要对程序进行调试,当编写的程序出现问题后,就要对程序进行调试。调试不是仅使用一个printf()或MessageBox()进行简单的输出来观察某个函数的返回值(虽然在调试的时候的确是对返回值观察较多),也不是对某个变量、某一时间的具体值的输出。调试是有专业的调试分析工具的,VC6不但提供代码编辑、代码编译、编译连接等功能,还提供了一个非常好用的调试工具。在编写完代码后,如果程序输出的结果是未知的,或者是没有预测到的,都可以通过调试来对代码的逻辑进行分析,以找到问题的所在。掌握调试的技能,对软件的开发有非常大的帮助。掌握好的调试工具,对于调试者来说,也同样会起到事半功倍的作用。下面通过一个简单的例子了解一下VC6提供的调试功能吧。

1.2.1 编写我们的第一个程序

下面介绍用VC6写一个控制台版的HelloWorld来学习VC6的开发。也许大家认为这个程序很简单,但是请记住,我们的重点是要介绍VC6这个集成开发环境中提供的调试功能。

启动 VC6,单击菜单“File”->“New”命令,在弹出的对话框中选择“Projects”选项卡,然后在左边的列表框中选择“Win32 Console Application”选项,在“Project Name:”文本框中填写“HelloWorld”,如图1-4所示。

单击“OK”按钮,出现如图1-5所示窗口。

选择“An empty project”单选项,单击“Finish”按钮,然后在弹出的对话框中单击“OK”按钮。

单击菜单“File”->“New”命令,选择“Files”选项卡,在左边的列表中选择“C++ Source File”选项,在右边的“File”文本框中填写“Hello World”,如图1-6所示。图1-4 “Projects”选项卡图1-5 “Win32 Console Application”项目向导图1-6 “Files”选项卡

单击“OK”按钮就可以进行代码编辑了。

在代码编辑处录入如下代码:

#include

int main()

{

printf("Hello World ! \r\n");

return 0;

}

按F7 键进行编译连接(按Ctrl+ F7 组合键是只编译不进行连接),按Ctrl + F5 组合键进行运行,如图1-7所示。图1-7 “Hello World”运行界面

这就是我们值得纪念的第一个程序。这个程序很简单,有C语言基础的读者应该都能看懂,这里就不进行介绍了。如果看不懂,请先找本关于C语言入门的书学习一下。

1.2.2 用VC6调试第一个程序

现在来学习如何使用 VC6 对第一个程序进行调试。在代码编辑状态下,按下键盘上的F10键,进入调试状态,如图1-8所示。

常用的调试窗口有两个,一个是“Watch”窗口(标注“1”的那个窗口),一个是“Memory”窗口(标注“2”的那个窗口)。打开“Watch”窗口的方法是单击“View”->“Debug Windows”->“Watch”命令(或按Alt +3 组合键)打开。打开“Memory”窗口的方法是单击“View”->“Debug Windows”->“Memory”命令(或按Alt+ 6 组合键)打开。“Watch”窗口用来监视我们感兴趣的变量,而当我们有时无法通过变量的值进行判断时,就需要借助“Memory”窗口中的值,比如,指针的值来进行判断。

除了这两个窗口以外,还有“Call Stack”、“Register”和“Disassembly”这 3 个窗口,分别如图1-9、图1-10和图1-11所示。图1-8 VC6处于调试状态图1-9 “CallStack”窗口图1-10 “Register”窗口图1-11 “Disassembly”窗口“Call Stack”窗口是调用栈窗口,该窗口可以很方便地查看调用关系,很容易通过调用栈来找到上层、上上层的调用者。另外,也可以通过调用栈来定位错误。比如,有时程序会崩溃,但是发生崩溃的地方却在系统提供的代码中,而不在我们编写的代码中,这种错误在通常情况下是我们的程序对于参数的输入有误造成的,我们可以通过调用栈查看是谁调用了该函数,以便进行进一步分析。“Register”窗口是用来观察寄存器的。有时需要观察返回值或者参数。“Disassembly”窗口是用来观察C代码对应的反汇编代码的。有时在看C的代码无法解决的问题时,需要查看在底层实现时分析程序的问题。

以上就是VC6下常用的调试窗口,可根据实际情况使用,并不是每次调试都会用到这些窗口。下面再简单介绍一下常用的调试快捷键,以方便今后进行调试时使用。

VC6调试时的常用快捷键如下。

F5键:运行程序。

F9键:设定断点/取消断点。

F10键:单步步过,依次执行每一条代码。

F11键:单步步入,依次执行每一条代码,遇到函数调用时则进入到被调用的函数中。

F7键:停止调试。

在后面的章节中我们会用到这些快捷键来调试程序,让大家在学习的过程中真正地应用起这些调试功能。

1.2.3 专业的应用程序调试工具——OllyDbg

OllyDbg,简称OD,是专业的应用程序调试工具。接触过破解,或者做过外挂开发的读者一定对这款工具不陌生。在这里,简单介绍一下这款工具。

让我们先来看看它的界面吧,如图1-12所示。图1-12 OllyDbg的窗口

OD 的大多数情况是在没有源代码的情况下对软件进行调试的。也许没有源代码也就不叫调试了,而叫做动态分析。OD 的主界面中有 6 个主要的窗口,分别是反汇编窗口、寄存器窗口、提示信息窗口、数据窗口(也叫转存窗口)、栈窗口和命令提示窗口。

下面逐个介绍一下各个窗口的作用。(1)反汇编窗口:这是调试或动态分析时的主要窗口,我们主要是针对软件的功能实现进行分析,因此主要需查看的就是反汇编窗口的内容。(2)寄存器窗口:该窗口的作用是实时地显示寄存器的变化情况。寄存器也可以反映代码的执行情况。例如,我们常常查看返回值的eax的值。(3)提示信息窗口:这里往往会显示一些内存地址的值、寄存器的值、调用方的地址等信息。(4)数据窗口:该窗口主要是用来显示数据的,单击右键可以把数据按照不同的方式进行解析,对于我们分析程序的过程是非常有用的。(5)栈窗口:该窗口可以用来查看函数调用时参数的值。(6)命令提示窗口:该窗口是用来输入调试命令的。

OD调试时的常用快捷键如下。

F8键:单步步过,依次执行每一条代码。

F7键:单步步入,依次执行每一条代码,遇到函数调用时则进入到被调用的函数中。

F4键:执行到选中的代码处(前提条件是该条代码在程序的流程中一定会被执行到)。

F2键:断点中断。

F9键:运行程序。

OD的介绍到此为止,在后面的内容中我们会再次提到OD,到那时会有一定的机会练习使用OD。如果有对OD感兴趣的读者,请另行阅读其他书籍。

1.3 简单API的介绍

下面介绍一些在黑客编程中会用到的API函数,尽量排一点简单易用的函数,用简单的几行代码来完成一定的功能,希望大家能在这里体会到编程乐趣,不至于被大段的代码影响了自己前进的心情。

1.3.1 复制自身程序到Windows目录和系统目录下

一般的病毒木马都有这种类似的功能,完成这个功能其实并不复杂,我们来拆解思考一下实现这段代码的步骤。

复制是一个拷贝的过程。既然是拷贝,就要知道拷贝的原位置和目的位置。也就是整个过程其实分3步,首先要得到自身程序所在的路径,然后获得Windows目录和系统目录,最后分别拷贝自身程序到这两个目录中。这3个步骤要如何完成,下面我们来看看完成这些功能的API函数。

获得自身程序所在路径的API函数的定义:

DWORD GetModuleFileName(

HMODULE hModule, // handle to module

LPTSTR lpFilename, // file name of module

DWORD nSize  // size of buffer

);

该函数有 3个参数,分别如下。(1)hModule:该参数在获得自身程序时使用为NULL。(2)lpFilename:该参数指定一个字符型的缓冲区,用于保存程序自身所在的路径。(3)nSize:该参数指定缓冲区的大小。

获得Windows目录的API函数的定义:

UINT GetWindowsDirectory(

LPTSTR lpBuffer, // buffer for Windows directory

UINT uSize   // size of directory buffer

);

该函数有两个参数,分别如下。(1)lpBuffer:该参数指定一个字符型的缓冲区,用于保存Windows目录的路径。(2)uSize:该参数指定缓冲区的大小。

获得系统目录的API函数的定义:

UINT GetSystemDirectory(

LPTSTR lpBuffer, // buffer for system directory

UINT uSize   // size of directory buffer

);

该函数有两个参数,分别如下。(1)lpBuffer:该参数指定一个字符型的缓冲区,用于保存系统目录的路径。(2)uSize:该参数指定缓冲区的大小。

拷贝文件的API函数的定义:

BOOL CopyFile(

LPCTSTR lpExistingFileName, // name of an existing file

LPCTSTR lpNewFileName,  // name of new file

BOOL bFailIfExists   // operation if file exists

);

该函数有 3个参数,分别如下。(1)lpExistingFileName:该参数指向一个已存在文件的路径,即原文件路径。(2)lpNewFileName:该参数指向一个新的文件的位置,即欲拷贝到的文件的目的路径。(3)bFailIfExists:该参数是一个布尔型参数,如果参数为TRUE,若目的文件已存在则返回,复制失败;如果参数为FALSE,若目的文件已存在则强行覆盖原有的文件。

需要使用的 API 函数已经介绍完了,下面就来真正完成这个复制自身程序到 Windows目录和系统目录下的程序,代码如下:

void CopySelf()

{

// 保存自身程序的路径

char szSelfName[MAX_PATH] = { 0 };

// 保存Windows目录的路径

char szWindowsPath[MAX_PATH] = { 0 };

// 保存系统目录的路径

char szSystemPath[MAX_PATH] = { 0 };

// 临时路径变量

char szTmpPath[MAX_PATH] = { 0 };

GetModuleFileName(NULL, szSelfName, MAX_PATH);

GetWindowsDirectory(szWindowsPath, MAX_PATH);

GetSystemDirectory(szSystemPath, MAX_PATH);

strcat(szWindowsPath, "\\backdoor.exe");

strcat(szSystemPath, "\\backdoor.exe");

CopyFile(szSelfName, szWindowsPath, FALSE);

CopyFile(szSelfName, szSystemPath, FALSE);

}

该函数需要包含Windows.h这个头文件,也就是在该段程序的最开始处加一句:

#include

1.3.2 获得系统的相关信息

了解一个系统相关信息也是一项比较重要的内容,强大的扫描软件Nmap在对目标主机进行扫描时,也能对目标主机的系统等信息进行识别,真的是很强大。这里简单地获取一些与系统相关的信息,主要获取的内容有操作系统的版本、操作系统的名字及当前登录的用户名称。接下来逐个介绍这些API函数。(1)获取操作系统版本

代码如下:

BOOL GetVersionEx(

LPOSVERSIONINFO lpVersionInfo // version information

);

该函数就一个参数,这个参数是指向一个 OSVERSIONINFO 结构的指针。看一下OSVERSIONINFO这个结构体。

typedef struct _OSVERSIONINFO{

DWORD dwOSVersionInfoSize; // 结构体大小

DWORD dwMajorVersion;  // 主版本号

DWORD dwMinorVersion;  // 次版本号

DWORD dwBuildNumber;

DWORD dwPlatformId;   // 平台ID

TCHAR szCSDVersion[ 128 ]; // 补丁包

} OSVERSIONINFO;

dwPlatformId的取值有 3个,而现在主要使用一个,即VER_PLATFORM_WIN32_NT。(2)获取计算机名称

代码如下:

BOOL GetComputerName(

LPTSTR lpBuffer, // computer name

LPDWORD lpnSize // size of name buffer

);

该函数有两个参数,介绍如下。

① lpBuffer:保存计算机名称缓冲区。

② lpnSize:保存缓冲区的长度,该参数是一个输入/输出参数。(3)获取当前用户名称

代码如下:

BOOL GetUserName(

LPTSTR lpBuffer, // name buffer

LPDWORD nSize // size of name buffer

);

该函数有两个参数,介绍如下。

① lpBuffer:保存当前用户名称的缓冲区。

② nSize:保存缓冲区的长度,该参数是一个输入/输出参数。

我们封装一个简单的函数来获取系统的这 3个信息,代码如下:

void GetSysInfo()

{

char szComputerName[MAXBYTE] = { 0 };

char szUserName[MAXBYTE] = { 0 };

unsigned long nSize = MAXBYTE;

OSVERSIONINFO OsVer;

OsVer.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

GetVersionEx(&OsVer);

if ( OsVer.dwPlatformId == VER_PLATFORM_WIN32_NT )

{

if ( OsVer.dwMajorVersion == 5 && OsVer.dwMinorVersion == 1 )

{

printf("Windows XP %s \r\n", OsVer.szCSDVersion);

}

else if ( OsVer.dwMajorVersion == 5 && OsVer.dwMinorVersion == 0)

{

printf("Windows 2K \r\n");

}

}

else

{

printf("Ohter System \r\n");

}

GetComputerName(szComputerName, &nSize);

printf("Computer Name is %s \r\n", szComputerName);

nSize = MAXBYTE;

GetUserName(szUserName, &nSize);

printf("User Name is %s \r\n", szUserName);

}

将代码进行编译连接并运行,其执行结果如图 1-13所示。图1-13 当前操作系统版本、计算机名及当前用户名

这个程序完成了我们想要的功能,对于编程的部分就介绍到这里。下面介绍 Debug 和Release方面的内容。

1.3.3 Debug和Release的编译方式

关于获取系统信息的程序,我们编写完成了,也编译连接并运行过了。找到刚才编译的程序,查看一下它的文件大小,如图1-14所示。图1-14 GetSysInfo的程序大小

从图1-14中可以看出,该程序竟然有153KB大小。是不是很惊人?我们一共写了不过十几行代码,但是却生成了如此大体积的程序,这是为什么呢?因为代码默认编译连接是Debug版本的,如图1-15所示。图1-15 Win32 Debug方式

从图1-15中可以看出,我们的代码是由Debug方式编译的。Debug被称为调试版本,在这种方式的编译下,可执行程序中会附带很多和调试相关的数据或代码,而且不做任何的优化,以此为开发人员提供大量的调试信息,从而方便了程序的调试工作。除了Debug方式编译以外,还有一种方式是Release 方式编译,单击“Win32 Debug”右边的下拉箭头可以选择“Win32 Release”,如图1-16 所示。图1-16 Win32 Release方式

Release方式被称作发布版本,是为最终用户使用的,这种方式对代码做了大量的优化工作,不再包含与调试相关的信息,从而使程序的运行效率更高,体积更小,如图1-17所示。图1-17 Release版的GetSysInfo的文件大小

从图1-17可以看出,两个程序的文件大小发生了截然不同的变化。因此,当我们自己写程序调试时,应该使用调试版,以方便我们对程序进行调试。当我们的程序已经调试完毕,那么可以使用发布版来与大家进行交流。

1.3.4 查看函数定义

很多时候,我们都需要查看函数的定义,而函数的定义都在 SDK 的头文件中。虽然从MSDN中也能找到函数的定义,但是还是有略微的不同,而且对于查找自定义函数的函数定义也是很方便的。

回到我们的代码当中,随便选中一个API函数,比如GetComputerName()这个函数。加入要查看该函数的定义应该如何查看呢?我们在 GetComputerName()这个函数上单击鼠标右键,在弹出的快捷菜单上选择“Go To Definition Of GetComputerName”(到GetComputerName函数的定义处)命令,如图1-18所示。

当选择“Go To Definition Of GetComputerName”命令以后,会来到“Winbase.h”头文件中的GetComputerName()函数的定义处,如图1-19和图1-20所示。图1-18 “Go To Definition Of GetComputerName”命令图1-19 “Winbase.h”头文件图1-20 GetComputerName()的定义

从图1-20 中可以看出, GetComputerName 是一个宏,其对应的函数为GetComputerNameA()。关于 GetComputerName()和 GetComputerNameA(),包括可以看到的GetComputerNameW(),我们都不进行介绍。通过图 1-20 的函数定义和前面介绍这个函数的定义来比较一下,可以看到,头文件中的定义比MSDN中的定义对于函数的描述更加详细,比如WINAPI表示函数的调用方式。

除了“Go To Definition Of GetComputerName”以外,还有一个“Go To Reference ToGetComputerName”,这个是查看何处引用了函数。大家可以自行进行练习。

1.4 总结

本书的编程内容主要以C语言为编程语言(本书部分内容会涉及其他语言,但C语言是主要的),以VC6为开发环境,着重介绍了VC6的基本概念及简单的使用,在此基础上带领大家认识了专业的应用程序的调试工具——OD。在最后的内容中介绍了一些简单的API函数的使用。万丈高楼平地起,希望每一位初学者不要过于着急,在后面的章节中我们会慢慢地深入学习黑客编程的内容。希望在每学完一个知识后,大家多思考多动手,这样才能真正地起到学习的效果。

第2章 木马开发剖析

黑客们对系统成功入侵后,为了方便下次登录被入侵的计算机,通常会留下一个后门,或者是木马。国产著名的木马有冰河、广外女生、灰鸽子、上兴、PCShare……这些木马对每个黑客来说都已经很熟悉了,那么接下来就介绍一下它们的共性。不管是哪种木马,它们都要依靠网络来进行客户端与服务器端的通信,也就是说,不管木马有多么复杂的功能,其基础都是建立在网络的通信上。

2.1 网络通信基础

一台计算机和另一台计算机通信,只需要知道两个内容就可以了,即 IP 地址和端口号。这两个名词想必大家不但听过,而且已经有感性上的认识了。我们在扫描网络主机的时候,会在扫描器中填写扫描主机的IP地址,然后扫描器会逐个扫描IP地址,并且把指定IP的所有开放端口全部列出来。那么IP地址是什么?端口号是什么?下面简单介绍一下两者的作用。

2.1.1 IP地址的作用与分类

IP 地址的全称是网际协议地址,互联网上的每个主机和路由器都有 IP 地址,没有两个机器有相同的IP地址,因此它是用来标识一台网络主机的。所有的IP地址都是32位长,其用点分十进制法来表示,比如“10.10.30.12”。IP地址指定的不是主机,而是网络接口设备。因此,一个主机有两个网络接口,那么就会有两个IP地址。通常情况下,一台主机只有一个IP地址。

IP地址被分为5类,分别是A类、B类……E类,每一类IP地址的范围如表2-1所示。表2-1 IP地址分类及范围

上面的IP地址划分是照搬了其他参考书里面给出的IP地址的划分方式,笔者把这些IP地址重新进行了归类。共分为4类,一类是环回IP地址(表示本机IP地址),一类是内网IP地址,一类是公网IP地址,一类是其他。下面分别进行简短的介绍。

1.环回IP地址。

A 类网络号为 127 就是为环回地址预留的。通常情况下是 127.0.0.1 这个地址,或者是localhost,该IP地址不管机主是否联网,对这个IP地址的通信都是可以连通的,因为它就表示本地主机。ping 一下环回地址试试,在命令行下输入:ping127.1,不论是否联网这个地址都是通的。

2.内网IP地址。

用于局域网内,比如网吧、企业、政府、学校的内部。它主要负责局域网的通信。内网地址开头都是10、192……一些地址。

3.公网IP地址。

是ISP(网络服务供应商),比如网通、电信、铁通……提供的这些地址都是由动态ISP分配的,这个地址是真正在互联网上进行通信的IP地址。

4.其他IP地址。

是TCP/IP中保留的用来做实验的一些IP地址,就比如D类和E类这些基本我们普通用户用不到的IP地址。

2.1.2 端口的作用与分类

IP 地址可以在互联网上唯一地标识一台主机,那么端口号有什么用呢?两台计算机通信,其实是两台计算机上的不同进程间的通信。也就是说,木马的客户端和木马的服务端都是操作系统下的一个进程。Windows操作系统下是多进程的,即使IP地址可以标识一台主机,却无法得知是和哪个进程在通信,因为打开的网络软件实在太多了,比如QQ、IE……因此,端口号就是用来标识进程的。

端口号是16位的,范围在0 ~ 65535 之间。端口号根据传输层的传输协议的不同分为两种,一种是TCP端口,另一种是UDP端口。用扫描端口的软件可以区分出TCP和UDP端口。TCP和UDP是两种不同的协议,TCP协议用于可靠传输中,而UDP协议用于不可靠传输中。TCP 的端口在通信过程中始终保持连接,直到通信结束,而 UDP 的通信是不需要连接的。两种协议各有优缺点,视具体情况而定。

2.2 网络编程基础知识

上面对两台计算机之间如何进行通信有了一个大概的了解,现在开始真正学习Windows下网络编程的相关知识。

2.2.1 通信模型

既然是通信,必然要遵循一个通信模型,常见的通信模型有C/S模型(即客户端/服务端模型)、B/S模型(即浏览器/服务端模型)。常见的木马是C/S模式的,不过也有木马是B/S模式的,只不过这种模式相对少见。接下来主要介绍C/S模型的开发。

C/S模型的开发,就是在服务器端上绑定一个IP地址和一个端口号,然后进行监听,等待客户端的连接,而客户端则是向服务器端指定的IP地址和端口号发起连接,服务器接受客户端的连接后,双方可以进行通信,这是基于TCP协议的通信。而基于UDP协议就简单多了,服务器只要绑定IP地址和端口号就可以了,客户端不需要进行连接直接就可以和服务器进行通信。从基于TCP和基于UDP的服务器可以看出,TCP比UDP要可靠,而UDP要比TCP效率高。

2.2.2 Winsock

Windows下的网络应用开发大部分是通过Winsock完成的(除了Winsock以外还有其他的),Winsock有两种开发模式,一种是阻塞模式,另一种是非阻塞模式。阻塞模式是基于同步的开发方式,非阻塞模式是基于异步的开发方式。非阻塞模式结合了Windows的消息机制,更符合Windows下的开发。在我们的学习过程中主要讨论非阻塞模式的开发。

下面开始讲解Windows下网络开发所需要了解的API函数。在介绍这些函数的时候,笔者只重点介绍部分参数,详细的参数介绍不是本书的重点,但是很多函数都有其固定的参数,大家会用即可。

2.2.3 Winsock的相关函数

每个需要使用Winsock进行网络开发的Windows应用程序都必须包含Winsock2.h(这是第二个版本的Winsock库),除了这个头文件以外,还有一个静态库ws2_32.lib。在使用它们的时候需要对这个库进行一次初始化,使用完毕后要对该库进行释放。下面分别介绍这两个函数。

首先来看初始化ws2_32.dll动态链接库的函数:

int WSAStartup(

WORD wVersionRequested,

LPWSADATA lpWSAData

);

这个函数是用来初始化ws2_32.dll动态链接库的,这个动态链接库是所有网络应用程序会加载的动态链接库,在使用这个动态链接库时就需要用WSAStartup()函数进行初始化。如果不初始化这个动态链接库,其余相关的基于这个动态链接库的网络函数的调用都会失败。

参数说明如下。(1)wVersionRequested:Windows Sockets API 提供的调用方可使用的最高版本号。高位字节指出副版本(修正)号,低位字节指出主版本号。(2)lpWSAData:指向WSADATA数据结构的指针,用来接收Windows Sockets 实现的细节。

释放ws2_32.dll动态链接库:

int WSACleanup (void);

这个函数是结束这个动态链接库的,一般在程序退出时使用。

创建套接字:

SOCKET socket(

int af,

int type,

int protocol

);

参数说明如下。(1)af:指定应用程序使用的通信协议族,对于TCP/IP协议族,该参数始终为PF_INET。有的教材里这个参数使用的是 AF_INET。AF_INET 是地址族,虽然使用这个没错,但还是建议使用PF_INET。(2)type:指定要创建的套接字的类型,流套接字类型为SOCK_STREAM,数据包套接字类型为SOCK_DGRAM。前者通常是TCP协议使用,后者通常是UDP协议使用;(3)protocal:指定应用程序所使用的通信协议。该参数根据第二个参数的不同而不同,第二个参数为 SOCK_STREAM ,该参数为 IPPROTO_TCP;如果第二个参数为SOCK_DGRAM,那么该参数为IPPROTO_UDP。

该函数的返回值是一个新创建的类型为SOCKET的套接字的描述符。

关闭套接字:

int closesocket(

SOCKET s

);

程序结束时要对Socket创建的套接字进行关闭,完成资源的释放。

参数说明如下。

s:socket()函数创建的套接字描述符。

当创建了一个Socket 后,服务器程序必须要绑定一个IP地址和特定的端口号。客户端程序不需要绑定端口号和IP地址,因为Socket会选择合适IP地址及端口来使用。

绑定IP地址和端口号:

int bind(

SOCKET s,

const struct sockaddr FAR *name,

int namelen

);

参数说明如下。(1)s:指定待绑定的Socket描述符。(2)name:指定一个sockaddr结构,该结构的定义如下:

struct sockaddr {

u_short sa_family;

char  sa_data[14];

};

函数中提供的参数类型是sockaddr,在实际使用的过程中,结构体是sockaddr_in,该结构的定义如下:

struct sockaddr_in {

short sin_family;

u_short sin_port;

struct in_addr sin_addr;

char sin_zero[8];

};

成员变量sin_family设置为PF_INET;sin_port设置为端口号;sin_addr结构体中只包含一个公用体,in_addr的定义如下:

struct in_addr {

union {

struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;

struct { u_short s_w1,s_w2; }   S_un_w;

u_long         S_addr;

} S_un;

};

该成员变量是一个整数,一般用函数 inet_addr()把字符串形式的 IP地址转换成 unsignedlong整型的整数值。

namelen:指定 name缓冲区的长度。

inet_addr函数的原型如下:

unsigned long inet_addr(

const char FAR *cp

);

参数 cp为一个点分多进制的 IP地址。

inet_addr函数的逆函数如下:

char FAR * inet_ntoa(

struct in_addr in

);

参数为一个 addr_in类型的变量。

监听端口:

int listen(

SOCKET s,

int backlog

);

参数说明如下。(1)s:使流套接字 s处于监听状态。(2)backlog:为处于监听状态的流套接字 s维护一个客户连接请求队列。

接受请求:

SOCKET accept(

SOCKET s,

struct sockaddr FAR *addr,

int FAR *addrlen

);

服务端程序调用该函数从处于监听状态的流套接字的客户端请求队列中取出第一个请求,并创建一个新的套接字与客户端进行连接通信。

参数说明如下。(1)s:指定处于监听状态的套接字。(2)addr:用来返回新创建的套接字的地址。(3)addrlen:用来返回新创建套接字的地址结构的长度。

连接函数如下:

int connect(

SOCKET s,

const struct sockaddr FAR *name,

int namelen

);

客户端程序调用该函数来完成与远程服务器端的连接。

参数说明如下。(1)s:客户端创建的套接字。(2)name:该结构中包含了要服务器端中的 IP地址和端口号。(3)namelen:指定name缓冲区的长度。

具体进行通信的函数分为两类,一类是基于TCP协议的,另一类是基于UDP协议的。数据的通信主要体现在数据的收发上,分别看一下这两种协议的收发数据的函数定义。

基于TCP的发送函数:

int send(

SOCKET s,

const char FAR *buf,

int len,

int flags

);

参数说明如下。(1)s:指定发送端套接字描述符。(2)buf:指明一个存放应用程序要发送数据的缓冲区。(3)len:指明实际要发送到数据的字节数。(4)flags:一般设置为0。

基于TCP的接收函数:

int recv(

SOCKET s,

char FAR *buf,

int len,

int flags

);

参数说明如下。(1)s:指定接收端套接字描述符。(2)buf:指定一个缓冲区,用来存放接收到的数据。(3)len:指定缓冲区的长度。(4)flags:一般设置为0。

基于UDP的发送函数:

int sendto(

SOCKET s,

const char FAR *buf,

int len,

int flags,

const struct sockaddr FAR *to,

int tolen

);

基于UDP的接收函数:

int recvfrom(

SOCKET s,

char FAR* buf,

int len,

int flags,

struct sockaddr FAR *from,

int FAR *fromlen

);

2.2.4 字节顺序

数据在存储器中是按一定方式的,根据不同的 CPU 架构,其存储方式也不相同。比如Intel x86CPU架构使用小尾顺序,即高位存放高字节,低位存放低字节。例如0x12345678,在内存里的表示方式为78 56 34 12。

还有一种是大尾方式,即按网络字节顺序,也就是说所有的网络中传输的数据使用的均是大尾方式。大尾方式是高位存放低字节,低位存放高字节。例如0x12345678,在内存里的表示方式为12 34 56 78,如图2-1 所示。图2-1 字节顺序

本地字节顺序根据CPU架构的不同可能是小尾方式,也可能是大尾方式,但是对于网络字节顺序来说,一定是大尾方式。Winsock 提供了一些用来处理本地的字节顺序和网络的字节顺序的转换函数。这些函数如下:

本地字节顺序转换为网络字节顺序:

u_short htons(u_short hostshort);

u_long htonl(u_long hostlong);

网络字节顺序转换为本地字节顺序:

u_short ntohs(u_short netshort);

u_long ntohl(u_long netlong);

当我们在调试软件的时候,经常会在内存中查找某一个数据,但是怎么也找不到,在这个时候就应该想到字节顺序的问题。在做破解和外挂的时候,不了解这些基本概念的话经常会感到困惑,请大家一定要注意这个细小的问题。

2.3 简单的通信程序

网络通信方面的基础函数我们已经了解了一些,把这些知识都连起来,就可以编写一个网络程序了。我们分别来写一个基于TCP协议和基于UDP协议的小程序。这是一个非常经典的程序,就是在编程界举世闻名的输出“HelloWorld!”!的程序,只不过是Winsock 版本。

2.3.1 基于TCP协议的“Hello World!”

服务器端的代码编写流程如下:

WSAStartup()->socket()->bind()->listen()->accept()->send()/recv()->closesocket()->W SACleanup()。

只要把这些函数依次写完,服务器端的代码就写完了。

服务器端代码如下:

#include

#pragma comment (lib, "ws2_32")

int main(int argc, char* argv[])

{

WSADATA wsaData;

WSAStartup(MAKEWORD(2,2), &wsaData);

SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

sockaddr_in sockaddr;

sockaddr.sin_family = PF_INET;

sockaddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

sockaddr.sin_port = htons(827);

bind(s, (SOCKADDR*)&sockaddr, sizeof(SOCKADDR));

listen(s, 1);

SOCKADDR clientAddr;

int nSize = sizeof(SOCKADDR);

SOCKET clientSock;

clientSock = accept(s, (SOCKADDR*)&clientAddr, &nSize);

send(clientSock,

"hello client \r\n",

strlen("hello client \r\n") + sizeof(char),

NULL);

closesocket(clientSock);

closesocket(s);

WSACleanup();

return 0;

}

客户端的代码编写流程如下:

WSAStarup()->socket()->connect()->recv()/send()->closesocket()->WSACleanup()。

客户端的流程比服务器端的流程要更少一些,主要是省去了绑定IP和端口、监听等一些步骤。

客户端代码如下:

#include

#include

#pragma comment (lib, "ws2_32")

int main(int argc, char* argv[])

{

WSADATA wsaData;

WSAStartup(MAKEWORD(2, 2), &wsaData);

SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

sockaddr_in sockAddr;

sockAddr.sin_family = PF_INET;

sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

sockAddr.sin_port = htons(827);

connect(s, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

char szBuffer[MAXBYTE] = { 0 };

recv(s, szBuffer, MAXBYTE, NULL);

printf("szBuffer = %s \r\n", szBuffer);

closesocket(s);

WSACleanup();

return 0;

}

在上面的代码中,我们只是简单地把所有的API函数都串起来写了一遍,就能实现想要的功能。但是请注意,大家在写的时候一定要把所有函数的返回值加以判断,形成一个好的习惯。

2.3.2 基于UDP协议的“Hello World!”

UDP客户端与服务器端的编写方法与TCP的相似,只要注意其中的差别就可以了。

服务端代码如下:

#include

#include

#pragma comment (lib, "ws2_32")

int main(int argc, char* argv[])

{

WSADATA wsaData;

WSAStartup(MAKEWORD(2, 2), &wsaData);

SOCKET s = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);

sockaddr_in sockAddr;

sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

sockAddr.sin_port = htons(827);

sockAddr.sin_family = PF_INET;

bind(s, (SOCKADDR*)&sockAddr, sizeof(SOCKADDR));

sockaddr_in clientAddr;

int len = sizeof(sockaddr_in);

char buf[MAXBYTE] = { 0 };

recvfrom(s, buf, MAXBYTE, 0, (SOCKADDR*)&clientAddr, &len);

printf("%s \r\n", buf);

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载