| Before reading this article, if you do not know much about stack connection, read the basic knowledge behind this article. Anyone who has been familiar with programming knows that advanced languages can use variable names to access data in the memory. How are these variables stored in the memory? How does a program use these variables? We will discuss this in depth below. If the C language code below does not have a special statement, the release version compiled by VC is used by default. First, let's take a look at how C variables are distributed in the memory. The C language includes global, local, static, and regeister variables ). Each variable has a different allocation method. Let's take a look at the following code: # Include <stdio. h> Int G1 = 0, G2 = 0, G3 = 0; Int main () { Static int S1 = 0, S2 = 0, S3 = 0; Int V1 = 0, V2 = 0, V3 = 0; // Print the memory address of each variable Printf ("0x % 08x \ n", & V1); // print the memory address of each local variable Printf ("0x % 08x \ n", & V2 ); Printf ("0x % 08x \ n", & V3 ); Printf ("0x % 08x \ n", & G1); // print the memory address of each global variable Printf ("0x % 08x \ n", & G2 ); Printf ("0x % 08x \ n", & G3 ); Printf ("0x % 08x \ n", & s1); // print the memory address of each static variable Printf ("0x % 08x \ n", & s2 ); Printf ("0x % 08x \ n", & s3 ); Return 0; } The compiled execution result is: 0x0012ff78 0x0012ff7c 0x0012ff80 0x004068d0 0x004068d4 0x004068d8 0x004068dc 0x004068e0 0x004068e4 The output result is the memory address of the variable. V1, v2, v3 are local variables, g1, g2, g3 are global variables, s1, s2, and s3 are static variables. As you can see, these variables are continuously distributed in the memory, but the memory address allocated by the local and global variables is 108,000 different, while the memory allocated by the global and static variables is continuous. This is because local variables and global/static variables are allocated in different types of memory areas. The memory space of a process can be logically divided into three parts: Code zone, static data zone, and dynamic data zone. The Dynamic Data zone is generally a "stack ". "Stack" and "heap" are two different dynamic data zones. stack is a linear structure, and stack is a chain structure. Each thread of a process has a private "stack". Therefore, although the Code of each thread is the same, the data of local variables does not interfere with each other. A stack can be described through the "base address" and "Stack top" addresses. Global and static variables are distributed in the static data area, and local variables are distributed in the dynamic data area, that is, the stack. The program accesses local variables through the base address and offset of the stack. Lower ------- lower-end memory area │ ...... │ Certificate ------- Certificate │ Dynamic Data zone │ Certificate ------- Certificate │ ...... │ Certificate ------- Certificate │ Code zone │ Certificate ------- Certificate │ Static data zone │ Certificate ------- Certificate │ ...... │ Middleware ------- memory high-end memory area Stack is an advanced and post-release data structure. The top address of the stack is always less than or equal to the base address of the stack. We can first take a look at the function call process, so that we can have a deeper understanding of the role of the stack in the program. Different Languages have different function calling rules, and these factors have a balance between the parameter pressing rules and the stack. The Calling rules of windows APIs are different from those of ansi c. The former is called function to adjust the stack, and the latter is called function to adjust the stack. The two are distinguished by the prefix "_ stdcall" and "_ cdecl. Let's take a look at the following code: # Include <stdio. h> Void _ stdcall func (int param1, int param2, int param3) { Int var1 = param1; Int var2 = param2; Int var3 = param3; Printf ("0x % 08x \ n", memory m1); // print the memory address of each variable Printf ("0x % 08x \ n", ipvm2 ); Printf ("0x % 08x \ n", ipvm3 ); Printf ("0x % 08x \ n", & var1 ); Printf ("0x % 08x \ n", & var2 ); Printf ("0x % 08x \ n", & var3 ); Return; } Int main () { Func (1, 2, 3 ); Return 0; } The compiled execution result is: 0x0012ff78 0x0012ff7c 0x0012ff80 0x0012ff68 0x0012ff6c 0x0012ff70 Upper ------- lower <-top stack (ESP) and low-end memory during function execution │ ...... │ Certificate ------- Certificate │ Var 1 │ Certificate ------- Certificate │ Var 2 │ Certificate ------- Certificate │ Var 3 │ Certificate ------- Certificate │ RET │ Stack ------- stack <-"_ cdecl" function returns the top stack (ESP) │ Parameter 1 │ Certificate ------- Certificate │ Parameter 2 │ Certificate ------- Certificate │ Parameter 3 │ Stack ------- stack <-"_ stdcall" function returns the top stack (ESP) │ ...... │ Bottom ------- bottom <-stack (base address EBP), high-end memory area This is what the stack looks like during the function call process. First, the three parameters are pushed into the stack in the order from the back to the left. First, press "param3", then "param2", and finally press "param1 "; press the return address (RET) of the function, jump to the function address, and execute the function. (here, we need to add a point. This article introduces the buffer overflow principle in UNIX, press the current EBP and use the current ESP to replace the EBP. However, there is an article about function calling in windows that says that function calling in windows also has this step, but according to my actual debugging, I did not find this step, this can also be seen from the four-byte gap between param3 and var1); Step 3: subtract a number from the top stack (ESP) to allocate memory space for local variables, in the above example, 12 bytes are subtracted (ESP = ESP-3 * 4, each int variable occupies 4 bytes); then the memory space of the local variable is initialized. Because the "_ stdcall" call is adjusted by the called function, the stack must be restored before the function is returned. The memory occupied by local variables (ESP = ESP + 3*4) must be recycled first ), then, extract the return address, fill in the EIP register, reclaim the memory occupied by the previously pushed parameters (ESP = ESP + 3*4), and continue executing the caller's code. See the following assembly code: ; -------------- Assembly code of the func function ------------------- : 00401000 83EC0C sub esp, 0000000C // create the memory space of the local variable : 00401003 8B442410 mov eax, dword ptr [esp + 10] : 00401007 8B4C2414 mov ecx, dword ptr [esp + 14] : 0040100B 8B542418 mov edx, dword ptr [esp + 18] : 0040100F 89442400 mov dword ptr [esp], eax : 00401013 8D442410 lea eax, dword ptr [esp + 10] : 00401017 894C2404 mov dword ptr [esp + 04], ecx ........................ (Omitted code) : 00401075 83C43C add esp, 0000003C; restore the stack and reclaim the memory space of local variables : 00401078 C3 ret 000C; function return, recover the memory space occupied by the Parameter If it is "_ cdecl", here is "ret", the stack will be restored by the caller ; ----------------- Function end ------------------------- ; -------------- Code for the main program to call the func function -------------- : 00401080 6A03 push 00000003 // push parameter param3 : 00401082 6A02 push 00000002 // push parameter param2 : 00401084 6A01 push 00000001 // The push parameter param1 : 00401086 E875FFFFFF call 00401000 // call the func Function If it is "_ cdecl", the stack will be restored here, "add esp, 0000000C" Smart readers can see the principle of buffer overflow. Let's take a look at the following code: # Include <stdio. h> # Include <string. h> Void _ stdcall func () { Char lpbuff [8] = "\ 0 "; Strcat (lpbuff, "aaaaaaaaaaa "); Return; } Int main () { Func (); Return 0; } After compilation, how about executing the code? Ha, "0x00414141" memory referenced by the "0x00000000" command. The memory cannot be "read ".", "Illegal operation! "41" is the hexadecimal ASCII code of "A", which is obviously a problem with strcat. "Lpbuff" is only 8 bytes in size and is counted as '\ 0' at the end. strcat can write up to 7 "", but the program actually writes 11 "A" plus 1 '\ 0 '. Let's take a look at the figure above. The four extra bytes overwrite the memory space of ret. As a result, the function returns a wrong memory address and executes the wrong command. If you can carefully construct this string and divide it into three parts, the first part is only the meaningless data filled for overflow, followed by a data that overwrites ret, followed by a piece of shellcode, so long as a RET address can point to the first command of this shellcode, then the function can execute shellcode when returning. However, different software versions and different runtime environments may affect the location of the shellcode in the memory. Therefore, it is very difficult to construct this ret. Generally, a large number of Nop commands are filled between RET and shellcode, making exploit more universal. Region ------- region <-low-end memory area │ ...... │ Begin ------- begin <-start when data is filled in by Exploit │ │ Buffer │ <-enter useless data │ Certificate ------- Certificate │ RET │ <-points to the shellcode or NOP command range Certificate ------- Certificate │ NOP │ │ ...... │ <-The entered NOP command is the range that RET can point │ NOP │ Certificate ------- Certificate │ │ Shellcode │ │ Summary ------- summary <-end of Data filled in by exploit │ ...... │ Middleware ------- memory <-high-end memory area Dynamic Data in windows can be stored in the stack and heap. Anyone who knows about C ++ knows that C ++ can use the new keyword to dynamically allocate memory. Let's look at the following C ++ code: # Include <stdio. h> # Include # Include <windows. h> Void func () { Char * buffer = new char [128]; Char bufflocal [128]; Static char buffstatic [128]; Printf ("0x % 08x \ n", buffer); // print the memory address of the variable in the heap. Printf ("0x % 08x \ n", bufflocal); // print the memory address of the local variable Printf ("0x % 08x \ n", buffstatic); // print the memory address of the static variable } Void main () { Func (); Return; } The program execution result is: 0x0000007d0 0x0012ff04 0x004068c0 We can find that the memory allocated with the new keyword is neither in the stack nor in the static data zone. The VC compiler uses heap in windows to dynamically allocate memory with the new keyword. Before talking about "heap", let's take a look at several API functions related to "heap: HeapAlloc applies for memory space in the heap HeapCreate creates a new heap object. HeapDestroy destroys a heap object HeapFree releases applied memory HeapWalk enumerative all memory blocks of heap objects GetProcessHeap obtains the default heap object of the process. GetProcessHeaps obtains all heap objects of the process. LocalAlloc GlobalAlloc When a process is initialized, the system automatically creates a default heap for the process, which occupies 1 MB of memory by default. The heap object is managed by the system. It exists in the memory in a chain structure. The following code dynamically applies for memory space through the heap: HANDLE hHeap = GetProcessHeap (); Char * buff = HeapAlloc (hHeap, 0, 8 ); HHeap is the handle of the heap object, and buff is the address pointing to the applied memory space. So what is this hHeap? Is its value meaningful? Let's take a look at the following code: # Pragma comment (linker, "/entry: main") // defines the entry of the program # Include <windows. h> _ Cribd int (_ cdecl * printf) (const char *,...); // defines the STL function printf /*--------------------------------------------------------------------------- Here, let's take a look at the previous knowledge: (* Note) the printf function is a function in the C language standard function library. The VC standard function library is implemented by the msvcrt. dll module. As can be seen by the function definition, the number of printf parameters is variable, and the number of parameters pushed by the caller cannot be known in advance within the function, the function can only obtain information about the pressed parameter by analyzing the format of the first parameter string. because the number of parameters is dynamic, the caller must balance the stack, the _ cdecl call rule is used here. BTW, Windows system's API functions are basically in the form of _ stdcall calls, except for one API, that is, wsprintf, which uses the _ cdecl call rules, just like the printf function, this is because the number of its parameters is variable. ---------------------------------------------------------------------------*/ Void main () { HANDLE hHeap = GetProcessHeap (); Char * buff = HeapAlloc (hHeap, 0, 0x10 ); Char * buff2 = HeapAlloc (hHeap, 0, 0x10 ); HMODULE hMsvcrt = LoadLibrary ("msvcrt. dll "); Printf = (void *) GetProcAddress (hMsvcrt, "printf "); Printf ("0x % 08x \ n", hHeap ); Printf ("0x % 08x \ n", buff ); Printf ("0x % 08x \ n", buff2 ); } The execution result is: Zero x 00130000 Zero x 00133100 Zero x 00133118 How is the hHeap value so close to that buff value? In fact, the hHeap handle is the address pointing to the HEAP header. A structure called PEB (process environment block) is stored in the user area of the process, which stores important information about the process, the ProcessHeap stored at the offset 0x18 at the beginning of PEB is the default heap address of the process, and the offset 0x90 stores the pointer to the address list of all heap processes. In windows, many APIs use the default heap of processes to store dynamic data, for example, all ANSI functions in windows 2000 apply for memory in the default heap to convert ANSI strings to Unicode strings. Access to a heap is sequential. At the same time, only one thread can access the data in the heap. When multiple threads have access requirements at the same time, they can only wait in queue, in this way, program execution efficiency is reduced. Finally, data alignment in the memory. Alignment of the BIT data indicates that the memory address of the index data must be an integer multiple of the Data Length. The memory start address of the DWORD data can be 4 out of memory, the memory start address of WORD data can be exhausted by 2, and the x86 CPU can directly access alignment data. When he tries to access an alignment data, a series of internal adjustments will be made, these adjustments are transparent to the program, but will reduce the running speed. Therefore, the compiler tries its best to ensure data alignment during program compilation. For the same piece of code, let's take a look at the execution results of the program compiled by three different compilers: # Include <stdio. h> Int main () { Int; Char B; Int c; Printf ("0x % 08x \ n", & ); Printf ("0x % 08x \ n", & B ); Printf ("0x % 08x \ n", & c ); Return 0; } This is the execution result compiled with VC: 0x0012ff7c 0x0012ff7b 0x0012ff80 The order of variables in the memory: B (1 byte)-a (4 byte)-c (4 byte ). This is the execution result after compiling with Dev-C ++: 0x0022ff7c 0x0022ff7b 0x0022ff74 The order of variables in the memory: c (4 bytes)-3 bytes in the middle-B (1 byte)-a (4 bytes ). This is the execution result after the object is compiled using the following code: 0x0012ff6c 0x0012ff6b 0x0012ff64 The order of variables in the memory: Same as above. The three compilers have achieved Data Alignment, but the last two compilers are obviously not VC "smart", so that a char occupies 4 bytes, wasting memory. Basic knowledge: A stack is a simple data structure. It is a linear table that can be inserted or deleted only at one end. One end that allows an insert or delete operation is called the stack top, the other end is called the stack bottom, and the stack insertion and deletion operations are called the inbound and outbound stacks. There is a set of CPU commands to implement stack access to the memory of the process. Among them, the POP command implements the outbound operation, and the PUSH command implements the inbound operation. The ESP register of the CPU stores the top pointer of the current thread, and the EBP register stores the bottom pointer of the current thread. The EIP register of the CPU stores the memory address of the next CPU instruction. After the CPU executes the current instruction, it reads the memory address of the next instruction from the EIP register and continues executing the instruction. Reference: HEAP Overflow and utilization in Windows by: isno Windows core programming by: Jeffrey Richter |