jump to navigation

用linux做nat服务,用tc限制流量 二月 28, 2005

Posted by wansion in 技术.
add a comment

最近有同事用bt和电驴疯狂下载,我们上网打cs受到极大影响,所以对nat上网做了流量控制,将一点经验介绍给网友,希望对cs fans 有所帮助.
我们上网环境如下:
eth0 外网ip :a.b.c.d
eth1 内网ip1:192.168.0.0/24 给老板和bt
eth2 内网ip2:192.168.1.0/24 给我和csfans
用linux 做nat 命令如下:
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -F
iptables -t nat -F —-清除旧规则
iptables -t nat -A POSTROUTING -s 192.168.0.0/24 -o eth0 -j SNAT –to a.b.c.d —为内网ip1 做nat
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j SNAT — to a.b.c.d —为内网ip2 做nat
————为流量控制做基于fw过滤器的标记
iptables -I PREROUTING -t mangle -p tcp -s 192.168.0.0/24 -j MARK –set-mark 1
iptables -I PREROUTING -t mangle -p tcp -s 192.168.1.0/24 -j MARK –set-mark 2
————为上传速率做流量控制
tc 要求内核2.4.18以上,所以不够的要升级
tc 只能控制网卡发送包的速率,所以上传速率的限制要在eth0上做
—-删除旧有队列
tc qdisc del dev eth0 root
—-加一个根队列,速率用网卡的速率10Mbit,也可用上传的速率
tc qdisc add dev eth0 root handle 100: cbq bandwidth 10Mbit avpkt 1000
—-加一个根类
tc class add dev eth0 parent 100:0 classid 100:1 cbq bandwidth 10Mbit rate 10Mbit allot 1514 weight 1Mbit prio 8 maxburst 8 avpkt 1000 bounded
—-加一个子类用于内网1速率限制为300Kbit
tc class add dev eth0 parent 100:1 classid 100:2 cbq bandwidth 10Mbit rate 300Kbit allot 1513 weight 30Kbit prio 5 maxburst 8 avpkt 1000 bounded
—-加一个子类用于内网2速率限制为320Kbit
tc class add dev eth0 parent 100:1 classid 100:3 cbq bandwidth 10Mbit rate 320Kbit allot 1513 weight 32Kbit prio 6 maxburst 8 avpkt 1000 bounded
—-设置队列规则
tc qdisc add dev eth0 parent 100:2 sfq quantum 1514b perturb 15
tc qdisc add dev eth0 parent 100:3 sfq quantum 1514b perturb 15
——将队列和fw过滤器映射起来 其中hand 1 的1是开始用iptables 做的标记,hand 2 的2也是开始用iptables 做的标记
tc filter add dev eth0 parent 100:0 protocol ip prio 1 handle 1 fw classid 100:2
tc filter add dev eth0 parent 100:0 protocol ip prio 2 handle 2 fw classid 100:3
———————–再做下载限制我只限制了老板和bt的下载速率,过滤器是用u32
tc qdisc del dev eth1 root
tc qdisc add dev eth1 root handle 200: cbq bandwidth 10Mbit avpkt 1000
tc class add dev eth1 parent 200:0 classid 200:1 cbq bandwidth 10Mbit rate 10Mbit allot 1514 weight 2Kbit prio 8 maxburst 8 avpkt 1000 bounded
tc class add dev eth1 parent 200:1 classid 200:2 cbq bandwidth 10Mbit rate 1000Kbit allot 1513 weight 1Mbit prio 5 maxburst 8 avpkt 1000 bounded
tc qdisc add dev eth1 parent 200:2 sfq quantum 1514b perturb 15
tc filter add dev eth1 parent 200:0 protocol ip prio 25 u32 match ip dst 192.168.0.0/24 flowid 200:2

深度探索Win32可执行文件格式 二月 23, 2005

Posted by wansion in Life.
add a comment

摘要

对可执行文件的深入认识将带你深入到系统深处。如果你知道你的exe/dll里是些什么东东,你就是一个更有知识的程序员。作为系列文章的第一章,将关注这几年来PE格式的变化,同时也简单介绍一下PE格式。经过这次更新,作者加入了PE格式是如何与.NET协作的及PE文件表格(PE FILE SECTIONS),RVA,The DataDirectory,函数的输入等内容。

====================

很久以前,我给Microsoft Systems Journal(现在的MSDN)写了一篇名为“Peering Inside the PE: A Tour of the Win32 Portable Executable File Format”的文章。后来比我期望的还流行,到现在我还听说有人在用它(它还在MSDN里)。不幸的是,那篇文章的问题依旧存在,WIN32的世界静悄悄地变了好多,那篇文章已显得过期了。从这个月开始我将用这两篇文章来弥补。

