我很想清楚地解释一下,在 Windows 环境(PE 可执行文件)中,CALL XXXXXXXXXXXXXXX 指令是如何工作的。我一直在研究 PE 格式,但我对 CALL ADDRESS 指令、从 dll 中导入函数以及 CALL ADDRESS 如何访问 DLL 中的代码之间的关系感到非常困惑。除了 ASLR 和其他安全功能可能会在 DLL 中移动,可执行文件如何应对呢?
3 回答
它(即直接通过正常的相对调用调用导入)不起作用,这就是为什么它不是这样做的原因。
要调用导入的函数,您需要通过称为导入地址表 (IAT) 的东西。简而言之,IAT 中的条目首先指向函数名称(即,它从导入名称表的副本开始),然后这些指针被加载程序更改为指向实际函数。
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
具有指定地址的 a 通过调用由单个jmp
指令组成的最小转发例程来工作。该jmp
指令通过查表调用 DLL 函数。通常,DLL 的导入库会导出带有名称前缀的 DLL 函数本身和__imp__
不带名称前缀的包装例程,例如__imp__MessageBoxA@16
和_MessageBoxA@16
。
即,除了我发明了以下名称外,汇编程序通常会翻译
call MessageBox
进入
call MessageBox_forwarder
;
不管在这里
MessageBox_forwarder: jmp ds:[MessageBox_tableEntry]
加载 DLL 时,加载程序将相关地址放入表中。
在汇编语言级别
call
,将例程指定为标识符的 a 可以映射到 acall
到转发器,或call
通过表查找直接映射到 DLL 函数,具体取决于为标识符声明的类型。可以有多个 DLL 函数地址表,即使是从同一个 DLL 导入。但总的来说,它们被认为是一张大表,然后称为“导入地址表” ,或简称IAT。IAT 表(或更准确地说是表)每个都位于图像中的固定位置,即当代码加载到不喜欢的地方而不是固定地址时,它们会随代码一起移动。
当前选择的解决方案答案在以下方面不正确:
答案是“它不起作用,这就是为什么它不是这样做的。”,其中“它”可能是指一个呼叫地址。但是在汇编或机器代码级别使用 CALL ADDRESS 可以很好地调用 DLL 函数。前提是正确完成。
答案认为 IAT 位于固定地址。但事实并非如此。
呼叫地址工作得很好。
让我们考虑一个具体的CALL ADDRESS 指令,其中地址是一个众所周知的 DLL 函数,即MessageBoxA
从 [user32.dll] DLL 调用 Windows API 函数:
call MessageBoxA
使用这个指令没有问题。
正如您将在下面看到的,在机器代码级别,该call
指令本身仅包含一个偏移量,该偏移量导致调用执行一条jmp
指令,该指令在函数指针的导入地址表中查找 DLL 例程地址,该表通常由加载程序在加载有问题的 DLL 时。
为了能够检查机器代码,这里有一个使用该具体示例指令的完整 32 位 x86 汇编语言程序:
.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
使用 Microsoft 的工具链进行组装和链接,其中/debug
链接器选项要求链接器生成 PDB 调试信息文件以与 Visual Studio 调试器一起使用:
[d:\dev\test\call] > ml /nologo /c asm_call.asm 组装:asm_call.asm [d:\dev\test\call] >链接 /nologo asm_call.obj kernel32.lib user32.lib /entry:start /subsystem:windows /debug [d:\dev\test\call] >目录 asm* /b asm_call.asm asm_call.exe asm_call.ilk asm_call.obj asm_call.pdb [d:\dev\test\call] > _
现在,一种简单的调试方法是启动 Visual Studio([devenv.exe] 程序)并在 Visual Studio 中单击 [调试→步入],或者直接按 F11:
[d:\dev\test\call] > devenv asm_call.exe [d:\dev\test\call] > _
在上图中,显示了运行中的 Visual Studio 2012 调试器,最左边的大红色箭头显示了机器代码指令中的地址信息,即0000004E
十六进制(注意:最低有效字节位于最低地址,在内存中首先),以及另一个大红色箭头向您显示,尽管看起来令人难以置信,但这个相当小的幻数以某种方式指定了_MessageBoxA@16
就调试器所知,驻留在地址01161064h
十六进制的函数。
CALL ADDRESS 指令中的地址数据是一个偏移量,它是相对于下一条指令的地址的,因此它不需要对更改的 DLL 位置进行任何修复。
调用的地址只包含一个
jmp ds:[IAT_entry_for_MessageBoxA]
.此转发器代码来自导入库,而不是来自 DLL,因此它也不需要修复(但显然它确实得到了一些特殊处理,DLL 函数地址也是如此)。
第二条调用指令直接jmp
执行第一条的操作,即在 IAT 表中查找 DLL 函数地址。
现在可以看到第三条调用指令在机器代码级别与第二条相同。declspec( dllimport )
显然,如何在汇编中模拟 Visual C++ 并不为人所知。上述声明是一种方式,可能与文本 equ 结合使用。
IAT 不在固定地址。
下面的 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
);
}
一个自我复制的 Windows 机器代码程序。
最后,根据我的原始答案,这是我为另一个目的制作的 Microsoft 汇编程序 (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
链接器。当您的可执行文件被链接时,链接器将替换所有 DLL 地址并将其作为基础。由于虚拟内存,所有进程都加载到相同的基地址,从而使寻址更容易。因为 DLL 是 PIL(位置无关代码),加载程序可以为应用程序重新设置 DLL。因为代码引用了liker 可以重定位的符号,所以它永远不需要关心它的位置。
编辑:刚刚意识到其中一些是关闭的——Linux 动态库是 PIL,Windows 不是(这就是我们必须重新设置基准的原因)。