これで彼はHelloWorldをコンパイルできるようになりましたが、この記事では、コンパイラの解析や内部構造ではなく、exeファイルのバイトごとのアセンブリなどの重要な部分について説明します。
開始
スポイラーが欲しいですか?プログラムは2048バイトになります。
通常、exeファイルの操作は、それらの構造を調査または変更することです。実行可能ファイル自体はコンパイラによって形成され、このプロセスは開発者にとって少し魔法のように見えます。
しかし今、私たちはそれを修正しようとします!
プログラムをビルドするには、HEXエディターが必要です(私は個人的にHxDを使用しました)。
まず、疑似コードを見てみましょう。
ソース
func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']
func main()
{
MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)
}
最初の2行は、WinAPIライブラリからインポートされた関数を示しています。MessageBoxA関数は、テキストを含むダイアログボックスを表示し、ExitProcessはプログラムの終了についてシステムに通知します。
main関数は上記の関数を使用するため、個別に検討する意味はありません。
DOSヘッダー
まず、正しいDOSヘッダーを生成する必要があります。これはDOSプログラムのヘッダーであり、Windowsでのexeの起動に影響を与えることはありません。
私は多かれ少なかれ重要なフィールドに気づきました、残りはゼロで埋められます。
IMAGE_DOS_HEADER構造
Struct IMAGE_DOS_HEADER
{
u16 e_magic // 0x5A4D "MZ"
u16 e_cblp // 0x0080 128
u16 e_cp // 0x0001 1
u16 e_crlc
u16 e_cparhdr // 0x0004 4
u16 e_minalloc // 0x0010 16
u16 e_maxalloc // 0xFFFF 65535
u16 e_ss
u16 e_sp // 0x0140 320
u16 e_csum
u16 e_ip
u16 e_cs
u16 e_lfarlc // 0x0040 64
u16 e_ovno
u16[4] e_res
u16 e_oemid
u16 e_oeminfo
u16[10] e_res2
u32 e_lfanew // 0x0080 128
}
最も重要なことに、このヘッダーには、これが実行可能ファイルであることを意味するe_magicフィールドと、ファイルの先頭からのPEヘッダーのオフセットを示すe_lfanewが含まれます(このファイルでは、このオフセットは0x80 = 128バイトです)。
DOSヘッダー構造がわかったので、それをファイルに書き込みましょう。
(1)RAW DOSヘッダー(オフセット0x00000000)
4D 5A 80 00 01 00 00 00 04 00 10 00 FF FF 00 00
40 01 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 00
完了し、最初の64バイトが書き込まれます。さらに64を追加する必要があります。これは、いわゆるDOSスタブ(スタブ)です。DOSで起動する場合、プログラムがこのモードで実行するように設計されていないことをユーザーに通知する必要があります。
, , .
, (Offset) .
, 0x00000000, 64 (0x40 16- ), 0x00000040 ..
しかし、一般的に、これは行を出力してプログラムを終了する小さなDOSプログラムです。
スタブをファイルに書き込んで、さらに詳しく検討してみましょう。
(2)RAW DOSスタブ(オフセット0x00000040)
0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
6D 6F 64 65 2E 0D 0A 24 00 00 00 00 00 00 00 00
そして今は同じコードですが、分解された形式です
AsmDOSスタブ
0000 push cs ; Code Segment(CS) ( )
0001 pop ds ; Data Segment(DS) = CS
0002 mov dx, 0x0E ; DS+DX, $( )
0005 mov ah, 0x09 ; ( )
0007 int 0x21 ; 0x21
0009 mov ax, 0x4C01 ; 0x4C ( )
; 0x01 ()
000c int 0x21 ; 0x21
000e "This program cannot be run in DOS mode.\x0D\x0A$" ;
これは次のように機能します。最初に、スタブはプログラムを開始できないことを示す行を出力し、次にコード1でプログラムを終了します。これは通常の終了(コード0)とは異なります。
スタブコードは(コンパイラごとに)わずかに異なる場合があります。gccとdelphiを比較しましたが、一般的な意味は同じです。
スタブラインが\ x0D \ x0D \ x0A $で終わるのも面白いです。この動作の理由として最も可能性が高いのは、c ++がデフォルトでファイルをテキストモードで開くためです。その結果、文字\ x0Aはシーケンス\ x0D \ x0Aに置き換えられます。その結果、3バイトが得られます。2バイトのキャリッジリターン(0x0D)は無意味であり、1はラインフィード(0x0A)です。バイナリモード(std :: ios :: binary)では、この置換は発生しません。
値の書き込みの正確さを確認するために、ImpExプラグインでFarを使用します。
NTヘッダー
128(0x80)バイトの後、NTヘッダー(IMAGE_NT_HEADERS64)に到達しました。これには、PEヘッダー(IMAGE_OPTIONAL_HEADER64)も含まれています。名前にもかかわらず、IMAGE_OPTIONAL_HEADER64が必要ですが、x64アーキテクチャとx86アーキテクチャでは異なります。
IMAGE_NT_HEADERS64構造
Struct IMAGE_NT_HEADERS64
{
u32 Signature // 0x4550 "PE"
Struct IMAGE_FILE_HEADER
{
u16 Machine // 0x8664 x86-64
u16 NumberOfSections // 0x03
u32 TimeDateStamp //
u32 PointerToSymbolTable
u32 NumberOfSymbols
u16 SizeOfOptionalHeader // IMAGE_OPTIONAL_HEADER64 ()
u16 Characteristics // 0x2F
}
Struct IMAGE_OPTIONAL_HEADER64
{
u16 Magic // 0x020B PE64
u8 MajorLinkerVersion
u8 MinorLinkerVersion
u32 SizeOfCode
u32 SizeOfInitializedData
u32 SizeOfUninitializedData
u32 AddressOfEntryPoint // 0x1000
u32 BaseOfCode // 0x1000
u64 ImageBase // 0x400000
u32 SectionAlignment // 0x1000 (4096 )
u32 FileAlignment // 0x200
u16 MajorOperatingSystemVersion // 0x05 Windows XP
u16 MinorOperatingSystemVersion // 0x02 Windows XP
u16 MajorImageVersion
u16 MinorImageVersion
u16 MajorSubsystemVersion // 0x05 Windows XP
u16 MinorSubsystemVersion // 0x02 Windows XP
u32 Win32VersionValue
u32 SizeOfImage // 0x4000
u32 SizeOfHeaders // 0x200 (512 )
u32 CheckSum
u16 Subsystem // 0x02 (GUI) 0x03 (Console)
u16 DllCharacteristics
u64 SizeOfStackReserve // 0x100000
u64 SizeOfStackCommit // 0x1000
u64 SizeOfHeapReserve // 0x100000
u64 SizeOfHeapCommit // 0x1000
u32 LoaderFlags
u32 NumberOfRvaAndSizes // 0x16
Struct IMAGE_DATA_DIRECTORY [16]
{
u32 VirtualAddress
u32 Size
}
}
}
この構造に何が格納されているかを見てみましょう。
説明IMAGE_NT_HEADERS64
Signature — PE
IMAGE_FILE_HEADER x86 x64.
Machine — x64
NumberOfSections — ( )
TimeDateStamp —
SizeOfOptionalHeader — IMAGE_OPTIONAL_HEADER64, IMAGE_OPTIONAL_HEADER32.
Characteristics — , , (EXECUTABLE_IMAGE) 2 RAM (LARGE_ADDRESS_AWARE), ( ) (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).
SizeOfCode — ( .text)
SizeOfInitializedData — ( .rodata)
SizeOfUninitializedData — ( .bss)
BaseOfCode —
SectionAlignment —
FileAlignment —
SizeOfImage —
SizeOfHeaders — (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) FileAlignment
Subsystem — GUI Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — exe, . 5.2 Windows XP (x64).
SizeOfStackReserve — . 1 , 1. Rust , C++ .
SizeOfStackCommit — 4 . .
SizeOfHeapReserve — . 1 .
SizeOfHeapCommit — 4 . SizeOfStackCommit, .
IMAGE_DATA_DIRECTORY — . , , 16 . .
, , . :
Export(0) — . DLL. .
Import(1) — DLL. VirtualAddress = 0x3000 Size = 0xB8. , .
Resource(2) — (, , ..)
.
IMAGE_FILE_HEADER x86 x64.
Machine — x64
NumberOfSections — ( )
TimeDateStamp —
SizeOfOptionalHeader — IMAGE_OPTIONAL_HEADER64, IMAGE_OPTIONAL_HEADER32.
Characteristics — , , (EXECUTABLE_IMAGE) 2 RAM (LARGE_ADDRESS_AWARE), ( ) (RELOCS_STRIPPED | LINE_NUMS_STRIPPED | LOCAL_SYMS_STRIPPED).
SizeOfCode — ( .text)
SizeOfInitializedData — ( .rodata)
SizeOfUninitializedData — ( .bss)
BaseOfCode —
SectionAlignment —
FileAlignment —
SizeOfImage —
SizeOfHeaders — (IMAGE_DOS_HEADER, DOS Stub, IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER[IMAGE_FILE_HEADER.NumberOfSections]) FileAlignment
Subsystem — GUI Console
MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorSubsystemVersion, MinorSubsystemVersion — exe, . 5.2 Windows XP (x64).
SizeOfStackReserve — . 1 , 1. Rust , C++ .
SizeOfStackCommit — 4 . .
SizeOfHeapReserve — . 1 .
SizeOfHeapCommit — 4 . SizeOfStackCommit, .
IMAGE_DATA_DIRECTORY — . , , 16 . .
, , . :
Export(0) — . DLL. .
Import(1) — DLL. VirtualAddress = 0x3000 Size = 0xB8. , .
Resource(2) — (, , ..)
.
NTヘッダーが何で構成されているかを確認したので、他のヘッダーと同様に0x80でファイルに書き込みます。
(3)RAW NT-ヘッダー(オフセット0x00000080)
50 45 00 00 64 86 03 00 F4 70 E8 5E 00 00 00 00
00 00 00 00 F0 00 2F 00 0B 02 00 00 3D 00 00 00
13 00 00 00 00 00 00 00 00 10 00 00 00 10 00 00
00 00 40 00 00 00 00 00 00 10 00 00 00 02 00 00
05 00 02 00 00 00 00 00 05 00 02 00 00 00 00 00
00 40 00 00 00 02 00 00 00 00 00 00 02 00 00 00
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00
00 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00
00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00
00 30 00 00 B8 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
その結果、この種のIMAGE_FILE_HEADER、IMAGE_OPTIONAL_HEADER64、およびIMAGE_DATA_DIRECTORYヘッダーが取得されます。
次に、IMAGE_SECTION_HEADER構造に従って、アプリケーションのすべてのセクションについて説明します。
IMAGE_SECTION_HEADER構造
Struct IMAGE_SECTION_HEADER
{
i8[8] Name
u32 VirtualSize
u32 VirtualAddress
u32 SizeOfRawData
u32 PointerToRawData
u32 PointerToRelocations
u32 PointerToLinenumbers
u16 NumberOfRelocations
u16 NumberOfLinenumbers
u32 Characteristics
}
IMAGE_SECTION_HEADERの説明
Name — 8 ,
VirtualSize —
VirtualAddress — SectionAlignment
SizeOfRawData — FileAlignment
PointerToRawData — FileAlignment
Characteristics — (, , , , .)
VirtualSize —
VirtualAddress — SectionAlignment
SizeOfRawData — FileAlignment
PointerToRawData — FileAlignment
Characteristics — (, , , , .)
この場合、3つのセクションがあります。
仮想アドレス(VA)が最初からではなく1000から始まる理由はわかりませんが、私が検討したすべてのコンパイラがこれを行っています。その結果、SizeOfImageに書き込んだ1000 +3セクション* 1000(SectionAlignment)= 4000になります。これは、仮想メモリ内のプログラムの合計サイズです。おそらく、メモリ内のプログラムにスペースを割り当てるために使用されます。
Name | RAW Addr | RAW Size | VA | VA Size | Attr
--------+---------------+---------------+-------+---------+--------
.text | 200 | 200 | 1000 | 3D | CER
.rdata | 400 | 200 | 2000 | 13 | I R
.idata | 600 | 200 | 3000 | B8 | I R
属性のデコード:
I-初期化されたデータ、初期化されたデータ
U-初期化されていないデータ、初期化されていないデータ
C-コード、実行可能なコードを含む
E-実行、実行を許可
R-コードの読み取り、
W-書き込みセクションからのデータの読み取り、セクションへのデータの書き込みを許可
.text(.code)-実行可能コード(プログラム自体)、CE属性を
格納します。.rdata(.rodata)-定数、文字列などの読み取り専用データを格納します。IR属性
.data-格納します。静的変数やグローバル変数など、読み取りと書き込みが可能なデータ。 IRW属性
.bss-静的変数やグローバル変数などの初期化されていないデータを格納します。さらに、このセクションは通常、RAWサイズがゼロでVAサイズがゼロ以外であるため、ファイル内のスペースを占有しません。
URW.idata属性-他のライブラリからインポートされた関数を含むセクション。IR属性
重要なポイントは、セクションが互いに続く必要があるということです。さらに、ファイルとメモリの両方にあります。少なくとも順序を任意に変更すると、プログラムの実行が停止しました。
プログラムに含まれるセクションがわかったので、それらをファイルに書き込みます。ここでオフセットは8で終了し、記録はファイルの中央から開始されます。
(4)RAWセクション(オフセット0x00000188)
2E 74 65 78 74 00 00 00
3D 00 00 00 00 10 00 00 00 02 00 00 00 02 00 00
00 00 00 00 00 00 00 00 00 00 00 00 20 00 00 60
2E 72 64 61 74 61 00 00 13 00 00 00 00 20 00 00
00 02 00 00 00 04 00 00 00 00 00 00 00 00 00 00
00 00 00 00 40 00 00 40 2E 69 64 61 74 61 00 00
B8 00 00 00 00 30 00 00 00 02 00 00 00 06 00 00
00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 40
次のエントリアドレスは、PE-HeaderのSizeOfHeadersフィールドに対応する00000200になります。セクションをもう1つ追加し、これがプラス40バイトの場合、ヘッダーは512(0x200)バイトに収まらず、FileAlignmentで整列された512 + 40 = 552バイト、つまり1024(0x400)バイトを使用する必要があります。そして、0x228(552)からアドレス0x400まで残っているものはすべて、何かで埋める必要があります。もちろん、ゼロで埋める方がよいでしょう。
Farでセクションのブロックがどのように見えるかを見てみましょう。
次に、セクション自体をファイルに書き込みますが、微妙な違いが1つあります。
SizeOfHeadersの例からわかるように、ヘッダーを書き込んで次のセクションに進むことはできません。見出しを記録するには、すべての見出しが一緒にかかる時間を知る必要があるためです。そのため、必要なスペースを事前に計算するか、空の(ゼロ)値を書き込んでから、すべてのヘッダーを書き込んだ後、実際のサイズを返して書き留める必要があります。
したがって、プログラムはいくつかのパスでコンパイルされます。たとえば、.rdataセクションは.textセクションの後にありますが、.textセクションが0x1000(SectionAlignment)バイトを超えて大きくなると、範囲のアドレス0x2000を占めるため、.rdata内の変数の仮想アドレスを見つけることができません。したがって、.rdataセクションは0x2000ではなく、0x3000に配置されます。そして、戻って、.rdataの前にある.textセクションのすべての変数のアドレスを再計算する必要があります。
しかし、この場合、私はすでにすべてを計算しているので、すぐにコードのブロックを書き留めます。
。テキストセクション
Asmセグメント.text
0000 push rbp
0001 mov rbp, rsp
0004 sub rsp, 0x20
0008 mov rcx, 0x0
000F mov rdx, 0x402000
0016 mov r8, 0x40200D
001D mov r9, 0x40
0024 call QWORD PTR [rip + 0x203E]
002A mov rcx, 0x0
0031 call QWORD PTR [rip + 0x2061]
0037 add rsp, 0x20
003B pop rbp
003C ret
特にこのプログラムでは、最後の3行とまったく同じように、最初の3行は必要ありません。
プログラムは2番目の呼び出し関数で終了するため、最後の3つは実行されません。
しかし、これがメイン関数ではなくサブ関数である場合は、この方法で実行する必要があるとしましょう。
ただし、この場合の最初の3つは必須ではありませんが、望ましいものです。たとえば、MessageBoxAではなくprintfを使用した場合、これらの行がないとエラーが発生します。
64ビットMSDNシステムの呼び出し規則に従って、最初の4つのパラメーターはレジスタRCX、RDX、R8、R9に渡されます。それらがそこに収まり、たとえば浮動小数点数ではない場合。そして残りはスタックを通過します。
理論的には、関数に2つの引数を渡す場合、それらをレジスタに渡し、スタック内の2つの場所を予約して、必要に応じて関数がレジスタをスタックにプッシュできるようにする必要があります。また、これらのレジスタが元の状態で返されることを期待するべきではありません。
したがって、printf関数の問題は、引数を1つだけ渡すと、スタック内の4つの場所すべてが上書きされることですが、引数の数だけ1つだけ上書きする必要があるようです。
したがって、プログラムがおかしな動作をしたくない場合は、関数に少なくとも1つの引数を渡すと、常に少なくとも8バイト* 4引数= 32(0x20)バイトを予約します。
関数呼び出しを含むコードのブロックを検討してください
MessageBoxA(0, 'Hello World!', 'MyApp', 64)
ExitProcess(0)
まず、引数を渡します
。rcx= 0 rdx =
メモリ内の文字列の絶対アドレスImageBase + Sections ["。Rdata"]。VirtualAddress +セクションの先頭からの文字列のオフセット、文字列はバイト0に読み取られます
r8 =前の
r9 = 64(0x40)と同様MB_ICONINFORMATION 、情報アイコン
次に、MessageBoxA関数の呼び出しがありますが、これではすべてがそれほど単純ではありません。重要なのは、コンパイラが可能な限り短いコマンドを使用しようとすることです。命令サイズが小さいほど、そのような命令はそれぞれプロセッサのキャッシュに収まり、キャッシュミス、過負荷が少なくなり、プログラムの速度が速くなります。コマンドとプロセッサの内部動作の詳細については、Intel64およびIA-32アーキテクチャソフトウェア開発者マニュアルを参照してください。
フルアドレスで関数を呼び出すこともできますが、少なくとも(1つのopcode +8アドレス= 9バイト)かかり、相対アドレスを使用すると、callコマンドは6バイトしかかかりません。
この魔法を詳しく見てみましょう。rip+ 0x203Eは、オフセットで指定されたアドレスでの関数呼び出しにすぎません。
少し先を見て、必要なオフセットのアドレスを見つけました。 MessageBoxAの場合は0x3068で、ExitProcessの場合は0x3098です。
魔法を科学に変える時が来ました。 opcodeがプロセッサにヒットするたびに、その長さを計算し、現在の命令アドレス(RIP)に追加します。したがって、命令内でRIPを使用する場合、このアドレスは現在の命令の終了/次の命令の開始を示します。
最初の呼び出しの場合、オフセットは呼び出しコマンドの終了を示します。これは002Aです。メモリ内でこのアドレスはオフセットセクション["。Text"]にあることを忘れないでください。VirtualAddress、つまり 0x1000。したがって、コールのRIPは102Aになります。MessageBoxAに必要なアドレスは0x3068です。0x3068-0x102A = 0x203Eを検討してください。2番目のアドレスの場合、すべてが0x1000 + 0x0037 = 0x1037、0x3098-0x1037 = 0x2061と同じです。
アセンブラコマンドで見たのはこれらのオフセットです。
0024 call QWORD PTR [rip + 0x203E]
002A mov rcx, 0x0
0031 call QWORD PTR [rip + 0x2061]
0037 add rsp, 0x20
アドレス0x400にゼロを追加して、ファイルに.textセクションを書き込みましょう。
(5)RAW .textセクション(オフセット0x00000200-0x00000400)
55 48 89 E5 48 83 EC 20 48 C7 C1 00 00 00 00 48
C7 C2 00 20 40 00 49 C7 C0 0D 20 40 00 49 C7 C1
40 00 00 00 FF 15 3E 20 00 00 48 C7 C1 00 00 00
00 FF 15 61 20 00 00 48 83 C4 20 5D C3 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
4 . FileAlignment. 0x000003F0, 0x00000400, . 1024 , ! .
.Rdataセクション
これはおそらく最も単純なセクションです。ここに2行入れて、512バイトにゼロを追加します。
.rdata
0400 "Hello World!\0"
040D "MyApp\0"
(6)RAW .rdataセクション(オフセット0x00000400-0x00000600)
48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 4D 79 41
70 70 00 00 00 00 00 00 00 00 00 00 00 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
.Idataセクション
さて、これが最後のセクションで、ライブラリからインポートされた関数について説明しています。
私たちを待っている最初のものは、新しい構造IMAGE_IMPORT_DESCRIPTORです
IMAGE_IMPORT_DESCRIPTOR構造
Struct IMAGE_IMPORT_DESCRIPTOR
{
u32 OriginalFirstThunk (INT)
u32 TimeDateStamp
u32 ForwarderChain
u32 Name
u32 FirstThunk (IAT)
}
説明IMAGE_IMPORT_DESCRIPTOR
OriginalFirstThunk — , Import Name Table (INT)
Name — ,
FirstThunk — , Import Address Table (IAT)
Name — ,
FirstThunk — , Import Address Table (IAT)
まず、2つのインポートされたライブラリを追加する必要があります。想起:
func MessageBoxA(u32 handle, PChar text, PChar caption, u32 type) i32 ['user32.dll']
func ExitProcess(u32 code) ['kernel32.dll']
(7)RAW IMAGE_IMPORT_DESCRIPTOR(オフセット0x00000600)
58 30 00 00 00 00 00 00 00 00 00 00 3C 30 00 00
68 30 00 00 88 30 00 00 00 00 00 00 00 00 00 00
48 30 00 00 98 30 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
2つのライブラリを使用し、それらのリストを終了したと言います。最後の構造はゼロで埋められます。
INT | Time | Forward | Name | IAT
--------+--------+----------+--------+--------
0x3058 | 0x0 | 0x0 | 0x303C | 0x3068
0x3088 | 0x0 | 0x0 | 0x3048 | 0x3098
0x0000 | 0x0 | 0x0 | 0x0000 | 0x0000
次に、ライブラリ自体の名前を追加しましょう。
ライブラリ名
063 "user32.dll\0"
0648 "kernel32.dll\0"
(8)RAWライブラリ名(オフセット0x0000063C)
75 73 65 72
33 32 2E 64 6C 6C 00 00 6B 65 72 6E 65 6C 33 32
2E 64 6C 6C 00 00 00 00
次に、user32ライブラリについて説明します。
(9)RAW user32.dll(オフセット0x00000658)
78 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 78 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 4D 65 73 73 61 67
65 42 6F 78 41 00 00 00
最初のライブラリの[名前]フィールドは、少し高く見ると0x303Cを指しています。アドレス0x063Cに、ライブラリ "user32.dll \ 0"があることがわかります。
ヒント、.idataセクションはファイルオフセット0x0600およびメモリオフセット0x3000に対応することに注意してください。最初のライブラリの場合、INTは3058です。これは、ファイル内でオフセット0x0658になることを意味します。このアドレスには、エントリ0x3078と2番目のゼロがあります。リストの終わりを意味します。 3078は0x0678を参照します。これはRAW文字列です
"00 00 4D 65 73 73 61 67 65 42 6F 78 41 00 00 00"
最初の2バイトは重要ではなく、ゼロに等しくなります。そして、関数の名前がゼロで終わる行があります。つまり、「\ 0 \ 0MessageBoxA \ 0」として表すことができます。
この場合、IATはIATテーブルと同様の構造を参照しますが、プログラムの起動時に関数アドレスのみがロードされます。たとえば、メモリ内の最初のエントリ0x3068は、ファイル内で0x0668以外の値になります。プログラムコードの呼び出し呼び出しを通じて参照するシステムによってロードされるMessageBoxA関数のアドレスがあります。
そしてパズルの最後のピース、kernel32。また、SectionAlignmentにゼロを追加することを忘れないでください。
(10)RAW kernel32.dll(オフセット0x00000688-0x00000800)
A8 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 A8 30 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 45 78 69 74 50 72
6F 63 65 73 73 00 00 00 00 00 00 00 00 00 00 00
........
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Farがインポートした関数を正しく識別できたことを確認し
ます。すべてが順調だったので、ファイルを実行する準備が整いました。
ドラムロール…
最終
おめでとうございます、やりました!
ファイルは2KB =ヘッダー512バイト+512バイトの3セクションを占有します。
数値512(0x200)は、プログラムのヘッダーで指定したFileAlignmentにすぎません。
さらに:
もう少し深く行きたい場合は、「HelloWorld!」という碑文を置き換えることができます。プログラムコード(セクション.text)の行のアドレスを変更することを忘れないでください。メモリ内のアドレスは0x00402000ですが、ファイルには逆バイト順00 20 40 00が含まれます。
または、クエストはもう少し複雑です。コードに別のMessageBox呼び出しを追加します。これを行うには、前の呼び出しをコピーして、その中の相対アドレス(0x3068-RIP)を再計算する必要があります。
結論
記事はかなりしわくちゃになっていることが判明しました。もちろん、見出し、プログラム、インポートテーブルの3つの部分で構成されます。
誰かが彼らのexeをコンパイルした場合、私の仕事は無駄ではありませんでした。
すぐに同様の方法でELFファイルを作成することを考えていますが、そのような記事は興味深いでしょうか?)
リンク:
- Intel64およびIA-32アーキテクチャソフトウェア開発者マニュアル
コマンドおよびプロセッサアーキテクチャガイド。
- PE(Portable Executable):StrangerTides
についてexeファイルの構造に関する優れた記事。 - Microsoft Documentation Repository
ここでは、ヘッダー、構造、タイプ、およびそれらの説明に関する情報を見つけることができます。