你可能会问为什么我应当了解PE格式,答案依旧:操作系统的可执行文件格式和数据结构暴露出系统的底层细节。通过了解这些,你的程序将编的更出色。

当然,你可以阅读微软的文档来了解我将要告诉你的。但是,像很多文档一样,‘宁可晦涩,但为瓦全’。

我把焦点放在提供一些不适合放在正式文档里的内容。另外,这篇文章里的一些知识不见得能在官方文档里找到。

1. 裂缝的撕开

让我给你一些从1994年我写那篇文章来PE格式变化的例子。WIN16已经成为历史,也就没有必要作什么比较和说明了。另外一个可憎的东西就是用在WINDOWS 3.1 中的WIN32S,在它上面运行程序是那么的不稳定。

那时候,WINDOWS 95(也叫Chicago)还没有发行。NT还是3.5版。微软的连接器还没开始大规模的优化,尽管如此,there were MIPS and DEC Alpha implementations of Windows NT that added to the story.

那么究竟,这么些年来,有些什么新的东西出来呢?64位的WINDOWS有了它自己的PE变种,WINDOWS CE 支持各种CPU了,各种优化如DLL的延迟载入,节表的合并,动态捆绑等也已出台。

有很多类似的东西发生了。

让我们最好忘了.NET。它是如何与系统切入的呢?对于操作系统,.NET的可执行文件格式是与旧的PE格式兼容的。虽然这么说,在运行时期,.NET还是按元数据和中间语言来组织数据的,这毕竟是它的核心。这篇文章当中,我将打开.NET元数据这扇门,但不做深入讨论。

如果WIN32的这些变化都不足以让我重写这篇文章,就是原来的那些错误也让我汗颜。比如我对TLS的描述只是一带而过,我对时间戳的描述只有你生活在美国西部才行等等。还有,一些东西已是今是作非了,我曾说过.RDATA几乎没排上用场,今天也是,我还说过.IDATA节是可读可写的,但是一些搞API拦截的人发现好像是错的。

在更新这篇文章的过程当中,我也检查了PEDUMP这个用来倾印PE文件的程序.这个程序能够在0X86和IA-64平台下编译和运行。

2. PE格式概览

微软的可执行文件格式,也就是大家熟悉的PE 格式,是官方文档的一部分。但是,它是从VAX/VMS上的COFF派生出来的,就WINDOWS NT小组的大部分是从DEC转过来的看来,这是可以理解的。很自然,这些人在NT的开发上会用他们以往的代码。

采用术语“PORTABLE EXECUTABLE”是因为微软希望有一个通用在所有WINDOWS平台上和所有CPU上的文件格式。从大的方面讲,这个目标已经实现。它适用于NT及其后代,95及其后代,和CE.

微软产生的OBJ文件是用COFF格式的。当你看到它的很多域都是用八进制的编码的,你会发现她是多么古老了。COFF OBJ文件用到了很多和PE一样的数据结构和枚举,我马上会提到一些。

64位的WINDOWS只对PE格式作了一点点改变。这个新的格式叫做PE32+。没有增加一个字段,且只删了一个字段。其他的改变就是把以前的32位字段扩展成64位。对于C++代码,通过宏定义WINDOWS的头文件已经屏蔽了这些差别。

EXE与DLL的差别完全是语义上的。它们用的都是同样一种文件格式-PE。唯一的区别就是其中有一个字段标识出是EXE还是DLL.还有很多DLL的扩展比如OCX,CPL等都是DLL.它们有一样的实体。

你首先要知道的关于PE的知识就是磁盘中的数据结构布局和内存中的数据结构布局是一样的。载入可执行文件(比如LOADLIBARY)的首要任务就是把磁盘中的文件映射到进程的地址空间.因此像IMAGE_NT_HEADER(下面解释)在磁盘和内存中是一样的。关键的是你要懂得你怎样在磁盘中获得PE文件某些信息的,当它载入内存时你可以一样获得,基本上是没什么不同的(即内存映射文件)。但是知道与映射普通的内存映射文件不同是很重要的。WINDOWS载入器察看PE文件才决定映射到哪里,然后从文件的开始处往更高的地址映射,但是有的东西在文件中的偏移和在内存中的偏移会不一样。尽管如此,你也有了足够的信息把文件偏移转化成内存偏移。见图一:

图一 位移

