Wednesday, February 02, 2005

 

Some notes on PE

1.
From http://blog.csdn.net/clin003/archive/2004/12/13/214995.aspx

为PE文件添加新节显示启动信息
病毒并不神秘,也不复杂。相当多的大侠已经在这方面作出了杰出的贡献,例如 29A 组织,我对他们的崇拜之情啊,真是……咳咳,先别扔鸡蛋。其实我想说的是:技术是一柄双刃剑,我们应该把它运用在对社会有益的事情上。所以请勿利用本文的代码进行违法违纪的活动,否则本人保留追究的权利。

本文的技术其实早已是老掉牙的东西了,so如果你已经懂得了编写病毒的方法,请跳过本文;如果你对病毒抱有好奇心,但是还没知道怎么编写,那么本文应该适合你。 :)

言归正传。在 Windows 环境下,所有的可执行文件都是 PE 格式,因此编写病毒最重要的环节之一就是对 PE 文件进行操作。但是在此我不打算对 PE 格式进行讲解,请读者自行参考有关资料。我只对我在实际编写中遇到的难点进行分析:

首先,计算机病毒之所以叫做病毒,是因为它跟自然界中病毒一样,都需要有一个宿主——它本身是无法单独执行的。那么,当病毒寄生在宿主上后,怎样让它的代码执行呢?我们先来看一些概念。

PE 的代码映象分为几个 SECTION,在文件中会对齐页边界(4K)。一般来说,文件会加载在 400000h 开始的空间,而第一个 SECTION 在 401000h 处,同时入口地址也是 401000h。这个入口地址 401000h 是怎么计算出来的呢?如果你查看 PE 头的 IMAGE_OPTIONAL_HEADER ,就会发现它的 ImageBase 一般是 400000h ,而 AddressOrEntryPoint 一般是 1000h 。 400000 + 1000 = 401000h ,明白了吧?掌握了这一点,我们就可以在 PE 中添加我们自己的新节,然后把这个入口地址改成指向新节的第一条代码。当新节执行完毕后,再把原入口恢复,这样一来就能继续执行宿主的代码了。

在几乎每个 Win32 病毒的开头都有这样的语句:

call nStart
nStart:
pop ebp
sub ebp, offset nStart

这些语句是用来干嘛的呢?好像是吃饱了饭没事干哦……其实不然。让我们来仔细考虑一下。当正常的 PE 程序执行时,它的基址(如前所述)一般是 400000h ,这个地址会由操作系统为你重定位,因此总是能保证程序被成功地装载运行。但是,如果我们在 PE 中插入了一段新的代码,假设它要从 654321h 处开始执行,那么事情就没有那么简单了。因为宿主程序并没有预料到这段代码的存在,而操作系统也不可能为你修正这个偏移。因此我们就要自己进行重定位操作。上面的语句就是取得病毒在宿主中的实际偏移地址。Call 指令实际上是 push 和 jmp 的组合。当 call nStart 时,实际上是把 call nStart 的下一条指令(也就是pop ebp)的地址压入堆栈然后 jmp 到 nStart ,由于之前已经把 pop ebp 的地址压入了堆栈,所以当真正执行到 pop ebp 这条指令的时候,实际上就是把 pop ebp 这条指令的地址放到了 ebp 中。这样就得到了当前病毒代码的真正的偏移地址。这也是病毒中常用的手法。几乎无一例外。

接下来还有一个关键的问题。我们的病毒代码是附属在宿主上的,如果要在病毒中使用 API ,则必须首先得到 API 的入口地址。不过这可不是一件容易的事情啊。为什么这样说呢?让我们先来看看下面的代码:

invoke ExitProcess, 0

在经过编译器的编译、连接后,它在内存中形如:
:00401015 Call 0040101A
:0040101A Jmp dword ptr [00402000]


也就是说,ExitProcess 的调用是通过 Call 0040101A ,而 0040101A 处的代码是一个 Jmp ,指向 [00402000] ,这个 [00402000] 处储存的才是真正的 ExitProcess 的入口地址。

为什么要经过那么多周折呢?呵呵,其实我也不知道。但是我们知道的是,调用一个 API 实际上是调用它在内存中的地址。而病毒由于是在宿主编译完之后才附属上去的,所以如果病毒要运行 API ,则必须自己指定 API 的入口地址。

是不是很烦呢?Hoho,坚持一下吧,就快大功告成了。

要得到 API 的入口地址,方法有很多种,例如可以通过硬编码,这是比较简单的方法,但是它的缺陷是不能在不同的 Windows 版本下运行,不过由于它实现起来比较简便,因此本文还是采用这种方法。

在同一个版本的 Windows 下,同一个核心函数的入口总是固定不变的(指由 Kernel32, Gdi32, User32 导出的函数),所以我们就可以利用下面的方法得到 API 的入口:



szDllName db "User32", 0
szMessageBoxA db "MessageBoxA", 0
MessageBoxA_Addr dd 0

invoke GetModuleHandle, addr szDllName
invoke LoadLibrary, addr szDllName
invoke GetProcAddress, eax, addr szMessageBoxA
mov MessageBoxA_Addr, eax


在病毒中我们就可以用 Call MessageBoxA_Addr[ebp] 来执行 MessageBoxA 这个 API 了。

好啦,我已经把我认为比较重要的难点解释了一次了,如果你还有什么不清楚的地方,欢迎给我来信。lcother@163.net

下面我给出了一个例子程序,它的作用是为 PE 文件添加一个新节以显示启动信息。这个东东会在 PE 文件的末尾添加一个新节,我给这个节命名为“.LC”,被附加的程序在运行的时候会先弹出一个对话框,显示我们的提示信息。你可以对它稍作修改,例如加上自己的版权信息,然后给 CS 的主程序打上这个“病毒”,接着……呵呵,等着看舍友的惊讶的目光吧!实际上只要对它进行一些额外的补充,它就可以算是一个小小的病毒了。

值得注意的是,本程序要对代码段进行写操作(也就是SMC),所以在编译连接的时候应该这样做:

rc Add_Section.rc
ml /c /coff Add_Section.asm
link /subsystem:windows /section:.text,RWE Add_Section.res Add_Section.obj


Have fun!


