Bookmarks for staticanalysis
Invalid filename.
73 bookmarks —
73 scraped,
73 summarized,
0 pending
| URL | View | |
|---|---|---|
|
Malware development part 4 - anti static analysis tricks – 0xPat blog – Red/purple teamer
https://0xpat.github.io/Malware_development_part_4/
Introduction
This is the fourth post of a series which regards the development of malicious software. In this series we will explore and try to implement multiple techniques used by malicious applications to execute code, hide from defenses and persist.
In the previous part of the series we discussed methods for detecting sandboxes, virtual machines, automated analysis and making manual debugging harder for an analyst.
In this post we will talk more about compiling and linking the code with Visual Studio. Then we will focus on static analysis and obfuscation.
|
||
|
Malware development part 6 - advanced obfuscation with LLVM and template metaprogramming – 0xPat blog – Red/purple teamer
https://0xpat.github.io/Malware_development_part_6/
Introduction
This is the sixth post of a series which regards the development of malicious software. In this series we will explore and try to implement multiple techniques used by malicious applications to execute code, hide from defenses and persist.
Today we will explore anti-disassembly obfuscation using LLVM and template metaprogramming.
|
||
|
A dive into the PE file format - PE file structure - Part 1: Overview - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe2/
A dive into the PE file format - PE file structure - Part 1: Overview Introduction The aim of this post is to provide a basic introduction to the PE file structure without talking about any details. PE files PE stands for Portable Executable, it’s a file format for executables used in Windows operating systems, it’s based on the COFF file format (Common Object File Format). Not only .exe files are PE files, dynamic link libraries (.dll), Kernel modules (.srv), Control panel applications (.cpl) and many others are also PE files. A PE file is a data structure that holds information necessary for the OS loader to be able to load that executable into memory and execute it. Structure Overview A typical PE file follows the structure outlined in the following figure: If we open an executable file with PE-bear we’ll see the same thing: DOS Header Every PE file starts with a 64-bytes-long structure called the DOS header, it’s what makes the PE file an MS-DOS executable. DOS Stub After the DOS header comes the DOS stub which is a small MS-DOS 2.0 compatible executable that just prints an error message saying “This program cannot be run in DOS mode” when the program is run in DOS mode. NT Headers The NT Headers part contains three main parts: PE signature: A 4-byte signature that identifies the file as a PE file. File Header: A standard COFF File Header. It holds some information about the PE file. Optional Header: The most important header of the NT Headers, its name is the Optional Header because some files like object files don’t have it, however it’s required for image files (files like .exe files). This header provides important information to the OS loader. Section Table The section table follows the Optional Header immediately, it is an array of Image Section Headers, there’s a section header for every section in the PE file. Each header contains information about the section it refers to. Sections Sections are where the actual contents of the file are stored, these include things like data and resources that the program uses, and also the actual code of the program, there are several sections each one with its own purpose. Conclusion In this post we looked at a very basic overview of the PE file structure and talked briefly about the main parts of a PE files. In the upcoming posts we’ll talk about each one of these parts in much more detail., A dive into the PE file format - PE file structure - Part 1: OverviewIntroductionThe aim of this post is to provide a basic introduction to the PE file structure without talking about any details.PE filesPE stands for Portable Executable, it’s a file format for executables used in Windows operating systems, it’s based on the COFF file format (Common Object File Format).Not only .exe files are PE files, dynamic link libraries (.dll), Kernel modules (.srv), Control panel applications (.cpl) and many others are also PE files.A PE file is a data structure that holds information necessary for the OS loader to be able to load that executable into memory and execute it.Structure OverviewA typical PE file follows the structure outlined in the following figure:If we open an executable file with PE-bear we’ll see the same thing:DOS HeaderEvery PE file starts with a 64-bytes-long structure called the DOS header, it’s what makes the PE file an MS-DOS executable.DOS StubAfter the DOS header comes the DOS stub which is a small MS-DOS 2.0 compatible executable that just prints an error message saying “This program cannot be run in DOS mode” when the program is run in DOS mode.NT HeadersThe NT Headers part contains three main parts: PE signature: A 4-byte signature that identifies the file as a PE file. File Header: A standard COFF File Header. It holds some information about the PE file. Optional Header: The most important header of the NT Headers, its name is the Optional Header because some files like object files don’t have it, however it’s required for image files (files like .exe files). This header provides important information to the OS loader.Section TableThe section table follows the Optional Header immediately, it is an array of Image Section Headers, there’s a section header for every section in the PE file.Each header contains information about the section it refers to.SectionsSections are where the actual contents of the file are stored, these include things like data and resources that the program uses, and also the actual code of the program, there are several sections each one with its own purpose.ConclusionIn this post we looked at a very basic overview of the PE file structure and talked briefly about the main parts of a PE files.In the upcoming posts we’ll talk about each one of these parts in much more detail.
|
||
|
A dive into the PE file format - PE file structure - Part 2: DOS Header, DOS Stub and Rich Header - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe3/
A dive into the PE file format - PE file structure - Part 2: DOS Header, DOS Stub and Rich Header Introduction In the previous post we looked at a high level overview of the PE file structure, in this post we’re going to talk about the first two parts which are the DOS Header and the DOS Stub. The PE viewer I’m going to use throughout the series is called PE-bear, it’s full of features and has a good UI. DOS Header Overview The DOS header (also called the MS-DOS header) is a 64-byte-long structure that exists at the start of the PE file. it’s not important for the functionality of PE files on modern Windows systems, however it’s there because of backward compatibility reasons. This header makes the file an MS-DOS executable, so when it’s loaded on MS-DOS the DOS stub gets executed instead of the actual program. Without this header, if you attempt to load the executable on MS-DOS it will not be loaded and will just produce a generic error. Structure As mentioned before, it’s a 64-byte-long structure, we can take a look at the contents of that structure by looking at the IMAGE_DOS_HEADER structure definition from winnt.h: typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; This structure is important to the PE loader on MS-DOS, however only a few members of it are important to the PE loader on Windows Systems, so we’re not going to cover everything in here, just the important members of the structure. e_magic: This is the first member of the DOS Header, it’s a WORD so it occupies 2 bytes, it’s usually called the magic number. It has a fixed value of 0x5A4D or MZ in ASCII, and it serves as a signature that marks the file as an MS-DOS executable. e_lfanew: This is the last member of the DOS header structure, it’s located at offset 0x3C into the DOS header and it holds an offset to the start of the NT headers. This member is important to the PE loader on Windows systems because it tells the loader where to look for the file header. The following picture shows contents of the DOS header in an actual PE file using PE-bear: As you can see, the first member of the header is the magic number with the fixed value we talked about which was 5A4D. The last member of the header (at offset 0x3C) is given the name “File address of new exe header”, it has the value 100, we can follow to that offset and we’ll find the start of the NT headers as expected: DOS Stub Overview The DOS stub is an MS-DOS program that prints an error message saying that the executable is not compatible with DOS then exits. This is what gets executed when the program is loaded in MS-DOS, the default error message is “This program cannot be run in DOS mode.”, however this message can be changed by the user during compile time. That’s all we need to know about the DOS stub, we don’t really care about it, but let’s take a look at what it’s doing just for fun. Analysis To be able to disassemble the machine code of the DOS stub, I copied the code of the stub from PE-bear, then I created a new file with the stub contents using a hex editor (HxD) and gave it the name dos-stub.exe. Stub code: 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 0D 0A 24 00 00 00 00 00 00 00 After that I used IDA to disassemble the executable, MS-DOS programs are 16-bit programs, so I chose the intel 8086 processor type and the 16-bit disassembly mode. It’s a fairly simple program, let’s step through it line by line: seg000:0000 push cs seg000:0001 pop ds First line pushes the value of cs onto the stack and the second line pops that value from the top of stack into ds. This is just a way of setting the value of the data segment to the same value as the code segment. seg000:0002 mov dx, 0Eh seg000:0005 mov ah, 9 seg000:0007 int 21h ; DOS - PRINT STRING seg000:0007 ; DS:DX -> string terminated by "$" These three lines are responsible for printing the error message, first line sets dx to the address of the string “This program cannot be run in DOS mode.” (0xe), second line sets ah to 9 and the last line invokes interrupt 21h. Interrupt 21h is a DOS interrupt (API call) that can do a lot of things, it takes a parameter that determines what function to execute and that parameter is passed in the ah register. We see here that the value 9 is given to the interrupt, 9 is the code of the function that prints a string to the screen, that function takes a parameter which is the address of the string to print, that parameter is passed in the dx register as we can see in the code. Information about the DOS API can be found on wikipedia. seg000:0009 mov ax, 4C01h seg000:000C int 21h ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT) seg000:000C ; AL = exit code The last three lines of the program are again an interrupt 21h call, this time there’s a mov instruction that puts 0X4C01 into ax, this sets al to 0x01 and ah to 0x4c. 0x4c is the function code of the function that exits with an error code, it takes the error code from al, which in this case is 1. So in summary, all the DOS stub is doing is print the error message then exit with code 1. Rich Header So now we’ve seen the DOS Header and the DOS Stub, however there’s still a chunk of data we haven’t talked about lying between the DOS Stub and the start of the NT Headers. This chunk of data is commonly referred to as the Rich Header, it’s an undocumented structure that’s only present in executables built using the Microsoft Visual Studio toolset. This structure holds some metadata about the tools used to build the executable like their names or types and their specific versions and build numbers. All of the resources I have read about PE files didn’t mention this structure, however when searching about the Rich Header itself I found a decent amount of resources, and that makes sense because the Rich Header is not actually a part of the PE file format structure and can be completely zeroed-out without interfering with the executable’s functionality, it’s just something that Microsoft adds to any executable built using their Visual Studio toolset. I only know about the Rich Header because I’ve read the reports on the Olympic Destroyer malware, and for those who don’t know what Olympic Destroyer is, it’s a malware that was written and used by a threat group in an attempt to disrupt the 2018 Winter Olympics. This piece of malware is known for having a lot of false flags that were intentionally put to cause confusion and misattribution, one of the false flags present there was a Rich Header. The authors of the malware overwrote the original Rich Header in the malware executable with the Rich Header of another malware attributed to the Lazarus threat group to make it look like it was Lazarus. You can check Kaspersky’s report for more information about this. The Rich Header consists of a chunk of XORed data followed by a signature (Rich) and a 32-bit checksum value that is the XOR key. The encrypted data consists of a DWORD signature DanS, 3 zeroed-out DWORDs for padding, then pairs of DWORDS each pair representing an entry, and each entry holds a tool name, its build number and the number of times it’s been used. In each DWORD pair the first pair holds the type ID or the product ID in the high WORD and the build ID in the low WORD, the second pair holds the use count. PE-bear parses the Rich Header automatically: As you can see the DanS signature is the first thing in the structure, then there are 3 zeroed-out DWORDs and after that comes the entries. We can also see the corresponding tools and Visual Studio versions of the product and build IDs. As an exercise I wrote a script to parse this header myself, it’s a very simple process, all we need to do is to XOR the data, then read the entry pairs and translate them. Rich Header data: 7E 13 87 AA 3A 72 E9 F9 3A 72 E9 F9 3A 72 E9 F9 33 0A 7A F9 30 72 E9 F9 F1 1D E8 F8 38 72 E9 F9 F1 1D EC F8 2B 72 E9 F9 F1 1D ED F8 30 72 E9 F9 F1 1D EA F8 39 72 E9 F9 61 1A E8 F8 3F 72 E9 F9 3A 72 E8 F9 0A 72 E9 F9 BC 02 E0 F8 3B 72 E9 F9 BC 02 16 F9 3B 72 E9 F9 BC 02 EB F8 3B 72 E9 F9 52 69 63 68 3A 72 E9 F9 00 00 00 00 00 00 00 00 Script: import textwrap def xor(data, key): return bytearray( ((data[i] ^ key[i % len(key)]) for i in range(0, len(data))) ) def rev_endiannes(data): tmp = [data[i:i+8] for i in range(0, len(data), 8)] for i in range(len(tmp)): tmp[i] = "".join(reversed([tmp[i][x:x+2] for x in range(0, len(tmp[i]), 2)])) return "".join(tmp) data = bytearray.fromhex("7E1387AA3A72E9F93A72E9F93A72E9F9330A7AF93072E9F9F11DE8F83872E9F9F11DECF82B72E9F9F11DEDF83072E9F9F11DEAF83972E9F9611AE8F83F72E9F93A72E8F90A72E9F9BC02E0F83B72E9F9BC0216F93B72E9F9BC02EBF83B72E9F9") key = bytearray.fromhex("3A72E9F9") rch_hdr = (xor(data,key)).hex() rch_hdr = textwrap.wrap(rch_hdr, 16) for i in range(2,len(rch_hdr)): tmp = textwrap.wrap(rch_hdr[i], 8) f1 = rev_endiannes(tmp[0]) f2 = rev_endiannes(tmp[1]) print("{} {} : {}.{}.{}".format(f1, f2, str(int(f1[4:],16)), str(int(f1[0:4],16)), str(int(f2,16)) )) Please note that I had to reverse the byte-order because the data was presented in little-endian. After running the script we can see an output that’s identical to PE-bear’s interpretation, meaning that the script works fine. Translating these values into the actual tools types and versions is a matter of collecting the values from actual Visual Studio installations. I checked the source code of bearparser (the parser used in PE-bear) and I found comments mentioning where these values were collected from. //list from: https://github.com/kirschju/richheader //list based on: https://github.com/kirschju/richheader + pnx's notes You can check the source code for yourself, it’s on hasherezade’s (PE-bear author) Github page. Conclusion In this post we talked about the first two parts of the PE file, the DOS header and the DOS stub, we looked at the members of the DOS header structure and we reversed the DOS stub program. We also looked at the Rich Header, a structure that’s not essentially a part of the PE file format but was worth checking. The following image summarizes what we’ve talked about in this post:, A dive into the PE file format - PE file structure - Part 2: DOS Header, DOS Stub and Rich HeaderIntroductionIn the previous post we looked at a high level overview of the PE file structure, in this post we’re going to talk about the first two parts which are the DOS Header and the DOS Stub.The PE viewer I’m going to use throughout the series is called PE-bear, it’s full of features and has a good UI.DOS HeaderOverviewThe DOS header (also called the MS-DOS header) is a 64-byte-long structure that exists at the start of the PE file.it’s not important for the functionality of PE files on modern Windows systems, however it’s there because of backward compatibility reasons.This header makes the file an MS-DOS executable, so when it’s loaded on MS-DOS the DOS stub gets executed instead of the actual program.Without this header, if you attempt to load the executable on MS-DOS it will not be loaded and will just produce a generic error.StructureAs mentioned before, it’s a 64-byte-long structure, we can take a look at the contents of that structure by looking at the IMAGE_DOS_HEADER structure definition from winnt.h:typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;This structure is important to the PE loader on MS-DOS, however only a few members of it are important to the PE loader on Windows Systems, so we’re not going to cover everything in here, just the important members of the structure. e_magic: This is the first member of the DOS Header, it’s a WORD so it occupies 2 bytes, it’s usually called the magic number.It has a fixed value of 0x5A4D or MZ in ASCII, and it serves as a signature that marks the file as an MS-DOS executable. e_lfanew: This is the last member of the DOS header structure, it’s located at offset 0x3C into the DOS header and it holds an offset to the start of the NT headers.This member is important to the PE loader on Windows systems because it tells the loader where to look for the file header.The following picture shows contents of the DOS header in an actual PE file using PE-bear:As you can see, the first member of the header is the magic number with the fixed value we talked about which was 5A4D.The last member of the header (at offset 0x3C) is given the name “File address of new exe header”, it has the value 100, we can follow to that offset and we’ll find the start of the NT headers as expected:DOS StubOverviewThe DOS stub is an MS-DOS program that prints an error message saying that the executable is not compatible with DOS then exits.This is what gets executed when the program is loaded in MS-DOS, the default error message is “This program cannot be run in DOS mode.”, however this message can be changed by the user during compile time.That’s all we need to know about the DOS stub, we don’t really care about it, but let’s take a look at what it’s doing just for fun.AnalysisTo be able to disassemble the machine code of the DOS stub, I copied the code of the stub from PE-bear, then I created a new file with the stub contents using a hex editor (HxD) and gave it the name dos-stub.exe.Stub code:0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 6869 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 0D 0A 24 00 00 00 00 00 00 00After that I used IDA to disassemble the executable, MS-DOS programs are 16-bit programs, so I chose the intel 8086 processor type and the 16-bit disassembly mode.It’s a fairly simple program, let’s step through it line by line:seg000:0000 push csseg000:0001 pop dsFirst line pushes the value of cs onto the stack and the second line pops that value from the top of stack into ds. This is just a way of setting the value of the data segment to the same value as the code segment.seg000:0002 mov dx, 0Ehseg000:0005 mov ah, 9seg000:0007 int 21h ; DOS - PRINT STRINGseg000:0007 ; DS:DX -> string terminated by "$"These three lines are responsible for printing the error message, first line sets dx to the address of the string “This program cannot be run in DOS mode.” (0xe), second line sets ah to 9 and the last line invokes interrupt 21h.Interrupt 21h is a DOS interrupt (API call) that can do a lot of things, it takes a parameter that determines what function to execute and that parameter is passed in the ah register.We see here that the value 9 is given to the interrupt, 9 is the code of the function that prints a string to the screen, that function takes a parameter which is the address of the string to print, that parameter is passed in the dx register as we can see in the code.Information about the DOS API can be found on wikipedia.seg000:0009 mov ax, 4C01hseg000:000C int 21h ; DOS - 2+ - QUIT WITH EXIT CODE (EXIT)seg000:000C ; AL = exit codeThe last three lines of the program are again an interrupt 21h call, this time there’s a mov instruction that puts 0X4C01 into ax, this sets al to 0x01 and ah to 0x4c.0x4c is the function code of the function that exits with an error code, it takes the error code from al, which in this case is 1.So in summary, all the DOS stub is doing is print the error message then exit with code 1.Rich HeaderSo now we’ve seen the DOS Header and the DOS Stub, however there’s still a chunk of data we haven’t talked about lying between the DOS Stub and the start of the NT Headers.This chunk of data is commonly referred to as the Rich Header, it’s an undocumented structure that’s only present in executables built using the Microsoft Visual Studio toolset.This structure holds some metadata about the tools used to build the executable like their names or types and their specific versions and build numbers.All of the resources I have read about PE files didn’t mention this structure, however when searching about the Rich Header itself I found a decent amount of resources, and that makes sense because the Rich Header is not actually a part of the PE file format structure and can be completely zeroed-out without interfering with the executable’s functionality, it’s just something that Microsoft adds to any executable built using their Visual Studio toolset.I only know about the Rich Header because I’ve read the reports on the Olympic Destroyer malware, and for those who don’t know what Olympic Destroyer is, it’s a malware that was written and used by a threat group in an attempt to disrupt the 2018 Winter Olympics.This piece of malware is known for having a lot of false flags that were intentionally put to cause confusion and misattribution, one of the false flags present there was a Rich Header.The authors of the malware overwrote the original Rich Header in the malware executable with the Rich Header of another malware attributed to the Lazarus threat group to make it look like it was Lazarus.You can check Kaspersky’s report for more information about this.The Rich Header consists of a chunk of XORed data followed by a signature (Rich) and a 32-bit checksum value that is the XOR key.The encrypted data consists of a DWORD signature DanS, 3 zeroed-out DWORDs for padding, then pairs of DWORDS each pair representing an entry, and each entry holds a tool name, its build number and the number of times it’s been used.In each DWORD pair the first pair holds the type ID or the product ID in the high WORD and the build ID in the low WORD, the second pair holds the use count.PE-bear parses the Rich Header automatically:As you can see the DanS signature is the first thing in the structure, then there are 3 zeroed-out DWORDs and after that comes the entries.We can also see the corresponding tools and Visual Studio versions of the product and build IDs.As an exercise I wrote a script to parse this header myself, it’s a very simple process, all we need to do is to XOR the data, then read the entry pairs and translate them.Rich Header data:7E 13 87 AA 3A 72 E9 F9 3A 72 E9 F9 3A 72 E9 F933 0A 7A F9 30 72 E9 F9 F1 1D E8 F8 38 72 E9 F9 F1 1D EC F8 2B 72 E9 F9 F1 1D ED F8 30 72 E9 F9 F1 1D EA F8 39 72 E9 F9 61 1A E8 F8 3F 72 E9 F9 3A 72 E8 F9 0A 72 E9 F9 BC 02 E0 F8 3B 72 E9 F9 BC 02 16 F9 3B 72 E9 F9 BC 02 EB F8 3B 72 E9 F9 52 69 63 68 3A 72 E9 F9 00 00 00 00 00 00 00 00Script:import textwrapdef xor(data, key): return bytearray( ((data[i] ^ key[i % len(key)]) for i in range(0, len(data))) )def rev_endiannes(data): tmp = [data[i:i+8] for i in range(0, len(data), 8)] for i in range(len(tmp)): tmp[i] = "".join(reversed([tmp[i][x:x+2] for x in range(0, len(tmp[i]), 2)])) return "".join(tmp)data = bytearray.fromhex("7E1387AA3A72E9F93A72E9F93A72E9F9330A7AF93072E9F9F11DE8F83872E9F9F11DECF82B72E9F9F11DEDF83072E9F9F11DEAF83972E9F9611AE8F83F72E9F93A72E8F90A72E9F9BC02E0F83B72E9F9BC0216F93B72E9F9BC02EBF83B72E9F9")key = bytearray.fromhex("3A72E9F9")rch_hdr = (xor(data,key)).hex()rch_hdr = textwrap.wrap(rch_hdr, 16)for i in range(2,len(rch_hdr)): tmp = textwrap.wrap(rch_hdr[i], 8) f1 = rev_endiannes(tmp[0]) f2 = rev_endiannes(tmp[1]) print("{} {} : {}.{}.{}".format(f1, f2, str(int(f1[4:],16)), str(int(f1[0:4],16)), str(int(f2,16)) ))Please note that I had to reverse the byte-order because the data was presented in little-endian.After running the script we can see an output that’s identical to PE-bear’s interpretation, meaning that the script works fine.Translating these values into the actual tools types and versions is a matter of collecting the values from actual Visual Studio installations.I checked the source code of bearparser (the parser used in PE-bear) and I found comments mentioning where these values were collected from.//list from: https://github.com/kirschju/richheader//list based on: https://github.com/kirschju/richheader + pnx's notesYou can check the source code for yourself, it’s on hasherezade’s (PE-bear author) Github page.ConclusionIn this post we talked about the first two parts of the PE file, the DOS header and the DOS stub, we looked at the members of the DOS header structure and we reversed the DOS stub program.We also looked at the Rich Header, a structure that’s not essentially a part of the PE file format but was worth checking.The following image summarizes what we’ve talked about in this post:
|
||
|
A dive into the PE file format - PE file structure - Part 3: NT Headers - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe4/
A dive into the PE file format - PE file structure - Part 3: NT Headers Introduction In the previous post we looked at the structure of the DOS header and we reversed the DOS stub. In this post we’re going to talk about the NT Headers part of the PE file structure. Before we get into the post, we need to talk about an important concept that we’re going to see a lot, and that is the concept of a Relative Virtual Address or an RVA. An RVA is just an offset from where the image was loaded in memory (the Image Base). So to translate an RVA into an absolute virtual address you need to add the value of the RVA to the value of the Image Base. PE files rely heavily on the use of RVAs as we’ll see later. NT Headers (IMAGE_NT_HEADERS) NT headers is a structure defined in winnt.h as IMAGE_NT_HEADERS, by looking at its definition we can see that it has three members, a DWORD signature, an IMAGE_FILE_HEADER structure called FileHeader and an IMAGE_OPTIONAL_HEADER structure called OptionalHeader. It’s worth mentioning that this structure is defined in two different versions, one for 32-bit executables (Also named PE32 executables) named IMAGE_NT_HEADERS and one for 64-bit executables (Also named PE32+ executables) named IMAGE_NT_HEADERS64. The main difference between the two versions is the used version of IMAGE_OPTIONAL_HEADER structure which has two versions, IMAGE_OPTIONAL_HEADER32 for 32-bit executables and IMAGE_OPTIONAL_HEADER64 for 64-bit executables. typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; Signature First member of the NT headers structure is the PE signature, it’s a DWORD which means that it occupies 4 bytes. It always has a fixed value of 0x50450000 which translates to PE\0\0 in ASCII. Here’s a screenshot from PE-bear showing the PE signature: File Header (IMAGE_FILE_HEADER) Also called “The COFF File Header”, the File Header is a structure that holds some information about the PE file. It’s defined as IMAGE_FILE_HEADER in winnt.h, here’s the definition: typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; It’s a simple structure with 7 members: Machine: This is a number that indicates the type of machine (CPU Architecture) the executable is targeting, this field can have a lot of values, but we’re only interested in two of them, 0x8864 for AMD64 and 0x14c for i386. For a complete list of possible values you can check the official Microsoft documentation. NumberOfSections: This field holds the number of sections (or the number of section headers aka. the size of the section table.). TimeDateStamp: A unix timestamp that indicates when the file was created. PointerToSymbolTable and NumberOfSymbols: These two fields hold the file offset to the COFF symbol table and the number of entries in that symbol table, however they get set to 0 which means that no COFF symbol table is present, this is done because the COFF debugging information is deprecated. SizeOfOptionalHeader: The size of the Optional Header. Characteristics: A flag that indicates the attributes of the file, these attributes can be things like the file being executable, the file being a system file and not a user program, and a lot of other things. A complete list of these flags can be found on the official Microsoft documentation. Here’s the File Header contents of an actual PE file: Optional Header (IMAGE_OPTIONAL_HEADER) The Optional Header is the most important header of the NT headers, the PE loader looks for specific information provided by that header to be able to load and run the executable. It’s called the optional header because some file types like object files don’t have it, however this header is essential for image files. It doesn’t have a fixed size, that’s why the IMAGE_FILE_HEADER.SizeOfOptionalHeader member exists. The first 8 members of the Optional Header structure are standard for every implementation of the COFF file format, the rest of the header is an extension to the standard COFF optional header defined by Microsoft, these additional members of the structure are needed by the Windows PE loader and linker. As mentioned earlier, there are two versions of the Optional Header, one for 32-bit executables and one for 64-bit executables. The two versions are different in two aspects: The size of the structure itself (or the number of members defined within the structure): IMAGE_OPTIONAL_HEADER32 has 31 members while IMAGE_OPTIONAL_HEADER64 only has 30 members, that additional member in the 32-bit version is a DWORD named BaseOfData which holds an RVA of the beginning of the data section. The data type of some of the members: The following 5 members of the Optional Header structure are defined as DWORD in the 32-bit version and as ULONGLONG in the 64-bit version: ImageBase SizeOfStackReserve SizeOfStackCommit SizeOfHeapReserve SizeOfHeapCommit Let’s take a look at the definition of both structures. typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; Magic: Microsoft documentation describes this field as an integer that identifies the state of the image, the documentation mentions three common values: 0x10B: Identifies the image as a PE32 executable. 0x20B: Identifies the image as a PE32+ executable. 0x107: Identifies the image as a ROM image. The value of this field is what determines whether the executable is 32-bit or 64-bit, IMAGE_FILE_HEADER.Machine is ignored by the Windows PE loader. MajorLinkerVersion and MinorLinkerVersion: The linker major and minor version numbers. SizeOfCode: This field holds the size of the code (.text) section, or the sum of all code sections if there are multiple sections. SizeOfInitializedData: This field holds the size of the initialized data (.data) section, or the sum of all initialized data sections if there are multiple sections. SizeOfUninitializedData: This field holds the size of the uninitialized data (.bss) section, or the sum of all uninitialized data sections if there are multiple sections. AddressOfEntryPoint: An RVA of the entry point when the file is loaded into memory. The documentation states that for program images this relative address points to the starting address and for device drivers it points to initialization function. For DLLs an entry point is optional, and in the case of entry point absence the AddressOfEntryPoint field is set to 0. BaseOfCode: An RVA of the start of the code section when the file is loaded into memory. BaseOfData (PE32 Only): An RVA of the start of the data section when the file is loaded into memory. ImageBase: This field holds the preferred address of the first byte of image when loaded into memory (the preferred base address), this value must be a multiple of 64K. Due to memory protections like ASLR, and a lot of other reasons, the address specified by this field is almost never used, in this case the PE loader chooses an unused memory range to load the image into, after loading the image into that address the loader goes into a process called the relocating where it fixes the constant addresses within the image to work with the new image base, there’s a special section that holds information about places that will need fixing if relocation is needed, that section is called the relocation section (.reloc), more on that in the upcoming posts. SectionAlignment: This field holds a value that gets used for section alignment in memory (in bytes), sections are aligned in memory boundaries that are multiples of this value. The documentation states that this value defaults to the page size for the architecture and it can’t be less than the value of FileAlignment. FileAlignment: Similar to SectionAligment this field holds a value that gets used for section raw data alignment on disk (in bytes), if the size of the actual data in a section is less than the FileAlignment value, the rest of the chunk gets padded with zeroes to keep the alignment boundaries. The documentation states that this value should be a power of 2 between 512 and 64K, and if the value of SectionAlignment is less than the architecture’s page size then the sizes of FileAlignment and SectionAlignment must match. MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, MajorSubsystemVersion and MinorSubsystemVersion: These members of the structure specify the major version number of the required operating system, the minor version number of the required operating system, the major version number of the image, the minor version number of the image, the major version number of the subsystem and the minor version number of the subsystem respectively. Win32VersionValue: A reserved field that the documentation says should be set to 0. SizeOfImage: The size of the image file (in bytes), including all headers. It gets rounded up to a multiple of SectionAlignment because this value is used when loading the image into memory. SizeOfHeaders: The combined size of the DOS stub, PE header (NT Headers), and section headers rounded up to a multiple of FileAlignment. CheckSum: A checksum of the image file, it’s used to validate the image at load time. Subsystem: This field specifies the Windows subsystem (if any) that is required to run the image, A complete list of the possible values of this field can be found on the official Microsoft documentation. DLLCharacteristics: This field defines some characteristics of the executable image file, like if it’s NX compatible and if it can be relocated at run time. I have no idea why it’s named DLLCharacteristics, it exists within normal executable image files and it defines characteristics that can apply to normal executable files. A complete list of the possible flags for DLLCharacteristics can be found on the official Microsoft documentation. SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve and SizeOfHeapCommit: These fields specify the size of the stack to reserve, the size of the stack to commit, the size of the local heap space to reserve and the size of the local heap space to commit respectively. LoaderFlags: A reserved field that the documentation says should be set to 0. NumberOfRvaAndSizes : Size of the DataDirectory array. DataDirectory: An array of IMAGE_DATA_DIRECTORY structures. We will talk about this in the next post. Let’s take a look at the Optional Header contents of an actual PE file. We can talk about some of these fields, first one being the Magic field at the start of the header, it has the value 0x20B meaning that this is a PE32+ executable. We can see that the entry point RVA is 0x12C4 and the code section start RVA is 0x1000, it follows the alignment defined by the SectionAlignment field which has the value of 0x1000. File alignment is set to 0x200, and we can verify this by looking at any of the sections, for example the data section: As you can see, the actual contents of the data section are from 0x2200 to 0x2229, however the rest of the section is padded until 0x23FF to comply with the alignment defined by FileAlignment. SizeOfImage is set to 7000 and SizeOfHeaders is set to 400, both are multiples of SectionAlignment and FileAlignment respectively. The Subsystem field is set to 3 which is the Windows console, and that makes sense because the program is a console application. I didn’t include the DataDirectory in the optional header contents screenshot because we still haven’t talked about it yet. Conclusion We’ve reached the end of this post. In summary we looked at the NT Headers structure, and we discussed the File Header and Optional Header structures in detail. In the next post we will take a look at the Data Directories, the Section Headers, and the sections. Thanks for reading., A dive into the PE file format - PE file structure - Part 3: NT HeadersIntroductionIn the previous post we looked at the structure of the DOS header and we reversed the DOS stub.In this post we’re going to talk about the NT Headers part of the PE file structure.Before we get into the post, we need to talk about an important concept that we’re going to see a lot, and that is the concept of a Relative Virtual Address or an RVA.An RVA is just an offset from where the image was loaded in memory (the Image Base). So to translate an RVA into an absolute virtual address you need to add the value of the RVA to the value of the Image Base.PE files rely heavily on the use of RVAs as we’ll see later.NT Headers (IMAGE_NT_HEADERS)NT headers is a structure defined in winnt.h as IMAGE_NT_HEADERS, by looking at its definition we can see that it has three members, a DWORD signature, an IMAGE_FILE_HEADER structure called FileHeader and an IMAGE_OPTIONAL_HEADER structure called OptionalHeader.It’s worth mentioning that this structure is defined in two different versions, one for 32-bit executables (Also named PE32 executables) named IMAGE_NT_HEADERS and one for 64-bit executables (Also named PE32+ executables) named IMAGE_NT_HEADERS64. The main difference between the two versions is the used version of IMAGE_OPTIONAL_HEADER structure which has two versions, IMAGE_OPTIONAL_HEADER32 for 32-bit executables and IMAGE_OPTIONAL_HEADER64 for 64-bit executables.typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader;} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader;} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;SignatureFirst member of the NT headers structure is the PE signature, it’s a DWORD which means that it occupies 4 bytes.It always has a fixed value of 0x50450000 which translates to PE\0\0 in ASCII.Here’s a screenshot from PE-bear showing the PE signature:File Header (IMAGE_FILE_HEADER)Also called “The COFF File Header”, the File Header is a structure that holds some information about the PE file.It’s defined as IMAGE_FILE_HEADER in winnt.h, here’s the definition:typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;It’s a simple structure with 7 members: Machine: This is a number that indicates the type of machine (CPU Architecture) the executable is targeting, this field can have a lot of values, but we’re only interested in two of them, 0x8864 for AMD64 and 0x14c for i386. For a complete list of possible values you can check the official Microsoft documentation. NumberOfSections: This field holds the number of sections (or the number of section headers aka. the size of the section table.). TimeDateStamp: A unix timestamp that indicates when the file was created. PointerToSymbolTable and NumberOfSymbols: These two fields hold the file offset to the COFF symbol table and the number of entries in that symbol table, however they get set to 0 which means that no COFF symbol table is present, this is done because the COFF debugging information is deprecated. SizeOfOptionalHeader: The size of the Optional Header. Characteristics: A flag that indicates the attributes of the file, these attributes can be things like the file being executable, the file being a system file and not a user program, and a lot of other things. A complete list of these flags can be found on the official Microsoft documentation.Here’s the File Header contents of an actual PE file:Optional Header (IMAGE_OPTIONAL_HEADER)The Optional Header is the most important header of the NT headers, the PE loader looks for specific information provided by that header to be able to load and run the executable.It’s called the optional header because some file types like object files don’t have it, however this header is essential for image files.It doesn’t have a fixed size, that’s why the IMAGE_FILE_HEADER.SizeOfOptionalHeader member exists.The first 8 members of the Optional Header structure are standard for every implementation of the COFF file format, the rest of the header is an extension to the standard COFF optional header defined by Microsoft, these additional members of the structure are needed by the Windows PE loader and linker.As mentioned earlier, there are two versions of the Optional Header, one for 32-bit executables and one for 64-bit executables.The two versions are different in two aspects: The size of the structure itself (or the number of members defined within the structure): IMAGE_OPTIONAL_HEADER32 has 31 members while IMAGE_OPTIONAL_HEADER64 only has 30 members, that additional member in the 32-bit version is a DWORD named BaseOfData which holds an RVA of the beginning of the data section. The data type of some of the members: The following 5 members of the Optional Header structure are defined as DWORD in the 32-bit version and as ULONGLONG in the 64-bit version: ImageBase SizeOfStackReserve SizeOfStackCommit SizeOfHeapReserve SizeOfHeapCommit Let’s take a look at the definition of both structures.typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64; Magic: Microsoft documentation describes this field as an integer that identifies the state of the image, the documentation mentions three common values: 0x10B: Identifies the image as a PE32 executable. 0x20B: Identifies the image as a PE32+ executable. 0x107: Identifies the image as a ROM image. The value of this field is what determines whether the executable is 32-bit or 64-bit, IMAGE_FILE_HEADER.Machine is ignored by the Windows PE loader. MajorLinkerVersion and MinorLinkerVersion: The linker major and minor version numbers. SizeOfCode: This field holds the size of the code (.text) section, or the sum of all code sections if there are multiple sections. SizeOfInitializedData: This field holds the size of the initialized data (.data) section, or the sum of all initialized data sections if there are multiple sections. SizeOfUninitializedData: This field holds the size of the uninitialized data (.bss) section, or the sum of all uninitialized data sections if there are multiple sections. AddressOfEntryPoint: An RVA of the entry point when the file is loaded into memory.The documentation states that for program images this relative address points to the starting address and for device drivers it points to initialization function. For DLLs an entry point is optional, and in the case of entry point absence the AddressOfEntryPoint field is set to 0. BaseOfCode: An RVA of the start of the code section when the file is loaded into memory. BaseOfData (PE32 Only): An RVA of the start of the data section when the file is loaded into memory. ImageBase: This field holds the preferred address of the first byte of image when loaded into memory (the preferred base address), this value must be a multiple of 64K.Due to memory protections like ASLR, and a lot of other reasons, the address specified by this field is almost never used, in this case the PE loader chooses an unused memory range to load the image into, after loading the image into that address the loader goes into a process called the relocating where it fixes the constant addresses within the image to work with the new image base, there’s a special section that holds information about places that will need fixing if relocation is needed, that section is called the relocation section (.reloc), more on that in the upcoming posts. SectionAlignment: This field holds a value that gets used for section alignment in memory (in bytes), sections are aligned in memory boundaries that are multiples of this value.The documentation states that this value defaults to the page size for the architecture and it can’t be less than the value of FileAlignment. FileAlignment: Similar to SectionAligment this field holds a value that gets used for section raw data alignment on disk (in bytes), if the size of the actual data in a section is less than the FileAlignment value, the rest of the chunk gets padded with zeroes to keep the alignment boundaries.The documentation states that this value should be a power of 2 between 512 and 64K, and if the value of SectionAlignment is less than the architecture’s page size then the sizes of FileAlignment and SectionAlignment must match. MajorOperatingSystemVersion, MinorOperatingSystemVersion, MajorImageVersion, MinorImageVersion, MajorSubsystemVersion and MinorSubsystemVersion: These members of the structure specify the major version number of the required operating system, the minor version number of the required operating system, the major version number of the image, the minor version number of the image, the major version number of the subsystem and the minor version number of the subsystem respectively. Win32VersionValue: A reserved field that the documentation says should be set to 0. SizeOfImage: The size of the image file (in bytes), including all headers. It gets rounded up to a multiple of SectionAlignment because this value is used when loading the image into memory. SizeOfHeaders: The combined size of the DOS stub, PE header (NT Headers), and section headers rounded up to a multiple of FileAlignment. CheckSum: A checksum of the image file, it’s used to validate the image at load time. Subsystem: This field specifies the Windows subsystem (if any) that is required to run the image, A complete list of the possible values of this field can be found on the official Microsoft documentation. DLLCharacteristics: This field defines some characteristics of the executable image file, like if it’s NX compatible and if it can be relocated at run time.I have no idea why it’s named DLLCharacteristics, it exists within normal executable image files and it defines characteristics that can apply to normal executable files.A complete list of the possible flags for DLLCharacteristics can be found on the official Microsoft documentation. SizeOfStackReserve, SizeOfStackCommit, SizeOfHeapReserve and SizeOfHeapCommit: These fields specify the size of the stack to reserve, the size of the stack to commit, the size of the local heap space to reserve and the size of the local heap space to commit respectively. LoaderFlags: A reserved field that the documentation says should be set to 0. NumberOfRvaAndSizes : Size of the DataDirectory array. DataDirectory: An array of IMAGE_DATA_DIRECTORY structures. We will talk about this in the next post. Let’s take a look at the Optional Header contents of an actual PE file.We can talk about some of these fields, first one being the Magic field at the start of the header, it has the value 0x20B meaning that this is a PE32+ executable.We can see that the entry point RVA is 0x12C4 and the code section start RVA is 0x1000, it follows the alignment defined by the SectionAlignment field which has the value of 0x1000.File alignment is set to 0x200, and we can verify this by looking at any of the sections, for example the data section:As you can see, the actual contents of the data section are from 0x2200 to 0x2229, however the rest of the section is padded until 0x23FF to comply with the alignment defined by FileAlignment.SizeOfImage is set to 7000 and SizeOfHeaders is set to 400, both are multiples of SectionAlignment and FileAlignment respectively.The Subsystem field is set to 3 which is the Windows console, and that makes sense because the program is a console application.I didn’t include the DataDirectory in the optional header contents screenshot because we still haven’t talked about it yet.ConclusionWe’ve reached the end of this post. In summary we looked at the NT Headers structure, and we discussed the File Header and Optional Header structures in detail.In the next post we will take a look at the Data Directories, the Section Headers, and the sections.Thanks for reading.
|
||
|
A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and Sections - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe5/
A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and Sections Introduction In the last post we talked about the NT Headers and we skipped the last part of the Optional Header which was the data directories. In this post we’re going to talk about what data directories are and where they are located. We’re also going to cover section headers and sections in this post. Data Directories The last member of the IMAGE_OPTIONAL_HEADER structure was an array of IMAGE_DATA_DIRECTORY structures defined as follows: IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; IMAGE_NUMBEROF_DIRECTORY_ENTRIES is a constant defined with the value 16, meaning that this array can have up to 16 IMAGE_DATA_DIRECTORY entries: #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 An IMAGE_DATA_DIRETORY structure is defines as follows: typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; It’s a very simple structure with only two members, first one being an RVA pointing to the start of the Data Directory and the second one being the size of the Data Directory. So what is a Data Directory? Basically a Data Directory is a piece of data located within one of the sections of the PE file. Data Directories contain useful information needed by the loader, an example of a very important directory is the Import Directory which contains a list of external functions imported from other libraries, we’ll discuss it in more detail when we go over PE imports. Please note that not all Data Directories have the same structure, the IMAGE_DATA_DIRECTORY.VirtualAddress points to the Data Directory, however the type of that directory is what determines how that chunk of data is going to be parsed. Here’s a list of Data Directories defined in winnt.h. (Each one of these values represents an index in the DataDirectory array): // Directory Entries #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 // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #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 If we take a look at the contents of IMAGE_OPTIONAL_HEADER.DataDirectory of an actual PE file, we might see entries where both fields are set to 0: This means that this specific Data Directory is not used (doesn’t exist) in the executable file. Sections and Section Headers Sections Sections are the containers of the actual data of the executable file, they occupy the rest of the PE file after the headers, precisely after the section headers. Some sections have special names that indicate their purpose, we’ll go over some of them, and a full list of these names can be found on the official Microsoft documentation under the “Special Sections” section. .text: Contains the executable code of the program. .data: Contains the initialized data. .bss: Contains uninitialized data. .rdata: Contains read-only initialized data. .edata: Contains the export tables. .idata: Contains the import tables. .reloc: Contains image relocation information. .rsrc: Contains resources used by the program, these include images, icons or even embedded binaries. .tls: (Thread Local Storage), provides storage for every executing thread of the program. Section Headers After the Optional Header and before the sections comes the Section Headers. These headers contain information about the sections of the PE file. A Section Header is a structure named IMAGE_SECTION_HEADER defined in winnt.h as follows: typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; Name: First field of the Section Header, a byte array of the size IMAGE_SIZEOF_SHORT_NAME that holds the name of the section. IMAGE_SIZEOF_SHORT_NAME has the value of 8 meaning that a section name can’t be longer than 8 characters. For longer names the official documentation mentions a work-around by filling this field with an offset in the string table, however executable images do not use a string table so this limitation of 8 characters holds for executable images. PhysicalAddress or VirtualSize: A union defines multiple names for the same thing, this field contains the total size of the section when it’s loaded in memory. VirtualAddress: The documentation states that for executable images this field holds the address of the first byte of the section relative to the image base when loaded in memory, and for object files it holds the address of the first byte of the section before relocation is applied. SizeOfRawData: This field contains the size of the section on disk, it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment. SizeOfRawData and VirtualSize can be different, we’ll discuss the reason for this later in the post. PointerToRawData: A pointer to the first page of the section within the file, for executable images it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment. PointerToRelocations: A file pointer to the beginning of relocation entries for the section. It’s set to 0 for executable files. PointerToLineNumbers: A file pointer to the beginning of COFF line-number entries for the section. It’s set to 0 because COFF debugging information is deprecated. NumberOfRelocations: The number of relocation entries for the section, it’s set to 0 for executable images. NumberOfLinenumbers: The number of COFF line-number entries for the section, it’s set to 0 because COFF debugging information is deprecated. Characteristics: Flags that describe the characteristics of the section. These characteristics are things like if the section contains executable code, contains initialized/uninitialized data, can be shared in memory. A complete list of section characteristics flags can be found on the official Microsoft documentation. SizeOfRawData and VirtualSize can be different, and this can happen for multiple of reasons. SizeOfRawData must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment, so if the section size is less than that value the rest gets padded and SizeOfRawData gets rounded to the nearest multiple of IMAGE_OPTIONAL_HEADER.FileAlignment. However when the section is loaded into memory it doesn’t follow that alignment and only the actual size of the section is occupied. In this case SizeOfRawData will be greater than VirtualSize The opposite can happen as well. If the section contains uninitialized data, these data won’t be accounted for on disk, but when the section gets mapped into memory, the section will expand to reserve memory space for when the uninitialized data gets later initialized and used. This means that the section on disk will occupy less than it will do in memory, in this case VirtualSize will be greater than SizeOfRawData. Here’s the view of Section Headers in PE-bear: We can see Raw Addr. and Virtual Addr. fields which correspond to IMAGE_SECTION_HEADER.PointerToRawData and IMAGE_SECTION_HEADER.VirtualAddress. Raw Size and Virtual Size correspond to IMAGE_SECTION_HEADER.SizeOfRawData and IMAGE_SECTION_HEADER.VirtualSize. We can see how these two fields are used to calculate where the section ends, both on disk and in memory. For example if we take the .text section, it has a raw address of 0x400 and a raw size of 0xE00, if we add them together we get 0x1200 which is displayed as the section end on disk. Similarly we can do the same with virtual size and address, virtual address is 0x1000 and virtual size is 0xD2C, if we add them together we get 0x1D2C. The Characteristics field marks some sections as read-only, some other sections as read-write and some sections as readable and executable. PointerToRelocations, NumberOfRelocations and NumberOfLinenumbers are set to 0 as expected. Conclusion That’s it for this post, we’ve discussed what Data Directories are and we talked about sections. The next post will be about PE imports. Thanks for reading., A dive into the PE file format - PE file structure - Part 4: Data Directories, Section Headers and SectionsIntroductionIn the last post we talked about the NT Headers and we skipped the last part of the Optional Header which was the data directories.In this post we’re going to talk about what data directories are and where they are located.We’re also going to cover section headers and sections in this post.Data DirectoriesThe last member of the IMAGE_OPTIONAL_HEADER structure was an array of IMAGE_DATA_DIRECTORY structures defined as follows:IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];IMAGE_NUMBEROF_DIRECTORY_ENTRIES is a constant defined with the value 16, meaning that this array can have up to 16 IMAGE_DATA_DIRECTORY entries:#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16An IMAGE_DATA_DIRETORY structure is defines as follows:typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;It’s a very simple structure with only two members, first one being an RVA pointing to the start of the Data Directory and the second one being the size of the Data Directory.So what is a Data Directory? Basically a Data Directory is a piece of data located within one of the sections of the PE file.Data Directories contain useful information needed by the loader, an example of a very important directory is the Import Directory which contains a list of external functions imported from other libraries, we’ll discuss it in more detail when we go over PE imports.Please note that not all Data Directories have the same structure, the IMAGE_DATA_DIRECTORY.VirtualAddress points to the Data Directory, however the type of that directory is what determines how that chunk of data is going to be parsed.Here’s a list of Data Directories defined in winnt.h. (Each one of these values represents an index in the DataDirectory array):// Directory Entries#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// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)#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 descriptorIf we take a look at the contents of IMAGE_OPTIONAL_HEADER.DataDirectory of an actual PE file, we might see entries where both fields are set to 0:This means that this specific Data Directory is not used (doesn’t exist) in the executable file.Sections and Section HeadersSectionsSections are the containers of the actual data of the executable file, they occupy the rest of the PE file after the headers, precisely after the section headers.Some sections have special names that indicate their purpose, we’ll go over some of them, and a full list of these names can be found on the official Microsoft documentation under the “Special Sections” section. .text: Contains the executable code of the program. .data: Contains the initialized data. .bss: Contains uninitialized data. .rdata: Contains read-only initialized data. .edata: Contains the export tables. .idata: Contains the import tables. .reloc: Contains image relocation information. .rsrc: Contains resources used by the program, these include images, icons or even embedded binaries. .tls: (Thread Local Storage), provides storage for every executing thread of the program.Section HeadersAfter the Optional Header and before the sections comes the Section Headers.These headers contain information about the sections of the PE file.A Section Header is a structure named IMAGE_SECTION_HEADER defined in winnt.h as follows:typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; Name: First field of the Section Header, a byte array of the size IMAGE_SIZEOF_SHORT_NAME that holds the name of the section.IMAGE_SIZEOF_SHORT_NAME has the value of 8 meaning that a section name can’t be longer than 8 characters.For longer names the official documentation mentions a work-around by filling this field with an offset in the string table, however executable images do not use a string table so this limitation of 8 characters holds for executable images. PhysicalAddress or VirtualSize: A union defines multiple names for the same thing, this field contains the total size of the section when it’s loaded in memory. VirtualAddress: The documentation states that for executable images this field holds the address of the first byte of the section relative to the image base when loaded in memory, and for object files it holds the address of the first byte of the section before relocation is applied. SizeOfRawData: This field contains the size of the section on disk, it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.SizeOfRawData and VirtualSize can be different, we’ll discuss the reason for this later in the post. PointerToRawData: A pointer to the first page of the section within the file, for executable images it must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment. PointerToRelocations: A file pointer to the beginning of relocation entries for the section. It’s set to 0 for executable files. PointerToLineNumbers: A file pointer to the beginning of COFF line-number entries for the section. It’s set to 0 because COFF debugging information is deprecated. NumberOfRelocations: The number of relocation entries for the section, it’s set to 0 for executable images. NumberOfLinenumbers: The number of COFF line-number entries for the section, it’s set to 0 because COFF debugging information is deprecated. Characteristics: Flags that describe the characteristics of the section.These characteristics are things like if the section contains executable code, contains initialized/uninitialized data, can be shared in memory.A complete list of section characteristics flags can be found on the official Microsoft documentation.SizeOfRawData and VirtualSize can be different, and this can happen for multiple of reasons.SizeOfRawData must be a multiple of IMAGE_OPTIONAL_HEADER.FileAlignment, so if the section size is less than that value the rest gets padded and SizeOfRawData gets rounded to the nearest multiple of IMAGE_OPTIONAL_HEADER.FileAlignment.However when the section is loaded into memory it doesn’t follow that alignment and only the actual size of the section is occupied.In this case SizeOfRawData will be greater than VirtualSizeThe opposite can happen as well.If the section contains uninitialized data, these data won’t be accounted for on disk, but when the section gets mapped into memory, the section will expand to reserve memory space for when the uninitialized data gets later initialized and used.This means that the section on disk will occupy less than it will do in memory, in this case VirtualSize will be greater than SizeOfRawData.Here’s the view of Section Headers in PE-bear:We can see Raw Addr. and Virtual Addr. fields which correspond to IMAGE_SECTION_HEADER.PointerToRawData and IMAGE_SECTION_HEADER.VirtualAddress.Raw Size and Virtual Size correspond to IMAGE_SECTION_HEADER.SizeOfRawData and IMAGE_SECTION_HEADER.VirtualSize.We can see how these two fields are used to calculate where the section ends, both on disk and in memory.For example if we take the .text section, it has a raw address of 0x400 and a raw size of 0xE00, if we add them together we get 0x1200 which is displayed as the section end on disk.Similarly we can do the same with virtual size and address, virtual address is 0x1000 and virtual size is 0xD2C, if we add them together we get 0x1D2C.The Characteristics field marks some sections as read-only, some other sections as read-write and some sections as readable and executable.PointerToRelocations, NumberOfRelocations and NumberOfLinenumbers are set to 0 as expected.ConclusionThat’s it for this post, we’ve discussed what Data Directories are and we talked about sections.The next post will be about PE imports.Thanks for reading.
|
||
|
A dive into the PE file format - PE file structure - Part 5: PE Imports (Import Directory Table, ILT, IAT) - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe6/
A dive into the PE file format - PE file structure - Part 5: PE Imports (Import Directory Table, ILT, IAT) Introduction In this post we’re going to talk about a very important aspect of PE files, the PE imports. To understand how PE files handle their imports, we’ll go over some of the Data Directories present in the Import Data section (.idata), the Import Directory Table, the Import Lookup Table (ILT) or also referred to as the Import Name Table (INT) and the Import Address Table (IAT). Import Directory Table The Import Directory Table is a Data Directory located at the beginning of the .idata section. It consists of an array of IMAGE_IMPORT_DESCRIPTOR structures, each one of them is for a DLL. It doesn’t have a fixed size, so the last IMAGE_IMPORT_DESCRIPTOR of the array is zeroed-out (NULL-Padded) to indicate the end of the Import Directory Table. IMAGE_IMPORT_DESCRIPTOR is defined as follows: typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; OriginalFirstThunk: RVA of the ILT. TimeDateStamp: A time date stamp, that’s initially set to 0 if not bound and set to -1 if bound. In case of an unbound import the time date stamp gets updated to the time date stamp of the DLL after the image is bound. In case of a bound import it stays set to -1 and the real time date stamp of the DLL can be found in the Bound Import Directory Table in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR . We’ll discuss bound imports in the next section. ForwarderChain: The index of the first forwarder chain reference. This is something responsible for DLL forwarding. (DLL forwarding is when a DLL forwards some of its exported functions to another DLL.) Name: An RVA of an ASCII string that contains the name of the imported DLL. FirstThunk: RVA of the IAT. Bound Imports A bound import essentially means that the import table contains fixed addresses for the imported functions. These addresses are calculated and written during compile time by the linker. Using bound imports is a speed optimization, it reduces the time needed by the loader to resolve function addresses and fill the IAT, however if at run-time the bound addresses do not match the real ones then the loader will have to resolve these addresses again and fix the IAT. When discussing IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp, I mentioned that in case of a bound import, the time date stamp is set to -1 and the real time date stamp of the DLL can be found in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR in the Bound Import Data Directory. Bound Import Data Directory The Bound Import Data Directory is similar to the Import Directory Table, however as the name suggests, it holds information about the bound imports. It consists of an array of IMAGE_BOUND_IMPORT_DESCRIPTOR structures, and ends with a zeroed-out IMAGE_BOUND_IMPORT_DESCRIPTOR. IMAGE_BOUND_IMPORT_DESCRIPTOR is defined as follows: typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR { DWORD TimeDateStamp; WORD OffsetModuleName; WORD NumberOfModuleForwarderRefs; // Array of zero or more IMAGE_BOUND_FORWARDER_REF follows } IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR; TimeDateStamp: The time date stamp of the imported DLL. OffsetModuleName: An offset to a string with the name of the imported DLL. It’s an offset from the first IMAGE_BOUND_IMPORT_DESCRIPTOR NumberOfModuleForwarderRefs: The number of the IMAGE_BOUND_FORWARDER_REF structures that immediately follow this structure. IMAGE_BOUND_FORWARDER_REF is a structure that’s identical to IMAGE_BOUND_IMPORT_DESCRIPTOR, the only difference is that the last member is reserved. That’s all we need to know about bound imports. Import Lookup Table (ILT) Sometimes people refer to it as the Import Name Table (INT). Every imported DLL has an Import Lookup Table. IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk holds the RVA of the ILT of the corresponding DLL. The ILT is essentially a table of names or references, it tells the loader which functions are needed from the imported DLL. The ILT consists of an array of 32-bit numbers (for PE32) or 64-bit numbers for (PE32+), the last one is zeroed-out to indicate the end of the ILT. Each entry of these entries encodes information as follows: Bit 31/63 (most significant bit): This is called the Ordinal/Name flag, it specifies whether to import the function by name or by ordinal. Bits 15-0: If the Ordinal/Name flag is set to 1 these bits are used to hold the 16-bit ordinal number that will be used to import the function, bits 30-15/62-15 for PE32/PE32+ must be set to 0. Bits 30-0: If the Ordinal/Name flag is set to 0 these bits are used to hold an RVA of a Hint/Name table. Hint/Name Table A Hint/Name table is a structure defined in winnt.h as IMAGE_IMPORT_BY_NAME: typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; Hint: A word that contains a number, this number is used to look-up the function, that number is first used as an index into the export name pointer table, if that initial check fails a binary search is performed on the DLL’s export name pointer table. Name: A null-terminated string that contains the name of the function to import. Import Address Table (IAT) On disk, the IAT is identical to the ILT, however during bounding when the binary is being loaded into memory, the entries of the IAT get overwritten with the addresses of the functions that are being imported. Summary So to summarize what we discussed in this post, for every DLL the executable is loading functions from, there will be an IMAGE_IMPORT_DESCRIPTOR within the Image Directory Table. The IMAGE_IMPORT_DESCRIPTOR will contain the name of the DLL, and two fields holding RVAs of the ILT and the IAT. The ILT will contain references for all the functions that are being imported from the DLL. The IAT will be identical to the ILT until the executable is loaded in memory, then the loader will fill the IAT with the actual addresses of the imported functions. If the DLL import is a bound import, then the import information will be contained in IMAGE_BOUND_IMPORT_DESCRIPTOR structures in a separate Data Directory called the Bound Import Data Directory. Let’s take a quick look at the import information inside of an actual PE file. Here’s the Import Directory Table of the executable: All of these entries are IMAGE_IMPORT_DESCRIPTORs. As you can see, the TimeDateStamp of all the imports is set to 0, meaning that none of these imports are bound, this is also confirmed in the Bound? column added by PE-bear. For example, if we take USER32.dll and follow the RVA of its ILT (referenced by OriginalFirstThunk), we’ll find only 1 entry (because only one function is imported), and that entry looks like this: This is a 64-bit executable, so the entry is 64 bits long. As you can see, the last byte is set to 0, indicating that a Hint/Table name should be used to look-up the function. We know that the RVA of this Hint/Table name should be referenced by the first 2 bytes, so we should follow RVA 0x29F8: Now we’re looking at an IMAGE_IMPORT_BY_NAME structure, first two bytes hold the hint, which in this case is 0x283, the rest of the structure holds the full name of the function which is MessageBoxA. We can verify that our interpretation of the data is correct by looking at how PE-bear parsed it, and we’ll see the same results: Conclusion That’s all I have to say about PE imports, in the next post I’ll discuss PE base relocations. Thanks for reading., A dive into the PE file format - PE file structure - Part 5: PE Imports (Import Directory Table, ILT, IAT)IntroductionIn this post we’re going to talk about a very important aspect of PE files, the PE imports.To understand how PE files handle their imports, we’ll go over some of the Data Directories present in the Import Data section (.idata), the Import Directory Table, the Import Lookup Table (ILT) or also referred to as the Import Name Table (INT) and the Import Address Table (IAT).Import Directory TableThe Import Directory Table is a Data Directory located at the beginning of the .idata section.It consists of an array of IMAGE_IMPORT_DESCRIPTOR structures, each one of them is for a DLL.It doesn’t have a fixed size, so the last IMAGE_IMPORT_DESCRIPTOR of the array is zeroed-out (NULL-Padded) to indicate the end of the Import Directory Table.IMAGE_IMPORT_DESCRIPTOR is defined as follows:typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk;} IMAGE_IMPORT_DESCRIPTOR;typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; OriginalFirstThunk: RVA of the ILT. TimeDateStamp: A time date stamp, that’s initially set to 0 if not bound and set to -1 if bound.In case of an unbound import the time date stamp gets updated to the time date stamp of the DLL after the image is bound.In case of a bound import it stays set to -1 and the real time date stamp of the DLL can be found in the Bound Import Directory Table in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR .We’ll discuss bound imports in the next section. ForwarderChain: The index of the first forwarder chain reference.This is something responsible for DLL forwarding. (DLL forwarding is when a DLL forwards some of its exported functions to another DLL.) Name: An RVA of an ASCII string that contains the name of the imported DLL. FirstThunk: RVA of the IAT.Bound ImportsA bound import essentially means that the import table contains fixed addresses for the imported functions.These addresses are calculated and written during compile time by the linker.Using bound imports is a speed optimization, it reduces the time needed by the loader to resolve function addresses and fill the IAT, however if at run-time the bound addresses do not match the real ones then the loader will have to resolve these addresses again and fix the IAT.When discussing IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp, I mentioned that in case of a bound import, the time date stamp is set to -1 and the real time date stamp of the DLL can be found in the corresponding IMAGE_BOUND_IMPORT_DESCRIPTOR in the Bound Import Data Directory.Bound Import Data DirectoryThe Bound Import Data Directory is similar to the Import Directory Table, however as the name suggests, it holds information about the bound imports.It consists of an array of IMAGE_BOUND_IMPORT_DESCRIPTOR structures, and ends with a zeroed-out IMAGE_BOUND_IMPORT_DESCRIPTOR.IMAGE_BOUND_IMPORT_DESCRIPTOR is defined as follows:typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR { DWORD TimeDateStamp; WORD OffsetModuleName; WORD NumberOfModuleForwarderRefs;// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR; TimeDateStamp: The time date stamp of the imported DLL. OffsetModuleName: An offset to a string with the name of the imported DLL.It’s an offset from the first IMAGE_BOUND_IMPORT_DESCRIPTOR NumberOfModuleForwarderRefs: The number of the IMAGE_BOUND_FORWARDER_REF structures that immediately follow this structure.IMAGE_BOUND_FORWARDER_REF is a structure that’s identical to IMAGE_BOUND_IMPORT_DESCRIPTOR, the only difference is that the last member is reserved.That’s all we need to know about bound imports.Import Lookup Table (ILT)Sometimes people refer to it as the Import Name Table (INT).Every imported DLL has an Import Lookup Table.IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk holds the RVA of the ILT of the corresponding DLL.The ILT is essentially a table of names or references, it tells the loader which functions are needed from the imported DLL.The ILT consists of an array of 32-bit numbers (for PE32) or 64-bit numbers for (PE32+), the last one is zeroed-out to indicate the end of the ILT.Each entry of these entries encodes information as follows: Bit 31/63 (most significant bit): This is called the Ordinal/Name flag, it specifies whether to import the function by name or by ordinal. Bits 15-0: If the Ordinal/Name flag is set to 1 these bits are used to hold the 16-bit ordinal number that will be used to import the function, bits 30-15/62-15 for PE32/PE32+ must be set to 0. Bits 30-0: If the Ordinal/Name flag is set to 0 these bits are used to hold an RVA of a Hint/Name table.Hint/Name TableA Hint/Name table is a structure defined in winnt.h as IMAGE_IMPORT_BY_NAME:typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1];} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; Hint: A word that contains a number, this number is used to look-up the function, that number is first used as an index into the export name pointer table, if that initial check fails a binary search is performed on the DLL’s export name pointer table. Name: A null-terminated string that contains the name of the function to import.Import Address Table (IAT)On disk, the IAT is identical to the ILT, however during bounding when the binary is being loaded into memory, the entries of the IAT get overwritten with the addresses of the functions that are being imported.SummarySo to summarize what we discussed in this post, for every DLL the executable is loading functions from, there will be an IMAGE_IMPORT_DESCRIPTOR within the Image Directory Table.The IMAGE_IMPORT_DESCRIPTOR will contain the name of the DLL, and two fields holding RVAs of the ILT and the IAT.The ILT will contain references for all the functions that are being imported from the DLL.The IAT will be identical to the ILT until the executable is loaded in memory, then the loader will fill the IAT with the actual addresses of the imported functions.If the DLL import is a bound import, then the import information will be contained in IMAGE_BOUND_IMPORT_DESCRIPTOR structures in a separate Data Directory called the Bound Import Data Directory.Let’s take a quick look at the import information inside of an actual PE file.Here’s the Import Directory Table of the executable:All of these entries are IMAGE_IMPORT_DESCRIPTORs.As you can see, the TimeDateStamp of all the imports is set to 0, meaning that none of these imports are bound, this is also confirmed in the Bound? column added by PE-bear.For example, if we take USER32.dll and follow the RVA of its ILT (referenced by OriginalFirstThunk), we’ll find only 1 entry (because only one function is imported), and that entry looks like this:This is a 64-bit executable, so the entry is 64 bits long.As you can see, the last byte is set to 0, indicating that a Hint/Table name should be used to look-up the function.We know that the RVA of this Hint/Table name should be referenced by the first 2 bytes, so we should follow RVA 0x29F8:Now we’re looking at an IMAGE_IMPORT_BY_NAME structure, first two bytes hold the hint, which in this case is 0x283, the rest of the structure holds the full name of the function which is MessageBoxA.We can verify that our interpretation of the data is correct by looking at how PE-bear parsed it, and we’ll see the same results:ConclusionThat’s all I have to say about PE imports, in the next post I’ll discuss PE base relocations.Thanks for reading.
|
||
|
A dive into the PE file format - PE file structure - Part 6: PE Base Relocations - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe7/
A dive into the PE file format - PE file structure - Part 6: PE Base Relocations Introduction In this post we’re going to talk about PE base relocations. We’re going to discuss what relocations are, then we’ll take a look at the relocation table. Relocations When a program is compiled, the compiler assumes that the executable is going to be loaded at a certain base address, that address is saved in IMAGE_OPTIONAL_HEADER.ImageBase, some addresses get calculated then hardcoded within the executable based on the base address. However for a variety of reasons, it’s not very likely that the executable is going to get its desired base address, it will get loaded in another base address and that will make all of the hardcoded addresses invalid. A list of all hardcoded values that will need fixing if the image is loaded at a different base address is saved in a special table called the Relocation Table (a Data Directory within the .reloc section). The process of relocating (done by the loader) is what fixes these values. Let’s take an example, the following code defines an int variable and a pointer to that variable: int test = 2; int* testPtr = &test; During compile-time, the compiler will assume a base address, let’s say it assumes a base address of 0x1000, it decides that test will be located at an offset of 0x100 and based on that it gives testPtr a value of 0x1100. Later on, a user runs the program and the image gets loaded into memory. It gets a base address of 0x2000, this means that the hardcoded value of testPtr will be invalid, the loader fixes that value by adding the difference between the assumed base address and the actual base address, in this case it’s a difference of 0x1000 (0x2000 - 0x1000), so the new value of testPtr will be 0x2100 (0x1100 + 0x1000) which is the correct new address of test. Relocation Table As described by Microsoft documentation, the base relocation table contains entries for all base relocations in the image. It’s a Data Directory located within the .reloc section, it’s divided into blocks, each block represents the base relocations for a 4K page and each block must start on a 32-bit boundary. Each block starts with an IMAGE_BASE_RELOCATION structure followed by any number of offset field entries. The IMAGE_BASE_RELOCATION structure specifies the page RVA, and the size of the relocation block. typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION; Each offset field entry is a WORD, first 4 bits of it define the relocation type (check Microsoft documentation for a list of relocation types), the last 12 bits store an offset from the RVA specified in the IMAGE_BASE_RELOCATION structure at the start of the relocation block. Each relocation entry gets processed by adding the RVA of the page to the image base address, then by adding the offset specified in the relocation entry, an absolute address of the location that needs fixing can be obtained. The PE file I’m looking at contains only one relocation block, its size is 0x28 bytes: We know that each block starts with an 8-byte-long structure, meaning that the size of the entries is 0x20 bytes (32 bytes), each entry’s size is 2 bytes so the total number of entries should be 16. Conclusion That’s all. Thanks for reading., A dive into the PE file format - PE file structure - Part 6: PE Base RelocationsIntroductionIn this post we’re going to talk about PE base relocations.We’re going to discuss what relocations are, then we’ll take a look at the relocation table.RelocationsWhen a program is compiled, the compiler assumes that the executable is going to be loaded at a certain base address, that address is saved in IMAGE_OPTIONAL_HEADER.ImageBase, some addresses get calculated then hardcoded within the executable based on the base address.However for a variety of reasons, it’s not very likely that the executable is going to get its desired base address, it will get loaded in another base address and that will make all of the hardcoded addresses invalid.A list of all hardcoded values that will need fixing if the image is loaded at a different base address is saved in a special table called the Relocation Table (a Data Directory within the .reloc section).The process of relocating (done by the loader) is what fixes these values.Let’s take an example, the following code defines an int variable and a pointer to that variable:int test = 2;int* testPtr = &test;During compile-time, the compiler will assume a base address, let’s say it assumes a base address of 0x1000, it decides that test will be located at an offset of 0x100 and based on that it gives testPtr a value of 0x1100.Later on, a user runs the program and the image gets loaded into memory.It gets a base address of 0x2000, this means that the hardcoded value of testPtr will be invalid, the loader fixes that value by adding the difference between the assumed base address and the actual base address, in this case it’s a difference of 0x1000 (0x2000 - 0x1000), so the new value of testPtr will be 0x2100 (0x1100 + 0x1000) which is the correct new address of test.Relocation TableAs described by Microsoft documentation, the base relocation table contains entries for all base relocations in the image.It’s a Data Directory located within the .reloc section, it’s divided into blocks, each block represents the base relocations for a 4K page and each block must start on a 32-bit boundary.Each block starts with an IMAGE_BASE_RELOCATION structure followed by any number of offset field entries.The IMAGE_BASE_RELOCATION structure specifies the page RVA, and the size of the relocation block.typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock;} IMAGE_BASE_RELOCATION;typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;Each offset field entry is a WORD, first 4 bits of it define the relocation type (check Microsoft documentation for a list of relocation types), the last 12 bits store an offset from the RVA specified in the IMAGE_BASE_RELOCATION structure at the start of the relocation block.Each relocation entry gets processed by adding the RVA of the page to the image base address, then by adding the offset specified in the relocation entry, an absolute address of the location that needs fixing can be obtained.The PE file I’m looking at contains only one relocation block, its size is 0x28 bytes:We know that each block starts with an 8-byte-long structure, meaning that the size of the entries is 0x20 bytes (32 bytes), each entry’s size is 2 bytes so the total number of entries should be 16.ConclusionThat’s all.Thanks for reading.
|
||
|
A dive into the PE file format - LAB 1: Writing a PE Parser - 0xRick’s Blog
https://0xrick.github.io/win-internals/pe8/
A dive into the PE file format - LAB 1: Writing a PE Parser Introduction In the previous posts we’ve discussed the basic structure of PE files, In this post we’re going to apply this knowledge into building a PE file parser in c++ as a proof of concept. The parser we’re going to build will not be a full parser and is not intended to be used as a reliable tool, this is only an exercise to better understand the PE file structure. We’re going to focus on PE32 and PE32+ files, and we’ll only parse the following parts of the file: DOS Header Rich Header NT Headers Data Directories (within the Optional Header) Section Headers Import Table Base Relocations Table The code of this project can be found on my github profile. Initial Setup Process Outline We want out parser to follow the following process: Read a file. Validate that it’s a PE file. Determine whether it’s a PE32 or a PE32+. Parse out the following structures: DOS Header Rich Header NT Headers Section Headers Import Data Directory Base Relocation Data Directory Print out the following information: File name and type. DOS Header: Magic value. Address of new exe header. Each entry of the Rich Header, decrypted and decoded. NT Headers - PE file signature. NT Headers - File Header: Machine value. Number of sections. Size of Optional Header. NT Headers - Optional Header: Magic value. Size of code section. Size of initialized data. Size of uninitialized data. Address of entry point. RVA of start of code section. Desired Image Base. Section alignment. File alignment. Size of image. Size of headers. For each Data Directory: its name, RVA and size. For each Section Header: Section name. Section virtual address and size. Section raw data pointer and size. Section characteristics value. Import Table: For each DLL: DLL name. ILT and IAT RVAs. Whether its a bound import or not. for every imported function: Ordinal if ordinal/name flag is 1. Name, hint and Hint/Name table RVA if ordinal/name flag is 0. Base Relocation Table: For each block: Page RVA. Block size. Number of entries. For each entry: Raw value. Relocation offset. Relocation Type. winnt.h Definitions We will need the following definitions from the winnt.h header: Types: BYTE WORD DWORD QWORD LONG LONGLONG ULONGLONG Constants: IMAGE_NT_OPTIONAL_HDR32_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC IMAGE_NUMBEROF_DIRECTORY_ENTRIES IMAGE_DOS_SIGNATURE IMAGE_DIRECTORY_ENTRY_EXPORT IMAGE_DIRECTORY_ENTRY_IMPORT IMAGE_DIRECTORY_ENTRY_RESOURCE IMAGE_DIRECTORY_ENTRY_EXCEPTION IMAGE_DIRECTORY_ENTRY_SECURITY IMAGE_DIRECTORY_ENTRY_BASERELOC IMAGE_DIRECTORY_ENTRY_DEBUG IMAGE_DIRECTORY_ENTRY_ARCHITECTURE IMAGE_DIRECTORY_ENTRY_GLOBALPTR IMAGE_DIRECTORY_ENTRY_TLS IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT IMAGE_DIRECTORY_ENTRY_IAT IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR IMAGE_SIZEOF_SHORT_NAME IMAGE_SIZEOF_SECTION_HEADER Structures: IMAGE_DOS_HEADER IMAGE_DATA_DIRECTORY IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER64 IMAGE_FILE_HEADER IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS64 IMAGE_IMPORT_DESCRIPTOR IMAGE_IMPORT_BY_NAME IMAGE_BASE_RELOCATION IMAGE_SECTION_HEADER I took these definitions from winnt.h and added them to a new header called winntdef.h. winntdef.h: typedef unsigned char BYTE; typedef unsigned short WORD; typedef unsigned long DWORD; typedef unsigned long long QWORD; typedef unsigned long LONG; typedef __int64 LONGLONG; typedef unsigned __int64 ULONGLONG; #define ___IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b #define ___IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b #define ___IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 #define ___IMAGE_DOS_SIGNATURE 0x5A4D #define ___IMAGE_DIRECTORY_ENTRY_EXPORT 0 #define ___IMAGE_DIRECTORY_ENTRY_IMPORT 1 #define ___IMAGE_DIRECTORY_ENTRY_RESOURCE 2 #define ___IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 #define ___IMAGE_DIRECTORY_ENTRY_SECURITY 4 #define ___IMAGE_DIRECTORY_ENTRY_BASERELOC 5 #define ___IMAGE_DIRECTORY_ENTRY_DEBUG 6 #define ___IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 #define ___IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 #define ___IMAGE_DIRECTORY_ENTRY_TLS 9 #define ___IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 #define ___IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 #define ___IMAGE_DIRECTORY_ENTRY_IAT 12 #define ___IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 #define ___IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 #define ___IMAGE_SIZEOF_SHORT_NAME 8 #define ___IMAGE_SIZEOF_SECTION_HEADER 40 typedef struct __IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew; } ___IMAGE_DOS_HEADER, * ___PIMAGE_DOS_HEADER; typedef struct __IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } ___IMAGE_DATA_DIRECTORY, * ___PIMAGE_DATA_DIRECTORY; typedef struct __IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } ___IMAGE_OPTIONAL_HEADER32, * ___PIMAGE_OPTIONAL_HEADER32; typedef struct __IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } ___IMAGE_OPTIONAL_HEADER64, * ___PIMAGE_OPTIONAL_HEADER64; typedef struct __IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } ___IMAGE_FILE_HEADER, * ___PIMAGE_FILE_HEADER; typedef struct __IMAGE_NT_HEADERS64 { DWORD Signature; ___IMAGE_FILE_HEADER FileHeader; ___IMAGE_OPTIONAL_HEADER64 OptionalHeader; } ___IMAGE_NT_HEADERS64, * ___PIMAGE_NT_HEADERS64; typedef struct __IMAGE_NT_HEADERS { DWORD Signature; ___IMAGE_FILE_HEADER FileHeader; ___IMAGE_OPTIONAL_HEADER32 OptionalHeader; } ___IMAGE_NT_HEADERS32, * ___PIMAGE_NT_HEADERS32; typedef struct __IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } ___IMAGE_IMPORT_DESCRIPTOR, * ___PIMAGE_IMPORT_DESCRIPTOR; typedef struct __IMAGE_IMPORT_BY_NAME { WORD Hint; char Name[100]; } ___IMAGE_IMPORT_BY_NAME, * ___PIMAGE_IMPORT_BY_NAME; typedef struct __IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } ___IMAGE_BASE_RELOCATION, * ___PIMAGE_BASE_RELOCATION; typedef struct __IMAGE_SECTION_HEADER { BYTE Name[___IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } ___IMAGE_SECTION_HEADER, * ___PIMAGE_SECTION_HEADER; Custom Structures I defined the following structures to help with the parsing process. They’re defined in the PEFILE_CUSTOM_STRUCTS.h header. RICH_HEADER_INFO A structure to hold information about the Rich Header during processing. typedef struct __RICH_HEADER_INFO { int size; char* ptrToBuffer; int entries; } RICH_HEADER_INFO, * PRICH_HEADER_INFO; size: Size of the Rich Header (in bytes). ptrToBuffer: A pointer to the buffer containing the data of the Rich Header. entries: Number of entries in the Rich Header. RICH_HEADER_ENTRY A structure to represent a Rich Header entry. typedef struct __RICH_HEADER_ENTRY { WORD prodID; WORD buildID; DWORD useCount; } RICH_HEADER_ENTRY, * PRICH_HEADER_ENTRY; prodID: Type ID / Product ID. buildID: Build ID. useCount: Use count. RICH_HEADER A structure to represent the Rich Header. typedef struct __RICH_HEADER { PRICH_HEADER_ENTRY entries; } RICH_HEADER, * PRICH_HEADER; entries: A pointer to a RICH_HEADER_ENTRY array. ILT_ENTRY_32 A structure to represent a 32-bit ILT entry during processing. typedef struct __ILT_ENTRY_32 { union { DWORD ORDINAL : 16; DWORD HINT_NAME_TABE : 32; DWORD ORDINAL_NAME_FLAG : 1; } FIELD_1; } ILT_ENTRY_32, * PILT_ENTRY_32; The structure will hold a 32-bit value and will return the appropriate piece of information (using bit fields) when the member corresponding to that piece of information is accessed. ILT_ENTRY_64 A structure to represent a 64-bit ILT entry during processing. typedef struct __ILT_ENTRY_64 { union { DWORD ORDINAL : 16; DWORD HINT_NAME_TABE : 32; } FIELD_2; DWORD ORDINAL_NAME_FLAG : 1; } ILT_ENTRY_64, * PILT_ENTRY_64; The structure will hold a 64-bit value and will return the appropriate piece of information (using bit fields) when the member corresponding to that piece of information is accessed. BASE_RELOC_ENTRY A structure to represent a base relocation entry during processing. typedef struct __BASE_RELOC_ENTRY { WORD OFFSET : 12; WORD TYPE : 4; } BASE_RELOC_ENTRY, * PBASE_RELOC_ENTRY; OFFSET: Relocation offset. TYPE: Relocation type. PEFILE Our parser will represent a PE file as an object type of either PE32FILE or PE64FILE. These 2 classes only differ in some member definitions but their functionality is identical. Throughout this post we will use the code from PE64FILE. Definition The class is defined as follows: class PE64FILE { public: PE64FILE(char* _NAME, FILE* Ppefile); void PrintInfo(); private: char* NAME; FILE* Ppefile; int _import_directory_count, _import_directory_size; int _basreloc_directory_count; // HEADERS ___IMAGE_DOS_HEADER PEFILE_DOS_HEADER; ___IMAGE_NT_HEADERS64 PEFILE_NT_HEADERS; // DOS HEADER DWORD PEFILE_DOS_HEADER_EMAGIC; LONG PEFILE_DOS_HEADER_LFANEW; // RICH HEADER RICH_HEADER_INFO PEFILE_RICH_HEADER_INFO; RICH_HEADER PEFILE_RICH_HEADER; // NT_HEADERS.Signature DWORD PEFILE_NT_HEADERS_SIGNATURE; // NT_HEADERS.FileHeader WORD PEFILE_NT_HEADERS_FILE_HEADER_MACHINE; WORD PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; WORD PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER; // NT_HEADERS.OptionalHeader DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE; ULONGLONG PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS; ___IMAGE_DATA_DIRECTORY PEFILE_EXPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_IMPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_RESOURCE_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_EXCEPTION_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_SECURITY_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_BASERELOC_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_DEBUG_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_ARCHITECTURE_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_GLOBALPTR_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_TLS_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_LOAD_CONFIG_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_BOUND_IMPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_IAT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_DELAY_IMPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_COM_DESCRIPTOR_DIRECTORY; // SECTION HEADERS ___PIMAGE_SECTION_HEADER PEFILE_SECTION_HEADERS; // IMPORT TABLE ___PIMAGE_IMPORT_DESCRIPTOR PEFILE_IMPORT_TABLE; // BASE RELOCATION TABLE ___PIMAGE_BASE_RELOCATION PEFILE_BASERELOC_TABLE; // FUNCTIONS // ADDRESS RESOLVERS int locate(DWORD VA); DWORD resolve(DWORD VA, int index); // PARSERS void ParseFile(); void ParseDOSHeader(); void ParseNTHeaders(); void ParseSectionHeaders(); void ParseImportDirectory(); void ParseBaseReloc(); void ParseRichHeader(); // PRINT INFO void PrintFileInfo(); void PrintDOSHeaderInfo(); void PrintRichHeaderInfo(); void PrintNTHeadersInfo(); void PrintSectionHeadersInfo(); void PrintImportTableInfo(); void PrintBaseRelocationsInfo(); }; The only public member beside the class constructor is a function called printInfo() which will print information about the file. The class constructor takes two parameters, a char array representing the name of the file and a file pointer to the actual data of the file. After that comes a long series of variables definitions, these class members are going to be used internally during the parsing process and we’ll mention each one of them later. In the end is a series of methods definitions, first two methods are called locate and resolve, I will talk about them in a minute. The rest are functions responsible for parsing different parts of the file, and functions responsible for printing information about the same parts. Constructor The constructor of the class simply sets the file pointer and name variables, then it calls the ParseFile() function. PE64FILE::PE64FILE(char* _NAME, FILE* _Ppefile) { NAME = _NAME; Ppefile = _Ppefile; ParseFile(); } The ParseFile() function calls the other parser functions: void PE64FILE::ParseFile() { // PARSE DOS HEADER ParseDOSHeader(); // PARSE RICH HEADER ParseRichHeader(); //PARSE NT HEADERS ParseNTHeaders(); // PARSE SECTION HEADERS ParseSectionHeaders(); // PARSE IMPORT DIRECTORY ParseImportDirectory(); // PARSE BASE RELOCATIONS ParseBaseReloc(); } Resolving RVAs Most of the time, we’ll have a RVA that we’ll need to change to a file offset. The process of resolving an RVA can be outlined as follows: Determine which section range contains that RVA: Iterate over all sections and for each section compare the RVA to the section virtual address and to the section virtual address added to the virtual size of the section. If the RVA exists within this range then it belongs to that section. Calculate the file offset: Subtract the RVA from the section virtual address. Add that value to the raw data pointer of the section. An example of this is locating a Data Directory. The IMAGE_DATA_DIRECTORY structure only gives us an RVA of the directory, to locate that directory we’ll need to resolve that address. I wrote two functions to do this, first one to locate the virtual address (locate()), second one to resolve the address (resolve()). int PE64FILE::locate(DWORD VA) { int index; for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) { if (VA >= PEFILE_SECTION_HEADERS[i].VirtualAddress && VA < (PEFILE_SECTION_HEADERS[i].VirtualAddress + PEFILE_SECTION_HEADERS[i].Misc.VirtualSize)){ index = i; break; } } return index; } DWORD PE64FILE::resolve(DWORD VA, int index) { return (VA - PEFILE_SECTION_HEADERS[index].VirtualAddress) + PEFILE_SECTION_HEADERS[index].PointerToRawData; } locate() iterates over the PEFILE_SECTION_HEADERS array, compares the RVA as described above, then it returns the index of the appropriate section header within the PEFILE_SECTION_HEADERS array. Please note that in order for these functions to work we’ll need to parse out the section headers and fill the PEFILE_SECTION_HEADERS array first. We still haven’t discussed this part, but I wanted to talk about the address resolvers first. main function The main function of the program is fairly simple, it only does 2 things: Create a file pointer to the given file, and validate that the file was read correctly. Call INITPARSE() on the file, and based on the return value it decides between three actions: Exit. Create a PE32FILE object, call PrintInfo(), close the file pointer then exit. Create a PE64FILE object, call PrintInfo(), close the file pointer then exit. PrintInfo() calls the other print info functions. int main(int argc, char* argv[]) { if (argc != 2) { printf("Usage: %s [path to executable]\n", argv[0]); return 1; } FILE * PpeFile; fopen_s(&PpeFile, argv[1], "rb"); if (PpeFile == NULL) { printf("Can't open file.\n"); return 1; } if (INITPARSE(PpeFile) == 1) { exit(1); } else if (INITPARSE(PpeFile) == 32) { PE32FILE PeFile_1(argv[1], PpeFile); PeFile_1.PrintInfo(); fclose(PpeFile); exit(0); } else if (INITPARSE(PpeFile) == 64) { PE64FILE PeFile_1(argv[1], PpeFile); PeFile_1.PrintInfo(); fclose(PpeFile); exit(0); } return 0; } INITPARSE() INITPARSE() is a function defined in PEFILE.cpp. Its only job is to validate that the given file is a PE file, then determine whether the file is PE32 or PE32+. It reads the DOS header of the file and checks the DOS MZ header, if not found it returns an error. After validating the PE file, it sets the file position to (DOS_HEADER.e_lfanew + size of DWORD (PE signature) + size of the file header) which is the exact offset of the beginning of the Optional Header. Then it reads a WORD, we know that the first WORD of the Optional Header is a magic value that indicates the file type, it then compares that word to IMAGE_NT_OPTIONAL_HDR32_MAGIC and IMAGE_NT_OPTIONAL_HDR64_MAGIC, and based on the comparison results it either returns 32 or 64 indicating PE32 or PE32+, or it returns an error. int INITPARSE(FILE* PpeFile) { ___IMAGE_DOS_HEADER TMP_DOS_HEADER; WORD PEFILE_TYPE; fseek(PpeFile, 0, SEEK_SET); fread(&TMP_DOS_HEADER, sizeof(___IMAGE_DOS_HEADER), 1, PpeFile); if (TMP_DOS_HEADER.e_magic != ___IMAGE_DOS_SIGNATURE) { printf("Error. Not a PE file.\n"); return 1; } fseek(PpeFile, (TMP_DOS_HEADER.e_lfanew + sizeof(DWORD) + sizeof(___IMAGE_FILE_HEADER)), SEEK_SET); fread(&PEFILE_TYPE, sizeof(WORD), 1, PpeFile); if (PEFILE_TYPE == ___IMAGE_NT_OPTIONAL_HDR32_MAGIC) { return 32; } else if (PEFILE_TYPE == ___IMAGE_NT_OPTIONAL_HDR64_MAGIC) { return 64; } else { printf("Error while parsing IMAGE_OPTIONAL_HEADER.Magic. Unknown Type.\n"); return 1; } } Parsing DOS Header ParseDOSHeader() Parsing out the DOS Header is nothing complicated, we just need to read from the beginning of the file an amount of bytes equal to the size of the DOS Header, then we can assign that data to the pre-defined class member PEFILE_DOS_HEADER. From there we can access all of the struct members, however we’re only interested in e_magic and e_lfanew. void PE64FILE::ParseDOSHeader() { fseek(Ppefile, 0, SEEK_SET); fread(&PEFILE_DOS_HEADER, sizeof(___IMAGE_DOS_HEADER), 1, Ppefile); PEFILE_DOS_HEADER_EMAGIC = PEFILE_DOS_HEADER.e_magic; PEFILE_DOS_HEADER_LFANEW = PEFILE_DOS_HEADER.e_lfanew; } PrintDOSHeaderInfo() This function prints e_magic and e_lfanew values. void PE64FILE::PrintDOSHeaderInfo() { printf(" DOS HEADER:\n"); printf(" -----------\n\n"); printf(" Magic: 0x%X\n", PEFILE_DOS_HEADER_EMAGIC); printf(" File address of new exe header: 0x%X\n", PEFILE_DOS_HEADER_LFANEW); } Parsing Rich Header Process To parse out the Rich Header we’ll need to go through multiple steps. We don’t know anything about the Rich Header, we don’t know its size, we don’t know where it’s exactly located, we don’t even know if the file we’re processing contains a Rich Header in the first place. First of all, we need to locate the Rich Header. We don’t know the exact location, however we have everything we need to locate it. We know that if a Rich Header exists, then it has to exist between the DOS Stub and the PE signature or the beginning of the NT Headers. We also know that any Rich Header ends with a 32-bit value Rich followed by the XOR key. One might rely on the fixed size of the DOS Header and the DOS Stub, however, the default DOS Stub message can be changed, so that size is not guaranteed to be fixed. A better approach would be to read from the beginning of the file to the start of the NT Headers, then search through that buffer for the Rich sequence, if found then we’ve successfully located the end of the Rich Header, if not found then most likely the file doesn’t contain a Rich Header. Once we’ve located the end of the Rich Header, we can read the XOR key, then go backwards starting from the Rich signature and keep XORing 4 bytes at a time until we reach the DanS signature which indicates the beginning of the Rich Header. After obtaining the position and the size of the Rich Header, we can normally read and process the data. ParseRichHeader() This function starts by allocating a buffer on the heap, then it reads e_lfanew size of bytes from the beginning of the file and stores the data in the allocated buffer. It then goes through a loop where it does a linear search byte by byte. In each iteration it compares the current byte and the byte the follows to 0x52 (R) and 0x69 (i). When the sequence is found, it stores the index in a variable then the loop breaks. char* dataPtr = new char[PEFILE_DOS_HEADER_LFANEW]; fseek(Ppefile, 0, SEEK_SET); fread(dataPtr, PEFILE_DOS_HEADER_LFANEW, 1, Ppefile); int index_ = 0; for (int i = 0; i <= PEFILE_DOS_HEADER_LFANEW; i++) { if (dataPtr[i] == 0x52 && dataPtr[i + 1] == 0x69) { index_ = i; break; } } if (index_ == 0) { printf("Error while parsing Rich Header."); PEFILE_RICH_HEADER_INFO.entries = 0; return; } After that it reads the XOR key, then goes into the decryption loop where in each iteration it increments RichHeaderSize by 4 until it reaches the DanS sequence. char key[4]; memcpy(key, dataPtr + (index_ + 4), 4); int indexpointer = index_ - 4; int RichHeaderSize = 0; while (true) { char tmpchar[4]; memcpy(tmpchar, dataPtr + indexpointer, 4); for (int i = 0; i < 4; i++) { tmpchar[i] = tmpchar[i] ^ key[i]; } indexpointer -= 4; RichHeaderSize += 4; if (tmpchar[1] = 0x61 && tmpchar[0] == 0x44) { break; } } After obtaining the size and the position, it allocates a new buffer for the Rich Header, reads and decrypts the Rich Header, updates PEFILE_RICH_HEADER_INFO with the appropriate data pointer, size and number of entries, then finally it deallocates the buffer it was using for processing. char* RichHeaderPtr = new char[RichHeaderSize]; memcpy(RichHeaderPtr, dataPtr + (index_ - RichHeaderSize), RichHeaderSize); for (int i = 0; i < RichHeaderSize; i += 4) { for (int x = 0; x < 4; x++) { RichHeaderPtr[i + x] = RichHeaderPtr[i + x] ^ key[x]; } } PEFILE_RICH_HEADER_INFO.size = RichHeaderSize; PEFILE_RICH_HEADER_INFO.ptrToBuffer = RichHeaderPtr; PEFILE_RICH_HEADER_INFO.entries = (RichHeaderSize - 16) / 8; delete[] dataPtr; The rest of the function reads each entry of the Rich Header and updates PEFILE_RICH_HEADER. PEFILE_RICH_HEADER.entries = new RICH_HEADER_ENTRY[PEFILE_RICH_HEADER_INFO.entries]; for (int i = 16; i < RichHeaderSize; i += 8) { WORD PRODID = (uint16_t)((unsigned char)RichHeaderPtr[i + 3] << 8) | (unsigned char)RichHeaderPtr[i + 2]; WORD BUILDID = (uint16_t)((unsigned char)RichHeaderPtr[i + 1] << 8) | (unsigned char)RichHeaderPtr[i]; DWORD USECOUNT = (uint32_t)((unsigned char)RichHeaderPtr[i + 7] << 24) | (unsigned char)RichHeaderPtr[i + 6] << 16 | (unsigned char)RichHeaderPtr[i + 5] << 8 | (unsigned char)RichHeaderPtr[i + 4]; PEFILE_RICH_HEADER.entries[(i / 8) - 2] = { PRODID, BUILDID, USECOUNT }; if (i + 8 >= RichHeaderSize) { PEFILE_RICH_HEADER.entries[(i / 8) - 1] = { 0x0000, 0x0000, 0x00000000 }; } } delete[] PEFILE_RICH_HEADER_INFO.ptrToBuffer; Here’s the full function: void PE64FILE::ParseRichHeader() { char* dataPtr = new char[PEFILE_DOS_HEADER_LFANEW]; fseek(Ppefile, 0, SEEK_SET); fread(dataPtr, PEFILE_DOS_HEADER_LFANEW, 1, Ppefile); int index_ = 0; for (int i = 0; i <= PEFILE_DOS_HEADER_LFANEW; i++) { if (dataPtr[i] == 0x52 && dataPtr[i + 1] == 0x69) { index_ = i; break; } } if (index_ == 0) { printf("Error while parsing Rich Header."); PEFILE_RICH_HEADER_INFO.entries = 0; return; } char key[4]; memcpy(key, dataPtr + (index_ + 4), 4); int indexpointer = index_ - 4; int RichHeaderSize = 0; while (true) { char tmpchar[4]; memcpy(tmpchar, dataPtr + indexpointer, 4); for (int i = 0; i < 4; i++) { tmpchar[i] = tmpchar[i] ^ key[i]; } indexpointer -= 4; RichHeaderSize += 4; if (tmpchar[1] = 0x61 && tmpchar[0] == 0x44) { break; } } char* RichHeaderPtr = new char[RichHeaderSize]; memcpy(RichHeaderPtr, dataPtr + (index_ - RichHeaderSize), RichHeaderSize); for (int i = 0; i < RichHeaderSize; i += 4) { for (int x = 0; x < 4; x++) { RichHeaderPtr[i + x] = RichHeaderPtr[i + x] ^ key[x]; } } PEFILE_RICH_HEADER_INFO.size = RichHeaderSize; PEFILE_RICH_HEADER_INFO.ptrToBuffer = RichHeaderPtr; PEFILE_RICH_HEADER_INFO.entries = (RichHeaderSize - 16) / 8; delete[] dataPtr; PEFILE_RICH_HEADER.entries = new RICH_HEADER_ENTRY[PEFILE_RICH_HEADER_INFO.entries]; for (int i = 16; i < RichHeaderSize; i += 8) { WORD PRODID = (uint16_t)((unsigned char)RichHeaderPtr[i + 3] << 8) | (unsigned char)RichHeaderPtr[i + 2]; WORD BUILDID = (uint16_t)((unsigned char)RichHeaderPtr[i + 1] << 8) | (unsigned char)RichHeaderPtr[i]; DWORD USECOUNT = (uint32_t)((unsigned char)RichHeaderPtr[i + 7] << 24) | (unsigned char)RichHeaderPtr[i + 6] << 16 | (unsigned char)RichHeaderPtr[i + 5] << 8 | (unsigned char)RichHeaderPtr[i + 4]; PEFILE_RICH_HEADER.entries[(i / 8) - 2] = { PRODID, BUILDID, USECOUNT }; if (i + 8 >= RichHeaderSize) { PEFILE_RICH_HEADER.entries[(i / 8) - 1] = { 0x0000, 0x0000, 0x00000000 }; } } delete[] PEFILE_RICH_HEADER_INFO.ptrToBuffer; } PrintRichHeaderInfo() This function iterates over each entry in PEFILE_RICH_HEADER and prints its value. void PE64FILE::PrintRichHeaderInfo() { printf(" RICH HEADER:\n"); printf(" ------------\n\n"); for (int i = 0; i < PEFILE_RICH_HEADER_INFO.entries; i++) { printf(" 0x%X 0x%X 0x%X: %d.%d.%d\n", PEFILE_RICH_HEADER.entries[i].buildID, PEFILE_RICH_HEADER.entries[i].prodID, PEFILE_RICH_HEADER.entries[i].useCount, PEFILE_RICH_HEADER.entries[i].buildID, PEFILE_RICH_HEADER.entries[i].prodID, PEFILE_RICH_HEADER.entries[i].useCount); } } Parsing NT Headers ParseNTHeaders() Similar to the DOS Header, all we need to do is to read from e_lfanew an amount of bytes equal to the size of IMAGE_NT_HEADERS. After that we can parse out the contents of the File Header and the Optional Header. The Optional Header contains an array of IMAGE_DATA_DIRECTORY structures which we care about. To parse out this information, we can use the IMAGE_DIRECTORY_[...] constants defined in winnt.h as array indexes to access the corresponding IMAGE_DATA_DIRECTORY structure of each Data Directory. void PE64FILE::ParseNTHeaders() { fseek(Ppefile, PEFILE_DOS_HEADER.e_lfanew, SEEK_SET); fread(&PEFILE_NT_HEADERS, sizeof(PEFILE_NT_HEADERS), 1, Ppefile); PEFILE_NT_HEADERS_SIGNATURE = PEFILE_NT_HEADERS.Signature; PEFILE_NT_HEADERS_FILE_HEADER_MACHINE = PEFILE_NT_HEADERS.FileHeader.Machine; PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS = PEFILE_NT_HEADERS.FileHeader.NumberOfSections; PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER = PEFILE_NT_HEADERS.FileHeader.SizeOfOptionalHeader; PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC = PEFILE_NT_HEADERS.OptionalHeader.Magic; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE = PEFILE_NT_HEADERS.OptionalHeader.SizeOfCode; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA = PEFILE_NT_HEADERS.OptionalHeader.SizeOfInitializedData; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA = PEFILE_NT_HEADERS.OptionalHeader.SizeOfUninitializedData; PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT = PEFILE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint; PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE = PEFILE_NT_HEADERS.OptionalHeader.BaseOfCode; PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE = PEFILE_NT_HEADERS.OptionalHeader.ImageBase; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT = PEFILE_NT_HEADERS.OptionalHeader.SectionAlignment; PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT = PEFILE_NT_HEADERS.OptionalHeader.FileAlignment; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE = PEFILE_NT_HEADERS.OptionalHeader.SizeOfImage; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS = PEFILE_NT_HEADERS.OptionalHeader.SizeOfHeaders; PEFILE_EXPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_EXPORT]; PEFILE_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_IMPORT]; PEFILE_RESOURCE_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_RESOURCE]; PEFILE_EXCEPTION_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_EXCEPTION]; PEFILE_SECURITY_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_SECURITY]; PEFILE_BASERELOC_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_BASERELOC]; PEFILE_DEBUG_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_DEBUG]; PEFILE_ARCHITECTURE_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_ARCHITECTURE]; PEFILE_GLOBALPTR_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_GLOBALPTR]; PEFILE_TLS_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_TLS]; PEFILE_LOAD_CONFIG_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG]; PEFILE_BOUND_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT]; PEFILE_IAT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_IAT]; PEFILE_DELAY_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT]; PEFILE_COM_DESCRIPTOR_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR]; } PrintNTHeadersInfo() This function prints the data obtained from the File Header and the Optional Header, and for each Data Directory it prints its RVA and size. void PE64FILE::PrintNTHeadersInfo() { printf(" NT HEADERS:\n"); printf(" -----------\n\n"); printf(" PE Signature: 0x%X\n", PEFILE_NT_HEADERS_SIGNATURE); printf("\n File Header:\n\n"); printf(" Machine: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_MACHINE); printf(" Number of sections: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS); printf(" Size of optional header: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER); printf("\n Optional Header:\n\n"); printf(" Magic: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC); printf(" Size of code section: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE); printf(" Size of initialized data: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA); printf(" Size of uninitialized data: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA); printf(" Address of entry point: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT); printf(" RVA of start of code section: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE); printf(" Desired image base: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE); printf(" Section alignment: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT); printf(" File alignment: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT); printf(" Size of image: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE); printf(" Size of headers: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS); printf("\n Data Directories:\n"); printf("\n * Export Directory:\n"); printf(" RVA: 0x%X\n", PEFILE_EXPORT_DIRECTORY.VirtualAddress); printf(" Size: 0x%X\n", PEFILE_EXPORT_DIRECTORY.Size); . . [REDACTED] . . printf("\n * COM Runtime Descriptor:\n"); printf(" RVA: 0x%X\n", PEFILE_COM_DESCRIPTOR_DIRECTORY.VirtualAddress); printf(" Size: 0x%X\n", PEFILE_COM_DESCRIPTOR_DIRECTORY.Size); } Parsing Section Headers ParseSectionHeaders() This function starts by assigning the PEFILE_SECTION_HEADERS class member to a pointer to an IMAGE_SECTION_HEADER array of the count of PEFILE_NT_HEADERS_FILE_HEADER_NUMBEROF_SECTIONS. Then it goes into a loop of PEFILE_NT_HEADERS_FILE_HEADER_NUMBEROF_SECTIONS iterations where in each iteration it changes the file offset to (e_lfanew + size of NT Headers + loop counter multiplied by the size of a section header) to reach the beginning of the next Section Header, then it reads the new Section Header and assigns it to the next element of PEFILE_SECTION_HEADERS. void PE64FILE::ParseSectionHeaders() { PEFILE_SECTION_HEADERS = new ___IMAGE_SECTION_HEADER[PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS]; for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) { int offset = (PEFILE_DOS_HEADER.e_lfanew + sizeof(PEFILE_NT_HEADERS)) + (i * ___IMAGE_SIZEOF_SECTION_HEADER); fseek(Ppefile, offset, SEEK_SET); fread(&PEFILE_SECTION_HEADERS[i], ___IMAGE_SIZEOF_SECTION_HEADER, 1, Ppefile); } } PrintSectionHeadersInfo() This function loops over the Section Headers array (filled by ParseSectionHeaders()), and it prints information about each section. void PE64FILE::PrintSectionHeadersInfo() { printf(" SECTION HEADERS:\n"); printf(" ----------------\n\n"); for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) { printf(" * %.8s:\n", PEFILE_SECTION_HEADERS[i].Name); printf(" VirtualAddress: 0x%X\n", PEFILE_SECTION_HEADERS[i].VirtualAddress); printf(" VirtualSize: 0x%X\n", PEFILE_SECTION_HEADERS[i].Misc.VirtualSize); printf(" PointerToRawData: 0x%X\n", PEFILE_SECTION_HEADERS[i].PointerToRawData); printf(" SizeOfRawData: 0x%X\n", PEFILE_SECTION_HEADERS[i].SizeOfRawData); printf(" Characteristics: 0x%X\n\n", PEFILE_SECTION_HEADERS[i].Characteristics); } } Parsing Imports ParseImportDirectory() To parse out the Import Directory Table we need to determine the count of IMAGE_IMPORT_DESCRIPTORs first. This function starts by resolving the file offset of the Import Directory, then it goes into a loop where in each loop it keeps reading the next import descriptor. In each iteration it checks if the descriptor has zeroed out values, if that is the case then we’ve reached the end of the Import Directory, so it breaks. Otherwise it increments _import_directory_count and the loop continues. After finding the size of the Import Directory, the function assigns the PEFILE_IMPORT_TABLE class member to a pointer to an IMAGE_IMPORT_DESCRIPTOR array of the count of _import_directory_count then goes into another loop similar to the one we’ve seen in ParseSectionHeaders() to parse out the import descriptors. void PE64FILE::ParseImportDirectory() { DWORD _import_directory_address = resolve(PEFILE_IMPORT_DIRECTORY.VirtualAddress, locate(PEFILE_IMPORT_DIRECTORY.VirtualAddress)); _import_directory_count = 0; while (true) { ___IMAGE_IMPORT_DESCRIPTOR tmp; int offset = (_import_directory_count * sizeof(___IMAGE_IMPORT_DESCRIPTOR)) + _import_directory_address; fseek(Ppefile, offset, SEEK_SET); fread(&tmp, sizeof(___IMAGE_IMPORT_DESCRIPTOR), 1, Ppefile); if (tmp.Name == 0x00000000 && tmp.FirstThunk == 0x00000000) { _import_directory_count -= 1; _import_directory_size = _import_directory_count * sizeof(___IMAGE_IMPORT_DESCRIPTOR); break; } _import_directory_count++; } PEFILE_IMPORT_TABLE = new ___IMAGE_IMPORT_DESCRIPTOR[_import_directory_count]; for (int i = 0; i < _import_directory_count; i++) { int offset = (i * sizeof(___IMAGE_IMPORT_DESCRIPTOR)) + _import_directory_address; fseek(Ppefile, offset, SEEK_SET); fread(&PEFILE_IMPORT_TABLE[i], sizeof(___IMAGE_IMPORT_DESCRIPTOR), 1, Ppefile); } } PrintImportTableInfo() After obtaining the import descriptors, further parsing is needed to retrieve information about the imported functions. This is done by the PrintImportTableInfo() function. This function iterates over the import descriptors, and for each descriptor it resolves the file offset of the DLL name, retrieves the DLL name then prints it, it also prints the ILT RVA, the IAT RVA and whether the import is bound or not. After that it resolves the file offset of the ILT then it parses out each ILT entry. If the Ordinal/Name flag is set it prints the function ordinal, otherwise it prints the function name, the hint RVA and the hint. If the ILT entry is zeroed out, the loop breaks and the next import descriptor parsing iteration starts. We’ve discussed the details about this in the PE imports post. void PE64FILE::PrintImportTableInfo() { printf(" IMPORT TABLE:\n"); printf(" ----------------\n\n"); for (int i = 0; i < _import_directory_count; i++) { DWORD NameAddr = resolve(PEFILE_IMPORT_TABLE[i].Name, locate(PEFILE_IMPORT_TABLE[i].Name)); int NameSize = 0; while (true) { char tmp; fseek(Ppefile, (NameAddr + NameSize), SEEK_SET); fread(&tmp, sizeof(char), 1, Ppefile); if (tmp == 0x00) { break; } NameSize++; } char* Name = new char[NameSize + 2]; fseek(Ppefile, NameAddr, SEEK_SET); fread(Name, (NameSize * sizeof(char)) + 1, 1, Ppefile); printf(" * %s:\n", Name); delete[] Name; printf(" ILT RVA: 0x%X\n", PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk); printf(" IAT RVA: 0x%X\n", PEFILE_IMPORT_TABLE[i].FirstThunk); if (PEFILE_IMPORT_TABLE[i].TimeDateStamp == 0) { printf(" Bound: FALSE\n"); } else if (PEFILE_IMPORT_TABLE[i].TimeDateStamp == -1) { printf(" Bound: TRUE\n"); } printf("\n"); DWORD ILTAddr = resolve(PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk, locate(PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk)); int entrycounter = 0; while (true) { ILT_ENTRY_64 entry; fseek(Ppefile, (ILTAddr + (entrycounter * sizeof(QWORD))), SEEK_SET); fread(&entry, sizeof(ILT_ENTRY_64), 1, Ppefile); BYTE flag = entry.ORDINAL_NAME_FLAG; DWORD HintRVA = 0x0; WORD ordinal = 0x0; if (flag == 0x0) { HintRVA = entry.FIELD_2.HINT_NAME_TABE; } else if (flag == 0x01) { ordinal = entry.FIELD_2.ORDINAL; } if (flag == 0x0 && HintRVA == 0x0 && ordinal == 0x0) { break; } printf("\n Entry:\n"); if (flag == 0x0) { ___IMAGE_IMPORT_BY_NAME hint; DWORD HintAddr = resolve(HintRVA, locate(HintRVA)); fseek(Ppefile, HintAddr, SEEK_SET); fread(&hint, sizeof(___IMAGE_IMPORT_BY_NAME), 1, Ppefile); printf(" Name: %s\n", hint.Name); printf(" Hint RVA: 0x%X\n", HintRVA); printf(" Hint: 0x%X\n", hint.Hint); } else if (flag == 1) { printf(" Ordinal: 0x%X\n", ordinal); } entrycounter++; } printf("\n ----------------------\n\n"); } } Parsing Base Relocations ParseBaseReloc() This function follows the same process we’ve seen in ParseImportDirectory(). It resolves the file offset of the Base Relocation Directory, then it loops over each relocation block until it reaches a zeroed out block. Then it parses out these blocks and saves each IMAGE_BASE_RELOCATION structure in PEFILE_BASERELOC_TABLE. One thing to note here that is different from what we’ve seen in ParseImportDirectory() is that in addition to keeping a block counter we also keep a size counter that’s incremented by adding the value of SizeOfBlock of each block in each iteration. We do this because relocation blocks don’t have a fixed size, and in order to correctly calculate the offset of the next relocation block we need the total size of the previous blocks. void PE64FILE::ParseBaseReloc() { DWORD _basereloc_directory_address = resolve(PEFILE_BASERELOC_DIRECTORY.VirtualAddress, locate(PEFILE_BASERELOC_DIRECTORY.VirtualAddress)); _basreloc_directory_count = 0; int _basereloc_size_counter = 0; while (true) { ___IMAGE_BASE_RELOCATION tmp; int offset = (_basereloc_size_counter + _basereloc_directory_address); fseek(Ppefile, offset, SEEK_SET); fread(&tmp, sizeof(___IMAGE_BASE_RELOCATION), 1, Ppefile); if (tmp.VirtualAddress == 0x00000000 && tmp.SizeOfBlock == 0x00000000) { break; } _basreloc_directory_count++; _basereloc_size_counter += tmp.SizeOfBlock; } PEFILE_BASERELOC_TABLE = new ___IMAGE_BASE_RELOCATION[_basreloc_directory_count]; _basereloc_size_counter = 0; for (int i = 0; i < _basreloc_directory_count; i++) { int offset = _basereloc_directory_address + _basereloc_size_counter; fseek(Ppefile, offset, SEEK_SET); fread(&PEFILE_BASERELOC_TABLE[i], sizeof(___IMAGE_BASE_RELOCATION), 1, Ppefile); _basereloc_size_counter += PEFILE_BASERELOC_TABLE[i].SizeOfBlock; } } PrintBaseRelocationInfo() This function iterates over the base relocation blocks, and for each block it resolves the file offset of the block, then it prints the block RVA, size and number of entries (calculated by subtracting the size of IMAGE_BASE_RELOCATION from the block size then dividing that by the size of a WORD). After that it iterates over the relocation entries and prints the relocation value, and from that value it separates the type and the offset and prints each one of them. We’ve discussed the details about this in the PE base relocations post. void PE64FILE::PrintBaseRelocationsInfo() { printf(" BASE RELOCATIONS TABLE:\n"); printf(" -----------------------\n"); int szCounter = sizeof(___IMAGE_BASE_RELOCATION); for (int i = 0; i < _basreloc_directory_count; i++) { DWORD PAGERVA, BLOCKSIZE, BASE_RELOC_ADDR; int ENTRIES; BASE_RELOC_ADDR = resolve(PEFILE_BASERELOC_DIRECTORY.VirtualAddress, locate(PEFILE_BASERELOC_DIRECTORY.VirtualAddress)); PAGERVA = PEFILE_BASERELOC_TABLE[i].VirtualAddress; BLOCKSIZE = PEFILE_BASERELOC_TABLE[i].SizeOfBlock; ENTRIES = (BLOCKSIZE - sizeof(___IMAGE_BASE_RELOCATION)) / sizeof(WORD); printf("\n Block 0x%X: \n", i); printf(" Page RVA: 0x%X\n", PAGERVA); printf(" Block size: 0x%X\n", BLOCKSIZE); printf(" Number of entries: 0x%X\n", ENTRIES); printf("\n Entries:\n"); for (int i = 0; i < ENTRIES; i++) { BASE_RELOC_ENTRY entry; int offset = (BASE_RELOC_ADDR + szCounter + (i * sizeof(WORD))); fseek(Ppefile, offset, SEEK_SET); fread(&entry, sizeof(WORD), 1, Ppefile); printf("\n * Value: 0x%X\n", entry); printf(" Relocation Type: 0x%X\n", entry.TYPE); printf(" Offset: 0x%X\n", entry.OFFSET); } printf("\n ----------------------\n\n"); szCounter += BLOCKSIZE; } } Conclusion Here’s the full output after running the parser on a file: Desktop>.\PE-Parser.exe .\SimpleApp64.exe FILE: .\SimpleApp64.exe TYPE: 0x20B (PE32+) ---------------------------------- DOS HEADER: ----------- Magic: 0x5A4D File address of new exe header: 0x100 ---------------------------------- RICH HEADER: ------------ 0x7809 0x93 0xA: 30729.147.10 0x6FCB 0x101 0x2: 28619.257.2 0x6FCB 0x105 0x11: 28619.261.17 0x6FCB 0x104 0xA: 28619.260.10 0x6FCB 0x103 0x3: 28619.259.3 0x685B 0x101 0x5: 26715.257.5 0x0 0x1 0x30: 0.1.48 0x7086 0x109 0x1: 28806.265.1 0x7086 0xFF 0x1: 28806.255.1 0x7086 0x102 0x1: 28806.258.1 ---------------------------------- NT HEADERS: ----------- PE Signature: 0x4550 File Header: Machine: 0x8664 Number of sections: 0x6 Size of optional header: 0xF0 Optional Header: Magic: 0x20B Size of code section: 0xE00 Size of initialized data: 0x1E00 Size of uninitialized data: 0x0 Address of entry point: 0x12C4 RVA of start of code section: 0x1000 Desired image base: 0x40000000 Section alignment: 0x1000 File alignment: 0x200 Size of image: 0x7000 Size of headers: 0x400 Data Directories: * Export Directory: RVA: 0x0 Size: 0x0 * Import Directory: RVA: 0x27AC Size: 0xB4 * Resource Directory: RVA: 0x5000 Size: 0x1E0 * Exception Directory: RVA: 0x4000 Size: 0x168 * Security Directory: RVA: 0x0 Size: 0x0 * Base Relocation Table: RVA: 0x6000 Size: 0x28 * Debug Directory: RVA: 0x2248 Size: 0x70 * Architecture Specific Data: RVA: 0x0 Size: 0x0 * RVA of GlobalPtr: RVA: 0x0 Size: 0x0 * TLS Directory: RVA: 0x0 Size: 0x0 * Load Configuration Directory: RVA: 0x22C0 Size: 0x130 * Bound Import Directory: RVA: 0x0 Size: 0x0 * Import Address Table: RVA: 0x2000 Size: 0x198 * Delay Load Import Descriptors: RVA: 0x0 Size: 0x0 * COM Runtime Descriptor: RVA: 0x0 Size: 0x0 ---------------------------------- SECTION HEADERS: ---------------- * .text: VirtualAddress: 0x1000 VirtualSize: 0xD2C PointerToRawData: 0x400 SizeOfRawData: 0xE00 Characteristics: 0x60000020 * .rdata: VirtualAddress: 0x2000 VirtualSize: 0xE3C PointerToRawData: 0x1200 SizeOfRawData: 0x1000 Characteristics: 0x40000040 * .data: VirtualAddress: 0x3000 VirtualSize: 0x638 PointerToRawData: 0x2200 SizeOfRawData: 0x200 Characteristics: 0xC0000040 * .pdata: VirtualAddress: 0x4000 VirtualSize: 0x168 PointerToRawData: 0x2400 SizeOfRawData: 0x200 Characteristics: 0x40000040 * .rsrc: VirtualAddress: 0x5000 VirtualSize: 0x1E0 PointerToRawData: 0x2600 SizeOfRawData: 0x200 Characteristics: 0x40000040 * .reloc: VirtualAddress: 0x6000 VirtualSize: 0x28 PointerToRawData: 0x2800 SizeOfRawData: 0x200 Characteristics: 0x42000040 ---------------------------------- IMPORT TABLE: ---------------- * USER32.dll: ILT RVA: 0x28E0 IAT RVA: 0x2080 Bound: FALSE Entry: Name: MessageBoxA Hint RVA: 0x29F8 Hint: 0x283 ---------------------- * VCRUNTIME140.dll: ILT RVA: 0x28F0 IAT RVA: 0x2090 Bound: FALSE Entry: Name: memset Hint RVA: 0x2A5E Hint: 0x3E Entry: Name: __current_exception_context Hint RVA: 0x2A40 Hint: 0x1C Entry: Name: __current_exception Hint RVA: 0x2A2A Hint: 0x1B Entry: Name: __C_specific_handler Hint RVA: 0x2A12 Hint: 0x8 ---------------------- * api-ms-win-crt-runtime-l1-1-0.dll: ILT RVA: 0x2948 IAT RVA: 0x20E8 Bound: FALSE Entry: Name: _crt_atexit Hint RVA: 0x2C12 Hint: 0x1E Entry: Name: terminate Hint RVA: 0x2C20 Hint: 0x67 Entry: Name: _exit Hint RVA: 0x2B30 Hint: 0x23 Entry: Name: _register_thread_local_exe_atexit_callback Hint RVA: 0x2B76 Hint: 0x3D Entry: Name: _c_exit Hint RVA: 0x2B6C Hint: 0x15 Entry: Name: exit Hint RVA: 0x2B28 Hint: 0x55 Entry: Name: _initterm_e Hint RVA: 0x2B1A Hint: 0x37 Entry: Name: _initterm Hint RVA: 0x2B0E Hint: 0x36 Entry: Name: _get_initial_narrow_environment Hint RVA: 0x2AEC Hint: 0x28 Entry: Name: _initialize_narrow_environment Hint RVA: 0x2ACA Hint: 0x33 Entry: Name: _configure_narrow_argv Hint RVA: 0x2AB0 Hint: 0x18 Entry: Name: _initialize_onexit_table Hint RVA: 0x2BDA Hint: 0x34 Entry: Name: _set_app_type Hint RVA: 0x2A8C Hint: 0x42 Entry: Name: _seh_filter_exe Hint RVA: 0x2A7A Hint: 0x40 Entry: Name: _cexit Hint RVA: 0x2B62 Hint: 0x16 Entry: Name: __p___argv Hint RVA: 0x2B54 Hint: 0x5 Entry: Name: __p___argc Hint RVA: 0x2B46 Hint: 0x4 Entry: Name: _register_onexit_function Hint RVA: 0x2BF6 Hint: 0x3C ---------------------- * api-ms-win-crt-math-l1-1-0.dll: ILT RVA: 0x2938 IAT RVA: 0x20D8 Bound: FALSE Entry: Name: __setusermatherr Hint RVA: 0x2A9C Hint: 0x9 ---------------------- * api-ms-win-crt-stdio-l1-1-0.dll: ILT RVA: 0x29E0 IAT RVA: 0x2180 Bound: FALSE Entry: Name: __p__commode Hint RVA: 0x2BCA Hint: 0x1 Entry: Name: _set_fmode Hint RVA: 0x2B38 Hint: 0x54 ---------------------- * api-ms-win-crt-locale-l1-1-0.dll: ILT RVA: 0x2928 IAT RVA: 0x20C8 Bound: FALSE Entry: Name: _configthreadlocale Hint RVA: 0x2BA4 Hint: 0x8 ---------------------- * api-ms-win-crt-heap-l1-1-0.dll: ILT RVA: 0x2918 IAT RVA: 0x20B8 Bound: FALSE Entry: Name: _set_new_mode Hint RVA: 0x2BBA Hint: 0x16 ---------------------- ---------------------------------- BASE RELOCATIONS TABLE: ----------------------- Block 0x0: Page RVA: 0x2000 Block size: 0x28 Number of entries: 0x10 Entries: * Value: 0xA198 Relocation Type: 0xA Offset: 0x198 * Value: 0xA1A0 Relocation Type: 0xA Offset: 0x1A0 * Value: 0xA1A8 Relocation Type: 0xA Offset: 0x1A8 * Value: 0xA1B0 Relocation Type: 0xA Offset: 0x1B0 * Value: 0xA1B8 Relocation Type: 0xA Offset: 0x1B8 * Value: 0xA1C8 Relocation Type: 0xA Offset: 0x1C8 * Value: 0xA1E0 Relocation Type: 0xA Offset: 0x1E0 * Value: 0xA1E8 Relocation Type: 0xA Offset: 0x1E8 * Value: 0xA220 Relocation Type: 0xA Offset: 0x220 * Value: 0xA228 Relocation Type: 0xA Offset: 0x228 * Value: 0xA318 Relocation Type: 0xA Offset: 0x318 * Value: 0xA330 Relocation Type: 0xA Offset: 0x330 * Value: 0xA338 Relocation Type: 0xA Offset: 0x338 * Value: 0xA3D8 Relocation Type: 0xA Offset: 0x3D8 * Value: 0xA3E0 Relocation Type: 0xA Offset: 0x3E0 * Value: 0xA3E8 Relocation Type: 0xA Offset: 0x3E8 ---------------------- ---------------------------------- I hope that seeing actual code has given you a better understanding of what we’ve discussed throughout the previous posts. I believe that there are better ways for implementation than the ones I have presented, I’m in no way a c++ programmer and I know that there’s always room for improvement, so feel free to reach out to me, any feedback would be much appreciated. Thanks for reading., A dive into the PE file format - LAB 1: Writing a PE ParserIntroductionIn the previous posts we’ve discussed the basic structure of PE files, In this post we’re going to apply this knowledge into building a PE file parser in c++ as a proof of concept.The parser we’re going to build will not be a full parser and is not intended to be used as a reliable tool, this is only an exercise to better understand the PE file structure.We’re going to focus on PE32 and PE32+ files, and we’ll only parse the following parts of the file: DOS Header Rich Header NT Headers Data Directories (within the Optional Header) Section Headers Import Table Base Relocations TableThe code of this project can be found on my github profile.Initial SetupProcess OutlineWe want out parser to follow the following process: Read a file. Validate that it’s a PE file. Determine whether it’s a PE32 or a PE32+. Parse out the following structures: DOS Header Rich Header NT Headers Section Headers Import Data Directory Base Relocation Data Directory Print out the following information: File name and type. DOS Header: Magic value. Address of new exe header. Each entry of the Rich Header, decrypted and decoded. NT Headers - PE file signature. NT Headers - File Header: Machine value. Number of sections. Size of Optional Header. NT Headers - Optional Header: Magic value. Size of code section. Size of initialized data. Size of uninitialized data. Address of entry point. RVA of start of code section. Desired Image Base. Section alignment. File alignment. Size of image. Size of headers. For each Data Directory: its name, RVA and size. For each Section Header: Section name. Section virtual address and size. Section raw data pointer and size. Section characteristics value. Import Table: For each DLL: DLL name. ILT and IAT RVAs. Whether its a bound import or not. for every imported function: Ordinal if ordinal/name flag is 1. Name, hint and Hint/Name table RVA if ordinal/name flag is 0. Base Relocation Table: For each block: Page RVA. Block size. Number of entries. For each entry: Raw value. Relocation offset. Relocation Type. winnt.h DefinitionsWe will need the following definitions from the winnt.h header: Types: BYTE WORD DWORD QWORD LONG LONGLONG ULONGLONG Constants: IMAGE_NT_OPTIONAL_HDR32_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC IMAGE_NUMBEROF_DIRECTORY_ENTRIES IMAGE_DOS_SIGNATURE IMAGE_DIRECTORY_ENTRY_EXPORT IMAGE_DIRECTORY_ENTRY_IMPORT IMAGE_DIRECTORY_ENTRY_RESOURCE IMAGE_DIRECTORY_ENTRY_EXCEPTION IMAGE_DIRECTORY_ENTRY_SECURITY IMAGE_DIRECTORY_ENTRY_BASERELOC IMAGE_DIRECTORY_ENTRY_DEBUG IMAGE_DIRECTORY_ENTRY_ARCHITECTURE IMAGE_DIRECTORY_ENTRY_GLOBALPTR IMAGE_DIRECTORY_ENTRY_TLS IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT IMAGE_DIRECTORY_ENTRY_IAT IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR IMAGE_SIZEOF_SHORT_NAME IMAGE_SIZEOF_SECTION_HEADER Structures: IMAGE_DOS_HEADER IMAGE_DATA_DIRECTORY IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER64 IMAGE_FILE_HEADER IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS64 IMAGE_IMPORT_DESCRIPTOR IMAGE_IMPORT_BY_NAME IMAGE_BASE_RELOCATION IMAGE_SECTION_HEADER I took these definitions from winnt.h and added them to a new header called winntdef.h.winntdef.h:typedef unsigned char BYTE;typedef unsigned short WORD;typedef unsigned long DWORD;typedef unsigned long long QWORD;typedef unsigned long LONG;typedef __int64 LONGLONG;typedef unsigned __int64 ULONGLONG;#define ___IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b#define ___IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b#define ___IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16#define ___IMAGE_DOS_SIGNATURE 0x5A4D#define ___IMAGE_DIRECTORY_ENTRY_EXPORT 0#define ___IMAGE_DIRECTORY_ENTRY_IMPORT 1#define ___IMAGE_DIRECTORY_ENTRY_RESOURCE 2#define ___IMAGE_DIRECTORY_ENTRY_EXCEPTION 3#define ___IMAGE_DIRECTORY_ENTRY_SECURITY 4#define ___IMAGE_DIRECTORY_ENTRY_BASERELOC 5#define ___IMAGE_DIRECTORY_ENTRY_DEBUG 6#define ___IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7#define ___IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8#define ___IMAGE_DIRECTORY_ENTRY_TLS 9#define ___IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10#define ___IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11#define ___IMAGE_DIRECTORY_ENTRY_IAT 12#define ___IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13#define ___IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14#define ___IMAGE_SIZEOF_SHORT_NAME 8#define ___IMAGE_SIZEOF_SECTION_HEADER 40typedef struct __IMAGE_DOS_HEADER { WORD e_magic; WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; WORD e_cs; WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew;} ___IMAGE_DOS_HEADER, * ___PIMAGE_DOS_HEADER;typedef struct __IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size;} ___IMAGE_DATA_DIRECTORY, * ___PIMAGE_DATA_DIRECTORY;typedef struct __IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} ___IMAGE_OPTIONAL_HEADER32, * ___PIMAGE_OPTIONAL_HEADER32;typedef struct __IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; ___IMAGE_DATA_DIRECTORY DataDirectory[___IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} ___IMAGE_OPTIONAL_HEADER64, * ___PIMAGE_OPTIONAL_HEADER64;typedef struct __IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics;} ___IMAGE_FILE_HEADER, * ___PIMAGE_FILE_HEADER;typedef struct __IMAGE_NT_HEADERS64 { DWORD Signature; ___IMAGE_FILE_HEADER FileHeader; ___IMAGE_OPTIONAL_HEADER64 OptionalHeader;} ___IMAGE_NT_HEADERS64, * ___PIMAGE_NT_HEADERS64;typedef struct __IMAGE_NT_HEADERS { DWORD Signature; ___IMAGE_FILE_HEADER FileHeader; ___IMAGE_OPTIONAL_HEADER32 OptionalHeader;} ___IMAGE_NT_HEADERS32, * ___PIMAGE_NT_HEADERS32;typedef struct __IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk;} ___IMAGE_IMPORT_DESCRIPTOR, * ___PIMAGE_IMPORT_DESCRIPTOR;typedef struct __IMAGE_IMPORT_BY_NAME { WORD Hint; char Name[100];} ___IMAGE_IMPORT_BY_NAME, * ___PIMAGE_IMPORT_BY_NAME;typedef struct __IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock;} ___IMAGE_BASE_RELOCATION, * ___PIMAGE_BASE_RELOCATION;typedef struct __IMAGE_SECTION_HEADER { BYTE Name[___IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics;} ___IMAGE_SECTION_HEADER, * ___PIMAGE_SECTION_HEADER;Custom StructuresI defined the following structures to help with the parsing process.They’re defined in the PEFILE_CUSTOM_STRUCTS.h header.RICH_HEADER_INFOA structure to hold information about the Rich Header during processing.typedef struct __RICH_HEADER_INFO { int size; char* ptrToBuffer; int entries;} RICH_HEADER_INFO, * PRICH_HEADER_INFO; size: Size of the Rich Header (in bytes). ptrToBuffer: A pointer to the buffer containing the data of the Rich Header. entries: Number of entries in the Rich Header.RICH_HEADER_ENTRYA structure to represent a Rich Header entry.typedef struct __RICH_HEADER_ENTRY { WORD prodID; WORD buildID; DWORD useCount;} RICH_HEADER_ENTRY, * PRICH_HEADER_ENTRY; prodID: Type ID / Product ID. buildID: Build ID. useCount: Use count.RICH_HEADERA structure to represent the Rich Header.typedef struct __RICH_HEADER { PRICH_HEADER_ENTRY entries;} RICH_HEADER, * PRICH_HEADER; entries: A pointer to a RICH_HEADER_ENTRY array.ILT_ENTRY_32A structure to represent a 32-bit ILT entry during processing.typedef struct __ILT_ENTRY_32 { union { DWORD ORDINAL : 16; DWORD HINT_NAME_TABE : 32; DWORD ORDINAL_NAME_FLAG : 1; } FIELD_1;} ILT_ENTRY_32, * PILT_ENTRY_32;The structure will hold a 32-bit value and will return the appropriate piece of information (using bit fields) when the member corresponding to that piece of information is accessed.ILT_ENTRY_64A structure to represent a 64-bit ILT entry during processing.typedef struct __ILT_ENTRY_64 { union { DWORD ORDINAL : 16; DWORD HINT_NAME_TABE : 32; } FIELD_2; DWORD ORDINAL_NAME_FLAG : 1;} ILT_ENTRY_64, * PILT_ENTRY_64;The structure will hold a 64-bit value and will return the appropriate piece of information (using bit fields) when the member corresponding to that piece of information is accessed.BASE_RELOC_ENTRYA structure to represent a base relocation entry during processing.typedef struct __BASE_RELOC_ENTRY { WORD OFFSET : 12; WORD TYPE : 4;} BASE_RELOC_ENTRY, * PBASE_RELOC_ENTRY; OFFSET: Relocation offset. TYPE: Relocation type.PEFILEOur parser will represent a PE file as an object type of either PE32FILE or PE64FILE.These 2 classes only differ in some member definitions but their functionality is identical.Throughout this post we will use the code from PE64FILE.DefinitionThe class is defined as follows:class PE64FILE{public: PE64FILE(char* _NAME, FILE* Ppefile); void PrintInfo();private: char* NAME; FILE* Ppefile; int _import_directory_count, _import_directory_size; int _basreloc_directory_count; // HEADERS ___IMAGE_DOS_HEADER PEFILE_DOS_HEADER; ___IMAGE_NT_HEADERS64 PEFILE_NT_HEADERS; // DOS HEADER DWORD PEFILE_DOS_HEADER_EMAGIC; LONG PEFILE_DOS_HEADER_LFANEW; // RICH HEADER RICH_HEADER_INFO PEFILE_RICH_HEADER_INFO; RICH_HEADER PEFILE_RICH_HEADER; // NT_HEADERS.Signature DWORD PEFILE_NT_HEADERS_SIGNATURE; // NT_HEADERS.FileHeader WORD PEFILE_NT_HEADERS_FILE_HEADER_MACHINE; WORD PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; WORD PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER; // NT_HEADERS.OptionalHeader DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE; ULONGLONG PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE; DWORD PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS; ___IMAGE_DATA_DIRECTORY PEFILE_EXPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_IMPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_RESOURCE_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_EXCEPTION_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_SECURITY_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_BASERELOC_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_DEBUG_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_ARCHITECTURE_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_GLOBALPTR_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_TLS_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_LOAD_CONFIG_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_BOUND_IMPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_IAT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_DELAY_IMPORT_DIRECTORY; ___IMAGE_DATA_DIRECTORY PEFILE_COM_DESCRIPTOR_DIRECTORY; // SECTION HEADERS ___PIMAGE_SECTION_HEADER PEFILE_SECTION_HEADERS; // IMPORT TABLE ___PIMAGE_IMPORT_DESCRIPTOR PEFILE_IMPORT_TABLE; // BASE RELOCATION TABLE ___PIMAGE_BASE_RELOCATION PEFILE_BASERELOC_TABLE; // FUNCTIONS // ADDRESS RESOLVERS int locate(DWORD VA); DWORD resolve(DWORD VA, int index); // PARSERS void ParseFile(); void ParseDOSHeader(); void ParseNTHeaders(); void ParseSectionHeaders(); void ParseImportDirectory(); void ParseBaseReloc(); void ParseRichHeader(); // PRINT INFO void PrintFileInfo(); void PrintDOSHeaderInfo(); void PrintRichHeaderInfo(); void PrintNTHeadersInfo(); void PrintSectionHeadersInfo(); void PrintImportTableInfo(); void PrintBaseRelocationsInfo();};The only public member beside the class constructor is a function called printInfo() which will print information about the file.The class constructor takes two parameters, a char array representing the name of the file and a file pointer to the actual data of the file.After that comes a long series of variables definitions, these class members are going to be used internally during the parsing process and we’ll mention each one of them later.In the end is a series of methods definitions, first two methods are called locate and resolve, I will talk about them in a minute.The rest are functions responsible for parsing different parts of the file, and functions responsible for printing information about the same parts.ConstructorThe constructor of the class simply sets the file pointer and name variables, then it calls the ParseFile() function.PE64FILE::PE64FILE(char* _NAME, FILE* _Ppefile) { NAME = _NAME; Ppefile = _Ppefile; ParseFile();}The ParseFile() function calls the other parser functions:void PE64FILE::ParseFile() { // PARSE DOS HEADER ParseDOSHeader(); // PARSE RICH HEADER ParseRichHeader(); //PARSE NT HEADERS ParseNTHeaders(); // PARSE SECTION HEADERS ParseSectionHeaders(); // PARSE IMPORT DIRECTORY ParseImportDirectory(); // PARSE BASE RELOCATIONS ParseBaseReloc();}Resolving RVAsMost of the time, we’ll have a RVA that we’ll need to change to a file offset.The process of resolving an RVA can be outlined as follows: Determine which section range contains that RVA: Iterate over all sections and for each section compare the RVA to the section virtual address and to the section virtual address added to the virtual size of the section. If the RVA exists within this range then it belongs to that section. Calculate the file offset: Subtract the RVA from the section virtual address. Add that value to the raw data pointer of the section. An example of this is locating a Data Directory.The IMAGE_DATA_DIRECTORY structure only gives us an RVA of the directory, to locate that directory we’ll need to resolve that address.I wrote two functions to do this, first one to locate the virtual address (locate()), second one to resolve the address (resolve()).int PE64FILE::locate(DWORD VA) { int index; for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) { if (VA >= PEFILE_SECTION_HEADERS[i].VirtualAddress && VA < (PEFILE_SECTION_HEADERS[i].VirtualAddress + PEFILE_SECTION_HEADERS[i].Misc.VirtualSize)){ index = i; break; } } return index;}DWORD PE64FILE::resolve(DWORD VA, int index) { return (VA - PEFILE_SECTION_HEADERS[index].VirtualAddress) + PEFILE_SECTION_HEADERS[index].PointerToRawData;}locate() iterates over the PEFILE_SECTION_HEADERS array, compares the RVA as described above, then it returns the index of the appropriate section header within the PEFILE_SECTION_HEADERS array.Please note that in order for these functions to work we’ll need to parse out the section headers and fill the PEFILE_SECTION_HEADERS array first.We still haven’t discussed this part, but I wanted to talk about the address resolvers first.main functionThe main function of the program is fairly simple, it only does 2 things: Create a file pointer to the given file, and validate that the file was read correctly. Call INITPARSE() on the file, and based on the return value it decides between three actions: Exit. Create a PE32FILE object, call PrintInfo(), close the file pointer then exit. Create a PE64FILE object, call PrintInfo(), close the file pointer then exit. PrintInfo() calls the other print info functions.int main(int argc, char* argv[]){ if (argc != 2) { printf("Usage: %s [path to executable]\n", argv[0]); return 1; } FILE * PpeFile; fopen_s(&PpeFile, argv[1], "rb"); if (PpeFile == NULL) { printf("Can't open file.\n"); return 1; } if (INITPARSE(PpeFile) == 1) { exit(1); } else if (INITPARSE(PpeFile) == 32) { PE32FILE PeFile_1(argv[1], PpeFile); PeFile_1.PrintInfo(); fclose(PpeFile); exit(0); } else if (INITPARSE(PpeFile) == 64) { PE64FILE PeFile_1(argv[1], PpeFile); PeFile_1.PrintInfo(); fclose(PpeFile); exit(0); } return 0;}INITPARSE()INITPARSE() is a function defined in PEFILE.cpp.Its only job is to validate that the given file is a PE file, then determine whether the file is PE32 or PE32+.It reads the DOS header of the file and checks the DOS MZ header, if not found it returns an error.After validating the PE file, it sets the file position to (DOS_HEADER.e_lfanew + size of DWORD (PE signature) + size of the file header) which is the exact offset of the beginning of the Optional Header.Then it reads a WORD, we know that the first WORD of the Optional Header is a magic value that indicates the file type, it then compares that word to IMAGE_NT_OPTIONAL_HDR32_MAGIC and IMAGE_NT_OPTIONAL_HDR64_MAGIC, and based on the comparison results it either returns 32 or 64 indicating PE32 or PE32+, or it returns an error.int INITPARSE(FILE* PpeFile) { ___IMAGE_DOS_HEADER TMP_DOS_HEADER; WORD PEFILE_TYPE; fseek(PpeFile, 0, SEEK_SET); fread(&TMP_DOS_HEADER, sizeof(___IMAGE_DOS_HEADER), 1, PpeFile); if (TMP_DOS_HEADER.e_magic != ___IMAGE_DOS_SIGNATURE) { printf("Error. Not a PE file.\n"); return 1; } fseek(PpeFile, (TMP_DOS_HEADER.e_lfanew + sizeof(DWORD) + sizeof(___IMAGE_FILE_HEADER)), SEEK_SET); fread(&PEFILE_TYPE, sizeof(WORD), 1, PpeFile); if (PEFILE_TYPE == ___IMAGE_NT_OPTIONAL_HDR32_MAGIC) { return 32; } else if (PEFILE_TYPE == ___IMAGE_NT_OPTIONAL_HDR64_MAGIC) { return 64; } else { printf("Error while parsing IMAGE_OPTIONAL_HEADER.Magic. Unknown Type.\n"); return 1; }}Parsing DOS HeaderParseDOSHeader()Parsing out the DOS Header is nothing complicated, we just need to read from the beginning of the file an amount of bytes equal to the size of the DOS Header, then we can assign that data to the pre-defined class member PEFILE_DOS_HEADER.From there we can access all of the struct members, however we’re only interested in e_magic and e_lfanew.void PE64FILE::ParseDOSHeader() { fseek(Ppefile, 0, SEEK_SET); fread(&PEFILE_DOS_HEADER, sizeof(___IMAGE_DOS_HEADER), 1, Ppefile); PEFILE_DOS_HEADER_EMAGIC = PEFILE_DOS_HEADER.e_magic; PEFILE_DOS_HEADER_LFANEW = PEFILE_DOS_HEADER.e_lfanew;}PrintDOSHeaderInfo()This function prints e_magic and e_lfanew values.void PE64FILE::PrintDOSHeaderInfo() { printf(" DOS HEADER:\n"); printf(" -----------\n\n"); printf(" Magic: 0x%X\n", PEFILE_DOS_HEADER_EMAGIC); printf(" File address of new exe header: 0x%X\n", PEFILE_DOS_HEADER_LFANEW);}Parsing Rich HeaderProcessTo parse out the Rich Header we’ll need to go through multiple steps.We don’t know anything about the Rich Header, we don’t know its size, we don’t know where it’s exactly located, we don’t even know if the file we’re processing contains a Rich Header in the first place.First of all, we need to locate the Rich Header.We don’t know the exact location, however we have everything we need to locate it.We know that if a Rich Header exists, then it has to exist between the DOS Stub and the PE signature or the beginning of the NT Headers.We also know that any Rich Header ends with a 32-bit value Rich followed by the XOR key.One might rely on the fixed size of the DOS Header and the DOS Stub, however, the default DOS Stub message can be changed, so that size is not guaranteed to be fixed.A better approach would be to read from the beginning of the file to the start of the NT Headers, then search through that buffer for the Rich sequence, if found then we’ve successfully located the end of the Rich Header, if not found then most likely the file doesn’t contain a Rich Header.Once we’ve located the end of the Rich Header, we can read the XOR key, then go backwards starting from the Rich signature and keep XORing 4 bytes at a time until we reach the DanS signature which indicates the beginning of the Rich Header.After obtaining the position and the size of the Rich Header, we can normally read and process the data.ParseRichHeader()This function starts by allocating a buffer on the heap, then it reads e_lfanew size of bytes from the beginning of the file and stores the data in the allocated buffer.It then goes through a loop where it does a linear search byte by byte. In each iteration it compares the current byte and the byte the follows to 0x52 (R) and 0x69 (i).When the sequence is found, it stores the index in a variable then the loop breaks. char* dataPtr = new char[PEFILE_DOS_HEADER_LFANEW]; fseek(Ppefile, 0, SEEK_SET); fread(dataPtr, PEFILE_DOS_HEADER_LFANEW, 1, Ppefile); int index_ = 0; for (int i = 0; i <= PEFILE_DOS_HEADER_LFANEW; i++) { if (dataPtr[i] == 0x52 && dataPtr[i + 1] == 0x69) { index_ = i; break; } } if (index_ == 0) { printf("Error while parsing Rich Header."); PEFILE_RICH_HEADER_INFO.entries = 0; return; }After that it reads the XOR key, then goes into the decryption loop where in each iteration it increments RichHeaderSize by 4 until it reaches the DanS sequence. char key[4]; memcpy(key, dataPtr + (index_ + 4), 4); int indexpointer = index_ - 4; int RichHeaderSize = 0; while (true) { char tmpchar[4]; memcpy(tmpchar, dataPtr + indexpointer, 4); for (int i = 0; i < 4; i++) { tmpchar[i] = tmpchar[i] ^ key[i]; } indexpointer -= 4; RichHeaderSize += 4; if (tmpchar[1] = 0x61 && tmpchar[0] == 0x44) { break; } }After obtaining the size and the position, it allocates a new buffer for the Rich Header, reads and decrypts the Rich Header, updates PEFILE_RICH_HEADER_INFO with the appropriate data pointer, size and number of entries, then finally it deallocates the buffer it was using for processing. char* RichHeaderPtr = new char[RichHeaderSize]; memcpy(RichHeaderPtr, dataPtr + (index_ - RichHeaderSize), RichHeaderSize); for (int i = 0; i < RichHeaderSize; i += 4) { for (int x = 0; x < 4; x++) { RichHeaderPtr[i + x] = RichHeaderPtr[i + x] ^ key[x]; } } PEFILE_RICH_HEADER_INFO.size = RichHeaderSize; PEFILE_RICH_HEADER_INFO.ptrToBuffer = RichHeaderPtr; PEFILE_RICH_HEADER_INFO.entries = (RichHeaderSize - 16) / 8; delete[] dataPtr;The rest of the function reads each entry of the Rich Header and updates PEFILE_RICH_HEADER. PEFILE_RICH_HEADER.entries = new RICH_HEADER_ENTRY[PEFILE_RICH_HEADER_INFO.entries]; for (int i = 16; i < RichHeaderSize; i += 8) { WORD PRODID = (uint16_t)((unsigned char)RichHeaderPtr[i + 3] << 8) | (unsigned char)RichHeaderPtr[i + 2]; WORD BUILDID = (uint16_t)((unsigned char)RichHeaderPtr[i + 1] << 8) | (unsigned char)RichHeaderPtr[i]; DWORD USECOUNT = (uint32_t)((unsigned char)RichHeaderPtr[i + 7] << 24) | (unsigned char)RichHeaderPtr[i + 6] << 16 | (unsigned char)RichHeaderPtr[i + 5] << 8 | (unsigned char)RichHeaderPtr[i + 4]; PEFILE_RICH_HEADER.entries[(i / 8) - 2] = { PRODID, BUILDID, USECOUNT }; if (i + 8 >= RichHeaderSize) { PEFILE_RICH_HEADER.entries[(i / 8) - 1] = { 0x0000, 0x0000, 0x00000000 }; } } delete[] PEFILE_RICH_HEADER_INFO.ptrToBuffer;Here’s the full function:void PE64FILE::ParseRichHeader() { char* dataPtr = new char[PEFILE_DOS_HEADER_LFANEW]; fseek(Ppefile, 0, SEEK_SET); fread(dataPtr, PEFILE_DOS_HEADER_LFANEW, 1, Ppefile); int index_ = 0; for (int i = 0; i <= PEFILE_DOS_HEADER_LFANEW; i++) { if (dataPtr[i] == 0x52 && dataPtr[i + 1] == 0x69) { index_ = i; break; } } if (index_ == 0) { printf("Error while parsing Rich Header."); PEFILE_RICH_HEADER_INFO.entries = 0; return; } char key[4]; memcpy(key, dataPtr + (index_ + 4), 4); int indexpointer = index_ - 4; int RichHeaderSize = 0; while (true) { char tmpchar[4]; memcpy(tmpchar, dataPtr + indexpointer, 4); for (int i = 0; i < 4; i++) { tmpchar[i] = tmpchar[i] ^ key[i]; } indexpointer -= 4; RichHeaderSize += 4; if (tmpchar[1] = 0x61 && tmpchar[0] == 0x44) { break; } } char* RichHeaderPtr = new char[RichHeaderSize]; memcpy(RichHeaderPtr, dataPtr + (index_ - RichHeaderSize), RichHeaderSize); for (int i = 0; i < RichHeaderSize; i += 4) { for (int x = 0; x < 4; x++) { RichHeaderPtr[i + x] = RichHeaderPtr[i + x] ^ key[x]; } } PEFILE_RICH_HEADER_INFO.size = RichHeaderSize; PEFILE_RICH_HEADER_INFO.ptrToBuffer = RichHeaderPtr; PEFILE_RICH_HEADER_INFO.entries = (RichHeaderSize - 16) / 8; delete[] dataPtr; PEFILE_RICH_HEADER.entries = new RICH_HEADER_ENTRY[PEFILE_RICH_HEADER_INFO.entries]; for (int i = 16; i < RichHeaderSize; i += 8) { WORD PRODID = (uint16_t)((unsigned char)RichHeaderPtr[i + 3] << 8) | (unsigned char)RichHeaderPtr[i + 2]; WORD BUILDID = (uint16_t)((unsigned char)RichHeaderPtr[i + 1] << 8) | (unsigned char)RichHeaderPtr[i]; DWORD USECOUNT = (uint32_t)((unsigned char)RichHeaderPtr[i + 7] << 24) | (unsigned char)RichHeaderPtr[i + 6] << 16 | (unsigned char)RichHeaderPtr[i + 5] << 8 | (unsigned char)RichHeaderPtr[i + 4]; PEFILE_RICH_HEADER.entries[(i / 8) - 2] = { PRODID, BUILDID, USECOUNT }; if (i + 8 >= RichHeaderSize) { PEFILE_RICH_HEADER.entries[(i / 8) - 1] = { 0x0000, 0x0000, 0x00000000 }; } } delete[] PEFILE_RICH_HEADER_INFO.ptrToBuffer;}PrintRichHeaderInfo()This function iterates over each entry in PEFILE_RICH_HEADER and prints its value.void PE64FILE::PrintRichHeaderInfo() { printf(" RICH HEADER:\n"); printf(" ------------\n\n"); for (int i = 0; i < PEFILE_RICH_HEADER_INFO.entries; i++) { printf(" 0x%X 0x%X 0x%X: %d.%d.%d\n", PEFILE_RICH_HEADER.entries[i].buildID, PEFILE_RICH_HEADER.entries[i].prodID, PEFILE_RICH_HEADER.entries[i].useCount, PEFILE_RICH_HEADER.entries[i].buildID, PEFILE_RICH_HEADER.entries[i].prodID, PEFILE_RICH_HEADER.entries[i].useCount); }}Parsing NT HeadersParseNTHeaders()Similar to the DOS Header, all we need to do is to read from e_lfanew an amount of bytes equal to the size of IMAGE_NT_HEADERS.After that we can parse out the contents of the File Header and the Optional Header.The Optional Header contains an array of IMAGE_DATA_DIRECTORY structures which we care about.To parse out this information, we can use the IMAGE_DIRECTORY_[...] constants defined in winnt.h as array indexes to access the corresponding IMAGE_DATA_DIRECTORY structure of each Data Directory.void PE64FILE::ParseNTHeaders() { fseek(Ppefile, PEFILE_DOS_HEADER.e_lfanew, SEEK_SET); fread(&PEFILE_NT_HEADERS, sizeof(PEFILE_NT_HEADERS), 1, Ppefile); PEFILE_NT_HEADERS_SIGNATURE = PEFILE_NT_HEADERS.Signature; PEFILE_NT_HEADERS_FILE_HEADER_MACHINE = PEFILE_NT_HEADERS.FileHeader.Machine; PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS = PEFILE_NT_HEADERS.FileHeader.NumberOfSections; PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER = PEFILE_NT_HEADERS.FileHeader.SizeOfOptionalHeader; PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC = PEFILE_NT_HEADERS.OptionalHeader.Magic; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE = PEFILE_NT_HEADERS.OptionalHeader.SizeOfCode; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA = PEFILE_NT_HEADERS.OptionalHeader.SizeOfInitializedData; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA = PEFILE_NT_HEADERS.OptionalHeader.SizeOfUninitializedData; PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT = PEFILE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint; PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE = PEFILE_NT_HEADERS.OptionalHeader.BaseOfCode; PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE = PEFILE_NT_HEADERS.OptionalHeader.ImageBase; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT = PEFILE_NT_HEADERS.OptionalHeader.SectionAlignment; PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT = PEFILE_NT_HEADERS.OptionalHeader.FileAlignment; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE = PEFILE_NT_HEADERS.OptionalHeader.SizeOfImage; PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS = PEFILE_NT_HEADERS.OptionalHeader.SizeOfHeaders; PEFILE_EXPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_EXPORT]; PEFILE_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_IMPORT]; PEFILE_RESOURCE_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_RESOURCE]; PEFILE_EXCEPTION_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_EXCEPTION]; PEFILE_SECURITY_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_SECURITY]; PEFILE_BASERELOC_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_BASERELOC]; PEFILE_DEBUG_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_DEBUG]; PEFILE_ARCHITECTURE_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_ARCHITECTURE]; PEFILE_GLOBALPTR_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_GLOBALPTR]; PEFILE_TLS_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_TLS]; PEFILE_LOAD_CONFIG_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG]; PEFILE_BOUND_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT]; PEFILE_IAT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_IAT]; PEFILE_DELAY_IMPORT_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT]; PEFILE_COM_DESCRIPTOR_DIRECTORY = PEFILE_NT_HEADERS.OptionalHeader.DataDirectory[___IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR];}PrintNTHeadersInfo()This function prints the data obtained from the File Header and the Optional Header, and for each Data Directory it prints its RVA and size.void PE64FILE::PrintNTHeadersInfo() { printf(" NT HEADERS:\n"); printf(" -----------\n\n"); printf(" PE Signature: 0x%X\n", PEFILE_NT_HEADERS_SIGNATURE); printf("\n File Header:\n\n"); printf(" Machine: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_MACHINE); printf(" Number of sections: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS); printf(" Size of optional header: 0x%X\n", PEFILE_NT_HEADERS_FILE_HEADER_SIZEOF_OPTIONAL_HEADER); printf("\n Optional Header:\n\n"); printf(" Magic: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_MAGIC); printf(" Size of code section: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_CODE); printf(" Size of initialized data: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_INITIALIZED_DATA); printf(" Size of uninitialized data: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_UNINITIALIZED_DATA); printf(" Address of entry point: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_ADDRESSOF_ENTRYPOINT); printf(" RVA of start of code section: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_BASEOF_CODE); printf(" Desired image base: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_IMAGEBASE); printf(" Section alignment: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SECTION_ALIGNMENT); printf(" File alignment: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_FILE_ALIGNMENT); printf(" Size of image: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_IMAGE); printf(" Size of headers: 0x%X\n", PEFILE_NT_HEADERS_OPTIONAL_HEADER_SIZEOF_HEADERS); printf("\n Data Directories:\n"); printf("\n * Export Directory:\n"); printf(" RVA: 0x%X\n", PEFILE_EXPORT_DIRECTORY.VirtualAddress); printf(" Size: 0x%X\n", PEFILE_EXPORT_DIRECTORY.Size); . . [REDACTED] . . printf("\n * COM Runtime Descriptor:\n"); printf(" RVA: 0x%X\n", PEFILE_COM_DESCRIPTOR_DIRECTORY.VirtualAddress); printf(" Size: 0x%X\n", PEFILE_COM_DESCRIPTOR_DIRECTORY.Size);}Parsing Section HeadersParseSectionHeaders()This function starts by assigning the PEFILE_SECTION_HEADERS class member to a pointer to an IMAGE_SECTION_HEADER array of the count of PEFILE_NT_HEADERS_FILE_HEADER_NUMBEROF_SECTIONS.Then it goes into a loop of PEFILE_NT_HEADERS_FILE_HEADER_NUMBEROF_SECTIONS iterations where in each iteration it changes the file offset to (e_lfanew + size of NT Headers + loop counter multiplied by the size of a section header) to reach the beginning of the next Section Header, then it reads the new Section Header and assigns it to the next element of PEFILE_SECTION_HEADERS.void PE64FILE::ParseSectionHeaders() { PEFILE_SECTION_HEADERS = new ___IMAGE_SECTION_HEADER[PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS]; for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) { int offset = (PEFILE_DOS_HEADER.e_lfanew + sizeof(PEFILE_NT_HEADERS)) + (i * ___IMAGE_SIZEOF_SECTION_HEADER); fseek(Ppefile, offset, SEEK_SET); fread(&PEFILE_SECTION_HEADERS[i], ___IMAGE_SIZEOF_SECTION_HEADER, 1, Ppefile); }}PrintSectionHeadersInfo()This function loops over the Section Headers array (filled by ParseSectionHeaders()), and it prints information about each section.void PE64FILE::PrintSectionHeadersInfo() { printf(" SECTION HEADERS:\n"); printf(" ----------------\n\n"); for (int i = 0; i < PEFILE_NT_HEADERS_FILE_HEADER_NUMBER0F_SECTIONS; i++) { printf(" * %.8s:\n", PEFILE_SECTION_HEADERS[i].Name); printf(" VirtualAddress: 0x%X\n", PEFILE_SECTION_HEADERS[i].VirtualAddress); printf(" VirtualSize: 0x%X\n", PEFILE_SECTION_HEADERS[i].Misc.VirtualSize); printf(" PointerToRawData: 0x%X\n", PEFILE_SECTION_HEADERS[i].PointerToRawData); printf(" SizeOfRawData: 0x%X\n", PEFILE_SECTION_HEADERS[i].SizeOfRawData); printf(" Characteristics: 0x%X\n\n", PEFILE_SECTION_HEADERS[i].Characteristics); }}Parsing ImportsParseImportDirectory()To parse out the Import Directory Table we need to determine the count of IMAGE_IMPORT_DESCRIPTORs first.This function starts by resolving the file offset of the Import Directory, then it goes into a loop where in each loop it keeps reading the next import descriptor.In each iteration it checks if the descriptor has zeroed out values, if that is the case then we’ve reached the end of the Import Directory, so it breaks.Otherwise it increments _import_directory_count and the loop continues.After finding the size of the Import Directory, the function assigns the PEFILE_IMPORT_TABLE class member to a pointer to an IMAGE_IMPORT_DESCRIPTOR array of the count of _import_directory_count then goes into another loop similar to the one we’ve seen in ParseSectionHeaders() to parse out the import descriptors.void PE64FILE::ParseImportDirectory() { DWORD _import_directory_address = resolve(PEFILE_IMPORT_DIRECTORY.VirtualAddress, locate(PEFILE_IMPORT_DIRECTORY.VirtualAddress)); _import_directory_count = 0; while (true) { ___IMAGE_IMPORT_DESCRIPTOR tmp; int offset = (_import_directory_count * sizeof(___IMAGE_IMPORT_DESCRIPTOR)) + _import_directory_address; fseek(Ppefile, offset, SEEK_SET); fread(&tmp, sizeof(___IMAGE_IMPORT_DESCRIPTOR), 1, Ppefile); if (tmp.Name == 0x00000000 && tmp.FirstThunk == 0x00000000) { _import_directory_count -= 1; _import_directory_size = _import_directory_count * sizeof(___IMAGE_IMPORT_DESCRIPTOR); break; } _import_directory_count++; } PEFILE_IMPORT_TABLE = new ___IMAGE_IMPORT_DESCRIPTOR[_import_directory_count]; for (int i = 0; i < _import_directory_count; i++) { int offset = (i * sizeof(___IMAGE_IMPORT_DESCRIPTOR)) + _import_directory_address; fseek(Ppefile, offset, SEEK_SET); fread(&PEFILE_IMPORT_TABLE[i], sizeof(___IMAGE_IMPORT_DESCRIPTOR), 1, Ppefile); }}PrintImportTableInfo()After obtaining the import descriptors, further parsing is needed to retrieve information about the imported functions.This is done by the PrintImportTableInfo() function.This function iterates over the import descriptors, and for each descriptor it resolves the file offset of the DLL name, retrieves the DLL name then prints it, it also prints the ILT RVA, the IAT RVA and whether the import is bound or not.After that it resolves the file offset of the ILT then it parses out each ILT entry.If the Ordinal/Name flag is set it prints the function ordinal, otherwise it prints the function name, the hint RVA and the hint.If the ILT entry is zeroed out, the loop breaks and the next import descriptor parsing iteration starts.We’ve discussed the details about this in the PE imports post.void PE64FILE::PrintImportTableInfo() { printf(" IMPORT TABLE:\n"); printf(" ----------------\n\n"); for (int i = 0; i < _import_directory_count; i++) { DWORD NameAddr = resolve(PEFILE_IMPORT_TABLE[i].Name, locate(PEFILE_IMPORT_TABLE[i].Name)); int NameSize = 0; while (true) { char tmp; fseek(Ppefile, (NameAddr + NameSize), SEEK_SET); fread(&tmp, sizeof(char), 1, Ppefile); if (tmp == 0x00) { break; } NameSize++; } char* Name = new char[NameSize + 2]; fseek(Ppefile, NameAddr, SEEK_SET); fread(Name, (NameSize * sizeof(char)) + 1, 1, Ppefile); printf(" * %s:\n", Name); delete[] Name; printf(" ILT RVA: 0x%X\n", PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk); printf(" IAT RVA: 0x%X\n", PEFILE_IMPORT_TABLE[i].FirstThunk); if (PEFILE_IMPORT_TABLE[i].TimeDateStamp == 0) { printf(" Bound: FALSE\n"); } else if (PEFILE_IMPORT_TABLE[i].TimeDateStamp == -1) { printf(" Bound: TRUE\n"); } printf("\n"); DWORD ILTAddr = resolve(PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk, locate(PEFILE_IMPORT_TABLE[i].DUMMYUNIONNAME.OriginalFirstThunk)); int entrycounter = 0; while (true) { ILT_ENTRY_64 entry; fseek(Ppefile, (ILTAddr + (entrycounter * sizeof(QWORD))), SEEK_SET); fread(&entry, sizeof(ILT_ENTRY_64), 1, Ppefile); BYTE flag = entry.ORDINAL_NAME_FLAG; DWORD HintRVA = 0x0; WORD ordinal = 0x0; if (flag == 0x0) { HintRVA = entry.FIELD_2.HINT_NAME_TABE; } else if (flag == 0x01) { ordinal = entry.FIELD_2.ORDINAL; } if (flag == 0x0 && HintRVA == 0x0 && ordinal == 0x0) { break; } printf("\n Entry:\n"); if (flag == 0x0) { ___IMAGE_IMPORT_BY_NAME hint; DWORD HintAddr = resolve(HintRVA, locate(HintRVA)); fseek(Ppefile, HintAddr, SEEK_SET); fread(&hint, sizeof(___IMAGE_IMPORT_BY_NAME), 1, Ppefile); printf(" Name: %s\n", hint.Name); printf(" Hint RVA: 0x%X\n", HintRVA); printf(" Hint: 0x%X\n", hint.Hint); } else if (flag == 1) { printf(" Ordinal: 0x%X\n", ordinal); } entrycounter++; } printf("\n ----------------------\n\n"); }}Parsing Base RelocationsParseBaseReloc()This function follows the same process we’ve seen in ParseImportDirectory().It resolves the file offset of the Base Relocation Directory, then it loops over each relocation block until it reaches a zeroed out block. Then it parses out these blocks and saves each IMAGE_BASE_RELOCATION structure in PEFILE_BASERELOC_TABLE.One thing to note here that is different from what we’ve seen in ParseImportDirectory() is that in addition to keeping a block counter we also keep a size counter that’s incremented by adding the value of SizeOfBlock of each block in each iteration.We do this because relocation blocks don’t have a fixed size, and in order to correctly calculate the offset of the next relocation block we need the total size of the previous blocks.void PE64FILE::ParseBaseReloc() { DWORD _basereloc_directory_address = resolve(PEFILE_BASERELOC_DIRECTORY.VirtualAddress, locate(PEFILE_BASERELOC_DIRECTORY.VirtualAddress)); _basreloc_directory_count = 0; int _basereloc_size_counter = 0; while (true) { ___IMAGE_BASE_RELOCATION tmp; int offset = (_basereloc_size_counter + _basereloc_directory_address); fseek(Ppefile, offset, SEEK_SET); fread(&tmp, sizeof(___IMAGE_BASE_RELOCATION), 1, Ppefile); if (tmp.VirtualAddress == 0x00000000 && tmp.SizeOfBlock == 0x00000000) { break; } _basreloc_directory_count++; _basereloc_size_counter += tmp.SizeOfBlock; } PEFILE_BASERELOC_TABLE = new ___IMAGE_BASE_RELOCATION[_basreloc_directory_count]; _basereloc_size_counter = 0; for (int i = 0; i < _basreloc_directory_count; i++) { int offset = _basereloc_directory_address + _basereloc_size_counter; fseek(Ppefile, offset, SEEK_SET); fread(&PEFILE_BASERELOC_TABLE[i], sizeof(___IMAGE_BASE_RELOCATION), 1, Ppefile); _basereloc_size_counter += PEFILE_BASERELOC_TABLE[i].SizeOfBlock; }}PrintBaseRelocationInfo()This function iterates over the base relocation blocks, and for each block it resolves the file offset of the block, then it prints the block RVA, size and number of entries (calculated by subtracting the size of IMAGE_BASE_RELOCATION from the block size then dividing that by the size of a WORD).After that it iterates over the relocation entries and prints the relocation value, and from that value it separates the type and the offset and prints each one of them.We’ve discussed the details about this in the PE base relocations post.void PE64FILE::PrintBaseRelocationsInfo() { printf(" BASE RELOCATIONS TABLE:\n"); printf(" -----------------------\n"); int szCounter = sizeof(___IMAGE_BASE_RELOCATION); for (int i = 0; i < _basreloc_directory_count; i++) { DWORD PAGERVA, BLOCKSIZE, BASE_RELOC_ADDR; int ENTRIES; BASE_RELOC_ADDR = resolve(PEFILE_BASERELOC_DIRECTORY.VirtualAddress, locate(PEFILE_BASERELOC_DIRECTORY.VirtualAddress)); PAGERVA = PEFILE_BASERELOC_TABLE[i].VirtualAddress; BLOCKSIZE = PEFILE_BASERELOC_TABLE[i].SizeOfBlock; ENTRIES = (BLOCKSIZE - sizeof(___IMAGE_BASE_RELOCATION)) / sizeof(WORD); printf("\n Block 0x%X: \n", i); printf(" Page RVA: 0x%X\n", PAGERVA); printf(" Block size: 0x%X\n", BLOCKSIZE); printf(" Number of entries: 0x%X\n", ENTRIES); printf("\n Entries:\n"); for (int i = 0; i < ENTRIES; i++) { BASE_RELOC_ENTRY entry; int offset = (BASE_RELOC_ADDR + szCounter + (i * sizeof(WORD))); fseek(Ppefile, offset, SEEK_SET); fread(&entry, sizeof(WORD), 1, Ppefile); printf("\n * Value: 0x%X\n", entry); printf(" Relocation Type: 0x%X\n", entry.TYPE); printf(" Offset: 0x%X\n", entry.OFFSET); } printf("\n ----------------------\n\n"); szCounter += BLOCKSIZE; }}ConclusionHere’s the full output after running the parser on a file:Desktop>.\PE-Parser.exe .\SimpleApp64.exe FILE: .\SimpleApp64.exe TYPE: 0x20B (PE32+) ---------------------------------- DOS HEADER: ----------- Magic: 0x5A4D File address of new exe header: 0x100 ---------------------------------- RICH HEADER: ------------ 0x7809 0x93 0xA: 30729.147.10 0x6FCB 0x101 0x2: 28619.257.2 0x6FCB 0x105 0x11: 28619.261.17 0x6FCB 0x104 0xA: 28619.260.10 0x6FCB 0x103 0x3: 28619.259.3 0x685B 0x101 0x5: 26715.257.5 0x0 0x1 0x30: 0.1.48 0x7086 0x109 0x1: 28806.265.1 0x7086 0xFF 0x1: 28806.255.1 0x7086 0x102 0x1: 28806.258.1 ---------------------------------- NT HEADERS: ----------- PE Signature: 0x4550 File Header: Machine: 0x8664 Number of sections: 0x6 Size of optional header: 0xF0 Optional Header: Magic: 0x20B Size of code section: 0xE00 Size of initialized data: 0x1E00 Size of uninitialized data: 0x0 Address of entry point: 0x12C4 RVA of start of code section: 0x1000 Desired image base: 0x40000000 Section alignment: 0x1000 File alignment: 0x200 Size of image: 0x7000 Size of headers: 0x400 Data Directories: * Export Directory: RVA: 0x0 Size: 0x0 * Import Directory: RVA: 0x27AC Size: 0xB4 * Resource Directory: RVA: 0x5000 Size: 0x1E0 * Exception Directory: RVA: 0x4000 Size: 0x168 * Security Directory: RVA: 0x0 Size: 0x0 * Base Relocation Table: RVA: 0x6000 Size: 0x28 * Debug Directory: RVA: 0x2248 Size: 0x70 * Architecture Specific Data: RVA: 0x0 Size: 0x0 * RVA of GlobalPtr: RVA: 0x0 Size: 0x0 * TLS Directory: RVA: 0x0 Size: 0x0 * Load Configuration Directory: RVA: 0x22C0 Size: 0x130 * Bound Import Directory: RVA: 0x0 Size: 0x0 * Import Address Table: RVA: 0x2000 Size: 0x198 * Delay Load Import Descriptors: RVA: 0x0 Size: 0x0 * COM Runtime Descriptor: RVA: 0x0 Size: 0x0 ---------------------------------- SECTION HEADERS: ---------------- * .text: VirtualAddress: 0x1000 VirtualSize: 0xD2C PointerToRawData: 0x400 SizeOfRawData: 0xE00 Characteristics: 0x60000020 * .rdata: VirtualAddress: 0x2000 VirtualSize: 0xE3C PointerToRawData: 0x1200 SizeOfRawData: 0x1000 Characteristics: 0x40000040 * .data: VirtualAddress: 0x3000 VirtualSize: 0x638 PointerToRawData: 0x2200 SizeOfRawData: 0x200 Characteristics: 0xC0000040 * .pdata: VirtualAddress: 0x4000 VirtualSize: 0x168 PointerToRawData: 0x2400 SizeOfRawData: 0x200 Characteristics: 0x40000040 * .rsrc: VirtualAddress: 0x5000 VirtualSize: 0x1E0 PointerToRawData: 0x2600 SizeOfRawData: 0x200 Characteristics: 0x40000040 * .reloc: VirtualAddress: 0x6000 VirtualSize: 0x28 PointerToRawData: 0x2800 SizeOfRawData: 0x200 Characteristics: 0x42000040 ---------------------------------- IMPORT TABLE: ---------------- * USER32.dll: ILT RVA: 0x28E0 IAT RVA: 0x2080 Bound: FALSE Entry: Name: MessageBoxA Hint RVA: 0x29F8 Hint: 0x283 ---------------------- * VCRUNTIME140.dll: ILT RVA: 0x28F0 IAT RVA: 0x2090 Bound: FALSE Entry: Name: memset Hint RVA: 0x2A5E Hint: 0x3E Entry: Name: __current_exception_context Hint RVA: 0x2A40 Hint: 0x1C Entry: Name: __current_exception Hint RVA: 0x2A2A Hint: 0x1B Entry: Name: __C_specific_handler Hint RVA: 0x2A12 Hint: 0x8 ---------------------- * api-ms-win-crt-runtime-l1-1-0.dll: ILT RVA: 0x2948 IAT RVA: 0x20E8 Bound: FALSE Entry: Name: _crt_atexit Hint RVA: 0x2C12 Hint: 0x1E Entry: Name: terminate Hint RVA: 0x2C20 Hint: 0x67 Entry: Name: _exit Hint RVA: 0x2B30 Hint: 0x23 Entry: Name: _register_thread_local_exe_atexit_callback Hint RVA: 0x2B76 Hint: 0x3D Entry: Name: _c_exit Hint RVA: 0x2B6C Hint: 0x15 Entry: Name: exit Hint RVA: 0x2B28 Hint: 0x55 Entry: Name: _initterm_e Hint RVA: 0x2B1A Hint: 0x37 Entry: Name: _initterm Hint RVA: 0x2B0E Hint: 0x36 Entry: Name: _get_initial_narrow_environment Hint RVA: 0x2AEC Hint: 0x28 Entry: Name: _initialize_narrow_environment Hint RVA: 0x2ACA Hint: 0x33 Entry: Name: _configure_narrow_argv Hint RVA: 0x2AB0 Hint: 0x18 Entry: Name: _initialize_onexit_table Hint RVA: 0x2BDA Hint: 0x34 Entry: Name: _set_app_type Hint RVA: 0x2A8C Hint: 0x42 Entry: Name: _seh_filter_exe Hint RVA: 0x2A7A Hint: 0x40 Entry: Name: _cexit Hint RVA: 0x2B62 Hint: 0x16 Entry: Name: __p___argv Hint RVA: 0x2B54 Hint: 0x5 Entry: Name: __p___argc Hint RVA: 0x2B46 Hint: 0x4 Entry: Name: _register_onexit_function Hint RVA: 0x2BF6 Hint: 0x3C ---------------------- * api-ms-win-crt-math-l1-1-0.dll: ILT RVA: 0x2938 IAT RVA: 0x20D8 Bound: FALSE Entry: Name: __setusermatherr Hint RVA: 0x2A9C Hint: 0x9 ---------------------- * api-ms-win-crt-stdio-l1-1-0.dll: ILT RVA: 0x29E0 IAT RVA: 0x2180 Bound: FALSE Entry: Name: __p__commode Hint RVA: 0x2BCA Hint: 0x1 Entry: Name: _set_fmode Hint RVA: 0x2B38 Hint: 0x54 ---------------------- * api-ms-win-crt-locale-l1-1-0.dll: ILT RVA: 0x2928 IAT RVA: 0x20C8 Bound: FALSE Entry: Name: _configthreadlocale Hint RVA: 0x2BA4 Hint: 0x8 ---------------------- * api-ms-win-crt-heap-l1-1-0.dll: ILT RVA: 0x2918 IAT RVA: 0x20B8 Bound: FALSE Entry: Name: _set_new_mode Hint RVA: 0x2BBA Hint: 0x16 ---------------------- ---------------------------------- BASE RELOCATIONS TABLE: ----------------------- Block 0x0: Page RVA: 0x2000 Block size: 0x28 Number of entries: 0x10 Entries: * Value: 0xA198 Relocation Type: 0xA Offset: 0x198 * Value: 0xA1A0 Relocation Type: 0xA Offset: 0x1A0 * Value: 0xA1A8 Relocation Type: 0xA Offset: 0x1A8 * Value: 0xA1B0 Relocation Type: 0xA Offset: 0x1B0 * Value: 0xA1B8 Relocation Type: 0xA Offset: 0x1B8 * Value: 0xA1C8 Relocation Type: 0xA Offset: 0x1C8 * Value: 0xA1E0 Relocation Type: 0xA Offset: 0x1E0 * Value: 0xA1E8 Relocation Type: 0xA Offset: 0x1E8 * Value: 0xA220 Relocation Type: 0xA Offset: 0x220 * Value: 0xA228 Relocation Type: 0xA Offset: 0x228 * Value: 0xA318 Relocation Type: 0xA Offset: 0x318 * Value: 0xA330 Relocation Type: 0xA Offset: 0x330 * Value: 0xA338 Relocation Type: 0xA Offset: 0x338 * Value: 0xA3D8 Relocation Type: 0xA Offset: 0x3D8 * Value: 0xA3E0 Relocation Type: 0xA Offset: 0x3E0 * Value: 0xA3E8 Relocation Type: 0xA Offset: 0x3E8 ---------------------- ----------------------------------I hope that seeing actual code has given you a better understanding of what we’ve discussed throughout the previous posts.I believe that there are better ways for implementation than the ones I have presented, I’m in no way a c++ programmer and I know that there’s always room for improvement, so feel free to reach out to me, any feedback would be much appreciated.Thanks for reading.
|
||
|
EMBER2024 -- A Benchmark Dataset for Holistic Evaluation of Malware Classifiers
https://arxiv.org/pdf/2506.05074
|
||
|
Theodosius - Jit linker, Symbol Mapper, and Obfuscator
https://back.engineering/blog/06/05/2022/
Existing software protection frameworks typically operate at a small range of compilation levels. The highest level of obfuscation typically operates upon source code directly (source2source), the second highest level is LLVM IR (via optimization passes), and the third and final most common is upon the native binary image (bin2bin).
|
||
|
Writing a Mutation Engine and breaking Aimware
https://back.engineering/blog/13/04/2022/
PERSES is a X86 code obfuscation engine that works with Portable Executable files. The obfuscation works by replacing a specific instruction with a larger more sophisticated set that is semantically compatible to the original.
|
||
|
Binary Ninja - Reverse Engineering a Cobalt Strike Dropper With Binary Ninja
https://binary.ninja/2022/07/22/reverse-engineering-cobalt-strike.html
Binary Ninja is a modern reverse engineering platform with a scriptable and extensible decompiler.
|
||
|
Avred background: Advances in Reversing Defender Signature Format
https://blog.deeb.ch/posts/avred-update/
|
||
| https://blog.es3n1n.eu/posts/obfuscator-pt-1/ | ||
|
Opaque Predicates and How to Hunt Them
https://blog.midi12.re/opaque-predicates-how-hunt-them/
Removing opaque predicates and control flow graph obfuscation with MIASM and IDA PRO
|
||
|
https://blog.quarkslab.com/obfuscation-vs-the-optimizer-an-llvm-middle-end-arms-race.html
How one Commit Broke Obfuscation: A blog post exploring the role of compilers and optimizations in the field of obfuscation and de-obfuscation.
|
||
| https://blog.scrt.ch/2022/04/11/statically-encrypt-strings-in-a-binary-with-keystone-lief-and-radare2-rizin/ | ||
|
GoStringUngarbler: Deobfuscating Strings in Garbled Binaries | Google Cloud Blog
https://cloud.google.com/blog/topics/threat-intelligence/gostringungarbler-deobfuscating-strings-in-garbled-binaries
We discuss how threat actors protect malware with garble obfuscation, and the process of automatically deobfuscating it.
|
||
|
The Art of Self-Mutating Malware
https://f00crew.org/0x48
In the beginning, there was the signature. A simple string of bytes that uniquely identified a piece of malware. Those were simpler times - append your virus to a file, patch the entry point, and you’re done. The AV industry responded with signature databases, and for a while, the game was predictable.
|
||
|
Dive into PEB Walk in Malware Analysis - root@fareed:~#
https://fareedfauzi.github.io/2024/07/13/PEB-Walk.html
Hello world!It’s July and this is another blog post on malware analysis. I came up with the idea to write this blog because I feel that beginners in the malw..., The only way to learn is to always have a newbie mindset
|
||
|
PE Internals Part 2: Exploring PE import tables (IDT, ILT, IAT, Hint/Name tables) – ferreirasc – Hacking, Programming and Random stuff
https://ferreirasc.github.io/PE-imports/
Whenever an imported function is used in our PE executable, the PE loader will have to somehow resolve and store the address of that function in the Import Address Table (IAT), for later reference. Other tables participate in the process, such as the Import Lookup Table (ILT) and the Import Directory Table (IDT).
Let’s dive a bit into PE imports!
|
||
|
Malware EDR Evasion Techniques - G3tSyst3m’s Infosec Blog
https://g3tsyst3m.com/edr%20bypass/Malware-EDR-Evasion-Techniques/
Yo, how’s it going everyone. Sorry it’s been a while since my last post. Fear not, I’m geared up and ready to dive in to a full discussion on Malware evasive maneuvers…specifically, .js files for the first drops on a machine. Oh and yes, you’re reading that correctly. I said .js, because believe it or not it is still actively used and HIGHLY effective at bypassing your most common EDR solutions today. Why is that? Well, it’s likely due to a few reasons:, Yo, how’s it going everyone. Sorry it’s been a while since my last post. Fear not, I’m geared up and ready to dive in to a full discussion on Malware evasive maneuvers…specifically, .js files for the first drops on a machine. Oh and yes, you’re reading that correctly. I said .js, because believe it or not it is still actively used and HIGHLY effective at bypassing your most common EDR solutions today. Why is that? Well, it’s likely due to a few reasons:
|
||
|
GitHub - 0xtengu/Cloak64
https://github.com/0xtengu/Cloak64
Contribute to 0xtengu/Cloak64 development by creating an account on GitHub.
|
||
|
GitHub - andrivet/ADVobfuscator: Obfuscation library based on C++20 and metaprogramming
https://github.com/andrivet/ADVobfuscator
Obfuscation library based on C++20 and metaprogramming - andrivet/ADVobfuscator
|
||
|
GitHub - burrowers/garble: Obfuscate Go builds
https://github.com/burrowers/garble
Obfuscate Go builds. Contribute to burrowers/garble development by creating an account on GitHub.
|
||
|
GitHub - connorjaydunn/BinaryShield: An x86-64 Code Virtualizer
https://github.com/connorjaydunn/BinaryShield
An x86-64 Code Virtualizer. Contribute to connorjaydunn/BinaryShield development by creating an account on GitHub.
|
||
|
GitHub - DosX-dev/Astral-PE: Astral-PE is a low-level mutator (Headers/EP obfuscator) for native Windows PE files (x32/x64) · GitHub
https://github.com/DosX-dev/Astral-PE
Astral-PE is a low-level mutator (Headers/EP obfuscator) for native Windows PE files (x32/x64) - DosX-dev/Astral-PE
|
||
|
GitHub - DosX-dev/obfus.h: Macro-header for compile-time C obfuscation (tcc, win x86/x64) · GitHub
https://github.com/DosX-dev/obfus.h
Macro-header for compile-time C obfuscation (tcc, win x86/x64) - DosX-dev/obfus.h
|
||
|
GitHub - es3n1n/obfuscator: PE (and elf now!) bin2bin obfuscator
https://github.com/es3n1n/obfuscator
PE (and elf now!) bin2bin obfuscator. Contribute to es3n1n/obfuscator development by creating an account on GitHub.
|
||
|
GitHub - eversinc33/MogVMP: Static devirtualizer for VMProtect 3.0-3.5. Lifts virtualized code to LLVM using Remill and strips the VM layer through optimization. · GitHub
https://github.com/eversinc33/MogVMP
Static devirtualizer for VMProtect 3.0-3.5. Lifts virtualized code to LLVM using Remill and strips the VM layer through optimization. - eversinc33/MogVMP
|
||
|
Windows-Internals/Processes and Jobs/Processes/PEB - Part 1.md at main · Faran-17/Windows-Internals · GitHub
https://github.com/Faran-17/Windows-Internals/blob/main/Processes%20and%20Jobs/Processes/PEB%20-%20Part%201.md
Important notes and topics on my journey towards mastering Windows Internals - Windows-Internals/Processes and Jobs/Processes/PEB - Part 1.md at main · Faran-17/Windows-Internals
|
||
|
GitHub - Fatmike-GH/Fatpack: A Windows PE packer for executables (x64) with LZMA compression and with full TLS (Thread Local Storage) support.
https://github.com/Fatmike-GH/Fatpack
A Windows PE packer for executables (x64) with LZMA compression and with full TLS (Thread Local Storage) support. - Fatmike-GH/Fatpack
|
||
|
GitHub - haunted-zeroday/PE-XRay-EDR · GitHub
https://github.com/haunted-zeroday/PE-XRay-EDR
Contribute to haunted-zeroday/PE-XRay-EDR development by creating an account on GitHub.
|
||
|
GitHub - keowu/Ryujin: Ryūjin Protector - Is a Intel Arch - BIN2BIN - PE Obfuscation/Protection/DRM tool
https://github.com/keowu/Ryujin
Ryūjin Protector - Is a Intel Arch - BIN2BIN - PE Obfuscation/Protection/DRM tool - keowu/Ryujin
|
||
|
GitHub - mike1k/perses: X86 Mutation Engine with Portable Executable compatibility.
https://github.com/mike1k/perses
X86 Mutation Engine with Portable Executable compatibility. - mike1k/perses
|
||
|
GitHub - pygrum/gimmick: Section-based payload obfuscation technique for x64
https://github.com/pygrum/gimmick
Section-based payload obfuscation technique for x64 - pygrum/gimmick
|
||
|
GitHub - raskolnikov90/Beatrice.py: Modify machine code in binaries with alternative x64 assembly opcodes for AV evasion · GitHub
https://github.com/raskolnikov90/Beatrice.py
Modify machine code in binaries with alternative x64 assembly opcodes for AV evasion - raskolnikov90/Beatrice.py
|
||
|
GitHub - Tylous/Mangle: Mangle is a tool that manipulates aspects of compiled executables (.exe or DLL) to avoid detection from EDRs · GitHub
https://github.com/Tylous/Mangle
Mangle is a tool that manipulates aspects of compiled executables (.exe or DLL) to avoid detection from EDRs - Tylous/Mangle
|
||
|
GitHub - vxlang/vxlang-page: protector & obfuscator & code virtualizer
https://github.com/vxlang/vxlang-page
protector & obfuscator & code virtualizer. Contribute to vxlang/vxlang-page development by creating an account on GitHub.
|
||
|
GitHub - vxunderground/VX-API: Collection of various malicious functionality to aid in malware development
https://github.com/vxunderground/VX-API
Collection of various malicious functionality to aid in malware development - vxunderground/VX-API
|
||
|
GitHub - weak1337/Alcatraz: x64 binary obfuscator
https://github.com/weak1337/Alcatraz
x64 binary obfuscator. Contribute to weak1337/Alcatraz development by creating an account on GitHub.
|
||
|
GitHub - x86byte/Obfusk8: Obfusk8: lightweight Obfuscation library based on C++17 / Header Only for windows binaries
https://github.com/x86byte/Obfusk8
Obfusk8: lightweight Obfuscation library based on C++17 / Header Only for windows binaries - x86byte/Obfusk8
|
||
|
GitHub - zx0CF1/shredder-rs: A high-fidelity x86_64 polymorphic mutation engine focused on instruction-level fragmentation and context preservation.
https://github.com/zx0CF1/shredder-rs
A high-fidelity x86_64 polymorphic mutation engine focused on instruction-level fragmentation and context preservation. - zx0CF1/shredder-rs
|
||
|
AV Evasion with Chisel
https://jadu101.github.io/RedTeam/AV-Evasion/AV-Evasion-with-Chisel
Chisel Client: From 16/65 to 1/65 flagged on VT Introduction I recently had the opportunity to beta test an excellent course, RTL (RedTeamLite), created by Red Raccoon.
|
||
|
Karma-X - Protection > Detection
https://karma-x.io/blog/post/18/?=xachilles
|
||
|
https://keowu.re/posts/Analyzing-Mutation-Coded-VM-Protect-and-Alcatraz-English/
Security Researcher | i like All OS Internals, Malware & Reverse Engineering, C++, Intel/ARM Assembly and cool things.
|
||
|
https://keowu.re/posts/Ry%C5%ABjin---Writing-a-Bin2Bin-Obfuscator-from-Scratch-for-Windows-PE-x64-and-Fully-Deobfuscating-It
Security Researcher | i like All OS Internals, Malware & Reverse Engineering, C++, Intel/ARM Assembly and cool things.
|
||
|
Bypassing static analysis - Deep Dive | Lorenzo Meacci
https://lorenzomeacci.com/bypassing-static-analysis-deep-dive
In this blog, I’ll walk you through the core concepts of static analysis used by AV and EDR products to inspect files written to disk. I’ll also demonstrate various techniques you can use to craft payloads that evade detection during this scanning phase.
|
||
|
Citadel 2.0: Predicting Maliciousness
https://mez0.cc/posts/citadel-ember/
Citadel 2.0 is a new version of my binary static analysis framework. This post looks at the new features, including the introduction of Ember 2024, a vibe-coded UI, and some new analysis features.
|
||
|
Citadel: Binary Static Analysis Framework
https://mez0.cc/posts/citadel/
Citadel: Binary Static Analysis Framework - A comprehensive guide on binary static analysis, including installation, workflow, PE parsing, and more.
|
||
|
Categorising DLL Exports with an LLM
https://mez0.cc/posts/dll-export-category/
Learn how to categorise DLL exports using a Large Language Model (LLM) for better understanding and analysis of Windows Dynamic Link Libraries.
|
||
|
Using EMBER2024 to evaluate red team implants
https://mez0.cc/posts/evaluating-implants-with-ember/
As EDR detections get better and utilise ML extensively, its important to understand what makes a sample good or bad. This blog looks at EMBER2024 and how it can be used to evaluate red team implants.
|
||
|
Dissecting the Windows Defender Driver - WdFilter (Part 2) :: Up is Down and Black is White — n4r1b
https://n4r1b.com/posts/2020/02/dissecting-the-windows-defender-driver-wdfilter-part-2/
|
||
|
Evading Static Analysis | Oblivion
https://oblivion-malware.xyz/posts/evading-static-analysis/
Static binary analysis is a process of examining an executable program without actually running it. I will present how it is performed and how not to get flagged.
|
||
|
Introducing New Packing Method: First Reflective PE Packer Amber – Pentest Blog
https://pentest.blog/introducing-new-packing-method-first-reflective-pe-packer/
|
||
|
Threat Hunting with File Entropy – Practical Security Analytics LLC
https://practicalsecurityanalytics.com/file-entropy/
|
||
|
PE Malware Machine Learning Dataset – Practical Security Analytics LLC
https://practicalsecurityanalytics.com/pe-malware-machine-learning-dataset/
|
||
|
Bluffy the AV Slayer: Bypassing Static Detection | pre.empt.blog | Cybersecurity Research & Technical Blog
https://pre.empt.blog/post/bluffy/
Playing with AV evasion techniques by using formats such as CSS, SVG, etc., Playing with AV evasion techniques by using formats such as CSS, SVG, etc.
|
||
|
Static Data Exploration of Malware and Goodware Samples | pre.empt.blog | Cybersecurity Research & Technical Blog
https://pre.empt.blog/post/static-data-exploration/
Exploring our dataset of malware and benign binaries to find patterns, anomalies, and generally seeing what we have., Exploring our dataset of malware and benign binaries to find patterns, anomalies, and generally seeing what we have.
|
||
| https://retooling.io/blog/an-unexpected-journey-into-microsoft-defenders-signature-world | ||
|
PE Relocation Table – Sabotage Sec
https://sabotagesec.com/pe-relocation-table/
|
||
|
Analysis of VMProtect. Packer confirmed for use in… | by Sachiel | Medium
https://sachiel-archangel.medium.com/analysis-of-vmprotect-0b28c8e47ca5
Analysis of VMProtect Abstract The malware dropper “PrivateLoader” that I analyzed last year used a packer that appeared to be VMProtect. The creators of VMProtect claim that it is a feature …
|
||
|
RISC-Y Business: Raging against the reduced machine | secret club
https://secret.club/2023/12/24/riscy-business.html
Abstract In recent years the interest in obfuscation has increased, mainly because people want to protect their intellectual property. Unfortunately, most of what’s been written is focused on the theoretical aspects. In this article, we will discuss the practical engineering challenges of developing a low-footprint virtual machine interpreter. The VM is easily embeddable, built on open-source technology and has various hardening features that were achieved with minimal effort. Introduction In addition to protecting intellectual property, a minimal virtual machine can be useful for other reasons. You might want to have an embeddable interpreter to execute business logic (shellcode), without having to deal with RWX memory. It can also be useful as an educational tool, or just for fun. Creating a custom VM architecture (similar to VMProtect/Themida) means that we would have to deal with binary rewriting/lifting or write our own compiler. Instead, we decided to use a preexisting architecture, which would be supported by LLVM: RISC-V. This architecture is already widely used for educational purposes and has the advantage of being very simple to understand and implement. Initially, the main contender was WebAssembly. However, existing interpreters were very bloated and would also require dealing with a binary format. Additionally, it looks like WASM64 is very underdeveloped and our memory model requires 64-bit pointer support. SPARC and PowerPC were also considered, but RISC-V seems to be more popular and there are a lot more resources available for it. WebAssembly was designed for sandboxing and therefore strictly separates guest and host memory. Because we will be writing our own RISC-V interpreter, we chose to instead share memory between the guest and the host. This means that pointers in the RISC-V execution context (the guest) are valid in the host process and vice-versa. As a result, the instructions responsible for reading/writing memory can be implemented as a simple memcpy call and we do not need additional code to translate/validate memory accesses (which helps with our goal of small code size). With this property, we need to implement only two system calls to perform arbitrary operations in the host process: uintptr_t riscvm_get_peb(); uintptr_t riscvm_host_call(uintptr_t rip, uintptr_t args[13]); The riscvm_get_peb is Windows-specific and it allows us to resolve exports, which we can then pass to the riscvm_host_call function to execute arbitrary code. Additionally, an optional host_syscall stub could be implemented, but this is not strictly necessary since we can just call the functions in ntdll.dll instead. Toolchain and CRT To keep the interpreter footprint as low as possible, we decided to develop a toolchain that outputs a freestanding binary. The goal is to copy this binary into memory and point the VM’s program counter there to start execution. Because we are in freestanding mode, there is no C runtime available to us, this requires us to handle initialization ourselves. As an example, we will use the following hello.c file: int _start() { int result = 0; for(int i = 0; i < 52; i++) { result += *(volatile int*)&i; } return result + 11; } We compile the program with the following incantation: clang -target riscv64 -march=rv64im -mcmodel=medany -Os -c hello.c -o hello.o And then verify by disassembling the object: $ llvm-objdump --disassemble hello.o hello.o: file format elf64-littleriscv 0000000000000000 <_start>: 0: 13 01 01 ff addi sp, sp, -16 4: 13 05 00 00 li a0, 0 8: 23 26 01 00 sw zero, 12(sp) c: 93 05 30 03 li a1, 51 0000000000000010 <.LBB0_1>: 10: 03 26 c1 00 lw a2, 12(sp) 14: 33 05 a6 00 add a0, a2, a0 18: 9b 06 16 00 addiw a3, a2, 1 1c: 23 26 d1 00 sw a3, 12(sp) 20: 63 40 b6 00 blt a2, a1, 0x20 <.LBB0_1+0x10> 24: 1b 05 b5 00 addiw a0, a0, 11 28: 13 01 01 01 addi sp, sp, 16 2c: 67 80 00 00 ret The hello.o is a regular ELF object file. To get a freestanding binary we need to invoke the linker with a linker script: ENTRY(_start) LINK_BASE = 0x8000000; SECTIONS { . = LINK_BASE; __base = .; .text : ALIGN(16) { . = LINK_BASE; *(.text) *(.text.*) } .data : { *(.rodata) *(.rodata.*) *(.data) *(.data.*) *(.eh_frame) } .init : { __init_array_start = .; *(.init_array) __init_array_end = .; } .bss : { *(.bss) *(.bss.*) *(.sbss) *(.sbss.*) } .relocs : { . = . + SIZEOF(.bss); __relocs_start = .; } } This script is the result of an excessive amount of swearing and experimentation. The format is .name : { ... } where .name is the destination section and the stuff in the brackets is the content to paste in there. The special . operator is used to refer to the current position in the binary and we define a few special symbols for use by the runtime: Symbol Meaning __base Base of the executable. __init_array_start Start of the C++ init arrays. __init_array_end End of the C++ init arrays. __relocs_start Start of the relocations (end of the binary). These symbols are declared as extern in the C code and they will be resolved at link-time. While it may seem confusing at first that we have a destination section, it starts to make sense once you realize the linker has to output a regular ELF executable. That ELF executable is then passed to llvm-objcopy to create the freestanding binary blob. This makes debugging a whole lot easier (because we get DWARF symbols) and since we will not implement an ELF loader, it also allows us to extract the relocations for embedding into the final binary. To link the intermediate ELF executable and then create the freestanding hello.pre.bin: ld.lld.exe -o hello.elf --oformat=elf -emit-relocs -T ..\lib\linker.ld --Map=hello.map hello.o llvm-objcopy -O binary hello.elf hello.pre.bin For debugging purposes we also output hello.map, which tells us exactly where the linker put the code/data: VMA LMA Size Align Out In Symbol 0 0 0 1 LINK_BASE = 0x8000000 0 0 8000000 1 . = LINK_BASE 8000000 0 0 1 __base = . 8000000 8000000 30 16 .text 8000000 8000000 0 1 . = LINK_BASE 8000000 8000000 30 4 hello.o:(.text) 8000000 8000000 30 1 _start 8000010 8000010 0 1 .LBB0_1 8000030 8000030 0 1 .init 8000030 8000030 0 1 __init_array_start = . 8000030 8000030 0 1 __init_array_end = . 8000030 8000030 0 1 .relocs 8000030 8000030 0 1 . = . + SIZEOF ( .bss ) 8000030 8000030 0 1 __relocs_start = . 0 0 18 8 .rela.text 0 0 18 8 hello.o:(.rela.text) 0 0 3b 1 .comment 0 0 3b 1 <internal>:(.comment) 0 0 30 1 .riscv.attributes 0 0 30 1 <internal>:(.riscv.attributes) 0 0 108 8 .symtab 0 0 108 8 <internal>:(.symtab) 0 0 55 1 .shstrtab 0 0 55 1 <internal>:(.shstrtab) 0 0 5c 1 .strtab 0 0 5c 1 <internal>:(.strtab) The final ingredient of the toolchain is a small Python script (relocs.py) that extracts the relocations from the ELF file and appends them to the end of the hello.pre.bin. The custom relocation format only supports R_RISCV_64 and is resolved by our CRT like so: typedef struct { uint8_t type; uint32_t offset; int64_t addend; } __attribute__((packed)) Relocation; extern uint8_t __base[]; extern uint8_t __relocs_start[]; #define LINK_BASE 0x8000000 #define R_RISCV_NONE 0 #define R_RISCV_64 2 static __attribute((noinline)) void riscvm_relocs() { if (*(uint32_t*)__relocs_start != 'ALER') { asm volatile("ebreak"); } uintptr_t load_base = (uintptr_t)__base; for (Relocation* itr = (Relocation*)(__relocs_start + sizeof(uint32_t)); itr->type != R_RISCV_NONE; itr++) { if (itr->type == R_RISCV_64) { uint64_t* ptr = (uint64_t*)((uintptr_t)itr->offset - LINK_BASE + load_base); *ptr -= LINK_BASE; *ptr += load_base; } else { asm volatile("ebreak"); } } } As you can see, the __base and __relocs_start magic symbols are used here. The only reason this works is the -mcmodel=medany we used when compiling the object. You can find more details in this article and in the RISC-V ELF Specification. In short, this flag allows the compiler to assume that all code will be emitted in a 2 GiB address range, which allows more liberal PC-relative addressing. The R_RISCV_64 relocation type gets emitted when you put pointers in the .data section: void* functions[] = { &function1, &function2, }; This also happens when using vtables in C++, and we wanted to support these properly early on, instead of having to fight with horrifying bugs later. The next piece of the CRT involves the handling of the init arrays (which get emitted by global instances of classes that have a constructor): typedef void (*InitFunction)(); extern InitFunction __init_array_start; extern InitFunction __init_array_end; static __attribute((optnone)) void riscvm_init_arrays() { for (InitFunction* itr = &__init_array_start; itr != &__init_array_end; itr++) { (*itr)(); } } Frustratingly, we were not able to get this function to generate correct code without the __attribute__((optnone)). We suspect this has to do with aliasing assumptions (the start/end can technically refer to the same memory), but we didn’t investigate this further. Interpreter internals Note: the interpreter was initially based on riscvm.c by edubart. However, we have since completely rewritten it in C++ to better suit our purpose. Based on the RISC-V Calling Conventions document, we can create an enum for the 32 registers: enum RegIndex { reg_zero, // always zero (immutable) reg_ra, // return address reg_sp, // stack pointer reg_gp, // global pointer reg_tp, // thread pointer reg_t0, // temporary reg_t1, reg_t2, reg_s0, // callee-saved reg_s1, reg_a0, // arguments reg_a1, reg_a2, reg_a3, reg_a4, reg_a5, reg_a6, reg_a7, reg_s2, // callee-saved reg_s3, reg_s4, reg_s5, reg_s6, reg_s7, reg_s8, reg_s9, reg_s10, reg_s11, reg_t3, // temporary reg_t4, reg_t5, reg_t6, }; We just need to add a pc register and we have the structure to represent the RISC-V CPU state: struct riscvm { int64_t pc; uint64_t regs[32]; }; It is important to keep in mind that the zero register is always set to 0 and we have to prevent writes to it by using a macro: #define reg_write(idx, value) \ do \ { \ if (LIKELY(idx != reg_zero)) \ { \ self->regs[idx] = value; \ } \ } while (0) The instructions (ignoring the optional compression extension) are always 32-bits in length and can be cleanly expressed as a union: union Instruction { struct { uint32_t compressed_flags : 2; uint32_t opcode : 5; uint32_t : 25; }; struct { uint32_t opcode : 7; uint32_t rd : 5; uint32_t funct3 : 3; uint32_t rs1 : 5; uint32_t rs2 : 5; uint32_t funct7 : 7; } rtype; struct { uint32_t opcode : 7; uint32_t rd : 5; uint32_t funct3 : 3; uint32_t rs1 : 5; uint32_t rs2 : 5; uint32_t shamt : 1; uint32_t imm : 6; } rwtype; struct { uint32_t opcode : 7; uint32_t rd : 5; uint32_t funct3 : 3; uint32_t rs1 : 5; uint32_t imm : 12; } itype; struct { uint32_t opcode : 7; uint32_t rd : 5; uint32_t imm : 20; } utype; struct { uint32_t opcode : 7; uint32_t rd : 5; uint32_t imm12 : 8; uint32_t imm11 : 1; uint32_t imm1 : 10; uint32_t imm20 : 1; } ujtype; struct { uint32_t opcode : 7; uint32_t imm5 : 5; uint32_t funct3 : 3; uint32_t rs1 : 5; uint32_t rs2 : 5; uint32_t imm7 : 7; } stype; struct { uint32_t opcode : 7; uint32_t imm_11 : 1; uint32_t imm_1_4 : 4; uint32_t funct3 : 3; uint32_t rs1 : 5; uint32_t rs2 : 5; uint32_t imm_5_10 : 6; uint32_t imm_12 : 1; } sbtype; int16_t chunks16[2]; uint32_t bits; }; static_assert(sizeof(Instruction) == sizeof(uint32_t), ""); There are 13 top-level opcodes (Instruction.opcode) and some of those opcodes have another field that further specializes the functionality (i.e. Instruction.itype.funct3). To keep the code readable, the enumerations for the opcode are defined in opcodes.h. The interpreter is structured to have handler functions for the top-level opcode in the following form: bool handler_rv64_<opcode>(riscvm_ptr self, Instruction inst); As an example, we can look at the handler for the lui instruction (note that the handlers themselves are responsible for updating pc): ALWAYS_INLINE static bool handler_rv64_lui(riscvm_ptr self, Instruction inst) { int64_t imm = bit_signer(inst.utype.imm, 20) << 12; reg_write(inst.utype.rd, imm); self->pc += 4; dispatch(); // return true; } The interpreter executes until one of the handlers returns false, indicating the CPU has to halt: void riscvm_run(riscvm_ptr self) { while (true) { Instruction inst; inst.bits = *(uint32_t*)self->pc; if (!riscvm_execute_handler(self, inst)) break; } } Plenty of articles have been written about the semantics of RISC-V, so you can look at the source code if you’re interested in the implementation details of individual instructions. The structure of the interpreter also allows us to easily implement obfuscation features, which we will discuss in the next section. For now, we will declare the handler functions as __attribute__((always_inline)) and set the -fno-jump-tables compiler option, which gives us a riscvm_run function that (comfortably) fits into a single page (0xCA4 bytes): Hardening features A regular RISC-V interpreter is fun, but an attacker can easily reverse engineer our payload by throwing it into Ghidra to decompile it. To force the attacker to at least look at our VM interpreter, we implemented a few security features. These features are implemented in a Python script that parses the linker MAP file and directly modifies the opcodes: encrypt.py. Opcode shuffling The most elegant (and likely most effective) obfuscation is to simply reorder the enums of the instruction opcodes and sub-functions. The shuffle.py script is used to generate shuffled_opcodes.h, which is then included into riscvm.h instead of opcodes.h to mix the opcodes: #ifdef OPCODE_SHUFFLING #warning Opcode shuffling enabled #include "shuffled_opcodes.h" #else #include "opcodes.h" #endif // OPCODE_SHUFFLING There is also a shuffled_opcodes.json file generated, which is parsed by encrypt.py to know how to shuffle the assembled instructions. Because enums are used for all the opcodes, we only need to recompile the interpreter to obfuscate it; there is no additional complexity cost in the implementation. Bytecode encryption To increase diversity between payloads for the same VM instance, we also employ a simple ‘encryption’ scheme on top of the opcode: ALWAYS_INLINE static uint32_t tetra_twist(uint32_t input) { /** * Custom hash function that is used to generate the encryption key. * This has strong avalanche properties and is used to ensure that * small changes in the input result in large changes in the output. */ constexpr uint32_t prime1 = 0x9E3779B1; // a large prime number input ^= input >> 15; input *= prime1; input ^= input >> 12; input *= prime1; input ^= input >> 4; input *= prime1; input ^= input >> 16; return input; } ALWAYS_INLINE static uint32_t transform(uintptr_t offset, uint32_t key) { uint32_t key2 = key + offset; return tetra_twist(key2); } ALWAYS_INLINE static uint32_t riscvm_fetch(riscvm_ptr self) { uint32_t data; memcpy(&data, (const void*)self->pc, sizeof(data)); #ifdef CODE_ENCRYPTION return data ^ transform(self->pc - self->base, self->key); #else return data; #endif // CODE_ENCRYPTION } The offset relative to the start of the bytecode is used as the seed to a simple transform function. The result of this function is XOR’d with the instruction data before decoding. The exact transformation doesn’t really matter, because an attacker can always observe the decrypted bytecode at runtime. However, static analysis becomes more difficult and pattern-matching the payload is prevented, all for a relatively small increase in VM implementation complexity. It would be possible to encrypt the contents of the .data section of the payload as well, but we would have to completely decrypt it in memory before starting execution anyway. Technically, it would be also possible to implement a lazy encryption scheme by customizing the riscvm_read and riscvm_write functions to intercept reads/writes to the payload region, but this idea was not pursued further. Threaded handlers The most interesting feature of our VM is that we only need to make minor code modifications to turn it into a so-called threaded interpreter. Threaded code is a well-known technique used both to speed up emulators and to introduce indirect branches that complicate reverse engineering. It is called threading because the execution can be visualized as a thread of handlers that directly branch to the next handler. There is no classical dispatch function, with an infinite loop and a switch case for each opcode inside. The performance improves because there are fewer false-positives in the branch predictor when executing threaded code. You can find more information about threaded interpreters in the Dispatch Techniques section of the YETI paper. The first step is to construct a handler table, where each handler is placed at the index corresponding to each opcode. To do this we use a small snippet of constexpr C++ code: typedef bool (*riscvm_handler_t)(riscvm_ptr, Instruction); static constexpr std::array<riscvm_handler_t, 32> riscvm_handlers = [] { // Pre-populate the table with invalid handlers std::array<riscvm_handler_t, 32> result = {}; for (size_t i = 0; i < result.size(); i++) { result[i] = handler_rv64_invalid; } // Insert the opcode handlers at the right index #define INSERT(op) result[op] = HANDLER(op) INSERT(rv64_load); INSERT(rv64_fence); INSERT(rv64_imm64); INSERT(rv64_auipc); INSERT(rv64_imm32); INSERT(rv64_store); INSERT(rv64_op64); INSERT(rv64_lui); INSERT(rv64_op32); INSERT(rv64_branch); INSERT(rv64_jalr); INSERT(rv64_jal); INSERT(rv64_system); #undef INSERT return result; }(); With the riscvm_handlers table populated we can define the dispatch macro: #define dispatch() \ Instruction next; \ next.bits = riscvm_fetch(self); \ if (next.compressed_flags != 0b11) \ { \ panic("compressed instructions not supported!"); \ } \ __attribute__((musttail)) return riscvm_handlers[next.opcode](self, next) The musttail attribute forces the call to the next handler to be a tail call. This is only possible because all the handlers have the same function signature and it generates an indirect branch to the next handler: The final piece of the puzzle is the new implementation of the riscvm_run function, which uses an empty riscvm_execute handler to bootstrap the chain of execution: ALWAYS_INLINE static bool riscvm_execute(riscvm_ptr self, Instruction inst) { dispatch(); } NEVER_INLINE void riscvm_run(riscvm_ptr self) { Instruction inst; riscvm_execute(self, inst); } Traditional obfuscation The built-in hardening features that we can get with a few #ifdefs and a small Python script are good enough for a proof-of-concept, but they are not going to deter a determined attacker for a very long time. An attacker can pattern-match the VM’s handlers to simplify future reverse engineering efforts. To address this, we can employ common obfuscation techniques using LLVM obfuscation passes: Instruction substitution (to make pattern matching more difficult) Opaque predicates (to hinder static analysis) Inject anti-debug checks (to make dynamic analysis more difficult) The paper Modern obfuscation techniques by Roman Oravec gives a nice overview of literature and has good data on what obfuscation passes are most effective considering their runtime overhead. Additionally, it would also be possible to further enhance the VM’s security by duplicating handlers, but this would require extra post-processing on the payload itself. The VM itself is only part of what could be obfuscated. Obfuscating the payloads themselves is also something we can do quite easily. Most likely, manually-integrated security features (stack strings with xorstr, lazy_importer and variable encryption) will be most valuable here. However, because we use LLVM to build the payloads we can also employ automated obfuscation there. It is important to keep in mind that any overhead created in the payloads themselves is multiplied by the overhead created by the handler obfuscation, so experimentation is required to find the sweet spot for your use case. Writing the payloads The VM described in this post so far technically has the ability to execute arbitrary code. That being said, it would be rather annoying for an end-user to write said code. For example, we would have to manually resolve all imports and then use the riscvm_host_call function to actually execute them. These functions are executing in the RISC-V context and their implementation looks like this: uintptr_t riscvm_host_call(uintptr_t address, uintptr_t args[13]) { register uintptr_t a0 asm("a0") = address; register uintptr_t a1 asm("a1") = (uintptr_t)args; register uintptr_t a7 asm("a7") = 20000; asm volatile("scall" : "+r"(a0) : "r"(a1), "r"(a7)); return a0; } uintptr_t riscvm_get_peb() { register uintptr_t a0 asm("a0") = 0; register uintptr_t a7 asm("a7") = 20001; asm volatile("scall" : "+r"(a0) : "r"(a7) : "memory"); return a0; } We can get a pointer to the PEB using riscvm_get_peb and then resolve a module by its’ x65599 hash: // Structure definitions omitted for clarity uintptr_t riscvm_resolve_dll(uint32_t module_hash) { static PEB* peb = 0; if (!peb) { peb = (PEB*)riscvm_get_peb(); } LIST_ENTRY* begin = &peb->Ldr->InLoadOrderModuleList; for (LIST_ENTRY* itr = begin->Flink; itr != begin; itr = itr->Flink) { LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(itr, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks); if (entry->BaseNameHashValue == module_hash) { return (uintptr_t)entry->DllBase; } } return 0; } Once we’ve obtained the base of the module we’re interested in, we can resolve the import by walking the export table: uintptr_t riscvm_resolve_import(uintptr_t image, uint32_t export_hash) { IMAGE_DOS_HEADER* dos_header = (IMAGE_DOS_HEADER*)image; IMAGE_NT_HEADERS* nt_headers = (IMAGE_NT_HEADERS*)(image + dos_header->e_lfanew); uint32_t export_dir_size = nt_headers->OptionalHeader.DataDirectory[0].Size; IMAGE_EXPORT_DIRECTORY* export_dir = (IMAGE_EXPORT_DIRECTORY*)(image + nt_headers->OptionalHeader.DataDirectory[0].VirtualAddress); uint32_t* names = (uint32_t*)(image + export_dir->AddressOfNames); uint32_t* funcs = (uint32_t*)(image + export_dir->AddressOfFunctions); uint16_t* ords = (uint16_t*)(image + export_dir->AddressOfNameOrdinals); for (uint32_t i = 0; i < export_dir->NumberOfNames; ++i) { char* name = (char*)(image + names[i]); uintptr_t func = (uintptr_t)(image + funcs[ords[i]]); // Ignore forwarded exports if (func >= (uintptr_t)export_dir && func < (uintptr_t)export_dir + export_dir_size) continue; uint32_t hash = hash_x65599(name, true); if (hash == export_hash) { return func; } } return 0; } Now we can call MessageBoxA from RISC-V with the following code: // NOTE: We cannot use Windows.h here #include <stdint.h> int main() { // Resolve LoadLibraryA auto kernel32_dll = riscvm_resolve_dll(hash_x65599("kernel32.dll", false)) auto LoadLibraryA = riscvm_resolve_import(kernel32_dll, hash_x65599("LoadLibraryA", true)) // Load user32.dll uint64_t args[13]; args[0] = (uint64_t)"user32.dll"; auto user32_dll = riscvm_host_call(LoadLibraryA, args); // Resolve MessageBoxA auto MessageBoxA = riscvm_resolve_import(user32_dll, hash_x65599("MessageBoxA", true)); // Show a message to the user args[0] = 0; // hwnd args[1] = (uint64_t)"Hello from RISC-V!"; // msg args[2] = (uint64_t)"riscvm"; // title args[3] = 0; // flags riscvm_host_call(MessageBoxA, args); } With some templates/macros/constexpr tricks we can probably get this down to something more readable, but fundamentally this code will always stay annoying to write. Even if calling imports were a one-liner, we would still have to deal with the fact that we cannot use Windows.h (or any of the Microsoft headers for that matter). The reason for this is that we are cross-compiling with Clang. Even if we were to set up the include paths correctly, it would still be a major pain to get everything to compile correctly. That being said, our VM works! A major advantage of RISC-V is that, since the instruction set is simple, once the fundamentals work, we can be confident that features built on top of this will execute as expected. Whole Program LLVM Usually, when discussing LLVM, the compilation process is running on Linux/macOS. In this section, we will describe a pipeline that can actually be used on Windows, without making modifications to your toolchain. This is useful if you would like to analyze/fuzz/obfuscate Windows applications, which might only compile an MSVC-compatible compiler: clang-cl. Link-time optimization (LTO) Without LTO, the object files produced by Clang are native COFF/ELF/Mach-O files. Every file is optimized and compiled independently. The linker loads these objects and merges them together into the final executable. When enabling LTO, the object files are instead LLVM Bitcode (.bc) files. This allows the linker to merge all the LLVM IR together and perform (more comprehensive) whole-program optimizations. After the LLVM IR has been optimized, the native code is generated and the final executable produced. The diagram below comes from the great Link-time optimisation (LTO) post by Ryan Stinnet: Compiler wrappers Unfortunately, it can be quite annoying to write an executable that can replace the compiler. It is quite simple when dealing with a few object files, but with bigger projects it gets quite tricky (especially when CMake is involved). Existing projects are WLLVM and gllvm, but they do not work nicely on Windows. When using CMake, you can use the CMAKE_<LANG>_COMPILER_LAUNCHER variables and intercept the compilation pipeline that way, but that is also tricky to deal with. On Windows, things are more complex than on Linux. This is because Clang uses a different program to link the final executable and correctly intercepting this process can become quite challenging. Embedding bitcode To achieve our goal of post-processing the bitcode of the whole program, we need to enable bitcode embedding. The first flag we need is -flto, which enables LTO. The second flag is -lto-embed-bitcode, which isn’t documented very well. When using clang-cl, you also need a special incantation to enable it: set(EMBED_TYPE "post-merge-pre-opt") # post-merge-pre-opt/optimized if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") if(WIN32) message(FATAL_ERROR "clang-cl is required, use -T ClangCL --fresh") else() message(FATAL_ERROR "clang compiler is required") endif() elseif(CMAKE_CXX_COMPILER_FRONTEND_VARIANT MATCHES "^MSVC$") # clang-cl add_compile_options(-flto) add_link_options(/mllvm:-lto-embed-bitcode=${EMBED_TYPE}) elseif(WIN32) # clang (Windows) add_compile_options(-fuse-ld=lld-link -flto) add_link_options(-Wl,/mllvm:-lto-embed-bitcode=${EMBED_TYPE}) else() # clang (Linux) add_compile_options(-fuse-ld=lld -flto) add_link_options(-Wl,-lto-embed-bitcode=${EMBED_TYPE}) endif() The -lto-embed-bitcode flag creates an additional .llvmbc section in the final executable that contains the bitcode. It offers three settings: -lto-embed-bitcode=<value> - Embed LLVM bitcode in object files produced by LTO =none - Do not embed =optimized - Embed after all optimization passes =post-merge-pre-opt - Embed post merge, but before optimizations Once the bitcode is embedded within the output binary, it can be extracted using llvm-objcopy and disassembled with llvm-dis. This is normally done as the follows: llvm-objcopy --dump-section=.llvmbc=program.bc program llvm-dis program.bc > program.ll Unfortunately, we discovered a bug/oversight in LLD on Windows. The section is extracted without errors, but llvm-dis fails to load the bitcode. The reason for this is that Windows executables have a FileAlignment attribute, leading to additional padding with zeroes. To get valid bitcode, you need to remove some of these trailing zeroes: import argparse import sys import pefile def main(): # Parse the arguments parser = argparse.ArgumentParser() parser.add_argument("executable", help="Executable with embedded .llvmbc section") parser.add_argument("--output", "-o", help="Output file name", required=True) args = parser.parse_args() executable: str = args.executable output: str = args.output # Find the .llvmbc section pe = pefile.PE(executable) llvmbc = None for section in pe.sections: if section.Name.decode("utf-8").strip("\x00") == ".llvmbc": llvmbc = section break if llvmbc is None: print("No .llvmbc section found") sys.exit(1) # Recover the bitcode and write it to a file with open(output, "wb") as f: data = bytearray(llvmbc.get_data()) # Truncate all trailing null bytes while data[-1] == 0: data.pop() # Recover alignment to 4 while len(data) % 4 != 0: data.append(0) # Add a block end marker for _ in range(4): data.append(0) f.write(data) if __name__ == "__main__": main() In our testing, this doesn’t have any issues, but there might be cases where this heuristic does not work properly. In that case, a potential solution could be to brute force the amount of trailing zeroes, until the bitcode parses without errors. Applications Now that we have access to our program’s bitcode, several applications become feasible: Write an analyzer to identify potentially interesting locations within the program. Instrument the bitcode and then re-link the executable, which is particularly useful for code coverage while fuzzing. Obfuscate the bitcode before re-linking the executable, enhancing security. IR retargeting, where the bitcode compiled for one architecture can be used on another. Relinking the executable The bitcode itself unfortunately does not contain enough information to re-link the executable (although this is something we would like to implement upstream). We could either manually attempt to reconstruct the linker command line (with tools like Process Monitor), or use LLVM plugin support. Plugin support is not really functional on Windows (although there is some indication that Sony is using it for their PS4/PS5 toolchain), but we can still load an arbitrary DLL using the -load command line flag. Once we loaded our DLL, we can hijack the executable command line and process the flags to generate a script for re-linking the program after our modifications are done. Retargeting LLVM IR Ideally, we would want to write code like this and magically get it to run in our VM: #include <Windows.h> int main() { MessageBoxA(0, "Hello from RISC-V!", "riscvm", 0); } Luckily this is entirely possible, it just requires writing a (fairly) simple tool to perform transformations on the Bitcode of this program (built using clang-cl). In the coming sections, we will describe how we managed to do this using Microsoft Visual Studio’s official LLVM integration (i.e. without having to use a custom fork of clang-cl). The LLVM IR of the example above looks roughly like this (it has been cleaned up slightly for readability): source_filename = "hello.c" target datalayout = "e-m:w-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-pc-windows-msvc19.38.33133" @message = dso_local global [19 x i8] c"Hello from RISC-V!\00", align 16 @title = dso_local global [7 x i8] c"riscvm\00", align 1 ; Function Attrs: noinline nounwind optnone uwtable define dso_local i32 @main() #0 { %1 = call i32 @MessageBoxA(ptr noundef null, ptr noundef @message, ptr noundef @title, i32 noundef 0) ret i32 0 } declare dllimport i32 @MessageBoxA(ptr noundef, ptr noundef, ptr noundef, i32 noundef) #1 attributes #0 = { noinline nounwind optnone uwtable "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" } attributes #1 = { "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" } !llvm.linker.options = !{!0, !0} !llvm.module.flags = !{!1, !2, !3} !llvm.ident = !{!4} !0 = !{!"/DEFAULTLIB:uuid.lib"} !1 = !{i32 1, !"wchar_size", i32 2} !2 = !{i32 8, !"PIC Level", i32 2} !3 = !{i32 7, !"uwtable", i32 2} !4 = !{!"clang version 16.0.5"} To retarget this code to RISC-V, we need to do the following: Collect all the functions with a dllimport storage class. Generate a riscvm_imports function that resolves all the function addresses of the imports. Replace the dllimport functions with stubs that use riscvm_host_call to call the import. Change the target triple to riscv64-unknown-unknown and adjust the data layout. Compile the retargeted bitcode and link it together with crt0 to create the final payload. Adjusting the metadata After loading the LLVM IR Module, the first step is to change the DataLayout and the TargetTriple to be what the RISC-V backend expects: module.setDataLayout("e-m:e-p:64:64-i64:64-i128:128-n32:64-S128"); module.setTargetTriple("riscv64-unknown-unknown"); module.setSourceFileName("transpiled.bc"); The next step is to collect all the dllimport functions for later processing. Additionally, a bunch of x86-specific function attributes are removed from every function: std::vector<Function*> importedFunctions; for (Function& function : module.functions()) { // Remove x86-specific function attributes function.removeFnAttr("target-cpu"); function.removeFnAttr("target-features"); function.removeFnAttr("tune-cpu"); function.removeFnAttr("stack-protector-buffer-size"); // Collect imported functions if (function.hasDLLImportStorageClass() && !function.getName().startswith("riscvm_")) { importedFunctions.push_back(&function); } function.setDLLStorageClass(GlobalValue::DefaultStorageClass); Finally, we have to remove the llvm.linker.options metadata to make sure we can pass the IR to llc or clang without errors. Import map The LLVM IR only has the dllimport storage class to inform us that a function is imported. Unfortunately, it does not provide us with the DLL the function comes from. Because this information is only available at link-time (in files like user32.lib), we decided to implement an extra -importmap argument. The extract-bc script that extracts the .llvmbc section now also has to extract the imported functions and what DLL they come from: with open(importmap, "wb") as f: for desc in pe.DIRECTORY_ENTRY_IMPORT: dll = desc.dll.decode("utf-8") for imp in desc.imports: name = imp.name.decode("utf-8") f.write(f"{name}:{dll}\n".encode("utf-8")) Currently, imports by ordinal and API sets are not supported, but we can easily make sure those do not occur when building our code. Creating the import stubs For every dllimport function, we need to add some IR to riscvm_imports to resolve the address. Additionally, we have to create a stub that forwards the function arguments to riscvm_host_call. This is the generated LLVM IR for the MessageBoxA stub: ; Global variable to hold the resolved import address @import_MessageBoxA = private global ptr null define i32 @MessageBoxA(ptr noundef %0, ptr noundef %1, ptr noundef %2, i32 noundef %3) local_unnamed_addr #1 { entry: %args = alloca ptr, i32 13, align 8 %arg3_zext = zext i32 %3 to i64 %arg3_cast = inttoptr i64 %arg3_zext to ptr %import_address = load ptr, ptr @import_MessageBoxA, align 8 %arg0_ptr = getelementptr ptr, ptr %args, i32 0 store ptr %0, ptr %arg0_ptr, align 8 %arg1_ptr = getelementptr ptr, ptr %args, i32 1 store ptr %1, ptr %arg1_ptr, align 8 %arg2_ptr = getelementptr ptr, ptr %args, i32 2 store ptr %2, ptr %arg2_ptr, align 8 %arg3_ptr = getelementptr ptr, ptr %args, i32 3 store ptr %arg3_cast, ptr %arg3_ptr, align 8 %return = call ptr @riscvm_host_call(ptr %import_address, ptr %args) %return_cast = ptrtoint ptr %return to i64 %return_trunc = trunc i64 %return_cast to i32 ret i32 %return_trunc } The uint64_t args[13] array is allocated on the stack using the alloca instruction and every function argument is stored in there (after being zero-extended). The GlobalVariable named import_MessageBoxA is read and finally riscvm_host_call is executed to call the import on the host side. The return value is truncated as appropriate and returned from the stub. The LLVM IR for the generated riscvm_imports function looks like this: ; Global string for LoadLibraryA @str_USER32.dll = private constant [11 x i8] c"USER32.dll\00" define void @riscvm_imports() { entry: %args = alloca ptr, i32 13, align 8 %kernel32.dll_base = call ptr @riscvm_resolve_dll(i32 1399641682) %import_LoadLibraryA = call ptr @riscvm_resolve_import(ptr %kernel32.dll_base, i32 -550781972) %arg0_ptr = getelementptr ptr, ptr %args, i32 0 store ptr @str_USER32.dll, ptr %arg0_ptr, align 8 %USER32.dll_base = call ptr @riscvm_host_call(ptr %import_LoadLibraryA, ptr %args) %import_MessageBoxA = call ptr @riscvm_resolve_import(ptr %USER32.dll_base, i32 -50902915) store ptr %import_MessageBoxA, ptr @import_MessageBoxA, align 8 ret void } The resolving itself uses the riscvm_resolve_dll and riscvm_resolve_import functions we discussed in a previous section. The final detail is that user32.dll is not loaded into every process, so we need to manually call LoadLibraryA to resolve it. Instead of resolving the DLL and import hashes at runtime, they are resolved by the transpiler at compile-time, which makes things a bit more annoying to analyze for an attacker. Trade-offs While the retargeting approach works well for simple C++ code that makes use of the Windows API, it currently does not work properly when the C/C++ standard library is used. Getting this to work properly will be difficult, but things like std::vector can be made to work with some tricks. The limitations are conceptually quite similar to driver development and we believe this is a big improvement over manually recreating types and manual wrappers with riscvm_host_call. An unexplored potential area for bugs is the unverified change to the DataLayout of the LLVM module. In our tests, we did not observe any differences in structure layouts between rv64 and x64 code, but most likely there are some nasty edge cases that would need to be properly handled. If the code written is mainly cross-platform, portable C++ with heavy use of the STL, an alternative design could be to compile most of it with a regular C++ cross-compiler and use the retargeting only for small Windows-specific parts. One of the biggest advantages of retargeting a (mostly) regular Windows C++ program is that the payload can be fully developed and tested on Windows itself. Debugging is much more difficult once the code becomes RISC-V and our approach fully decouples the development of the payload from the VM itself. CRT0 The final missing piece of the crt0 component is the _start function that glues everything together: static void exit(int exit_code); static void riscvm_relocs(); void riscvm_imports() __attribute__((weak)); static void riscvm_init_arrays(); extern int __attribute((noinline)) main(); // NOTE: This function has to be first in the file void _start() { riscvm_relocs(); riscvm_imports(); riscvm_init_arrays(); exit(main()); asm volatile("ebreak"); } void riscvm_imports() { // Left empty on purpose } The riscvm_imports function is defined as a weak symbol. This means the implementation provided in crt0.c can be overwritten by linking to a stronger symbol with the same name. If we generate a riscvm_imports function in our retargeted bitcode, that implementation will be used and we can be certain we execute before main! Example payload project Now that all the necessary tooling has been described, we can put everything together in a real project! In the repository, this is all done in the payload folder. To make things easy, this is a simple cmkr project with a template to enable the retargeting scripts: # Reference: https://build-cpp.github.io/cmkr/cmake-toml [cmake] version = "3.19" cmkr-include = "cmake/cmkr.cmake" [project] name = "payload" languages = ["CXX"] cmake-before = "set(CMAKE_CONFIGURATION_TYPES Debug Release)" include-after = ["cmake/riscvm.cmake"] msvc-runtime = "static" [fetch-content.phnt] url = "https://github.com/mrexodia/phnt-single-header/releases/download/v1.2-4d1b102f/phnt.zip" [template.riscvm] type = "executable" add-function = "add_riscvm_executable" [target.payload] type = "riscvm" sources = [ "src/main.cpp", "crt/minicrt.c", "crt/minicrt.cpp", ] include-directories = [ "include", ] link-libraries = [ "riscvm-crt0", "phnt::phnt", ] compile-features = ["cxx_std_17"] msvc.link-options = [ "/INCREMENTAL:NO", "/DEBUG", ] In this case, the add_executable function has been replaced with an equivalent add_riscvm_executable that creates an additional payload.bin file that can be consumed by the riscvm interpreter. The only thing we have to make sure of is to enable clang-cl when configuring the project: cmake -B build -T ClangCL After this, you can open build\payload.sln in Visual Studio and develop there as usual. The custom cmake/riscvm.cmake script does the following: Enable LTO Add the -lto-embed-bitcode linker flag Locale clang.exe, ld.lld.exe and llvm-objcopy.exe Compile crt0.c for the riscv64 architecture Create a Python virtual environment with the necessary dependencies The add_riscvm_executable adds a custom target that processes the regular output executable and executes the retargeter and relevant Python scripts to produce the riscvm artifacts: function(add_riscvm_executable tgt) add_executable(${tgt} ${ARGN}) if(MSVC) target_compile_definitions(${tgt} PRIVATE _NO_CRT_STDIO_INLINE) target_compile_options(${tgt} PRIVATE /GS- /Zc:threadSafeInit-) endif() set(BC_BASE "$<TARGET_FILE_DIR:${tgt}>/$<TARGET_FILE_BASE_NAME:${tgt}>") add_custom_command(TARGET ${tgt} POST_BUILD USES_TERMINAL COMMENT "Extracting and transpiling bitcode..." COMMAND "${Python3_EXECUTABLE}" "${RISCVM_DIR}/extract-bc.py" "$<TARGET_FILE:${tgt}>" -o "${BC_BASE}.bc" --importmap "${BC_BASE}.imports" COMMAND "${TRANSPILER}" -input "${BC_BASE}.bc" -importmap "${BC_BASE}.imports" -output "${BC_BASE}.rv64.bc" COMMAND "${CLANG_EXECUTABLE}" ${RV64_FLAGS} -c "${BC_BASE}.rv64.bc" -o "${BC_BASE}.rv64.o" COMMAND "${LLD_EXECUTABLE}" -o "${BC_BASE}.elf" --oformat=elf -emit-relocs -T "${RISCVM_DIR}/lib/linker.ld" "--Map=${BC_BASE}.map" "${CRT0_OBJ}" "${BC_BASE}.rv64.o" COMMAND "${OBJCOPY_EXECUTABLE}" -O binary "${BC_BASE}.elf" "${BC_BASE}.pre.bin" COMMAND "${Python3_EXECUTABLE}" "${RISCVM_DIR}/relocs.py" "${BC_BASE}.elf" --binary "${BC_BASE}.pre.bin" --output "${BC_BASE}.bin" COMMAND "${Python3_EXECUTABLE}" "${RISCVM_DIR}/encrypt.py" --encrypt --shuffle --map "${BC_BASE}.map" --shuffle-map "${RISCVM_DIR}/shuffled_opcodes.json" --opcodes-map "${RISCVM_DIR}/opcodes.json" --output "${BC_BASE}.enc.bin" "${BC_BASE}.bin" VERBATIM ) endfunction() While all of this is quite complex, we did our best to make it as transparent to the end-user as possible. After enabling Visual Studio’s LLVM support in the installer, you can start developing VM payloads in a few minutes. You can get a precompiled transpiler binary from the releases. Debugging in riscvm When debugging the payload, it is easiest to load payload.elf in Ghidra to see the instructions. Additionally, the debug builds of the riscvm executable have a --trace flag to enable instruction tracing. The execution of main in the MessageBoxA example looks something like this (labels added manually for clarity): main: 0x000000014000d3a4: addi sp, sp, -0x10 = 0x14002cfd0 0x000000014000d3a8: sd ra, 0x8(sp) = 0x14000d018 0x000000014000d3ac: auipc a0, 0x0 = 0x14000d4e4 0x000000014000d3b0: addi a1, a0, 0xd6 = 0x14000d482 0x000000014000d3b4: auipc a0, 0x0 = 0x14000d3ac 0x000000014000d3b8: addi a2, a0, 0xc7 = 0x14000d47b 0x000000014000d3bc: addi a0, zero, 0x0 = 0x0 0x000000014000d3c0: addi a3, zero, 0x0 = 0x0 0x000000014000d3c4: jal ra, 0x14 -> 0x14000d3d8 MessageBoxA: 0x000000014000d3d8: addi sp, sp, -0x70 = 0x14002cf60 0x000000014000d3dc: sd ra, 0x68(sp) = 0x14000d3c8 0x000000014000d3e0: slli a3, a3, 0x0 = 0x0 0x000000014000d3e4: srli a4, a3, 0x0 = 0x0 0x000000014000d3e8: auipc a3, 0x0 = 0x0 0x000000014000d3ec: ld a3, 0x108(a3=>0x14000d4f0) = 0x7ffb3c23a000 0x000000014000d3f0: sd a0, 0x0(sp) = 0x0 0x000000014000d3f4: sd a1, 0x8(sp) = 0x14000d482 0x000000014000d3f8: sd a2, 0x10(sp) = 0x14000d47b 0x000000014000d3fc: sd a4, 0x18(sp) = 0x0 0x000000014000d400: addi a1, sp, 0x0 = 0x14002cf60 0x000000014000d404: addi a0, a3, 0x0 = 0x7ffb3c23a000 0x000000014000d408: jal ra, -0x3cc -> 0x14000d03c riscvm_host_call: 0x000000014000d03c: lui a2, 0x5 = 0x14000d47b 0x000000014000d040: addiw a7, a2, -0x1e0 = 0x4e20 0x000000014000d044: ecall 0x4e20 0x000000014000d048: ret (0x14000d40c) 0x000000014000d40c: ld ra, 0x68(sp=>0x14002cfc8) = 0x14000d3c8 0x000000014000d410: addi sp, sp, 0x70 = 0x14002cfd0 0x000000014000d414: ret (0x14000d3c8) 0x000000014000d3c8: addi a0, zero, 0x0 = 0x0 0x000000014000d3cc: ld ra, 0x8(sp=>0x14002cfd8) = 0x14000d018 0x000000014000d3d0: addi sp, sp, 0x10 = 0x14002cfe0 0x000000014000d3d4: ret (0x14000d018) 0x000000014000d018: jal ra, 0x14 -> 0x14000d02c exit: 0x000000014000d02c: lui a1, 0x2 = 0x14002cf60 0x000000014000d030: addiw a7, a1, 0x710 = 0x2710 0x000000014000d034: ecall 0x2710 The tracing also uses the enums for the opcodes, so it works with shuffled and encrypted payloads as well. Outro Hopefully this article has been an interesting read for you. We tried to walk you through the process in the same order we developed it in, but you can always refer to the riscy-business GitHub repository and try things out for yourself if you got confused along the way. If you have any ideas for improvements, or would like to discuss, you are always welcome in our Discord server! We would like to thank the following people for proofreading and discussing the design and implementation with us (alphabetical order): Brit herrcore JustMagic Renegade veritas Additionally, we highly appreciate the open source projects that we built this project on! If you use this project, consider giving back your improvements to the community as well. Merry Christmas!
|
||
|
Striga: Lifting x86 to LLVM IR with Python | secret club
https://secret.club/2026/05/21/striga.html
Background While discussing with eversinc33 about lifting BinaryShield to LLVM IR I decided it would be useful to write a basic lifter in Python that can lift x86_64 instructions to LLVM IR. He has since released his blog post: Writing a Naive LLVM-based Devirtualizer, which I highly recommend you check out! This post assumes familiarity with the basics of LLVM IR. You can find some references at the end of this post. Over the years I noticed that a lot of people get stuck exploring lifters, because existing tooling is too difficult to compile. In October 2025 I spent around a month redoing Remill’s build system (remill#723) and earlier this month I did the same for the Dna project (Dna#9). Last year I also started working on Python bindings for LLVM, which I wanted to use for a real project. You can find the lifter at LLVMParty/striga. The goal of this post is to lower the barrier of entry and let you experiment with lifting to LLVM IR. For inspiration you can look at the Static Devirtualization of Themida post that was just released by Back Engineering Labs, as well as the Pushan: Trace-Free Deobfuscation of Virtualization-Obfuscated Binaries paper by ASU researchers published in March. If you enjoy this article and would like to learn more, see my website for information about my in-person trainings. Lifting Lifting is the process of translating assembly instructions to some kind of intermediate representation (IR). The motivation is usually that directly analyzing and manipulating (x86) assembly instructions is complex and error prone. The lifter translates the underlying instruction semantics directly to an IR that is easier to reason about (and therefore to manipulate as well). A few popular IRs: SMT-LIB, used by Triton (symbolic execution) VEX, used by angr Miasm IR Sleigh, used by Ghidra, Remill and Icicle LLVM IR, used by Rellume, revng and Remill Microcode, used by IDA (proprietary) BNIL, used by Binary Ninja (proprietary) For this project I picked LLVM IR, because I am the most familiar with it and it has a well-established ecosystem. LLVM already has all of the common compiler optimizations and it is used and maintained by teams at large corporations. Architecture The architecture of the lifter is very much inspired by remill, but I simplified some things to make it easier to follow. In LLVM a register is actually an SSA value, which means we can only assign to it once. CPU registers are variables that can be assigned to multiple times. We model this by creating a State structure in memory that represents the x86 CPU state: struct State { uint64_t rax; uint64_t rbx; uint64_t rcx; uint64_t rdx; // ... GPRs uint8_t cf; uint8_t zf; uint8_t of; // ... Flags // ... XMM }; Instructions that read or write to RAX will load/store to State->rax. If we play our cards right, the optimizer will use the mem2reg pass to translate this into SSA form for us and enable further optimizations. An important difference to an actual CPU is that flags are modelled as independent 8-bit registers. This makes it easier to reason about compared to a packed bitfield. For instance, it helps the optimizer to perform dead store elimination and propagation. In addition to the State, we need an opaque memory pointer that helps us differentiate a load/store in the State from memory accesses by the x86 CPU. In short: the State pointer is used to model the CPU and the memory pointer is used to model the RAM. While lifting, the prototype of the lifted function is void lifted(State* state, void* memory). Later on we will perform brightening, to turn this into something we can recompile. Below is the LLVM IR for the instruction mov rax, rcx, with comments in pseudo-C: define internal void @lifted_0x140001000(ptr %state, ptr %memory) { initialize: ; uint64_t* rcx = &state->rcx; %rcx = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 2 ; uint64_t* rax = &state->rax; %rax = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 0 ; Jump to the first instruction br label %insn_0x140001000 insn_0x140001000: ; preds = %initialize ; uint64_t v0 = *rcx; %0 = load i64, ptr %rcx, align 4 ; *rax = v0; store i64 %0, ptr %rax, align 4 ; Jump to the next instruction br label %insn_0x140001003 insn_0x140001003: ; preds = %insn_0x140001000 ; Block terminator to keep the IR valid ret void } We start out with the initialize block, which is used to get pointers to the relevant State members. Then every instruction gets its own basic block named insn_<addr>. Every instruction is responsible for emitting an unconditional branch to its successors. The basic block for the successor is created with just a ret terminator, to keep the module verifier happy. To illustrate memory accesses, here is the LLVM IR for mov rax, qword [rbx+42]: define internal void @lifted_0x140001000(ptr %state, ptr %memory) { initialize: %rbx = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 1 %rax = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 0 br label %insn_0x140001000 insn_0x140001000: ; preds = %initialize ; uint64_t v0 = *rbx; %0 = load i64, ptr %rbx, align 4 ; uint64_t v1 = v0 + 42; %1 = add i64 %0, 42 ; uint8_t* v2 = &memory[v1]; %2 = getelementptr i8, ptr %memory, i64 %1 ; uint64_t v3 = *(uint64_t*)v2; %3 = load i64, ptr %2, align 1 ; *rax = v3; store i64 %3, ptr %rax, align 4 br label %insn_0x140001004 insn_0x140001004: ; preds = %insn_0x140001000 ret void } Here you can see the getelementptr i8, ptr %memory, i64 %1 instruction which uses memory as a base, signaling that this is a read from the x86 memory (we will clean this up later). The lifter itself is contained in a ~500 line Semantics class with these main functions (some are omitted for brevity): # src/striga/semantics.py class Semantics: def __init__(self, module: Module): ... # Lifting def begin(self, address: int) -> Function: ... def get_or_create_block(self, address: int) -> BasicBlock: ... def lift_bytes(self, address: int, code: bytes) -> list[Successor]: ... # Semantic helpers def reg_read(self, name: str) -> Value: ... def reg_write(self, name: str, value: Value): ... def mem_read(self, addr: Value, ty: Type) -> Value: ... def mem_write(self, addr: Value, value: Value): ... def op_mem(self, op: X86Op) -> Value: ... def op_read(self, index: int) -> Value: ... def op_write(self, index: int, value: Value): ... def flag_read(self, name: str) -> Value: ... def flag_write(self, name: str, value: Value): ... # State (simplified) module: Module function: Function ir: Builder insn: CsInsn The begin(address) function is used to create the lifted_<address> function in LLVM IR and create the initialize block with a branch to the first instruction: def begin(self, address: int) -> Function: name = f"lifted_{hex(address)}" fn = self.module.get_function(name) if fn is None: fn = self.module.add_function(name, self.lifted_ty) fn.param_attributes(0).add("noalias") fn.param_attributes(1).add("noalias") state, memory = fn.params memory.name = "memory" state.name = "state" self.function = fn self.reg_ptrs = {} self.insn_blocks = {} entry = fn.append_basic_block("initialize") assert fn.last_basic_block == entry with entry.create_builder() as ir: ir.br(self.get_or_create_block(address)) else: # Omitted for brevity return self.function To create the instruction block, get_or_create_block is used: def get_or_create_block(self, address: int) -> BasicBlock: block = self.insn_blocks.get(address) if block is None: block = self.function.append_basic_block(f"insn_{hex(address)}") with block.create_builder() as ir: ir.ret_void() self.insn_blocks[address] = block assert block.function == self.function return block As mentioned above, an empty block is not valid LLVM IR so we populate it with a ret instruction. When actually lifting into the basic block, that instruction will be replaced with the lifted code. To lift a single instruction we pass its address and bytes to lift_bytes, which is responsible for producing LLVM IR: def lift_bytes(self, address: int, code: bytes) -> list[Successor]: # Ensure we have a function to lift into if not hasattr(self, "function"): self.begin(address) insn = self.cs_disasm(address, code) if self.verbose: print(";", hex(insn.address), insn.mnemonic, insn.op_str) # Skip lifting if the block is already populated block = self.get_or_create_block(address) assert block.first_instruction if block.first_instruction.opcode == Opcode.Ret: block.first_instruction.erase_from_parent() else: return [] with block.create_builder() as ir: # State used by semantic handlers self.ir = ir self.insn = insn handler = _semantics.get(insn.mnemonic) if handler is None and insn.mnemonic.startswith("lock "): # LOCK preserves the single-threaded architectural result; the # lifter does not model inter-thread atomicity separately. handler = _semantics.get(insn.mnemonic.removeprefix("lock ")) if handler is None: raise NotImplementedError(insn.mnemonic) successors = handler(self) if successors is None: # Linear fallthrough - handler didn't emit a terminator. fallthrough = address + insn.size ir.br(self.get_or_create_block(fallthrough)) successors = [Successor(address, self.const64(fallthrough))] # Make sure the handler produced valid IR self.module.verify_or_raise() return successors The function first ensures an empty insn_<address> block by removing the temporary ret instruction. Then it creates an IR Builder and calls the handler responsible for producing IR for the instruction being lifted (more on that below). If the handler does not return successors, lift_bytes handles the common fallthrough case by creating a basic block for the next instruction. It is up to the caller to handle the list of Successor tuples: class Successor(NamedTuple): src: int dst: Value We use an LLVM Value for the branch destination, because it is not always concrete (for example jmp reg). The semantic handlers are registered globally: # src/striga/semantic.py SemanticFn: TypeAlias = Callable[["Semantics"], list[Successor] | None] _semantics: dict[str, SemanticFn] = {} def semantic(fn: SemanticFn): name = getattr(fn, "__name__") _semantics[name.removesuffix("_")] = fn return fn # src/striga/x86/data.py @semantic def mov(sem: Semantics): value = sem.op_read(1) sem.op_write(0, value) Every handler gets an instance of Semantics, to allow easy access to x86 constructs like operands, registers, flags and memory. For example, op_read is implemented as follows: def op_read(self, index: int) -> Value: op: X86Op = self.insn.operands[index] if op.type == CS_OP_REG: name = self.reg_name(op.reg) # pyright: ignore[reportAssignmentType] return self.reg_read(name) if op.type == CS_OP_IMM: return self.const_n(op.imm, op.size * 8) if op.type == CS_OP_MEM: addr = self.op_mem(op) return self.mem_read(addr, self.types.int_n(op.size * 8)) assert False For our example mov rax, rcx, the function will forward to reg_read: def reg_read(self, name: str) -> Value: if name in self.reg_types: load = self.ir.load(self.reg_types[name], self.reg_ptr(name)) load.metadata["tbaa"] = self.tbaa_tags[name] return load full_name, size, bit_offset = self.subregs[name] load = self.ir.load(self.reg_types[full_name], self.reg_ptr(full_name)) load.metadata["tbaa"] = self.tbaa_tags[full_name] if bit_offset: load = self.ir.lshr(load, self.const64(bit_offset)) return self.ir.trunc(load, self.types.int_n(size)) This function transparently handles accesses to sub registers like eax, ax, al and ah and it returns an LLVM Value containing the loaded register value. The last missing piece is the reg_ptr function, which is responsible for creating the getelementptr in the function entry: def reg_ptr(self, name: str) -> Value: reg_ptr = self.reg_ptrs.get(name) if reg_ptr is not None: return reg_ptr entry = self.function.entry_block state = self.function.get_param(0) with entry.create_builder() as ir: ir.position_before(entry.terminator) reg_ptr = ir.struct_gep(self.state_ty, state, self.reg_indices[name], name) self.reg_ptrs[name] = reg_ptr return reg_ptr To help the optimizer we add TBAA Metadata to the register load/store instructions. In this case we know that a register loads/stores never alias with each other. By telling the optimizer about this, it can perform more aggressive dead-store elimination when optimizing a sequence of lifted instructions. Semantics So far we discussed the architecture of the lifter, but we only discussed the mov instruction so far. Almost every other instruction has more complex behavior, especially around flag handling. For instance here are the implementations of and/or/xor: # src/striga/x86/bitwise.py def write_logical_flags(sem: Semantics, result: Value): false = sem.const_n(0, 1) sem.flag_write("cf", false) sem.flag_write("pf", sem.result_parity_even(result)) sem.flag_write_undef("af") sem.flag_write("zf", sem.result_is_zero(result)) sem.flag_write("sf", sem.result_sign_bit(result)) sem.flag_write("of", false) def logical_binop(sem: Semantics, opcode: Opcode): dst = sem.op_read(0) src = sem.resize_int(sem.op_read(1), dst.type) result = sem.ir.binop(opcode, dst, src) sem.op_write(0, result) write_logical_flags(sem, result) @semantic def and_(sem: Semantics): logical_binop(sem, Opcode.And) @semantic def or_(sem: Semantics): logical_binop(sem, Opcode.Or) @semantic def xor(sem: Semantics): logical_binop(sem, Opcode.Xor) For reference here is the lifted LLVM IR for xor rax, rbx and the Python code responsible for each part: insn_0x140001000: ; preds = %initialize ; dst = sem.reg_read(0) %0 = load i64, ptr %rax, align 4 ; src = sem.resize_int(sem.op_read(1), dst.type) %1 = load i64, ptr %rbx, align 4 ; result = sem.ir.binop(Opcode.Xor, dst, src) %2 = xor i64 %0, %1 ; sem.op_write(0, result) store i64 %2, ptr %rax, align 4 ; sem.flag_write("cf", false) store i8 0, ptr %cf, align 1 ; sem.flag_write("pf", sem.result_parity_even(result)) %3 = trunc i64 %2 to i8 %4 = lshr i8 %3, 4 %5 = xor i8 %3, %4 %6 = lshr i8 %5, 2 %7 = xor i8 %5, %6 %8 = lshr i8 %7, 1 %9 = xor i8 %7, %8 %10 = and i8 %9, 1 %11 = icmp eq i8 %10, 0 %12 = zext i1 %11 to i8 store i8 %12, ptr %pf, align 1 ; sem.flag_write_undef("af") %13 = call i1 @__striga_undef_af(i64 5368713216) %14 = zext i1 %13 to i8 store i8 %14, ptr %af, align 1 ; sem.flag_write("zf", sem.result_is_zero(result)) %15 = icmp eq i64 %2, 0 %16 = zext i1 %15 to i8 store i8 %16, ptr %zf, align 1 ; sem.flag_write("sf", sem.result_sign_bit(result)) %17 = lshr i64 %2, 63 %18 = trunc i64 %17 to i1 %19 = zext i1 %18 to i8 store i8 %19, ptr %sf, align 1 ; sem.flag_write("of", false) store i8 0, ptr %of, align 1 ; Semantics.lift_bytes br label %insn_0x140001003 If you pay close attention you see a call to __striga_undef_af, which is a custom intrinsic used to represent something that has no clear analog in LLVM IR. In this case the description of the xor instruction says: The OF and CF flags are cleared; the SF, ZF, and PF flags are set according to the result. The state of the AF flag is undefined. This means that Intel/AMD does not want to document exactly how the value of AF is computed in silicon. In practice this can vary between CPU models/generations and it can be used as an anti-emulation trick, but we will not go into detail in this post. We emit __striga_undef_af, to allow the user to handle this however they see fit. If you are interested there is remill#766 with a little discussion about how to model this correctly. Another class of instructions to highlight here is the various branch instructions: # src/striga/x86/control.py def conditional_jump(sem: Semantics, cond: Value): brtrue = sem.insn.operands[0].imm brfalse = sem.insn.address + sem.insn.size sem.ir.cond_br( cond, sem.get_or_create_block(brtrue), sem.get_or_create_block(brfalse), ) src = sem.insn.address return [ Successor(src, sem.const64(brtrue)), Successor(src, sem.const64(brfalse)), ] def jcc(sem: Semantics, cc: str): return conditional_jump(sem, cc_cond(sem, cc)) @semantic def je(sem: Semantics): return jcc(sem, "e") @semantic def jmp(sem: Semantics): dst = sem.op_read(0) if dst.is_constant: sem.ir.br(sem.get_or_create_block(dst.const_zext_value)) else: sem.ir.call(sem.jmp_handler, [dst]) sem.ir.ret_void() return [Successor(sem.insn.address, dst)] @semantic def call(sem: Semantics): dst = sem.op_read(0) fallthrough = sem.insn.address + sem.insn.size sem.push(sem.const64(fallthrough)) sem.ir.call(sem.call_handler, [dst]) sem.ir.br(sem.get_or_create_block(fallthrough)) return [Successor(sem.insn.address, sem.const64(fallthrough))] @semantic def ret(sem: Semantics): dst = sem.pop(sem.i64) if sem.insn.operands: rsp = sem.reg_read("rsp") sem.reg_write("rsp", sem.ir.add(rsp, sem.const64(sem.insn.operands[0].imm))) sem.ir.call(sem.ret_handler, [dst]) sem.ir.ret_void() return [Successor(sem.insn.address, dst)] LLVM IR for je imm: insn_0x140001000: ; preds = %initialize %0 = load i8, ptr %zf, align 1 %1 = icmp ne i8 %0, 0 br i1 %1, label %insn_0x140001014, label %insn_0x140001002 insn_0x140001014: ; preds = %insn_0x140001000 ret void insn_0x140001002: ; preds = %insn_0x140001000 ret void } Note that the semantic handler for jcc is responsible for creating both the destination blocks as well as the br with the appropriate condition based on the flag(s). LLVM IR for jmp rbx: insn_0x140001000: ; preds = %initialize %0 = load i64, ptr %rbx, align 4 call void @__striga_jmp(i64 %0) ret void LLVM IR for call imm: insn_0x140001000: ; preds = %initialize %0 = load i64, ptr %rsp, align 4 %1 = sub i64 %0, 8 store i64 %1, ptr %rsp, align 4 %2 = getelementptr i8, ptr %memory, i64 %1 store i64 5368713221, ptr %2, align 1 call void @__striga_call(i64 5369761797) br label %insn_0x140001005 LLVM IR for ret: insn_0x140001000: ; preds = %initialize %0 = load i64, ptr %rsp, align 4 %1 = getelementptr i8, ptr %memory, i64 %0 %2 = load i64, ptr %1, align 1 %3 = add i64 %0, 8 store i64 %3, ptr %rsp, align 4 call void @__striga_ret(i64 %2) ret void } As you can see, we use the following intrinsics: __striga_jmp: indirect jump __striga_call: call instruction __striga_ret: ret instruction These are also used to give the user flexibility in how they want to handle these instructions. Control flow Because of the design choice where every instruction is a basic block, it becomes fairly straightforward to recover the control flow of a basic function: def lift(module: Module, container: Container, start: int, *, verbose=True): sem = Semantics(module, verbose=verbose) lifted_fn = sem.begin(start) queue: Queue[Successor] = Queue() queue.put(Successor(0, sem.const64(start))) # Keep destinations as LLVM Values instead of splitting constants into ints. # This keeps the worklist uniform and matches later slicing/data-flow uses. visited: set[Value] = set() while not queue.empty(): src, dst = queue.get() if not dst.is_constant: if sem.verbose: print(f"; non-constant branch destination: {hex(src)} -> {dst}") continue if dst in visited: continue visited.add(dst) va = dst.const_zext_value code = container.get_data(va, 15) successors = sem.lift_bytes(va, code) for successor in successors: if successor.dst in visited: continue queue.put(successor) sem.module.verify_or_raise() return lifted_fn This is a simple Breadth-first search over the control flow graph and it allows recovering functions without indirect branches. Note that we do not have to do anything special to handle back edges (loops) or block splitting. The lifted code is modeled with an LLVM basic block per instruction, so we can connect instructions arbitrarily. Below is a function with some simple control flow (if/else/loop): test_cfg: cmp rax, 0 je .else_block .if_true: add rax, 1 jmp .merge .else_block: add rax, 2 .merge: sub rax, 1 jne .merge .exit: ret The graph of the disassembly looks like this: The LLVM IR looks like this: define internal void @lifted_0x140001000(ptr %state, ptr %memory) { initialize: %rax = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 0 %zf = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 51 %rsp = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 6 br label %insn_0x140001000 insn_0x140001000: ; preds = %initialize ; cmp rax, 0 %0 = load i64, ptr %rax, align 4 %1 = sub i64 %0, 0 %19 = icmp eq i64 %1, 0 %20 = zext i1 %19 to i8 store i8 %20, ptr %zf, align 1 br label %insn_0x140001004 insn_0x140001004: ; preds = %insn_0x140001000 ; je 0x14000100c %30 = load i8, ptr %zf, align 1 %31 = icmp ne i8 %30, 0 br i1 %31, label %insn_0x14000100c, label %insn_0x140001006 insn_0x14000100c: ; preds = %insn_0x140001004 ; add rax, 2 %32 = load i64, ptr %rax, align 4 %33 = add i64 %32, 2 store i64 %33, ptr %rax, align 4 br label %insn_0x140001010 insn_0x140001006: ; preds = %insn_0x140001004 ; add rax, 1 %62 = load i64, ptr %rax, align 4 %63 = add i64 %62, 1 store i64 %63, ptr %rax, align 4 br label %insn_0x14000100a insn_0x140001010: ; preds = %insn_0x140001014, %insn_0x14000100a, %insn_0x14000100c ; sub rax, 1 %92 = load i64, ptr %rax, align 4 %93 = sub i64 %92, 1 store i64 %93, ptr %rax, align 4 %111 = icmp eq i64 %93, 0 %112 = zext i1 %111 to i8 store i8 %112, ptr %zf, align 1 br label %insn_0x140001014 insn_0x14000100a: ; preds = %insn_0x140001006 ; jmp 0x140001010 br label %insn_0x140001010 insn_0x140001014: ; preds = %insn_0x140001010 ; jne 0x140001010 %122 = load i8, ptr %zf, align 1 %123 = icmp ne i8 %122, 0 %124 = xor i1 %123, true br i1 %124, label %insn_0x140001010, label %insn_0x140001016 insn_0x140001016: ; preds = %insn_0x140001014 ; ret %125 = load i64, ptr %rsp, align 4 %126 = getelementptr i8, ptr %memory, i64 %125 %127 = load i64, ptr %126, align 1 %128 = add i64 %125, 8 store i64 %128, ptr %rsp, align 4 call void @__striga_ret(i64 %127) ret void } For clarity, some flag computations were omitted from this IR dump. Brightening Brightening was a term coined in 2019 by Peter Garba and Matteo Favaro in the SATURN paper: Brightening [COMP.] verb – Reshaping code to make it more readable and understandable for humans Concretely it means to transform the LLVM IR from the lifted shape (pseudo C): /* Lifted instructions: add rdi, rsi mov rax, rdi ret */ void lifted(State* state, void* memory) { state.rdi += state.rsi; state.rax = state.rdi; __striga_ret(...); } Back to a regular function for the lifted platform’s calling convention, such as: // Linux calling convention: https://wiki.osdev.org/System_V_ABI#x86-64 uint64_t /* rax */ brightened(uint64_t /* rdi */ x, uint64_t /* rsi */ y) { return x + y; } The brightened function sets up the State on the stack and assigns the arguments to the registers appropriate for the calling convention of our target platform. The result register is returned from the function. Conceptually this is not very difficult, but it requires a bit of mental gymnastics to wrap your head around the trick: // Symbolic variable for memory uint8_t RAM[0]; void lifted(State* state, void* memory) { ... } uint64_t brightened(uint64_t x, uint64_t y) { State state; state.rdi = x; state.rsi = y; lifted(&state, RAM); return state.rax; } After an inlining pass it would look something like this: uint64_t brightened(uint64_t x, uint64_t y) { State state; state.rdi = x; state.rsi = y; state.rdi += state.rsi; state.rax = state.rdi; __striga_ret(...); return state.rax; } We can get rid of the __striga_ret intrinsic in this case, which will let the optimizer reduce the function to its original shape: uint64_t brightened(uint64_t x, uint64_t y) { return x + y; } LLVM IR before optimizations: define i64 @brightened_0x1000(i64 %0, i64 %1) { entry: %state = alloca %State, align 8 %rdi = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 5 store i64 %0, ptr %rdi, align 4 %rsi = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 4 store i64 %1, ptr %rsi, align 4 %stack = alloca i8, i64 4096, align 1 %2 = getelementptr i8, ptr %stack, i64 4088 %3 = ptrtoint ptr %2 to i64 %rsp = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 6 store i64 %3, ptr %rsp, align 4 store i64 3735928559, ptr %2, align 1 call void @lifted_0x1000(ptr %state, ptr @RAM) %rax = getelementptr inbounds nuw %State, ptr %state, i32 0, i32 0 %4 = load i64, ptr %rax, align 4 ret i64 %4 } After optimizing the module with default<O1>: define i64 @brightened_0x1000(i64 %0, i64 %1) { entry: %2 = add i64 %1, %0 ret i64 %2 } Memory / Stack To handle memory accesses, we create a global RAM variable and pass that to our memory argument. In the previous example it folded away, but we need to handle it separately. The simplest form is access to a pointer parameter: uint64_t lift4_read(uint64_t *n) { return *n ^ 1337; } With our current brightening strategy the lifted code (after optimizations) would look like this: define i64 @brightened_0x1000(i64 %0) { entry: %1 = getelementptr i8, ptr @RAM, i64 %0 %2 = load i64, ptr %1, align 1, !alias.scope !19, !noalias !22 %3 = xor i64 %2, 1337 ret i64 %3 } We need to detect the getelementptr i8, ptr @RAM, i64 %0 shape and replace it with an inttoptr instruction: define i64 @brightened_0x1000(i64 %0) { entry: %1 = inttoptr i64 %0 to ptr %2 = load i64, ptr %1, align 1, !alias.scope !19, !noalias !22 %3 = xor i64 %2, 1337 ret i64 %3 } The stack can be modeled by allocating a local stack variable and pointing rsp to the end of that buffer (since on x86 the stack grows towards lower addresses): uint64_t brightened(uint64_t x, uint64_t y) { uint8_t stack[4096]; State state; state.rdi = x; state.rsi = y; state.rsp = (uint64_t)&stack[sizeof(stack) - 8]; lifted(&state, RAM); return state.rax; } Putting everything together in brighten.py: from llvm import Linkage, Module, Opcode, Value, global_context from bfs import lift_bfs from container import Container, RawContainer OPT_PIPELINE = "default<O1>" def rewrite_ram_geps(module: Module, ram: Value): """Replace GEPs rooted at @RAM with inttoptr(address).""" types = module.context.types for gep in ram.users: if not gep.is_instruction or gep.opcode != Opcode.GetElementPtr: raise ValueError(f"unexpected @RAM user: {gep}") if gep.get_operand(0) != ram: raise ValueError(f"unexpected @RAM GEP base: {gep}") if gep.num_operands == 2: if gep.gep_source_element_type != types.i8: raise ValueError(f"expected i8 ptradd-style @RAM GEP: {gep}") address = gep.get_operand(1) elif gep.num_operands == 3: zero = gep.get_operand(1) if not zero.is_constant_int or zero.const_zext_value != 0: raise ValueError(f"expected zero first @RAM GEP index: {gep}") address = gep.get_operand(2) else: raise ValueError(f"unexpected @RAM GEP shape: {gep}") with gep.create_builder() as ir: ptr = ir.inttoptr(address, types.ptr) gep.replace_all_uses_with(ptr) gep.erase_from_parent() if not ram.users: ram.delete_global() module.verify_or_raise() def define_ret_stub(module: Module): """Make the modeled return hook removable for this demo wrapper.""" ret_handler = module.get_function("__striga_ret") if ret_handler is not None and ret_handler.is_declaration: ret_handler.linkage = Linkage.Internal entry = ret_handler.append_basic_block("entry") with entry.create_builder() as ir: ir.ret_void() def lift_brightened(container: Container, entry: int, args: list[str]): with global_context().create_module("blog") as module: sem = lift_bfs(module, container, entry, verbose=True) # Convenience aliases types = module.context.types i8 = types.i8 i64 = types.i64 # Global RAM array ram = module.add_global(types.array(i8, 0), "RAM") # TODO: support different register sizes brightened_ty = types.function(i64, [i64 for _ in args]) brightened = module.add_function(f"brightened_{hex(entry)}", brightened_ty) with brightened.create_builder() as ir: state = ir.alloca(sem.state_ty, "state") def reg_ptr(name: str) -> Value: return ir.struct_gep(sem.state_ty, state, sem.reg_indices[name], name) # Assign arguments to register state for i, name in enumerate(args): ir.store(brightened.get_param(i), reg_ptr(name)) # Set up function stack stack = ir.alloca(i8, i64.constant(4096), "stack") stack_ptr = ir.gep(i8, stack, [i64.constant(4096 - 8)]) ir.store(ir.ptrtoint(stack_ptr, i64), reg_ptr("rsp")) # Set up return address retaddr_store = ir.store(i64.constant(0xDEADBEEF), stack_ptr) retaddr_store.inst_alignment = 1 # Call lifted function ir.call(sem.function, [state, ram]) # Load return value from rax and return it ir.ret(ir.load(i64, reg_ptr("rax"))) module.verify_or_raise() # 1. Inline/optimize with @RAM assigned to the lifted memory parameter. module.optimize(OPT_PIPELINE) # 2. Brighten lifted memory: @RAM + integer address -> inttoptr(address). rewrite_ram_geps(module, ram) # 3. Now that RAM accesses have been brightened, discard the modeled ret # hook for this demo and let LLVM clean up the remaining wrapper noise. # Undefined flag helpers are already declared memory(none) by Semantics, # so their dead uses fold away without local stub definitions. define_ret_stub(module) module.verify_or_raise() module.optimize(OPT_PIPELINE) print(brightened) This cleanly lifts the following (unoptimized) function: ; 0x1000 push rbp ; 0x1001 mov rbp, rsp ; 0x1004 mov qword ptr [rbp - 8], rdi ; 0x1008 mov rax, qword ptr [rbp - 8] ; 0x100c pop rbp ; 0x100d ret define i64 @brightened_0x1000(i64 returned %0) { entry: ret i64 %0 } Conclusion Hopefully this was an insightful introduction to lifting to LLVM IR. Feel free to check out the repository at LLVMParty/striga and reach out if you do something interesting with it! Note: Striga is not meant to be a production-ready lifter. There are no tests and only a very limited subset of x86 has been implemented. Thanks to the reviewers: Jack Royer Justas Masiulis Peter Goodman eversinc33 x86matthew LLVM IR references: A Gentle Introduction to LLVM IR A Journey to understand LLVM-IR! Mapping High Level Constructs to LLVM IR IR is better than assembly Introduction to LLVM Learning LLVM Part 1, Part 2 LLVM Passes for Security Part 1, Part 2, Part 3, Part 4
|
||
|
Introduction to the PE file format | Cyberdough
https://skr1x.github.io/portable-executable-format/
Let’s talk about the PE (portable executable) format and explore it in great detail.
|
||
|
Evading the Machine | 0xTriboulet
https://steve-s.gitbook.io/0xtriboulet/artificial-intelligence/evading-the-machine
An example evasion attack against (probably) the worst machine learning classifier of all time
|
||
|
Exciting Journey Towards Import Address Table (IAT) of an Executable
https://tech-zealots.com/malware-analysis/journey-towards-import-address-table-of-an-executable-file/
|
||
|
https://theepicpowner.gitlab.io/posts/Flying-Under-the-Radar-Part-1/
A blog where all things adversarial simulation, malware development and security research is shared.
|
||
|
TLSH - A Locality Sensitive Hash
https://tlsh.org/
|
||
|
Build-time String Encryption for Position-Independent Code
https://tmpest.dev/enc_pic_str.html
TLDR:
We turn this:
puts(pic_str("Hello, World!"));
Into this:
puts(((const char *)(({
struct _str_struct_1 {
char buf[14];
} _str_inst_1;
unsigned char...
|
||
|
TrustedSec | Behind the Code: Assessing Public Compile-Time…
https://trustedsec.com/blog/behind-the-code-assessing-public-compile-time-obfuscators-for-enhanced-opsec
LLVM obfuscation passes show minimal impact on native executable detection rates, with some exceptions.
|
||
|
Section Order, MASM, and the .text$mn Subsection | mindless-area
https://wbenny.github.io/2024/12/08/section-order-masm-text-mn-subsection.html
Undocumented feature fights back
|