当Windows载入器把PE载入内存,在内存中它称作模块(MODULE),文件从HMODULE这个地址开始映射。记住这点:给你个HMODULE,从那你可以知道一个数据结构(IMAGE_DOS_HEADER),然后你还可以知道所有得数据结构。这个强大的功能对于API拦截特别有意义。(准确地说:对于WINDOWS CE,这是不成立的,不过这是后话)。

内存中的模块代表着进程从这个可执行文件中所需要的所有代码,数据,资源。其他部分可以被读入,但是可能不映射(如,重定位节)。还有一些部分根本就不映射,比如当调试信息放到文件的尾部的时候。有一个字段告诉系统把文件映射到内存需要多少内存。不需要的数据放在文件的尾部,而在过去,所有部分都映射。 在WINNT.H描述了PE 格式。在这个文件中,几乎有所有的关于PE的数据结构,枚举,#DEFINE。当然,其它地方也有相关文档,但是还是WINNT.H说了算。

有很多检测PE文件的工具,有VISUAL STUDIO的DUMPBIN,SDK中的DEPENDS,我比较喜欢DEPENDS,因为它以一种简洁的方式检测出文件的引入引出。一个免费的PE察看器,PEBrowse,来自smidgenosoft。我的pedump也是很有用的,它和dumpbin有一样的功能。

从api的立场看,imagehlp.dll提供了读写pe文件的机制。

在开始讨论pe文件前,回顾一下pe文件的一些基本概念是有意义的。在下面几节,我将讨论:pe 节,相对虚拟地址(rva),数据目录,函数的引入。

3. PE节

PE节以某钟顺序表示代码或数据。代码就是代码了,但是却有多种类型的数据,可读写的程序数据(如全局变量),其它的节包含API的引入引出表,资源,重定位。每个节有自己的属性,包括是否是代码节,是否只读还是可读可写,节的数据是否全局共享。

通常,节中的数据逻辑上是关联的。PE文件一般至少要有两个节,一个是代码,另一个为数据。一般还有一个其它类型的数据的节。后面我将描述各种类型的节。

每个节都有一个独特的名字。这个名字是用来传达这个节的用途的。比如,.RDATA表示一个只读节,节的名字对于操作系统毫无意义,只是为了人们便于理解。把一个节命名为FOOBAR和.TEXT是一样有用的。微软给他们的节命名了个有特色的名字,但是这不是必需的。Borland的连接器用的是code和data。

一般编译器将产生一系列标准的节,但这没有什么不可思议的。你可以建立和命名自己的节,连接器会自动在程序文件中包含它们。在visual c++中,你能用#pragma指令让编译器插入数据到一个节中。像下面这样:

 #pragma data_seg("MY_DATA")
 …有必要初始化
 #pragma data_seg()

你也可以对.data做同样的事。大部分的程序都只用编译器产生的节,但是有时候你却需要这样。比如建立一个全局共享节。

节并不是全部由连接器确定的,他们可以在编译阶段由编译器放入obj文件。连接器的工作就是合并所有obj和库中需要的节成一个最终的合适的节。比如,你的工程中的所有obj可能都有一个包含代码的.text节,连接器把这些节合并成一个.text节。同样对于.data等。这些主题超出了这篇文章的范围了。还有更多的规则关于连接器的。在obj文件中是专门给linker用的,并不放入到pe文件中,这种节是用来给连接器传递信息的。

节有两个关于对齐的字段,一个对应磁盘文件,另一个对应内存中的文件。Pe文件头指出了这两个值,他们可以不一样。每个节的偏移从对齐值的倍数开始。比如,典型的对齐值是0x200,那么每个节的的偏移必须是0x200的倍数。一旦载入内存,节的起始地址总是以页对齐。X86cpu的页大小为4k,al-64为8k。

下面是pedump倾印出的Windows XP KERNEL32.DLL.的.text .data节的信息:

 Section Table
 01 .text VirtSize: 00074658 VirtAddr: 00001000
 raw data offs: 00000400 raw data size: 00074800
 …
 02 .data VirtSize: 000028CA VirtAddr: 00076000
 raw data offs: 00074C00 raw data size: 00002400

建立一个节在文件中的偏移和它相对于载入地址的偏移相同的pe文件是可能的。在98/me中,这会加速大文件的载入。Visual studio 6.0 的默认选项 /opt:win98j就是这样产生文件的。在Visual studio.net中是否用/opt:nowin98取决于文件是否够小。