;***********************************************
;程序名称:为PE文件添加新节显示启动信息
;作者:罗聪
;日期:2002-11-10
;出处:http://www.luocong.com(老罗的缤纷天地)
;本代码使用了病毒技术,但纯粹只用于技术研究。
;切记:请勿用于非法用途!!!!!!
;注意事项:如欲转载,请保持本程序的完整,并注明:
;转载自“老罗的缤纷天地”(http://www.luocong.com)
;***********************************************

.386
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\comdlg32.lib

WndProc proto :DWORD, :DWORD, :DWORD, :DWORD
AddNewSection proto :DWORD

;很有用的宏:
CTEXT MACRO y:VARARG
LOCAL sym
CONST segment
ifidni ,<>
sym db 0
else
sym db y,0
endif
CONST ends
exitm
ENDM

.const
IDI_LC equ 1
IDC_BUTTON_OPEN equ 3000
MAXSIZE equ 260
Head_Len equ sizeof IMAGE_NT_HEADERS + sizeof IMAGE_SECTION_HEADER

.data
szDlgName db "lc_dialog", 0
szCaption db "Section Add demo by LC", 0
ofn OPENFILENAME <>
szFileName db MAXSIZE dup(0)
szFilterString db "PE 可执行文件", 0, "*.exe", 0, 0
szMyTitle db "请打开一个PE可执行文件…", 0
PE_Header IMAGE_NT_HEADERS <0>
My_Section IMAGE_SECTION_HEADER <>
szDllName db "User32", 0
szMessageBoxA db "MessageBoxA", 0

.data?
hInstance HINSTANCE ?

.code
main:
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke DialogBoxParam, eax, offset szDlgName, 0, WndProc, 0
invoke ExitProcess, eax

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM

.if uMsg == WM_CLOSE
invoke EndDialog, hWnd, 0

.elseif uMsg == WM_INITDIALOG
;设置我的图标:
invoke LoadIcon, hInstance, IDI_LC
invoke SendMessage, hWnd, WM_SETICON, ICON_SMALL, eax

.elseif uMsg == WM_COMMAND
mov eax, wParam
mov edx, eax
shr edx, 16
movzx eax, ax
.if edx == BN_CLICKED
.if eax == IDCANCEL
invoke EndDialog, hWnd, NULL
.elseif eax == IDC_BUTTON_OPEN || eax == IDOK
;调用子程序,添加节:
invoke AddNewSection, hWnd
.endif
.endif
.else
mov eax, FALSE
ret
.endif
mov eax, TRUE
ret
WndProc endp

AddNewSection proc uses ecx hWnd:HWND
LOCAL hFile: HANDLE
LOCAL dwPE_Header_OffSet: DWORD
LOCAL dwFileReadWritten: DWORD
LOCAL dwMySectionOffSet: DWORD
LOCAL dwLastSection_SizeOfRawData: DWORD
LOCAL dwLastSection_PointerToRawData: DWORD

;“打开文件”对话框:
mov ofn.lStructSize, sizeof ofn
push hWnd
pop ofn.hwndOwner
push hInstance
pop ofn.hInstance
mov ofn.lpstrFilter, offset szFilterString
mov ofn.lpstrFile, offset szFileName
mov ofn.nMaxFile, MAXSIZE
mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER
mov ofn.lpstrTitle, offset szMyTitle
invoke GetOpenFileName, addr ofn

;如果没有选择文件名则退出:
.if eax == 0
jmp Err_CreateFile_Exit
.endif

;打开文件:
invoke CreateFile, addr szFileName, GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ or FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
.if eax == INVALID_HANDLE_VALUE
invoke MessageBox, hWnd, CTEXT("打开文件失败!"), addr szCaption, MB_OK or MB_ICONHAND
jmp Err_CreateFile_Exit
.endif
mov hFile, eax

;****************************************
;读取PE文件头:
;****************************************
invoke SetFilePointer, hFile, 3ch, 0, FILE_BEGIN
invoke ReadFile, hFile, addr dwPE_Header_OffSet, 4, addr dwFileReadWritten, NULL
invoke SetFilePointer, hFile, dwPE_Header_OffSet, 0, FILE_BEGIN
invoke ReadFile, hFile, addr PE_Header, Head_Len, addr dwFileReadWritten, NULL

;****************************************
;判断是否有效的PE文件,是的话才继续:
;****************************************
.if [PE_Header.Signature] != IMAGE_NT_SIGNATURE
;如果不是有效的PE文件,就给出提示:
invoke MessageBox, hWnd, CTEXT("这不是一个有效的Win32 PE文件!"), addr szCaption, MB_OK or MB_ICONHAND
jmp Exit
.endif

;****************************************
;判断是否有足够空间存储新节:
;****************************************
movzx eax, [PE_Header.FileHeader.NumberOfSections] ;得到添加新节前有多少个节:
mov ecx, 28h ;28h = sizeof IMAGE_SECTION_HEADER
mul ecx ;eax = NumberOfSections * sizeof IMAGE_SECTION_HEADER
add eax, dwPE_Header_OffSet ;eax = eax + PE文件头偏移
add eax, 18h ;18h = sizeof IMAGE_FILE_HEADER
movzx ecx, [PE_Header.FileHeader.SizeOfOptionalHeader]
add eax, ecx ;eax = eax + sizeof IMAGE_OPTIONAL_HEADER
add eax, 28h ;添加一个新节的大小
.if eax > [PE_Header.OptionalHeader.SizeOfHeaders]
;不够的话给出提示:
invoke MessageBox, NULL, CTEXT("没有足够的空间来加入一个新节!"), addr szCaption, MB_OK or MB_ICONHAND
jmp Exit
.endif

;****************************************
;保存原入口,后面要用到:
;****************************************
mov eax, [PE_Header.OptionalHeader.AddressOfEntryPoint]
mov Old_AddressOfEntryPoint, eax
mov eax, [PE_Header.OptionalHeader.ImageBase]
mov Old_ImageBase, eax

;**************************************************
;计算新节的偏移地址:
;(其实跟上面的“判断是否有足够空间存储新节”基本上一样)
;**************************************************
movzx eax, [PE_Header.FileHeader.NumberOfSections]
mov ecx, 28h
mul ecx ;eax = NumberOfSections * sizeof IMAGE_SECTION_HEADER
add eax, 4h ;4h = sizeof "PE\0\0"
add eax, dwPE_Header_OffSet
add eax, sizeof IMAGE_FILE_HEADER
add eax, sizeof IMAGE_OPTIONAL_HEADER
mov dwMySectionOffSet, eax ;现在得到了我们的新节的偏移地址

;****************************************
;填充我们自己的节的信息:
;(这部分请查看PE格式,很容易明白,不多说了)
;****************************************
mov dword ptr [My_Section.Name1], "CL." ;名字就叫做“.LC”吧,呵呵……
mov [My_Section.Misc.VirtualSize], offset vEnd - offset vStart
push [PE_Header.OptionalHeader.SizeOfImage]
pop [My_Section.VirtualAddress]
mov eax, [My_Section.Misc.VirtualSize]
mov ecx, [PE_Header.OptionalHeader.FileAlignment]
cdq
div ecx
inc eax
mul ecx
mov [My_Section.SizeOfRawData], eax ;SizeOfRawData在EXE文件中是对齐到FileAlignMent的整数倍的值
mov eax, dwMySectionOffSet
sub eax, 18h ;这个偏移是定位到最后一节的“SizeOfRawData”
invoke SetFilePointer, hFile, eax, 0, FILE_BEGIN
invoke ReadFile, hFile, addr dwLastSection_SizeOfRawData, 4, addr dwFileReadWritten, NULL
invoke ReadFile, hFile, addr dwLastSection_PointerToRawData, 4, addr dwFileReadWritten, NULL
;每个节的 PointerToRawData 等于它的上一节的 SizeOfRawData + PointerToRawData:
mov eax, dwLastSection_SizeOfRawData
add eax, dwLastSection_PointerToRawData
mov [My_Section.PointerToRawData], eax
mov [My_Section.PointerToRelocations], 0h
mov [My_Section.PointerToLinenumbers], 0h
mov [My_Section.NumberOfRelocations], 0h
mov [My_Section.NumberOfLinenumbers], 0h
mov [My_Section.Characteristics], 0E0000020h ;可读可写可执行

;**************************************************
;重新写入IMAGE_SECTION_HEADER:(包含了新节的信息)
;**************************************************
invoke SetFilePointer, hFile, dwMySectionOffSet, 0, FILE_BEGIN
invoke WriteFile, hFile, addr My_Section, sizeof IMAGE_SECTION_HEADER, addr dwFileReadWritten, NULL

;****************************************
;得到 MessageBoxA 的线性地址:
;****************************************
invoke GetModuleHandle, addr szDllName
invoke LoadLibrary, addr szDllName
invoke GetProcAddress, eax, addr szMessageBoxA
mov MessageBoxA_Addr, eax

;****************************************
;在文件的最后写入我们的新节:
;****************************************
invoke SetFilePointer, hFile, 0, 0, FILE_END
push 0
lea eax, dwFileReadWritten
push eax
push [My_Section.SizeOfRawData]
lea eax, vStart
push eax
push hFile
call WriteFile

;**************************************************
;改写IMAGE_NT_HEADERS,使新节可以首先执行:
;(需要改写 SizeOfImage 和 AddressOfEntryPoint)
;**************************************************
inc [PE_Header.FileHeader.NumberOfSections]
mov eax, [My_Section.Misc.VirtualSize]
mov ecx, [PE_Header.OptionalHeader.SectionAlignment]
cdq
div ecx
inc eax
mul ecx
add eax, [PE_Header.OptionalHeader.SizeOfImage]
mov [PE_Header.OptionalHeader.SizeOfImage], eax ;SizeOfImage是一个对齐到SectionAlignment的整数倍的值
mov eax, [My_Section.VirtualAddress]
mov [PE_Header.OptionalHeader.AddressOfEntryPoint], eax ;现在的 AddressOfEntryPoint 是指向新节的第一条指令
invoke SetFilePointer, hFile, dwPE_Header_OffSet, 0, FILE_BEGIN
invoke WriteFile, hFile, addr PE_Header, sizeof IMAGE_NT_HEADERS, addr dwFileReadWritten, NULL

;****************************************
;完成!显示成功信息:
;****************************************
invoke MessageBox, hWnd, CTEXT("添加新节成功!"), addr szCaption, MB_OK or MB_ICONINFORMATION

Exit:
;关闭文件:
invoke CloseHandle, hFile
Err_CreateFile_Exit:
ret
AddNewSection endp

;****************************************
;呵呵,我们自己的东东:(像不像病毒?)
;****************************************
vStart:
call nStart
nStart:
pop ebp
sub ebp, offset nStart ;得到新节在文件中的实际偏移地址

;显示对话框:
push MB_OK or MB_ICONINFORMATION
lea eax, szMyCaption[ebp]
push eax
lea eax, szMyMsg[ebp]
push eax
push 0
call MessageBoxA_Addr[ebp]

;恢复原入口地址。当这个节执行完毕后,就回到了原来的文件入口处继续执行:
mov eax, Old_ImageBase[ebp]
add eax, Old_AddressOfEntryPoint[ebp]
push eax
ret

;变量定义:
MessageBoxA_Addr dd 0
szMyMsg db "为PE文件添加新节显示启动信息", 13, 10, 13, 10, "老罗的缤纷天地",13, 10, "http://www.LuoCong.com", 0
szMyCaption db "老罗的病毒基础教程系列 by LC", 0
Old_ImageBase dd 0
Old_AddressOfEntryPoint dd 0
vEnd:

end main
;******************** over ********************
;by LC


它的资源文件:


#include "resource.h"

#define IDC_STATIC -1
#define IDI_LC 1
#define IDC_BUTTON_OPEN 3000

IDI_LC ICON "lc.ico"

LC_DIALOG DIALOGEX 10, 10, 195, 115
STYLE DS_SETFONT | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Section Add demo by LC, 2002-11-10"
FONT 9, "宋体", 0, 0, 0x0
BEGIN
GROUPBOX "Info", IDC_STATIC, 5, 5, 185, 75
CTEXT "- 为PE文件添加新节显示启动信息 -", IDC_STATIC, 10, 20, 175, 10
CTEXT "-= Virus Tutorial Series =-", IDC_STATIC, 10, 30, 175, 10
CTEXT "老罗的缤纷天地", IDC_STATIC, 10, 50, 175, 10
CTEXT "www.LuoCong.com", IDC_STATIC, 10, 60, 175, 10
DEFPUSHBUTTON "打开文件(&O)",IDC_BUTTON_OPEN, 70, 90, 55, 15, BS_FLAT | BS_CENTER
END

老罗
2002-11-10

2.
From http://blog.csdn.net/clin003/archive/2004/12/13/214904.aspx

最短小的PE文件之破译
发信人: rufi (如飞), 信区: Arch_Compiler
标 题: 最短小的PE文件之破译
发信站: 日月光华 (2004年03月24日23:45:03 星期三), 站内信件

最短小的PE文件之破译

研究PE格式是从看到chsoft的说明档开始的:
chsoft (Get busy living or get busy dying) 共上站 2086 次 [狮子座]
上 次 在:[2004年03月24日10:54:22 星期三] 从 [10.100.118.60] 到本站一游。
目前在线:[讯息器:(打开) 呼叫器:(打开)] 表现值:[资深人士] 信箱:[ ]
文 章 数:[3549] 经 验 值:[# ] 生命力:[149]
目前 chsoft 状态如下:
品味文章 览新文章
史上最短小精悍的PE File完整版
00000000h: 4D 5A B8 EC 00 40 00 E9 B0 00 00 00 50 45 00 00 4C 01 01 00
00000014h: 75 73 65 72 33 32 2E 64 6C 6C 00 00 C8 00 03 01 0B 01 00 00
00000028h: 00 10 00 00 00 10 00 00 00 00 00 00 02 00 00 00 02 00 00 00
0000003Ch: 0C 00 00 00 00 00 40 00 00 10 00 00 00 02 00 00 02 01 00 00
00000050h: 00 00 00 00 04 00 00 00 00 00 00 00 00 20 00 00 00 02 00 00
00000064h: 00 00 00 00 02 00 00 00 00 00 10 00 00 20 00 00 00 00 10 00
00000078h: 00 10 00 00 00 00 00 00 0D 00 00 00 00 00 00 00 00 00 00 00
0000008Ch: 94 00 00 00 28 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF
000000A0h: 14 00 00 00 4C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000000B4h: 00 00 00 00 00 00 00 00 6A 00 50 50 6A 00 FF 15 4C 00 40 00
000000C8h: C3 90 90 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000000DCh: 00 00 00 00 00 00 00 00 94 00 00 00 28 00 00 00 48 65 6C 6C
000000F0h: 6F 21 00 00 00 10 00 00 00 10 00 00 00 00 00 00 00 00 00 00
00000104h: 4D 65 73 73 61 67 65 42 6F 78 41 00 60 00 00 E0
共276个字节,猜猜看运行结果是什么?

这个文件字节数不多,分析起来不是很难,分析一个标准的PE文件反而花了很多的时间.
搞清楚标准的PE格式之后,对这个文件的机理也就明白了.

下面是破译出来的源程序:

org 0x400000 ;ImageBase
origin:

DOS_MZ_header:
.e_magic db "MZ"
start:
use32
mov eax,Section_Table.Name ; ASCII "Hello!"
jmp main

PE_header:
.Signature db "PE",0,0
FileHeader:
.Machine dw 0x014C ;Machine = IMAGE_FILE_MACHINE_I386
(14Ch)
.NumberOfSections dw 0x01
.TimeDateStamp dd "user" ;db "user32.dll",0
.PointerToSymbolTable dd "32.d"
.NumberOfSymbols dd "ll"
.SizeOfOptionalHeader dw 0x00C8
.Characteristics dw 0x0103 ;exe or dll
OptionalHeader: ;OptionalHeader has 31 fields
.Magic dw 0x010B
.MajorLinkerVersion db 0
.MinerLinkerVersion db 0
.SizeOfCode dd 0x1000
.SizeOfInitializedData dd 0x1000
.SizeOfUnInitializedData dd 0
.AddressOfEntryPoint dd start-origin
.BaseOfCode dd start-origin
.BaseOfData dd PE_header-origin ;e_lfanew
.ImageBase dd origin
.SectionAlignment dd 0x1000
.FileAlignment dd 0x0200
messagebox:
.MajorOSVersion dw 0x0102 ;point to dword before "MessageBoxA"
.MinorOSVersion dw 0x0000
.MajorImageVersion dw 0
.MinorImageVersion dw 0
.MajorSubSystemVerion dw 4
.MinorSubSystemVerion dw 0
.Win32VersionValue dd 0
.SizeOfImage dd 0x2000
.SizeOfHeaders dd 0x0200
.CheckSum dd 0
.SubSystem dw 2
.DllCharacteristics dw 0
.SizeOfStackReserve dd 0x100000
.SizeOfStackCommit dd 0x2000
.SizeOfHeapReserve dd 0x100000
.SizeOfHeapRCommit dd 0x1000
.LoaderFlags dd 0
.NumberOfDataDirectories dd 0x0D ;13

Data_Directories:
.Export_Table dd 0,0 ; Rva,Size
.Import_Table dd 0x94,0x28 ; Rva,Size
.Resource_Table dd 0,0 ; Rva,Size
.Exception_Table dd 0xFFFFFFFF,0x14 ; Rva,Size
.Security_Table dd 0x4C,0 ; Rva,Size
.Relocation_Table dd 0,0 ; Rva,Size
.Debug dd 0,0 ; Rva,Size
;.Description dd 0x5050006A,0x15FF006A ; Rva,Size
;.Global_PTR dd 0x0040004C,0x909090C3 ; Rva,Size
main:
PUSH 0 ; /Style = MB_OK|M
PUSH EAX ; |Title
PUSH EAX ; |Text
PUSH 0 ; |hOwner = NULL
CALL DWORD [DS:messagebox] ; \MessageBoxA
RETN
NOP
NOP
NOP
.TLS_Table dd 0,0 ; Rva,Size
.Load_Config_Table dd 0,0 ; Rva,Size
.Bound_Import dd 0,0 ; Rva,Size
.ImportAddressTable dd 0x94,0x28 ; Rva,Size

Section_Table:
.Name db "Hello!",0,0
.VirtualSize dd 0x1000
.VirtaulAddress dd 0x1000
.SizeOfRawData dd 0
.PointerToRawData dd 0 ; after five dwords, appear
"user32.dll"
.PointerToRelocations dd "Mess" ; db "MessageBoxA",0
.PointerToLinenumbers dd "ageB"
.NumberOfRelocations dw "ox"
.NumberOfLinenumbers dw "A"
.Characteristics dd 0xE0000060

编译后就得到了那276个字节,运行的结果是显示一个标题和内容都要是"Hello!"的消息
框.
分析这个源程序就会发现:
1.程序的入口点在DOS header内,之后又跳转到Data_Directories的Debug之后,于是
Data_Directories的Description和Global_PTR中的内容为Code.
2.PE header的BaseOfData字段同时又是DOS header的
e_lfanew字段,指向PE header的位置.
3.NumberOfDataDirectories设为了13,而正常情况下为16.
4.Section_Table中只有一个节,而这个节内容分散在前面的数据中,以致于整个文件的he
ader
之后没有RawData.
5.PointerToRawData为0,这样从文件开头数过5个DWORD后,找到dll的文件名.
6.Import_Table的RVA是0x94,这样Ox94之后的第5个DWORD,相当于ImageImportDescripto
r的
FirstThunk,这个字段的内容为0x4C, 而Ox4C处的字段为MajorOSVersion,所这个地方的
内容(0x0102)
相当于ThunkValue,指向一个ImageImportByName结构,0x0102处的双字节为Hint,Hint之
后的则是
函数名MessageBoxA.
概括地说,这个文件的机理就把文件头不重要的平常没有用到的字节,填充为有用的数据
或代码.
然后一些关键字段的内容为指针时,巧妙指向那些数据或代码,于是文件可以被执行.
--

add life,coding
push limits
mov reality,ideas

3.
根据PE文件获取LoadLibraryA/GetProcAddress

by scz
from http://bbs.nsfocus.net/index.php?act=ST&f=3&t=147799

本节与PE文件格式相关的术语以<<微软PE/COFF规范>>为准([13]),不再强调该点。
winnt.h中定义了部分相关数据结构,后面如未单独注明来自哪个头文件,均隐指来
自winnt.h。执行vulnerable_0.exe,进入windbg调试状态。

> !list -t _LIST_ENTRY.Flink -x "dd" -a "+18 L1" 241ec0
00241ed8 00400000
00241f30 77f50000
00241fd8 77e60000
... ...

从上节可知,ntdll.dll基址是0x77f50000,kernel32.dll基址是0x77e60000。最开
始的64字节按如下数据结构解析:

--------------------------------------------------------------------------
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ

typedef struct _IMAGE_DOS_HEADER // DOS .EXE header
{
WORD e_magic; // +0x00 Magic number
WORD e_cblp; // +0x02 Bytes on last page of file
WORD e_cp; // +0x04 Pages in file
WORD e_crlc; // +0x06 Relocations
WORD e_cparhdr; // +0x08 Size of header in paragraphs
WORD e_minalloc; // +0x0a Minimum extra paragraphs needed
WORD e_maxalloc; // +0x0c Maximum extra paragraphs needed
WORD e_ss; // +0x0e Initial (relative) SS value
WORD e_sp; // +0x10 Initial SP value
WORD e_csum; // +0x12 Checksum
WORD e_ip; // +0x14 Initial IP value
WORD e_cs; // +0x16 Initial (relative) CS value
WORD e_lfarlc; // +0x18 File address of relocation table
WORD e_ovno; // +0x1a Overlay number
WORD e_res[4]; // +0x1c Reserved words
WORD e_oemid; // +0x24 OEM identifier (for e_oeminfo)
WORD e_oeminfo; // +0x26 OEM information; e_oemid specific
WORD e_res2[10]; // +0x28 Reserved words
LONG e_lfanew; // +0x3c File address of new exe header
// +0x40
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
--------------------------------------------------------------------------

> db 77e60000 L0n64
77e60000 4d 5a 90 00 03 00 00 00-04 00 00 00 ff ff 00 00 MZ..............
77e60010 b8 00 00 00 00 00 00 00-40 00 00 00 00 00 00 00 ........@.......
77e60020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
77e60030 00 00 00 00 00 00 00 00-00 00 00 00 f8 00 00 00 ................
> dd 77e60000+3c L1 (显示e_lfanew成员)
77e6003c 000000f8

其中e_lfanew成员用于定位PE头。从注释中理解,e_lfanew是File pointer,非RVA,
不过在这里当成RVA处理也没关系。e_lfanew值为0x000000f8,PE头在基址加0xf8的
位置。PE头最开始是标识"PE\0\0",占4字节,然后是20字节固定头。

--------------------------------------------------------------------------
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
#define IMAGE_SIZEOF_FILE_HEADER 20

typedef struct _IMAGE_FILE_HEADER
{
WORD Machine; // +0x00
WORD NumberOfSections; // +0x02
DWORD TimeDateStamp; // +0x04
DWORD PointerToSymbolTable; // +0x08
DWORD NumberOfSymbols; // +0x0c
WORD SizeOfOptionalHeader; // +0x10
WORD Characteristics; // +0x12
// +0x14
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
--------------------------------------------------------------------------

> db 77e60000+f8 L0n24
77e600f8 50 45 00 00 4c 01 04 00-28 fa 6d 3d 00 00 00 00 PE..L...(.m=....
77e60108 00 00 00 00 e0 00 0e 21
^^^^^
接下来是可选头,SizeOfOptionalHeader成员表明可选头占用了224字节(0x00e0)。

--------------------------------------------------------------------------
typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic; // +0x00
BYTE MajorLinkerVersion; // +0x02
BYTE MinorLinkerVersion; // +0x03
DWORD SizeOfCode; // +0x04
DWORD SizeOfInitializedData; // +0x08
DWORD SizeOfUninitializedData; // +0x0c
DWORD AddressOfEntryPoint; // +0x10
DWORD BaseOfCode; // +0x14
DWORD BaseOfData; // +0x18
DWORD ImageBase; // +0x1c
DWORD SectionAlignment; // +0x20
DWORD FileAlignment; // +0x24
WORD MajorOperatingSystemVersion; // +0x28
WORD MinorOperatingSystemVersion; // +0x2a
WORD MajorImageVersion; // +0x2c
WORD MinorImageVersion; // +0x2e
WORD MajorSubsystemVersion; // +0x30
WORD MinorSubsystemVersion; // +0x32
DWORD Win32VersionValue; // +0x34
DWORD SizeOfImage; // +0x38
DWORD SizeOfHeaders; // +0x3c
DWORD CheckSum; // +0x40
WORD Subsystem; // +0x44
WORD DllCharacteristics; // +0x46
DWORD SizeOfStackReserve; // +0x48
DWORD SizeOfStackCommit; // +0x4c
DWORD SizeOfHeapReserve; // +0x50
DWORD SizeOfHeapCommit; // +0x54
DWORD LoaderFlags; // +0x58
DWORD NumberOfRvaAndSizes; // +0x5c Number of data-dictionary entries in the remainder of the Optional Header
// +0x60
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// +0xe0
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

typedef struct _IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress; // +0x00 RVA
DWORD Size; // +0x04 The size in bytes
// +0x08
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16

#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
--------------------------------------------------------------------------

IMAGE_DATA_DIRECTORY.VirtualAddress是RVA。一般情况DataDirectory[]是含有16
个元素的结构数组。前两个元素分别对应Export Directory与Import Directory。但
是规范3.4.3小节指出元素个数不固定,需要检查NumberOfRvaAndSizes成员确定元素
个数。

此外,不要假设IMAGE_DATA_DIRECTORY.VirtualAddress指向所在section的起始位置,
比如Import Directory一般位于.idata section中,但不能假设RVA指向.idata的起
始位置。不要假设Import Directory所在section一定拥有".idata"这个名字。

> db 77e60000+f8+0n24 L0n224
77e60110 0b 01 07 00 00 56 07 00-00 dc 06 00 00 00 00 00 .....V..........
77e60120 60 ae 01 00 00 10 00 00-00 20 07 00 00 00 e6 77 `........ .....w
77e60130 00 10 00 00 00 02 00 00-05 00 01 00 05 00 01 00 ................
77e60140 04 00 00 00 00 00 00 00-00 60 0e 00 00 04 00 00 .........`......
77e60150 d3 7e 0e 00 03 00 00 00-00 00 04 00 00 10 00 00 .~..............
77e60160 00 00 10 00 00 10 00 00-00 00 00 00 10 00 00 00 ................
77e60170 40 d0 06 00 39 6b 00 00-7c 3b 07 00 28 00 00 00 @...9k..|;..(...
77e60180 00 a0 07 00 d8 5e 06 00-00 00 00 00 00 00 00 00 .....^..........
77e60190 00 00 00 00 00 00 00 00-00 00 0e 00 54 53 00 00 ............TS..
77e601a0 fc 63 07 00 38 00 00 00-00 00 00 00 00 00 00 00 .c..8...........
77e601b0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
77e601c0 a8 76 07 00 40 00 00 00-90 02 00 00 1c 00 00 00 .v..@...........
77e601d0 00 10 00 00 0c 06 00 00-00 00 00 00 00 00 00 00 ................
77e601e0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
> dd 77e60000+f8+0n24+5c L1 (检查NumberOfRvaAndSizes成员)
77e6016c 00000010
> dd 77e60000+f8+0n24+60 L2 (定位Export Directory,当前基是16)
77e60170 0006d040 00006b39
^^^^^^^^
> ? 77e60000+0006d040+00006b39
Evaluate expression: 2012035961 = 77ed3b79
> ? 77e60000+0006d040
Evaluate expression: 2012008512 = 77ecd040

现在我们知道Export Directory在"77e60000+0006d040",格式如下:

--------------------------------------------------------------------------
typedef struct _IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics; // +0x00
DWORD TimeDateStamp; // +0x04
WORD MajorVersion; // +0x08
WORD MinorVersion; // +0x0a
DWORD Name; // +0x0c Name of the DLL
DWORD Base; // +0x10 Starting ordinal number for exports
DWORD NumberOfFunctions; // +0x14 Number of entries in the EAT
DWORD NumberOfNames; // +0x18 Number of entries in the ENPT/EOT
DWORD AddressOfFunctions; // +0x1c RVA from base of image
DWORD AddressOfNames; // +0x20 RVA from base of image
DWORD AddressOfNameOrdinals; // +0x24 RVA from base of image
// +0x28
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
--------------------------------------------------------------------------

AddressOfFunctions

指向Export Address Table。

DWORD EAT[NumberOfFunctions];

EAT[i]

可选头中IMAGE_DATA_DIRECTORY结构决定了引出信息范围。如果EAT[i]的值不在
引出信息范围,则为函数地址(Export RVA)。否则,该值指向形如dllname.#27
或者dllname.exportfunc的ASCIZ串,用MS术语说,这是一个Forwarder RVA,表
示由另外一个dll实际引出某函数。

AddressOfNames

指向Export Name Pointer Table

DWORD ENPT[NumberOfNames];

ENPT[i]

指向Export Name Table中某个位置

ENT由ASCIZ串构成

AddressOfNameOrdinals

指向Export Ordinal Table

WORD EOT[NumberOfNames];

参看规范6.3.4小节,这几个数组之间的关系用伪C语言描述如下:

--------------------------------------------------------------------------
/*
* 怀疑该规范与MS最终实现有出入,规范中EOT[0]应该等于ordinal_base的,但MS
* 最终实现里EOT[0]等于0。这里的伪代码符合MS的最终实现。看来,理论->实践->
* 再理论的学习过程永远都要牢记,否则死菜了都不知道怎么死菜的。
*/
index = Search_ENPT( function_name );
index = EOT[ index ];
function_addr = EAT[ index ];
ordinal = ordinal_base + index;
--------------------------------------------------------------------------

为何多出个EOT,因为可能不同的函数名对应同一个函数地址,参[14]的12.5小节。

假设源代码中有如下声明:

__declspec(dllexport) int __stdcall PublicFunc ( int a, int b, int c, int d );

假设没有使用DEF文件:

EXPORTS

PublicFunc

同时却又在源代码出现:

#pragma comment( linker, "/EXPORT:PublicFunc=_PublicFunc@32" )

此时生成PE文件时会同时引出两个符号,"PublicFunc"与"_PublicFunc@32",这两个
符号对应同一个函数。反映在上述几个数组中,即EOT[i]等于EOT[j]。

NumberOfFunctions与NumberOfNames在这种情形下不等。除此之外,还有一种不等情
形。NumberOfNames为0,NumberOfFunctions不为0,表示模块仅通过ordinal引出函
数,这是相当极端却有可能出现的情形。

VC有个现成的工具dumpbin,可用于观察引出(export)信息:

> dumpbin X:\XP\system32\kernel32.dll /exports

Section contains the following exports for KERNEL32.dll

00000000 characteristics
3D6DE616 time date stamp Thu Aug 29 17:15:02 2002
0.00 version
1 ordinal base
942 number of functions
942 number of names

ordinal hint RVA name

1 0 000137E8 ActivateActCtx
2 1 000093FE AddAtomA
3 2 0000D496 AddAtomW
4 3 000607C5 AddConsoleAliasA
5 4 0006078E AddConsoleAliasW
6 5 0004E0A1 AddLocalAlternateComputerNameA
7 6 0004DF8C AddLocalAlternateComputerNameW
8 7 00035098 AddRefActCtx
9 8 AddVectoredExceptionHandler (forwarded to NTDLL.RtlAddVectoredExceptionHandler)
10 9 00036909 AllocConsole
... ...
401 190 0001B332 GetProcAddress
... ...
571 23A 0001D961 LoadLibraryA
572 23B 0001D941 LoadLibraryExA
573 23C 0001D839 LoadLibraryExW
574 23D 00013B38 LoadLibraryW
... ...

回windbg验证一下:

> db 77e60000+0006d040 L28
77ecd040 00 00 00 00 16 e6 6d 3d-00 00 00 00 34 f5 06 00 ......m=....4...
77ecd050 01 00 00 00 ae 03 00 00-ae 03 00 00 68 d0 06 00 ............h...
77ecd060 20 df 06 00 d8 ed 06 00
> da 77e60000+poi(0x77e60000+0x0006d040+0xc) (Name)
77ecf534 "KERNEL32.dll"
> dd 0x77e60000+0x0006d040+0x10 L1 (Base)
77ecd050 00000001
> dd 0x77e60000+0x0006d040+0x14 L1 (NumberOfFunctions)
77ecd054 000003ae
> dd 0x77e60000+0x0006d040+0x18 L1 (NumberOfNames)
77ecd058 000003ae
> dd 0x77e60000+0x0006d040+0x1c L3 (AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals)
77ecd05c 0006d068 0006df20 0006edd8

套用这个公式:

--------------------------------------------------------------------------
0x190 = Search_ENPT( "GetProcAddress" );
0x190 = EOT[ 0x190 ];
0x77e7b332 = EAT[ 0x190 ];
0x191 = 1 + 0x190;
--------------------------------------------------------------------------

> da 77e60000+poi(77e60000+0006df20+0x190*4) (访问ENPT、ENT)
77ed1393 "GetProcAddress"
> dw 77e60000+0006edd8+0x190*2 L1 (访问EOT)
77ecf0f8 0190
> ? 77e60000+poi(77e60000+0006d068+0x190*4) (访问EAT)
Evaluate expression: 2011673394 = 77e7b332
> u 77e7b332 (这个地址不在[77ecd040, 77ed3b79)内)
kernel32!GetProcAddress:
77e7b332 55 push ebp
77e7b333 8bec mov ebp,esp
77e7b335 51 push ecx
77e7b336 51 push ecx
77e7b337 53 push ebx
77e7b338 57 push edi
77e7b339 8b7d0c mov edi,[ebp+0xc]
77e7b33c bbffff0000 mov ebx,0xffff

再来验证一下Forwarder RVA的情形:

--------------------------------------------------------------------------
8 = Search_ENPT( "AddVectoredExceptionHandler" );
8 = EOT[ 8 ];
0x77ed38ad = EAT[ 8 ];
9 = 1 + 8;
--------------------------------------------------------------------------

> da 77e60000+poi(77e60000+0006df20+8*4) (访问ENPT、ENT)
77ecf5cf "AddVectoredExceptionHandler"
> dw 77e60000+0006edd8+8*2 L1 (访问EOT)
77ecede8 0008
> ? 77e60000+poi(77e60000+0006d068+8*4) (访问EAT)
Evaluate expression: 2012035245 = 77ed38ad
> da 77e60000+poi(77e60000+0006d068+8*4) (这个地址在[77ecd040, 77ed3b79)内)
77ed38ad "NTDLL.RtlAddVectoredExceptionHan"
77ed38cd "dler"

现在总结一下"根据PE文件格式获取LoadLibraryA()/GetProcAddress()地址"全过程:

a. 通过TEB/PEB获取kernel32.dll基址

b. 在(基址+0x3c)处获取e_lfanew

c. 在(基址+e_lfanew+0x78)处获取Export Directory地址(后面为描述方便简称export)

d. 在(基址+export+0x1c)处获取AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals

e. 搜索ENPT,确定"LoadLibraryA"、"GetProcAddress"所对应的index

f. index = EOT[ index ];

g. function_addr = EAT[ index ];

下面是完整的C语言演示程序,汇编化留到编写完整shellcode时进行。

--------------------------------------------------------------------------
/*
* -----------------------------------------------------------------------
* Compile : For x86/EWindows XP SP1 & VC 7
* : cl GetAddr.c /nologo /Os /G6 /W3 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /link /RELEASE
* :
* Create : 2003-08-14 15:11
* Modify :
* -----------------------------------------------------------------------
*/

#include
#include
#include

#pragma comment( linker, "/INCREMENTAL:NO" )
#pragma comment( linker, "/subsystem:console" )

static void * __stdcall SearchAPI ( char *BASE, DWORD *EAT, DWORD *ENPT, WORD *EOT, DWORD num, char *name )
{
DWORD index = 0;
char *function_name = NULL;
void *function_addr = NULL;

for ( index = 0; index < num; index++ )
{
function_name = ENPT[index] + BASE;
/*
* 大小写敏感比较
*/
if ( 0 == strcmp( function_name, name ) )
{
index = EOT[index];
function_addr = EAT[index] + BASE;
return( function_addr );
}
} /* end of for */
return( NULL );
} /* end of SearchAPI */

int __cdecl main ( int argc, char * argv[] )
{
void *PEB = NULL,
*Ldr = NULL,
*Flink = NULL,
*kernel32_BaseAddress = NULL,
*kernel32_BaseDllName = NULL,
*ExportDirectory = NULL,
*PrivateLoadLibraryA = NULL,
*PrivateGetProcAddress = NULL;
DWORD NumberOfNames = 0,
*AddressOfFunctions = NULL,
*AddressOfNames = NULL;
WORD *AddressOfNameOrdinals = NULL;
LONG e_lfanew = 0;

__asm
{
mov eax,fs:[0x30]
mov PEB,eax
}
printf( "PEB = 0x%08X\n", PEB );
Ldr = *( ( void ** )( ( unsigned char * )PEB + 0x0c ) );
printf( "Ldr = 0x%08X\n", Ldr );
Flink = *( ( void ** )( ( unsigned char * )Ldr + 0x1c ) );
printf( "Flink = 0x%08X\n", Flink );
Flink = *( ( void ** )Flink );
kernel32_BaseAddress = *( ( void ** )( ( unsigned char * )Flink + 0x08 ) );
kernel32_BaseDllName = *( ( void ** )( ( unsigned char * )Flink + 0x20 ) );
printf( "kernel32_BaseAddress = 0x%08X\n", kernel32_BaseAddress );
wprintf( L"kernel32_BaseDllName = %s\n", kernel32_BaseDllName );
/*
* 根据PE文件格式进行解析
*/
e_lfanew = *( ( LONG * )( ( unsigned char * )kernel32_BaseAddress + 0x3c ) );
printf( "e_lfanew = 0x%08X\n", e_lfanew );
ExportDirectory = *( ( DWORD * )( ( unsigned char * )kernel32_BaseAddress + e_lfanew + 0x78 ) ) +
( unsigned char * )kernel32_BaseAddress;
printf( "ExportDirectory = 0x%08X\n", ExportDirectory );
NumberOfNames = *( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x18 ) );
printf( "NumberOfNames = %u\n", NumberOfNames );
AddressOfFunctions = ( DWORD * )
(
*( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x1c ) ) +
( unsigned char * )kernel32_BaseAddress
);
printf( "AddressOfFunctions = 0x%08X\n", AddressOfFunctions );
AddressOfNames = ( DWORD * )
(
*( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x20 ) ) +
( unsigned char * )kernel32_BaseAddress
);
printf( "AddressOfNames = 0x%08X\n", AddressOfNames );
AddressOfNameOrdinals = ( WORD * )
(
*( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x24 ) ) +
( unsigned char * )kernel32_BaseAddress
);
printf( "AddressOfNameOrdinals = 0x%08X\n", AddressOfNameOrdinals );
PrivateLoadLibraryA = SearchAPI
(
kernel32_BaseAddress,
AddressOfFunctions,
AddressOfNames,
AddressOfNameOrdinals,
NumberOfNames,
"LoadLibraryA"
);
printf( "PrivateLoadLibraryA = 0x%08X\n", PrivateLoadLibraryA );
printf( "LoadLibraryA = 0x%08X\n", LoadLibraryA );
PrivateGetProcAddress = SearchAPI
(
kernel32_BaseAddress,
AddressOfFunctions,
AddressOfNames,
AddressOfNameOrdinals,
NumberOfNames,
"GetProcAddress"
);
printf( "PrivateGetProcAddress = 0x%08X\n", PrivateGetProcAddress );
printf( "GetProcAddress = 0x%08X\n", GetProcAddress );
return( EXIT_SUCCESS );
} /* end of main */

#if 0

/*
* 按初始化顺序前向遍历链表,第二个节点对应kernel32.dll。
*/

#include
#include

#pragma comment( linker, "/INCREMENTAL:NO" )
#pragma comment( linker, "/subsystem:console" )

int __cdecl main ( int argc, char * argv[] )
{
void *PEB = NULL,
*Ldr = NULL,
*Flink = NULL,
*p = NULL,
*BaseAddress = NULL,
*BaseDllName = NULL;

__asm
{
mov eax,fs:[0x30]
mov PEB,eax
}
printf( "PEB = 0x%08X\n", PEB );
Ldr = *( ( void ** )( ( unsigned char * )PEB + 0x0c ) );
printf( "Ldr = 0x%08X\n", Ldr );
Flink = *( ( void ** )( ( unsigned char * )Ldr + 0x1c ) );
printf( "Flink = 0x%08X\n", Flink );
p = Flink;
do
{
BaseAddress = *( ( void ** )( ( unsigned char * )p + 0x08 ) );
BaseDllName = *( ( void ** )( ( unsigned char * )p + 0x20 ) );
printf( "p = 0x%08X 0x%08X ", p, BaseAddress );
wprintf( L"%s\n", BaseDllName );
p = *( ( void ** )p );
}
while ( Flink != p );
return( EXIT_SUCCESS );
} /* end of main */

#endif
--------------------------------------------------------------------------

执行效果如下,对比PrivateLoadLibraryA与LoadLibraryA的值,应该相等。

> GetAddr
PEB = 0x7FFDF000
Ldr = 0x00241E90
Flink = 0x00241F28
kernel32_BaseAddress = 0x77E60000
kernel32_BaseDllName = kernel32.dll
e_lfanew = 0x000000F8
ExportDirectory = 0x77ECD040
NumberOfNames = 942
AddressOfFunctions = 0x77ECD068
AddressOfNames = 0x77ECDF20
AddressOfNameOrdinals = 0x77ECEDD8
PrivateLoadLibraryA = 0x77E7D961
LoadLibraryA = 0x77E7D961
PrivateGetProcAddress = 0x77E7B332
GetProcAddress = 0x77E7B332

这里演示的技巧不只用于exploit、shellcode、virus,还大量用于特殊Driver编程
中。根据PE文件格式解析内存中的模块映像是很基础的知识,我今天才接触了一下,
惭愧。

SearchAPI()用到了被搜索函数名,直接在shellcode中保存完整函数名会占用不少空
间。可以考虑在shellcode中保存函数名的某种哈希值,同时SearchAPI()比较哈希值
而非函数名。任何一种哈希算法都会丢失部分原有信息,以致不同函数名产生的哈希
值相同,所谓"碰撞"。virus编程中为此常常使用标准CRC32算法([8]),参29A-4.227。
但是LSD认为CRC32的汇编算法太长了([15]),他们用了一个相当简单的算法:

while ( *c )
{
h = ( ( h << 5 ) | ( h >> 27 ) ) + *c++;
}

不超过10行汇编代码。据LSD的报告称,对超过5000个不同的dll测试,覆盖50000个
不同的函数名,该哈希算法未产生一次碰撞。若真是如此,对于编写shellcode来讲,
完全足够了。

下面是完整的C语言演示程序,真正汇编化后应提前计算函数名的哈希值。

--------------------------------------------------------------------------
/*
* -----------------------------------------------------------------------
* Compile : For x86/EWindows XP SP1 & VC 7
* : cl GetAddr_0.c /nologo /Os /G6 /W3 /D "WIN32" /D "NDEBUG" /D "_CONSOLE" /link /RELEASE
* :
* Create : 2003-08-14 17:24
* Modify :
* -----------------------------------------------------------------------
*/

#include
#include
#include

#pragma comment( linker, "/INCREMENTAL:NO" )
#pragma comment( linker, "/subsystem:console" )

static DWORD __stdcall GetHash ( char *c )
{
DWORD h = 0;

while ( *c )
{
h = ( ( h << 5 ) | ( h >> 27 ) ) + *c++;
}
return( h );
} /* end of GetHash */

static void * __stdcall SearchAPI ( char *BASE, DWORD *EAT, DWORD *ENPT, WORD *EOT, DWORD num, DWORD NameHash )
{
DWORD index = 0,
h = 0;
char *function_name = NULL;
void *function_addr = NULL;

for ( index = 0; index < num; index++ )
{
function_name = ENPT[index] + BASE;
h = GetHash( function_name );
if ( h == NameHash )
{
index = EOT[index];
function_addr = EAT[index] + BASE;
return( function_addr );
}
} /* end of for */
return( NULL );
} /* end of SearchAPI */

int __cdecl main ( int argc, char * argv[] )
{
void *PEB = NULL,
*Ldr = NULL,
*Flink = NULL,
*kernel32_BaseAddress = NULL,
*kernel32_BaseDllName = NULL,
*ExportDirectory = NULL,
*PrivateLoadLibraryA = NULL,
*PrivateGetProcAddress = NULL;
DWORD NumberOfNames = 0,
*AddressOfFunctions = NULL,
*AddressOfNames = NULL,
NameHash = 0;
WORD *AddressOfNameOrdinals = NULL;
LONG e_lfanew = 0;

__asm
{
mov eax,fs:[0x30]
mov PEB,eax
}
printf( "PEB = 0x%08X\n", PEB );
Ldr = *( ( void ** )( ( unsigned char * )PEB + 0x0c ) );
printf( "Ldr = 0x%08X\n", Ldr );
Flink = *( ( void ** )( ( unsigned char * )Ldr + 0x1c ) );
printf( "Flink = 0x%08X\n", Flink );
Flink = *( ( void ** )Flink );
kernel32_BaseAddress = *( ( void ** )( ( unsigned char * )Flink + 0x08 ) );
kernel32_BaseDllName = *( ( void ** )( ( unsigned char * )Flink + 0x20 ) );
printf( "kernel32_BaseAddress = 0x%08X\n", kernel32_BaseAddress );
wprintf( L"kernel32_BaseDllName = %s\n", kernel32_BaseDllName );
/*
* 根据PE文件格式进行解析
*/
e_lfanew = *( ( LONG * )( ( unsigned char * )kernel32_BaseAddress + 0x3c ) );
printf( "e_lfanew = 0x%08X\n", e_lfanew );
ExportDirectory = *( ( DWORD * )( ( unsigned char * )kernel32_BaseAddress + e_lfanew + 0x78 ) ) +
( unsigned char * )kernel32_BaseAddress;
printf( "ExportDirectory = 0x%08X\n", ExportDirectory );
NumberOfNames = *( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x18 ) );
printf( "NumberOfNames = %u\n", NumberOfNames );
AddressOfFunctions = ( DWORD * )
(
*( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x1c ) ) +
( unsigned char * )kernel32_BaseAddress
);
printf( "AddressOfFunctions = 0x%08X\n", AddressOfFunctions );
AddressOfNames = ( DWORD * )
(
*( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x20 ) ) +
( unsigned char * )kernel32_BaseAddress
);
printf( "AddressOfNames = 0x%08X\n", AddressOfNames );
AddressOfNameOrdinals = ( WORD * )
(
*( ( DWORD * )( ( unsigned char * )ExportDirectory + 0x24 ) ) +
( unsigned char * )kernel32_BaseAddress
);
printf( "AddressOfNameOrdinals = 0x%08X\n", AddressOfNameOrdinals );
NameHash = GetHash( "LoadLibraryA" );
printf( "NameHash = 0x%08X\n", NameHash );
PrivateLoadLibraryA = SearchAPI
(
kernel32_BaseAddress,
AddressOfFunctions,
AddressOfNames,
AddressOfNameOrdinals,
NumberOfNames,
NameHash
);
printf( "PrivateLoadLibraryA = 0x%08X\n", PrivateLoadLibraryA );
printf( "LoadLibraryA = 0x%08X\n", LoadLibraryA );
NameHash = GetHash( "GetProcAddress" );
printf( "NameHash = 0x%08X\n", NameHash );
PrivateGetProcAddress = SearchAPI
(
kernel32_BaseAddress,
AddressOfFunctions,
AddressOfNames,
AddressOfNameOrdinals,
NumberOfNames,
NameHash
);
printf( "PrivateGetProcAddress = 0x%08X\n", PrivateGetProcAddress );
printf( "GetProcAddress = 0x%08X\n", GetProcAddress );
return( EXIT_SUCCESS );
} /* end of main */
--------------------------------------------------------------------------

