我希望能够清晰地解释,在Windows环境(PE可执行文件)中,如何使用CALL XXXXXXXXXXXXXXX指令。我一直在研究PE格式,但是我对CALL ADDRESS指令、从DLL导入函数以及CALL ADDRESS如何访问DLL中的代码之间的关系感到困惑。除了ASLR和其他安全功能可能会移动DLL之外,可执行文件如何应对这种情况?
我希望能够清晰地解释,在Windows环境(PE可执行文件)中,如何使用CALL XXXXXXXXXXXXXXX指令。我一直在研究PE格式,但是我对CALL ADDRESS指令、从DLL导入函数以及CALL ADDRESS如何访问DLL中的代码之间的关系感到困惑。除了ASLR和其他安全功能可能会移动DLL之外,可执行文件如何应对这种情况?
直接使用普通的相对调用导入是不起作用的,这就是为什么不会这样做的原因。
要调用已导入的函数,需要通过 Import Address Table (IAT) 进行调用。简单来说,IAT 中的条目首先指向函数名称(即最初作为 Import Name Table 的副本),然后这些指针被加载程序更改为指向实际的函数。
IAT 位于固定地址,但如果映像已被重新基址化,则可以重新定位,因此通过它进行调用只涉及单个间接引用-因此使用带有内存操作数(即简单常量)的 call r/m
调用已导入的函数,例如 call [0x40206C]
。
2013年1月22日:添加了更多简单的具体例子和讨论,因为(A)一个错误的答案被选为解决方案,(B)我的原始答案显然没有被一些读者理解,包括OP。对此我很抱歉,是我的错。当时我匆忙回答,只是添加了一个我已经掌握的代码示例。
您问道,
“我一直在研究PE格式,但是我对CALL ADDRESS指令、从dll中导入函数以及CALL ADDRESS如何访问DLL中的代码之间的关系感到非常困惑。”
CALL ADDRESS这个术语在C++级别上没有太多意义,因此我假设您指的是汇编语言或机器码级别的CALL ADDRESS。
问题在于,当一个DLL被加载到某个地址而不是首选地址时,如何将call
指令与DLL函数连接起来?
call
通过调用一个最小的转发例程来工作,该例程由单个jmp
指令组成。 jmp
指令通过表查找调用DLL函数。 通常,DLL的导入库会导出DLL函数本身,带有__imp__
名称前缀的包装程序以及没有这种名称前缀的包装程序,例如__imp__MessageBoxA@16
和_MessageBoxA@16
。也就是说,除了我下面编造的名称外,汇编器通常将
call MessageBox
翻译为
call MessageBox_forwarder
;
这里可以是任何内容
MessageBox_forwarder: jmp ds:[MessageBox_tableEntry]
当加载DLL时,加载器会将相关地址放置在表中。
在汇编语言级别,使用标识符指定的例程的call
可能会映射到一个转发器的call
或通过表查找直接调用DLL函数的call
,这取决于为标识符声明的类型。
甚至对于从同一DLL导入的内容,DLL函数地址可以有多个表。但通常将它们视为一个大表,然后称为“导入地址表”(IAT)。IAT表(更准确地说是表)每个都在图像中的固定位置,即当代码被加载到不首选的位置时,它们会随着代码一起移动,而不是在固定的地址。
当前选择的解决方案答案在以下方面是不正确的:
该答案认为“这样做行不通,这就是为什么不会这样做。”,其中“它”可能指的是CALL ADDRESS。但在汇编或机器码级别上使用CALL ADDRESS调用DLL函数是完全可行的,只要正确使用。
该答案认为IAT位于固定地址。但实际上并非如此。
让我们考虑一个具体的CALL ADDRESS指令,其中地址是非常著名的DLL函数的地址,即从[user32.dll] DLL调用MessageBoxA
Windows API函数:
call MessageBoxA
.model flat, stdcall
option casemap :none ; Case sensitive identifiers, please.
_as32bit textequ <DWord ptr>
public start
ExitProcess proto stdcall :DWord
MessageBoxA_t typedef proto stdcall :DWord, :DWord, :DWord, :DWord
extern MessageBoxA : MessageBoxA_t
extern _imp__MessageBoxA@16 : ptr MessageBoxA_t
MB_ICONINFORMATION equ 0040h
MB_SETFOREGROUND equ 00010000h
infoBoxOptions equ MB_ICONINFORMATION or MB_SETFOREGROUND
.const
boxtitle_1 db "Just FYI 1 (of 3):", 0
boxtitle_2 db "Just FYI 2 (of 3):", 0
boxtitle_3 db "Just FYI 3 (of 3):", 0
boxtext db "There’s intelligence somewhere in the universe", 0
.code
start:
push infoBoxOptions
push offset boxtitle_1
push offset boxtext
push 0
call MessageBoxA ; Call #1 - to jmp to DLL-func.
push infoBoxOptions
push offset boxtitle_2
push offset boxtext
push 0
call ds:[_imp__MessageBoxA@16] ; Call #2 - directly to DLL-func.
push infoBoxOptions
push offset boxtitle_3
push offset boxtext
push 0
call _imp__MessageBoxA@16 ; Call #3 - same as #2, due to type of identifier.
push 0 ; Exit code, 0 indicates success.
call ExitProcess
end
/debug
链接器选项要求链接器生成一个PDB调试信息文件,以便与Visual Studio调试器一起使用:
[d:\dev\test\call] > ml /nologo /c asm_call.asm 正在汇编: asm_call.asm现在调试的一个简单方法是启动Visual Studio([devenv.exe]程序),然后在Visual Studio中,单击[调试→逐步执行],或者只需按F11即可:
[d:\dev\test\call] > link /nologo asm_call.obj kernel32.lib user32.lib /entry:start /subsystem:windows /debug [d:\dev\test\call] > dir asm* /b asm_call.asm asm_call.exe asm_call.ilk asm_call.obj asm_call.pdb
[d:\dev\test\call] > _
[d:\dev\test\call] > devenv asm_call.exe [d:\dev\test\call] > _
0000004E
十六进制(注意:最低位字节位于最低地址,即内存中首位),而另一个大红箭头则显示了这个非常小的神奇数字不可思议地指定了_MessageBoxA@16
函数,据调试器所知,该函数位于地址01161064h
hex处。
CALL ADDRESS指令中的地址数据是一个偏移量,相对于下一条指令的地址,因此它不需要进行任何DLL放置更改的修补。
调用转到的地址只包含一个jmp ds: [IAT_entry_for_MessageBoxA]
。
这个转发代码来自导入库,而不是DLL,因此它也不需要修补(但显然它确实会得到一些特殊处理,就像DLL函数地址一样)。
以下C++程序报告了它被加载的地址,它从哪些模块导入了什么DLL函数,以及各种IAT表位于何处。
当使用现代版本的Microsoft工具链进行构建时,只使用默认值,它通常在每次运行时都会加载到不同的地址。
您可以通过使用链接器选项/dynamicbase:no
来防止这种行为。
#include <assert.h> // assert
#include <stddef.h> // ptrdiff_t
#include <sstream>
using std::ostringstream;
#undef UNICODE
#define UNICODE
#include <windows.h>
template< class Result, class SomeType >
Result as( SomeType const p ) { return reinterpret_cast<Result>( p ); }
template< class Type >
class OffsetTo
{
private:
ptrdiff_t offset_;
public:
ptrdiff_t asInteger() const { return offset_; }
explicit OffsetTo( ptrdiff_t const offset ): offset_( offset ) {}
};
template< class ResultPointee, class SourcePointee >
ResultPointee* operator+(
SourcePointee* const p,
OffsetTo<ResultPointee> const offset
)
{
return as<ResultPointee*>( as<char const*>( p ) + offset.asInteger() );
}
int main()
{
auto const pImage =
as<IMAGE_DOS_HEADER const*>( ::GetModuleHandle( nullptr ) );
assert( pImage->e_magic == IMAGE_DOS_SIGNATURE );
auto const pNTHeaders =
pImage + OffsetTo<IMAGE_NT_HEADERS const>( pImage->e_lfanew );
assert( pNTHeaders->Signature == IMAGE_NT_SIGNATURE );
auto const& importDir =
pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
auto const pImportDescriptors = pImage + OffsetTo<IMAGE_IMPORT_DESCRIPTOR const>(
importDir.VirtualAddress //+ importSectionHeader.PointerToRawData
);
ostringstream stream;
stream << "I'm loaded at " << pImage << ", and I'm using...\n";
for( int i = 0; pImportDescriptors[i].Name != 0; ++i )
{
auto const pModuleName = pImage + OffsetTo<char const>( pImportDescriptors[i].Name );
DWORD const offsetNameTable = pImportDescriptors[i].OriginalFirstThunk;
DWORD const offsetAddressTable = pImportDescriptors[i].FirstThunk; // The module "IAT"
auto const pNameTable = pImage + OffsetTo<IMAGE_THUNK_DATA const>( offsetNameTable );
auto const pAddressTable = pImage + OffsetTo<IMAGE_THUNK_DATA const>( offsetAddressTable );
stream << "\n* '" << pModuleName << "'";
stream << " with IAT at " << pAddressTable << "\n";
stream << "\t";
for( int j = 0; pNameTable[j].u1.AddressOfData != 0; ++j )
{
auto const pFuncName =
pImage + OffsetTo<char const>( 2 + pNameTable[j].u1.AddressOfData );
stream << pFuncName << " ";
}
stream << "\n";
}
MessageBoxA(
0,
stream.str().c_str(),
"FYI:",
MB_ICONINFORMATION | MB_SETFOREGROUND
);
}
最后,根据我的原始回答,这里有一个我为另一个目的编写的微软汇编器(MASM)程序,它展示了一些问题,因为它的本质是(它产生的输出源代码在汇编和运行后会产生相同的源代码,以此类推),它必须是完全可重定位的代码,并且仅仅需要来自普通程序加载器的最基本帮助:
.model flat, stdcall
option casemap :none ; Case sensitive identifiers, please.
dword_aligned textequ <4> ; Just for readability.
; Windows API functions:
extern ExitProcess@4: proc ; from [kernel32.dll]
extern GetStdHandle@4: proc ; from [kernel32.dll]
extern WriteFile@20: proc ; from [kernel32.dll]
extern wsprintfA: proc ; from [user32.dll]
STD_OUTPUT_HANDLE equ -11
; The main code.
GlobalsStruct struct dword_aligned
codeStart dword ?
outputStreamHandle dword ?
GlobalsStruct ends
globals textequ <(GlobalsStruct ptr [edi])>
.code
startup:
jmp code_start
; Trampolines to add references to these functions.
myExitProcess: jmp ExitProcess@4
myGetStdHandle: jmp GetStdHandle@4
myWriteFile: jmp WriteFile@20
mywsprintfA: jmp wsprintfA
;------------------------------------------------------------------
;
; The code below is reproduced, so it's all relative.
code_start:
jmp main
prologue:
byte ".model flat, stdcall", 13, 10
byte "option casemap :none", 13, 10
byte 13, 10
byte " extern ExitProcess@4: proc", 13, 10
byte " extern GetStdHandle@4: proc", 13, 10
byte " extern WriteFile@20: proc", 13, 10
byte " extern wsprintfA: proc", 13, 10
byte 13, 10
byte " .code", 13, 10
byte "startup:", 13, 10
byte " jmp code_start", 13, 10
byte 13, 10
byte "jmp ExitProcess@4", 13, 10
byte "jmp GetStdHandle@4", 13, 10
byte "jmp WriteFile@20", 13, 10
byte "jmp wsprintfA", 13, 10
byte 13, 10
byte "code_start:", 13, 10
prologue_nBytes equ $ - prologue
epilogue:
byte "code_end:", 13, 10
byte " end startup", 13, 10
epilogue_nBytes equ $ - epilogue
dbDirective byte 4 dup( ' ' ), "byte "
dbDirective_nBytes equ $ - dbDirective
numberFormat byte " 0%02Xh", 0
numberFormat_nBytes equ $ - numberFormat
comma byte ","
windowsNewline byte 13, 10
write:
push 0 ; space for nBytesWritten
mov ecx, esp ; &nBytesWritten
push 0 ; lpOverlapped
push ecx ; &nBytesWritten
push ebx ; nBytes
push eax ; &s[0]
push globals.outputStreamHandle
call myWriteFile
pop eax ; nBytesWritten
ret
displayMachineCode:
dmc_LocalsStruct struct dword_aligned
numberStringLen dword ?
numberString byte 16*4 DUP( ? )
fileHandle dword ?
nBytesWritten dword ?
byteIndex dword ?
dmc_LocalsStruct ends
dmc_locals textequ <[ebp - sizeof dmc_LocalsStruct].dmc_LocalsStruct>
mov ebp, esp
sub esp, sizeof dmc_LocalsStruct
; Output prologue that makes MASM happy (placing machine code data in context):
; lea eax, prologue
mov eax, globals.codeStart
add eax, prologue - code_start
mov ebx, prologue_nBytes
call write
; Output the machine code bytes.
mov dmc_locals.byteIndex, 0
dmc_lineLoop:
; loop start
; Output a db directive
;lea eax, dbDirective
mov eax, globals.codeStart
add eax, dbDirective - code_start
mov ebx, dbDirective_nBytes
call write
dmc_byteIndexingLoop:
; loop start
; Create string representation of a number
mov ecx, dmc_locals.byteIndex
mov eax, 0
;mov al, byte ptr [code_start + ecx]
mov ebx, globals.codeStart
mov al, [ebx + ecx]
push eax
;push offset numberFormat
mov eax, globals.codeStart
add eax, numberFormat - code_start
push eax
lea eax, dmc_locals.numberString
push eax
call mywsprintfA
add esp, 3*(sizeof dword)
mov dmc_locals.numberStringLen, eax
; Output string representation of number
lea eax, dmc_locals.numberString
mov ebx, dmc_locals.numberStringLen
call write
; Are we finished looping yet?
inc dmc_locals.byteIndex
mov ecx, dmc_locals.byteIndex
cmp ecx, code_end - code_start
je dmc_finalNewline
and ecx, 07h
jz dmc_after_byteIndexingLoop
; Output a comma
; lea eax, comma
mov eax, globals.codeStart
add eax, comma - code_start
mov ebx, 1
call write
jmp dmc_byteIndexingLoop
; loop end
dmc_after_byteIndexingLoop:
; New line
; lea eax, windowsNewline
mov eax, globals.codeStart
add eax, windowsNewline - code_start
mov ebx, 2
call write
jmp dmc_lineLoop;
; loop end
dmc_finalNewline:
; New line
; lea eax, windowsNewline
mov eax, globals.codeStart
add eax, windowsNewline - code_start
mov ebx, 2
call write
; Output epilogue that makes MASM happy:
; lea eax, epilogue
mov eax, globals.codeStart
add eax, epilogue - code_start
mov ebx, epilogue_nBytes
call write
mov esp, ebp
ret
main:
sub esp, sizeof GlobalsStruct
mov edi, esp
call main_knownAddress
main_knownAddress:
pop eax
sub eax, main_knownAddress - code_start
mov globals.codeStart, eax
push STD_OUTPUT_HANDLE
call myGetStdHandle
mov globals.outputStreamHandle, eax
call displayMachineCode
; Well behaved process exit:
push 0 ; Process exit code, 0 indicates success.
call myExitProcess
code_end:
end startup
这里是自我复制的输出:
.model flat, stdcall
option casemap :none
extern ExitProcess@4: proc
extern GetStdHandle@4: proc
extern WriteFile@20: proc
extern wsprintfA: proc
.code
startup:
jmp code_start
jmp ExitProcess@4
jmp GetStdHandle@4
jmp WriteFile@20
jmp wsprintfA
code_start:
byte 0E9h, 03Bh, 002h, 000h, 000h, 02Eh, 06Dh, 06Fh
byte 064h, 065h, 06Ch, 020h, 066h, 06Ch, 061h, 074h
byte 02Ch, 020h, 073h, 074h, 064h, 063h, 061h, 06Ch
byte 06Ch, 00Dh, 00Ah, 06Fh, 070h, 074h, 069h, 06Fh
byte 06Eh, 020h, 063h, 061h, 073h, 065h, 06Dh, 061h
byte 070h, 020h, 03Ah, 06Eh, 06Fh, 06Eh, 065h, 00Dh
byte 00Ah, 00Dh, 00Ah, 020h, 020h, 020h, 020h, 065h
byte 078h, 074h, 065h, 072h, 06Eh, 020h, 020h, 045h
byte 078h, 069h, 074h, 050h, 072h, 06Fh, 063h, 065h
byte 073h, 073h, 040h, 034h, 03Ah, 020h, 070h, 072h
byte 06Fh, 063h, 00Dh, 00Ah, 020h, 020h, 020h, 020h
byte 065h, 078h, 074h, 065h, 072h, 06Eh, 020h, 020h
byte 047h, 065h, 074h, 053h, 074h, 064h, 048h, 061h
byte 06Eh, 064h, 06Ch, 065h, 040h, 034h, 03Ah, 020h
byte 070h, 072h, 06Fh, 063h, 00Dh, 00Ah, 020h, 020h
byte 020h, 020h, 065h, 078h, 074h, 065h, 072h, 06Eh
byte 020h, 020h, 057h, 072h, 069h, 074h, 065h, 046h
byte 069h, 06Ch, 065h, 040h, 032h, 030h, 03Ah, 020h
byte 070h, 072h, 06Fh, 063h, 00Dh, 00Ah, 020h, 020h
byte 020h, 020h, 065h, 078h, 074h, 065h, 072h, 06Eh
byte 020h, 020h, 077h, 073h, 070h, 072h, 069h, 06Eh
byte 074h, 066h, 041h, 03Ah, 020h, 070h, 072h, 06Fh
byte 063h, 00Dh, 00Ah, 00Dh, 00Ah, 020h, 020h, 020h
byte 020h, 02Eh, 063h, 06Fh, 064h, 065h, 00Dh, 00Ah
byte 073h, 074h, 061h, 072h, 074h, 075h, 070h, 03Ah
byte 00Dh, 00Ah, 020h, 020h, 020h, 020h, 06Ah, 06Dh
byte 070h, 020h, 020h, 020h, 020h, 020h, 063h, 06Fh
byte 064h, 065h, 05Fh, 073h, 074h, 061h, 072h, 074h
byte 00Dh, 00Ah, 00Dh, 00Ah, 06Ah, 06Dh, 070h, 020h
byte 045h, 078h, 069h, 074h, 050h, 072h, 06Fh, 063h
byte 065h, 073h, 073h, 040h, 034h, 00Dh, 00Ah, 06Ah
byte 06Dh, 070h, 020h, 047h, 065h, 074h, 053h, 074h
byte 064h, 048h, 061h, 06Eh, 064h, 06Ch, 065h, 040h
byte 034h, 00Dh, 00Ah, 06Ah, 06Dh, 070h, 020h, 057h
byte 072h, 069h, 074h, 065h, 046h, 069h, 06Ch, 065h
byte 040h, 032h, 030h, 00Dh, 00Ah, 06Ah, 06Dh, 070h
byte 020h, 077h, 073h, 070h, 072h, 069h, 06Eh, 074h
byte 066h, 041h, 00Dh, 00Ah, 00Dh, 00Ah, 063h, 06Fh
byte 064h, 065h, 05Fh, 073h, 074h, 061h, 072h, 074h
byte 03Ah, 00Dh, 00Ah, 063h, 06Fh, 064h, 065h, 05Fh
byte 065h, 06Eh, 064h, 03Ah, 00Dh, 00Ah, 020h, 020h
byte 020h, 020h, 065h, 06Eh, 064h, 020h, 073h, 074h
byte 061h, 072h, 074h, 075h, 070h, 00Dh, 00Ah, 020h
byte 020h, 020h, 020h, 062h, 079h, 074h, 065h, 020h
byte 020h, 020h, 020h, 020h, 020h, 020h, 020h, 030h
byte 025h, 030h, 032h, 058h, 068h, 000h, 02Ch, 00Dh
byte 00Ah, 06Ah, 000h, 08Bh, 0CCh, 06Ah, 000h, 051h
byte 053h, 050h, 0FFh, 077h, 004h, 0E8h, 074h, 0FEh
byte 0FFh, 0FFh, 058h, 0C3h, 08Bh, 0ECh, 083h, 0ECh
byte 050h, 08Bh, 007h, 005h, 005h, 000h, 000h, 000h
byte 0BBh, 036h, 001h, 000h, 000h, 0E8h, 0D7h, 0FFh
byte 0FFh, 0FFh, 0C7h, 045h, 0FCh, 000h, 000h, 000h
byte 000h, 08Bh, 007h, 005h, 057h, 001h, 000h, 000h
byte 0BBh, 00Fh, 000h, 000h, 000h, 0E8h, 0BFh, 0FFh
byte 0FFh, 0FFh, 08Bh, 04Dh, 0FCh, 0B8h, 000h, 000h
byte 000h, 000h, 08Bh, 01Fh, 08Ah, 004h, 019h, 050h
byte 08Bh, 007h, 005h, 066h, 001h, 000h, 000h, 050h
byte 08Dh, 045h, 0B4h, 050h, 0E8h, 02Ah, 0FEh, 0FFh
byte 0FFh, 083h, 0C4h, 00Ch, 089h, 045h, 0B0h, 08Dh
byte 045h, 0B4h, 08Bh, 05Dh, 0B0h, 0E8h, 08Fh, 0FFh
byte 0FFh, 0FFh, 0FFh, 045h, 0FCh, 08Bh, 04Dh, 0FCh
byte 081h, 0F9h, 068h, 002h, 000h, 000h, 074h, 02Bh
byte 083h, 0E1h, 007h, 074h, 013h, 08Bh, 007h, 005h
byte 06Eh, 001h, 000h, 000h, 0BBh, 001h, 000h, 000h
byte 000h, 0E8h, 06Bh, 0FFh, 0FFh, 0FFh, 0EBh, 0AAh
byte 08Bh, 007h, 005h, 06Fh, 001h, 000h, 000h, 0BBh
byte 002h, 000h, 000h, 000h, 0E8h, 058h, 0FFh, 0FFh
byte 0FFh, 0EBh, 086h, 08Bh, 007h, 005h, 06Fh, 001h
byte 000h, 000h, 0BBh, 002h, 000h, 000h, 000h, 0E8h
byte 045h, 0FFh, 0FFh, 0FFh, 08Bh, 007h, 005h, 03Bh
byte 001h, 000h, 000h, 0BBh, 01Ch, 000h, 000h, 000h
byte 0E8h, 034h, 0FFh, 0FFh, 0FFh, 08Bh, 0E5h, 0C3h
byte 083h, 0ECh, 008h, 08Bh, 0FCh, 0E8h, 000h, 000h
byte 000h, 000h, 058h, 02Dh, 04Ah, 002h, 000h, 000h
byte 089h, 007h, 06Ah, 0F5h, 0E8h, 098h, 0FDh, 0FFh
byte 0FFh, 089h, 047h, 004h, 0E8h, 023h, 0FFh, 0FFh
byte 0FFh, 06Ah, 000h, 0E8h, 084h, 0FDh, 0FFh, 0FFh
code_end:
end startup