一个有趣的连接器特征是合并节的能力。如果两个节有相似兼容的属性,连接的时候就可以合并为一个节。这取决于是否用/merger开关。像下面就把.rdata和.text合并为一个节.text

 /MERGE:.rdata=.text

合并节的优点就是对于磁盘和内存节省空间。每个节至少占用一页内存,如果你可以把可执行文件的节数从4减到3,很可能就可以少用一页内存。当然,这取决于两个节的空余空间加起来是否达到一页。

当你合并节事情会变得有意思,因为这没有什么硬性和容易的规则。比如你可以合并.rdata到.text,

但是你不可以把.rsrc.reloc.pdata合并到别的节。先前Visual Studio .NET允许把.idata合并,后来又不允许了。但是当发行的时候,连接器还是可以把.idata合并到别的节。

因为引入节的一部分在载入器载入时将被写入,你可能惊奇它是如何被放入一个只读节的。是这样的,在载入的时候系统会临时改变那些包含引入节的页为可读可写,初始化完成后,又恢复原来属性。

4. 相对虚拟地址

在可执行文件中,有很多地方需要指定内存地址,比如,引用全局变量时,需要指定它的地址。Pe文件尽管有一个首选的载入地址,但是他们可以载入到进程空间的任何地方,所以你不能依赖于pe的载入点。由于这点,必须有一个方法来指定地址而不依赖于pe载入点的地址。为了避免把内存地址硬编码进pe文件,提出了RVA。RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为(target address) 0x401000 – (load address)0x400000 = (RVA)0x1000。把RVA加上PE的载入点的实际地址就可以把RVA转化实际地址。顺便说一下,按PE的说法,内存中的实际地址称为VA(VIRTUAL ADDRESS).不要忘了早点我说的PE的载入点就是HMODULE

想对探索内存中的任意DLL吗?用GetModuleHanle(LPCTSTR)取得载入点,用你的PE知识来干活吧

5. 数据目录

PE文件中有很多数据结构需要快速定位。显然的例子有引入函数,引出函数,资源,重定位。这些东西是以一致的方式来定位的,这就是数据目录。