> GetAddr_0
PEB = 0x7FFDF000
Ldr = 0x00241E90
Flink = 0x00241F28
kernel32_BaseAddress = 0x77E60000
kernel32_BaseDllName = kernel32.dll
e_lfanew = 0x000000F8
ExportDirectory = 0x77ECD040
NumberOfNames = 942
AddressOfFunctions = 0x77ECD068
AddressOfNames = 0x77ECDF20
AddressOfNameOrdinals = 0x77ECEDD8
NameHash = 0x331ADDDC <- "LoadLibraryA"的哈希值
PrivateLoadLibraryA = 0x77E7D961
LoadLibraryA = 0x77E7D961
NameHash = 0x99C95590 <- "GetProcAddress"的哈希值
PrivateGetProcAddress = 0x77E7B332
GetProcAddress = 0x77E7B332

☆ 参考资源

[13] Microsoft Portable Executable and Common Object File Format Specification
http://www.microsoft.com/whdc/hwdev/download/hardware/pecoff.doc
http://www.microsoft.com/whdc/hwdev/download/hardware/pecoff.pdf

[14] <> - Jeffrey Richter

[15] http://www.lsd-pl.net/documents/winasm-1.0.1.pdf
http://www.lsd-pl.net/documents/winasm.ppt
http://lsd-pl.net/projects/winasm-1.0.1.tar.gz
---
说了世上一无牵挂为何有悲喜
说了朋友相交如水为何重别离
说了少年笑看将来为何常回忆
说了青春一去无悔为何还哭泣



<< Home

This page is powered by Blogger. Isn't yours?