Windows shellcode Development (2)
In the first part of this series, we learned the definition of Shellcode and its working principle. To correctly compile the Shellcode for Windows, the author will describe the required information in this article: process environment block, PE file format, and X86 assembly.
1. process environment block (PEB)
In Windows, PEB is a struct in a fixed position in the memory of all processes. This struct contains useful information about the process, such as the location where the executable file is loaded into the memory, the module list (DLL), a flag indicating whether the process is debugged, and many other information.
It is important to understand how the operating system calls this struct. This structure is not fixed in Different Windows operating system versions, so it may change with the new Windows release version, but some general information will remain unchanged.
As discussed in section 1st, DLL (because of the ASLR mechanism) can be loaded to different memory locations, so we cannot use a fixed memory address in shellcode. However, we can use the PEB structure, which is located at a fixed memory location to find the address where the DLL is loaded into the memory.
If you are familiar with the C/C ++ programming language, you can easily understand the information contained in this struct and its layout. The following fields are displayed in Microsoft official documents:
As you can see, some fields called "Reserved" do not have a corresponding description, while others have a corresponding document description.
If you are not familiar with C/C ++, you need to understand the following concepts: BYTE represents 1 BYTE. PVOID indicates 1 pointer (OR 1 memory address)-therefore, 4 bytes are occupied on the 0x86 system (32-bit system. PPEB_LDR_DATA is a pointer pointing to the custom structure PEB_LDR_DATAPEB_LDR_DATA. The 1st fields retain 2 bytes (Reserved1 [2] is an array containing 2 bytes ). The BeingDebugged flag is 1 byte, followed by another byte (Reserved2 ). Reserved3 [2] is an array containing 2 pointers (2*4 bytes = 8 bytes), and Ldr is a pointer-4 bytes.
PEB_LDR_DATA contains the following information:
As before, we can access the InMemoryOrderModuleList field after the offset of 20 bytes (in hexadecimal notation, the Reserved1 + 3*4 byte Reserved2 is 0 × 14: 8 bytes ). This field indicates information about the loaded DLL.
Next things will become a little complicated. We can use the LDR_DATA_TABLE_ENTRY struct to obtain the information of the loaded DLL. The official Microsoft documentation does not disclose the entire structure, but we can find more information here.
The LIST_ENTRY structure is a simple two-way linked list that contains a pointer to the next element (Flink) and a pointer to the previous element (Blink). Each pointer occupies 4 Bytes:
The InMemoryOrderModuleList field is a pointer pointing to the LIST_ENTRY field on the LDR_DATA_TABLE_ENTRY struct. However, it is not a pointer to the starting position of LDR_DATA_TABLE_ENTRY, but to the InMemoryOrderLinks field of this structure. Flink and Blink pointer to the LIST_ENTRY struct.
Let's take a step by step:
1. Read the PEB structure 2. Jump to the 0xC offset to read the Ldr pointer 3. Jump to the 0x14 offset to read the InMemoryOrderModuleList Field
Now we are going to the InMemoryOrderLinks element loaded into the first module of the memory. This is an executable file (for example, calc.exe ). We want to traverse all loaded DLL files. InMemoryOrderLinks is a LIST_ENTRY struct. The first four bytes are Flink pointers, and the last four bytes are Blink pointers. The previous four bytes can help us traverse to 2nd loaded modules. You only need to execute this process again to access the information of the 3rd loaded modules.
The InMemoryOrderModuleList Linked List displays all loaded modules in the following order:
1. calc.exe (Executable File) 2. ntdll. dll3. kernel32.dll
As discussed in section 1st, We need to access kernel32.dll to call functions similar to GetProcAddress and LoadLibrary to help us call other Windows API functions.
To achieve this goal, we need to read the Dllbase field from the current LDR_DATA_TABLE_ENTRY struct (the location where the DLL is loaded into the memory ). DLLBase is located at the 0 × 18 offset of this structure. However, considering that the InMemoryOrderLinks field is at the zero x 8 offset of the LDR_DATA_TABLE_ENTRY struct, to obtain the DllBase, we only need to offset 0x10 bytes. The following is an overview of the steps required to find the kernel32.dll memory address:
Although painting is not so good, I hope you can understand how it works. You only need to know how to use the "Flink" pointer to traverse all loaded modules. Don't scare this image. Next you will see that we can implement this traversal operation in around 8 lines of code.
Ii. PE File Format
The portable executable file (PE) is the file format used by the executable files and dynamic link libraries on Windows systems. This format describes the content of these files: headers and sections containing all code and data (sections, also known as sections and blocks ). There are many files on the Internet that introduce the PE file format, but here we only introduce the information necessary for writing shellcode: header, section, and export table.
PE files are simple:
As you can see in this image, the PE file contains:
DOS header DOS stub (stub) PE Header table section (code and Data Section)
Using the hex editor tool to open the PE file can bring us more detailed content:
The PE format is quite complex, but we only need to know how to parse the PE Header to obtain the export function. Let's start with the DOS header. The DOS header can be expressed as the following structure:
You can find the complete structure definition and other required structures in the "WinNT. h" header file of the C/C ++ compiler. All PE files (EXE or DLL) start with this structure. Therefore, if we find a module in the memory, we will also find this struct on that memory address. You can identify them by the first two bytes "MZ". These two bytes are the e_magic field, indicating the "signature" of the DOS header ".
We only need to know the e_lfanew field of this structure. This field is located at the 0x3C offset, which indicates the position of the PE Header. The PE Header is a struct containing the following information:
It contains the PE signature (if you open a PE file using the editor, you can see the "PE" string ). Dynamic) or dynamic link library (. dll ).
OptionalHeader (optional Header) is a struct containing more useful information:
It contains the following information:
AddressOfEntryPoint: Address of the code to be executed by exe/dll, that is, the entry point address. ImageBase: the address where the DLL is loaded into the memory, that is, the base address of the image. DataDirectory-import or export functions.
We are only interested in the last field, DataDirectory, because we need to obtain the export function. DLL Working principle: it contains the definition of various functions, and then export these functions. Therefore, other applications only need to load the DLL to the memory, find the export function, and call it. For example, "MessageBox" is an "user32.dll" export function (in fact, this function has two versions: ASCII and Unicode ).
The DataDirectory field of this structure is an array composed of IMAGE_DATA_DIRECTORY elements. The IMAGE_DATA_DIRECTORY structure is defined as follows:
The IMAGE_DATA_DIRECTORY structure (16 bytes) is located at the end of the OptionalHeader structure. For us, we only need to know that the 1st Data Directories are "export directories ".
To access the export directory, we only need to follow the VirtualAddress (relative virtual address) field of this structure, which points to the start position of the export directory. DWORD is a data type that occupies 4 bytes, while WORD only occupies 2 bytes. If you calculate the space occupied by all elements in the DataDirectory array, you will find that the total size from the start position of the PE header to the start position of the DataDirectory array is 120 bytes (0 × 78 ). Therefore, we can find the relative virtual address (VirtualAddress field) of the output directory at the offset of 0 × 78 ).
The structure of the exported directory is as follows:
We will use the following fields of this structure:
AddressOfFunctions: point to a DWORD array. Each array element points to a function address. AddressOfNames: point to a DWORD array. Each array element points to a function name string. AddressOfNameOrdinals: point to an array of the WORD type. Each array element indicates the sequence number (16-digit integer) of the corresponding function ).
The following uses the DLL file containing three functions as an example:
AddressOfFunctions = 0x11223344-> [0x11111111, 0x22222222, 0x33333333]: 0x11223344 points to an array containing the address of the function: 0x11111111, 0x22222222 and 0x33333333. AddressOfNames = 0x12345678-> [0 xaaaaaaaa-> "func0", 0 xbbbbbbbb-> "func1", 0 xcccccccc-> "func2"]: 0x12345678 is directed to an array, the array element points to the function name string. For example, 0xaaaaaaaaaa points to the string "func1", that is, the name of the exported function. AddressOfNameOrdinals = 0 xabcdef-> [0x00, 0x01, 0x02]: 0xabcdef is an integer (16-bit) array, the array element represents the Offset Value of the corresponding function on the AddressOfFunctions array.
To obtain the function address by using the function name, we need to check the name by parsing the AddressOfNames array. The number of the 1st functions (func0) is 0, the number of the 2nd functions (func1) is 1, and the number of the 3rd functions (func2) is 2. Therefore, if we need to find the func2 address of the function, we only need to access the 2nd elements of the AddressOfFunctions array (numbered from 0 ).
In short, it is like this:
Function address = AddressOfFunctions [No. (function name)]
Don't be scared. Next you will see that we can use 15-20 lines of assembly code to do everything.
Iii. Assembly Language
As you can see in the text, we can use C/C ++ advanced language to write shellcode. But if you want to understand what Shellcode is, how Shellcode works, and how to modify Shellcode, you need to understand and compile the assembly code.
This section only provides some basic knowledge about assembly languages. If you want to understand assembly languages in depth, please do not rely on this chapter. You can read such good articles. The introduction in this article is not complete. It only covers some common operations, so that you can write simple shellcode.
To avoid the complexity caused by different assembly languages, the following examples use the internal assembly language compiler on the Microsoft Visual C ++ Express compiler. Of course, you can also use compilers in assembly languages such as MASM, NASM, and YASM.
First, let's start with "variable. The processor uses different registers (when considering variables) to store temporary data. Each register has its own purpose, but here we regard it as a "global variable ". For more details, read this article.
General registers: EAX, EBX, ECX, EDX, ESI, and EDI. Each register can store 4 bytes of data. At the same time, the last two bytes can also be called AX, BX, CX, DX, SI and DI separately. The last byte can be accessed by the name of AL, BL, CL, and DL.
For example, the program starts from 0x12345678. A specific register stores the address of the currently executed command, which is called EIP ). After an instruction is executed, the register is automatically changed to the address of the next instruction. Now we have "variables". Let's see what we can do with them. To complete some useful operations, we need to use multiple commands.
Command:
Mov purpose, source: Copy data from the source operand to the destination operand. Add destination, source: add the source operand to the destination operand, or the destination operand = destination operand + Source operand. Sub destination, source: Destination operand minus source operand, or destination operand = destination operand-source operand. Objective: To add the value of the destination operand to 1dec objective: to reduce the value of the destination operand by 1
Example:
; Comments can be specified by starting with a ;mov EAX, 5 ; Put value 5 in the EAXadd EAX, 2 ; Add 2 to EAX, EAX will be 7inc EAX ; EAX will be 8mov EBX, 2 ; Store value 2 in EBXsub EAX, EBX ; EAX will be 6
You can test this program on the Visual C ++ platform like you do.
You can click the gray box on the left to place the breakpoint. The Visual C ++ debugger will pause the program execution at the breakpoint. When you start this program, it stops running at the specified breakpoint. At this point, you will see the "wattings" window at the bottom of the development environment. You can add Register names in this window to view their values. Therefore, add Register names such as EAX and EBX and observe their values.
You can press F11 to execute the command in a single step, and then observe how the register value changes in the watch window. Alternatively, you can place the cursor over the register name to view its values. Note that these are only Basic Debugging operations. To obtain more advanced debugging functions, you can use a Debugger such as Immunity Debugger, but for simplicity, you can use the debugger that comes with Visual C ++.
The control flow of the program goes through some decision sequences, that is, by comparing two values to take different actions. First, you need to learn to use labels ). The tag is only used to mark different locations of the Code. You can use "Jump to (jumps)" to access different code locations.
Useful commands:
Jump address/Tag. Jump to a tag or memory address cmp unconditionally. Source: Compare the destination and source operands by subtracting the destination operand from the source operand (the value of the destination operand is not changed ). The "result" will not be saved. Just remember that if the source operand is equal to the destination operand, the computer will set the "Zero Flag" Flag. This flag is used by the next conditional transfer command. Jz address/Tag: if the "Zero Flag" Flag has been set (jz = jump if it is Zero), jump to the specified tag or address. Therefore, if the parameters compared in the "cmp" command are equal, "Zero Flag" is set and the code jumps to the specified address or tag. If it doesn't wait, nothing will happen, and the program will run the next command. Jnz address/Tag: the opposite of jz (jnz = jump if not Zero). If "Zero Flag" is not set, the code will jump to the specified address. That is to say, the parameters compared with the preceding "cmp" command are not equal.
There are many other jump commands in the assembly language, but these are sufficient for beginners. As an example, you can try the following code:
Now, let's turn the topic to the key content of assembly language: Stack. Stack is a data structure in the memory, where you can store data. You can think of it as a memory space, and store data like a stacked plate. One data is placed on the other, and you can only retrieve data from the top.
There are two very useful commands for Stack:
Push data: push data into the stack pop register: extract data from the top of the stack, and then store the data in the specified register
At the same time, there are two registers pointing to the stack:
ESP register (Stack pointer): pointing to the top EBP register (base pointer, or frame pointer): pointing to the bottom of the stack
When dealing with stacks, some important things will happen. For example, ESP indicates the top stack. The value is 0 × 11223344. If we press the 4-byte data into the stack through the "push 0xaaaaaaaa" command, the 0xaaaaaaaa data will be stored at the top of the stack, and the ESP value will be reduced by 4 bytes. Therefore, we can say that the stack is growing to a lower address space. After the push command, the ESP value will change to 0 × 11223340.
If we get data from the stack, the situation will be reversed: the data is removed from the stack (in fact, due to compilation optimization, the data is still stored and not cleared ), the value of ESP is increased by four bytes.
It seems difficult, but it is not. For example:
Think about the mathematical operations on the stack. Suppose we press 0 × 20 bytes of data on the stack (through 8 push commands, 0 × 20 = 32 ), we can only modify the ESP value to easily clear the space on the stack: addESP, 0 × 20. This is simpler and more effective than 8 pop commands. Now we learn to call functions. There are two common function calling methods: stdcall and cdecl. WindowsAPI uses the stdcall call Convention (method). We only discuss this function call method. However, they are similar. You can find more information here. Let's use the following functions as an example:
int function(int x, int y){ return x + y;}
To call a function (0 × 11, 0 × 22), we need to understand the following:
1. Press the parameter from right to left into the stack. 2. use the "call function" command to call a function 3. the call Command will automatically push the address of the next command into the stack (the ESP value will also be reduced) 4. after the function is returned, the EAX register will save the result of the function execution.
After the function is executed, the value of the EAX register is 0 × 33 (0 × 11 + 0 × 22 = 0 × 33 ).
Therefore, these are the basic knowledge of assembly. However, we will also use other commands in shellcode, such:
Xor, source: binary operation, but we only use this command like "xor eax, eax. This command will assign the eax register to 0. Lea purpose, source (take valid address): The main function is to save the memory address specified by the source operand to the destination operand. Lodsd: store data from the specified address of the ESI register to the EAX register. For xchg purpose, the value of the source-exchange operand: The Source operand gets the value of the destination operand, And the destination operand also gets the value of the source operand.
Assembly Language is a difficult language, but it is not difficult to master it if you study it step by step.
Iv. Summary
Even if we haven't compiled any shellcode, we have learned all the knowledge required to write shellcode and have the ability to write basic shellcode. We know what PEB is and how it helps to write shellcode, the composition of PE files, and even several lines of simple assembly code. In the next section, we will actually write a shellcode, and we will also use this article and part 1 of the learning content to implement useful functions. Next, we will put the knowledge learned here into practice.