数据目录是一个结构数组,包含16个结构。每个元素有一个定义好的标识,如下:

 // Export Directory

 #define IMAGE_DIRECTORY_ENTRY_EXPORT 0

 // Import Directory

 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1

 // Resource Directory

 #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2

 // Exception Directory

 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3

 // Security Directory

 #define IMAGE_DIRECTORY_ENTRY_SECURITY 4

 // Base Relocation Table

 #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5

 // Debug Directory

 #define IMAGE_DIRECTORY_ENTRY_DEBUG 6

 // Description String

 #define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7

 // Machine Value (MIPS GP)

 #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8

 // TLS Directory

 #define IMAGE_DIRECTORY_ENTRY_TLS 9

 // Load Configuration Directory

 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10

 typedef struct _IMAGE_DATA_DIRECTORY {

   ULONG VirtualAddress;

   ULONG Size;

 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

6. 引入函数

当你使用别的DLL中的代码或数据,称为引入。当PE载入时,载入器的工作之一就是定位所有引入函数及数据,使那些地址对于载入的PE可见。具体细节在后面讨论,在这里只是大概讲一下。

当你用到了一个DLL中的代码或数据,你就暗中连接到这个DLL。但是你不必为“把这些地址变得对你的代码有效”做任何事情,载入器为你做这些。方法之一就是显式连接,这样你就要确定DLL已被载入,及函数的地址。调用LOADLIBARY和GETPROCADDRESS就可以了。

当你暗式连接DLL,LOADLIBARY和GETPROCADDRESS同样还是执行了的。只不过载入器为你做了这些。载入器还保证PE文件所需得任何附加的DLL都已被载入。比如,当你连接了KERNEL32.DLL,而它又引入了NTDLL.DLL的函数,又比如当你连接了GDI32.DLL,而它又依赖于USER32, ADVAPI32,NTDLL, 和 KERNEL32 DLLs的函数,载入器会保证这些DLL被载入及函数的决议。

暗式连接时,决议过程在PE文件在载入时就发生了。如果这时有什么问题(比如这个DLL文件找不到),进程终止。

VISUAL C++ 6.0 加入了DLL的延迟载入的特征。它是暗式连接和显式连接的混合。当你延迟载入DLL,连接器做出一些和引入标准规则DLL类似的东西,但是操作系统却不管这些东西,而是在第一次调用这个DLL中的函数的时候载入(如果还没载入),然后调用GetProcAddress取得函数的地址。

对于pe文件要引入的dll都有一个对应的结构数组,每个结构指出这个dll的名字及指向一个函数指针数组的指针,这个函数指针数组就是所谓的IAT(IMORT ADDRESS TABLE)。每个输入函数,在IAT中都有一个保留槽,载入器将在那里写入真正的函数地址。最后特别重要一点的是:模块一旦载入,IAT中包含所要调用的引入函数的地址。

把所有输入函数放在IAT一个地方是很有意义的,这样无论代码中多少次调用一个引入函数,都是通过IAT中的一个函数指针。

让我们看看是怎样调用一个引入函数的。有两种情况需要考虑:有效率的和效率差的。最好的情况像下面这样:

 CALL DWORD PTR [0x00405030]

直接调用[0x405030]中的函数,0x405030位于IAT部分。效率差的方式如下:

 CALL 0x0040100C

 …

 0x0040100C:

 JMP DWORD PTR [0x00405030]

这种情况,CALL把控制权转到一个子程序,子程序中的JMP指令跳转到位于IAT中的0x00405030,简单说,它多用了5字节和JMP多花的时间。

你可能惊讶引入函数就采用了这种方式,有个很好的解释,编译器无法区别引入函数的调用和普通函数调用,对于每个函数调用,编译器只产生如下指令:

 CALL XXXXXXXX

XXXXXXXX是一个由连接器填入的RVA。注意,这条指令不是通过函数指针来的,而是代码中的实际地址。

为了因果的平衡,连接器必须产生一块代码来代替取代XXXXXXXX,简单的方法就是象上面所示调用一个JMP STUB.

那么JMP STUB 从那里来呢?令人惊异的是,它取自输入函数的引入库。如果你去察看一个引入库,在输入函数名字的关联处,你会发现与上面JMP STUB相似的指令。

接着,另一个问题就是如何优化这种形式,答案是你给编译器的修饰符,__declspec(import) 修饰符告诉编译器,这个函数来自另一个dll,这样编译器就会产生第一种指令。另外,编译器将给函数加上__imp_前缀然后送给连接器决议,这样可以直接把__imp_xxx送到iat,就不需要jmp stub了。

对于我们这有什么意义呢,如果你在写一个引出函数的东西并提供一个头文件的话,别忘了在函数前加上修饰符__declspec(import)

 __declspec(dllimport) void Foo(void);

在winnt.h等系统头文件中就是这样做的。

7. PE 文件结构

现在让我们开始研究PE文件格式,我将从文件的头部开始,描述每个PE文件中都有的各种数据结构,然后,我将讨论更多的专门的数据结构比如引入表和资源,除非特殊说明,这些结构都定义在WINNT.H中。

一般地,这些结构都有32和64位之分,如IMAGE_NT_HEADERS32 ,IMAGE_NT_HEADER64等,他们基本上是一样的,除了64位的扩展了某些字段。通过#DEFINE WINNT.H都屏蔽了这些区别,选择那个数据结构取决于你要如何编译了(如,是否定义_WIN64)

The MS-DOS Header

每个PE文件是以一个DOS程序开始的,这让人想起WINDOWS在没有如此可观的使用者的早期年代。当可执行文件在非WINDOWS平台上运行的时候至少可以显示出一条信息表示它需要WINDOWS。

PE文件的开头是一个IMAGE_DOS_HEADER结构,结构中只有两个重要的字段e_magic and e_lfanew。e_lfanew指出pe file header的偏移,e_magic需要设定位0x5a4d,被#define 成IMAGE_DOS_SIGNATURE 它的ascii为’MZ’,Mark Zbikowski的首字母,DOS 的原始构建者之一。

The IMAGE_NT_HEADERS Header

这个结构是PE文件的主要定位信息的所在。它的偏移由IMAGE_DOS_HEADER的e_lfanew给出

确实有64和32位之分,但我在讨论中将不作考虑,他们几乎没有区别。

 typedef struct _IMAGE_NT_HEADERS {

  DWORD Signature;

  IMAGE_FILE_HEADER FileHeader;

  IMAGE_OPTIONAL_HEADER32 OptionalHeader;

 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

在一个有效的pe文件里,Signture被设为0x00004500,ascii 为’PE00’,#define IMAGE_NT_SIGNTURE 0X00004500;第二个字段是一个IMAGE_FILE_HEADER结构,它包含文件的基本信息,特别重要的是它指出了IMAGE_OPTIONAL_HEADER的大小(重要吗?);在PE文件中,IMAGE_OPTIONAL_HEADER是非常重要的,但是仍称作IMAGE_OPTIONAL_HEADER。

IMAGE_OPTIONAL_HEADER结构的末尾就是用来定位pe文件中重要信息的地址簿-数据目录,它的定义如下:

 typedef struct _IMAGE_DATA_DIRECTORY {

  DWORD VirtualAddress; // RVA of the data

  DWORD Size; // Size of the data

 };

The Section Table

紧接着IMAGE_NT_HEADERS后的就是节表,节表就是IMAGE_SECTION_HEADER的数组。IMAGE_SECTION_HEADER包含了它所关联的节的信息,如位置,长度,特征;该数组的数目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections指出。具体见下图

PE中的节的大小的总和最后是要对齐的,Visual Studio 6.0中的默认值是4k,除非你使用/OPT:NOWIN98 或/ALIGN开关;在.NET中,依然用了默认的/OPT:WIN98,但是如果文件小于一特定大小时,就会采用0X200为对齐值。

.NET文档中有关于对齐的另一件有趣的事。.NET文件的内存对齐值为8K而不是普通X86平台上的4K,这样就保证了在X86平台编译的程序可以在IA-64平台上运行。如果内存对齐值为4K,那么IA-64的载入器就不能载入这个程序,因为它的页为8K。

Unix/ELF文件格式及病毒分析 二月 21, 2005

Posted by wansion in 技术.
add a comment

转帖–Unix/ELF文件格式及病毒分析 http://www.chinaunix.net 作者:mzd73  发表于:2002-11-05 00:01:25
[这个贴子最后由mzd73在 2002/11/15 11:08pm 编辑]

★ 介绍

本文介绍了Unix病毒机制、具体实现以及ELF文件格式。简述了Unix病毒检测和反检
测技术,提供了Linux/i386架构下的一些例子。需要一些初步的Unix编程经验,能够
理解Linux/i386下汇编语言,如果理解ELF本身更好。

本文没有任何实际意义上的病毒编程技术,仅仅是把病毒原理应用到Unix环境下。这
里也不打算从头介绍ELF规范,感兴趣的读者请自行阅读ELF规范。

★ 感染 ELF 格式文件

进程映象包含"文本段"和"数据段",文本段的内存保护属性是r-x,因此一般自修改
代码不能用于文本段。数据段的内存保护属性是rw-。

段并不要求是页尺寸的整数倍,这里用到了填充。

关键字:

[…]   一个完整的页
M       已经使用了的内存
P       填充

页号
#1    [PPPPMMMMMMMMMMMM]  \
#2    [MMMMMMMMMMMMMMMM]   |– 一个段
#3    [MMMMMMMMMMMMPPPP]  /

段并没有限制一定使用多个页,因此单页的段是允许的。

页号
#1    [PPPPMMMMMMMMPPPP]  <– 一个段

典型的,数据段不需要从页边界开始,而文本段要求起始页边界对齐,一个进程映象
的内存布局可能如下:

关键字:

[…]   一个完整的页
T       文本段内容
D       数据段内容
P       填充

页号
#1    [TTTTTTTTTTTTTTTT]    <– 文本段内容
#2    [TTTTTTTTTTTTTTTT]    <– 文本段内容
#3    [TTTTTTTTTTTTPPPP]    <– 文本段内容(部分)
#4    [PPPPDDDDDDDDDDDD]    <– 数据段内容(部分)
#5    [DDDDDDDDDDDDDDDD]    <– 数据段内容
#6    [DDDDDDDDDDDDPPPP]    <– 数据段内容(部分)

页1、2、3组成了文本段
页4、5、6组成了数据段

从现在开始,为简便起见,段描述图表用单页,如下:

页号
#1    [TTTTTTTTTTTTPPPP]    <– 文本段
#2    [PPPPDDDDDDDDPPPP]    <– 数据段

在i386下,堆栈段总是在数据段被给予足够空间之后才定位的,一般堆栈位于内存高
端,它是向低端增长的。

在ELF文件中,可装载段都是物理映象:

    ELF Header
    .
    .
    Segment 1    <– 文本段
    Segment 2    <– 数据段
    .
    .

每个段都有一个定位自身起始位置的虚拟地址。可以在代码中使用这个地址。

为了插入寄生代码,必须保证原来的代码不被破坏,因此需要扩展相应段所需内存。

文本段事实上不仅仅包含代码,还有 ELF 头,其中包含动态链接信息等等。如果直
接扩展文本段插入寄生代码,带来的问题很多,比如引用绝对地址等问题。可以考虑
保持文本段不变,额外增加一个段存放寄生代码。然而引入一个额外的段的确容易引
起怀疑,很容易被发现。

向高端扩展文本段或者向低端扩展数据段都有可能引起段重叠,在内存中重定位一个
段又会使那些引用了绝对地址的代码产生问题。可以考虑向高端扩展数据段,这不是
个好主意,有些Unix完整地实现了内存保护机制,数据段是不可执行的。

段边界上的页填充提供了插入寄生代码的地方,只要空间允许。在这里插入寄生代码
不破坏原有段内容,不要求重定位。文本段结尾处的页填充是个很好的地方,最后看
上去象下面这个样子:

关键字:

[…]   一个完整的页
V       寄生代码
T       文本段内容
D       数据段内容
P       填充

页号
#1    [TTTTTTTTTTTTVVPP]    <– 文本段
#2    [PPPPDDDDDDDDPPPP]    <– 数据段

一个更完整的ELF可执行布局如下:

    ELF Header
    Program header table
    Segment 1
    Segment 2
    Section header table
    Section 1
    .
    .
    Section n

典型的,额外的节(那些没有相应段的节)用于存放调试信息、符号表等等。

下面是一些来自 ELF 规范的内容:

ELF 头位于最开始,保存一张"road map",描述了文件的组织结构。节保存大量链接
信息、符号表、重定位信息等等。

如果存在一个"program header table",将告诉操作系统如何建立进程映象(执行一
个程序)。可执行文件必须有一个"program header table",可重定位的文件不需要
该表。"section header table"描述了文件的节组织。每个节在该表中都有一个表项,
表项包含了诸如节名、节尺寸等信息。链接过程中被用到的文件自身必须有一个
"section header table",其他目标文件可有可无该表。

插入寄生代码之后,ELF 文件布局如下:

    ELF Header
    Program header table
    Segment 1   – 文本段(主体代码)
                – 寄生代码
    Segment 2
    Section header table
    Section 1
    .
    .
    Section n

寄生代码必须物理插入到ELF文件中,文本段必须扩展以包含新代码。

下面的信息来自/usr/include/elf.h

/* The ELF file header.  This appears at the start of every ELF file.  */

#define EI_NIDENT (16)

typedef struct
{
    unsigned char e_ident[EI_NIDENT];  /* Magic number and other info */
    Elf32_Half    e_type;              /* Object file type */
    Elf32_Half    e_machine;           /* Architecture */
    Elf32_Word    e_version;           /* Object file version */
    Elf32_Addr    e_entry;             /* Entry point virtual address */
    Elf32_Off     e_phoff;             /* Program header table file offset */
    Elf32_Off     e_shoff;             /* Section header table file offset */
    Elf32_Word    e_flags;             /* Processor-specific flags */
    Elf32_Half    e_ehsize;            /* ELF header size in bytes */
    Elf32_Half    e_phentsize;         /* Program header table entry size */
    Elf32_Half    e_phnum;             /* Program header table entry count */
    Elf32_Half    e_shentsize;         /* Section header table entry size */
    Elf32_Half    e_shnum;             /* Section header table entry count */
    Elf32_Half    e_shstrndx;          /* Section header string table index */
} Elf32_Ehdr;

e_entry 保存了程序入口点的虚拟地址。

e_phoff 是"program header table"在文件中的偏移。因此为了读取
"program header table",需要调用lseek()定位该表。

e_shoff 是"section header table"在文件中的偏移。该表位于文件尾部,在文本段
尾部插入寄生代码之后,必须更新e_shoff指向新的偏移。

/* Program segment header.  */

typedef struct
{
    Elf32_Word p_type;    /* Segment type */
    Elf32_Off  p_offset;  /* Segment file offset */
    Elf32_Addr p_vaddr;   /* Segment virtual address */
    Elf32_Addr p_paddr;   /* Segment physical address */
    Elf32_Word p_filesz;  /* Segment size in file */
    Elf32_Word p_memsz;   /* Segment size in memory */
    Elf32_Word p_flags;   /* Segment flags */
    Elf32_Word p_align;   /* Segment alignment */
} Elf32_Phdr;

可装载段(文本段/数据段)在"program header"中由成员变量p_type标识出是可装载
的,其值为PT_LOAD (1)。与"ELF header"中的e_shoff一样,这里的p_offset成员
必须在插入寄生代码后更新以指向新偏移。

p_vaddr 指定了段的起始虚拟地址。以p_vaddr为基地址,重新计算e_entry,就可以
指定程序流从何处开始。

可以利用p_vaddr指定程序流从何处开始。

p_filesz 和 p_memsz 分别对应该段占用的文件尺寸和内存尺寸。

.bss 节对应数据段里未初始化的数据部分。我们不想让未初始化的数据占用文件空
间,但是进程映象必须保证能够分配足够的内存空间。.bss 节位于数据段尾部,任
何超过文件尺寸的定位都假设位于该节中。

/* Section header.  */

typedef struct
{
    Elf32_Word sh_name;       /* Section name (string tbl index) */
    Elf32_Word sh_type;       /* Section type */
    Elf32_Word sh_flags;      /* Section flags */
    Elf32_Addr sh_addr;       /* Section virtual addr at execution */
    Elf32_Off  sh_offset;     /* Section file offset */
    Elf32_Word sh_size;       /* Section size in bytes */
    Elf32_Word sh_link;       /* Link to another section */
    Elf32_Word sh_info;       /* Additional section information */
    Elf32_Word sh_addralign;  /* Section alignment */
    Elf32_Word sh_entsize;    /* Entry size if section holds table */
} Elf32_Shdr;

sh_offset 指定了节在文件中的偏移。

为了在文本段末尾插入寄生代码,我们必须做下列事情:

    * 修正"ELF header"中的 p_shoff 
    * 定位"text segment program header"
        * 修正 p_filesz 
        * 修正 p_memsz 
    * 对于文本段phdr之后的其他phdr
        * 修正 p_offset 
    * 对于那些因插入寄生代码影响偏移的每节的shdr
        * 修正 sh_offset 
    * 在文件中物理地插入寄生代码到这个位置
      text segment p_offset + p_filesz (original)

这里存在一个大问题,ELF 规范中指出,

    p_vaddr mod PAGE_SIZE ==  p_offset mod PAGE_SIZE

为了满足这个要求:

    * 修正"ELF header"中的 p_shoff ,增加 PAGE_SIZE 大小
    * 定位"text segment program header"
        * 修正 p_filesz 
        * 修正 p_memsz 
    * 对于文本段phdr之后的其他phdr
        * 修正 p_offset ,增加 PAGE_SIZE 大小
    * 对于那些因插入寄生代码影响偏移的每节的shdr
        * 修正 sh_offset ,增加 PAGE_SIZE 大小
    * 在文件中物理地插入寄生代码以及填充(确保构成一个完整页)到这个位置
      text segment p_offset + p_filesz (original)

我们还需要修正程序入口点的虚拟地址,使得寄生代码先于宿主代码执行。同时需要
在寄生代码尾部能够跳转回宿主代码原入口点继续正常流程。

    * 修正"ELF header"中的 p_shoff ,增加 PAGE_SIZE 大小
    * 修正寄生代码的尾部,使之能够跳转回宿主代码原入口点
    * 定位"text segment program header"
        * 修正 "ELF header"中的 e_entry ,指向 p_vaddr + p_filesz
        * 修正 p_filesz 
        * 修正 p_memsz 
    * 对于文本段phdr之后的其他phdr
        * 修正 p_offset ,增加 PAGE_SIZE 大小
    * 对于文本段的最后一个shdr
        * 修正sh_len(应该是sh_size吧,不确定),增加寄生代码大小
    * 对于那些因插入寄生代码影响偏移的每节的shdr
        * 修正 sh_offset ,增加 PAGE_SIZE 大小
    * 在文件中物理地插入寄生代码以及填充(确保构成一个完整页)到这个位置
      text segment p_offset + p_filesz (original)

病毒可以随机遍历一个目录树,寻找那些e_type等于 ET_EXEC 或者 ET_DYN 的文件,
加以感染,这分别是可执行文件和动态链接库文件。

★ 分析Linux病毒

病毒要求不使用库,避开libc,转而使用系统调用机制。
为了动态申请堆内存用于phdr table和shdr table,应该使用brk系统调用。
利用与缓冲区溢出相同的技术取得常量字符串的地址。

使用gcc -S编译c代码,观察调整asm代码。
注意在进入/离开寄生代码的时候保存/恢复寄存器。

利用objdump -D观察调整一些需要确定的偏移量。

★ 检测病毒

这里描述的病毒很容易检测。最显眼的是程序入口点不在常规节中,甚至干脆不在任
何节中。清理病毒的过程和感染病毒的过程类似。

用objdump –all-headers很容易定位程序入口点,用objdump –disassemble-all
跟踪下去就可以得到程序原入口点。

缺省程序入口点是_start,但是可以在链接的时候更改它。

★ 结论

Unix病毒尽管不流行,但的确可行。