Post

Binary Exploitation 101 - ret2dlresolve

Binary Exploitation 101 - ret2dlresolve

This blog series is still a work in progress. The content may change without notice.

In this chapter, we’ll learn about ret2dlresolve. The materials for this chapter can be found in the chapter_09 folder.

Introduction

If we look at this chapter’s chal.c, we’ll notice the output functions such as printf and putchar have been removed:

As printf(buf) is gone, we can no longer use FSA, but since SSP is disabled, we can gain control via a buffer overflow easily. However, because the output functions are missing, we can’t leak the glibc base address needed to build a ROP chain. This raises the question: can we spawn a shell without any leaks? To answer that, we need a deeper understanding of how address resolution works via the GOT and PLT.

How Symbol Resolution Works

As explained in the previous chapter, calls to glibc functions are implemented through the GOT and PLT. On the first call, the function’s address is resolved by the dynamic linker and written into the corresponding GOT entry (this process is called symbol relocation). To do this, the dynamic linker needs certain information, such as the address of the GOT entry and the name of the function to resolve. How is this information actually provided?

Recall that in the previous chapter, when the putchar function was called, the following instructions were executed:

So what exactly is the 0 value that’s pushed onto the stack here? In fact, this value is used to pass the information the dynamic linker needs.

As explained in the Hello World chapter, an ELF file consists of multiple sections. In this chapter, the important sections are .rela.plt, .dynsym, and .dynstr.

The .rela.plt section is an array of ELF64_Rela structures, defined as follows:

1
2
3
4
5
typedef struct {
    Elf64_Addr r_offset;
    uint64_t   r_info;
    int64_t    r_addend;
} Elf64_Rela;

The important fields are r_offset and r_info. r_offset specifies the location where the address will be written. r_info indicates both the symbol to be relocated and the type of relocation: the upper 32 bits specify the index of the symbol to reference, and the lower 32 bits specify the relocation type:

1
2
3
#define ELF64_R_SYM(i)			((i) >> 32)
#define ELF64_R_TYPE(i)			((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type)		((((Elf64_Xword) (sym)) << 32) + (type))

The .dynsym section is an array of Elf64_Sym structures, defined as follows. The upper 32 bits of the r_info field above specify the index in this array. The important field is st_name, which is used to specify the function’s name:

1
2
3
4
5
6
7
8
typedef struct {
    uint32_t      st_name;
    unsigned char st_info;
    unsigned char st_other;
    uint16_t      st_shndx;
    Elf64_Addr    st_value;
    uint64_t      st_size;
} Elf64_Sym;

The .dynstr section is an array of null-terminated strings. The st_name field above holds the offset in this array.

Now, let’s check this in GDB. Run the following command (make sure to move into the chapter_08 folder before executing):

1
pwndbg -q --ex 'b *0x401030' --ex 'r' ./chal_patched

If we run x/3i $rip, we see the following instructions, which call the dynamic linker to resolve the address of putchar.

This 0 is the index indicating which Elf64_Rela structure in the .rela.plt section should be referenced. In fact, if we check the address of the .rela.plt section using the elfsections command and display the first structure (index 0), we get the following:

We can see that r_offset is 0x403330, which matches the address of the putchar GOT entry, ELF64_R_SYM(r_info) is 1, and ELF64_R_TYPE(r_info) is 7 (R_X86_64_JUMP_SLOT).

ELF64_R_SYM(r_info) specifies the index indicating which Elf64_Sym structure to reference. We can see this by displaying the second entry (index 1) of the .dynsym section:

We can see that st_name is 0x24. This is the offset indicating which string to reference. In fact, if we display the string at offset 0x24 from the start of the .dynstr section, we can see the string "putchar":

Now we understand why the value 0 was pushed onto the stack when resolving the address of the putchar function. By using this value, the dynamic linker can know information such as the address of the corresponding GOT entry and the name of the function to resolve.

ret2dlresolve

As we saw in the previous section, when calling the dynamic linker, the index specifying which Elf64_Rela structure to reference is pushed onto the stack. Now, what would happen if we passed an invalid index and made it reference a fake Elf64_Rela structure that we prepared? By properly setting up a corresponding fake Elf64_Sym structure and function name string, we can call any function present in glibc. This allows us to spawn a shell without any leaks. This technique is known as ret2dlresolve.

Now, the questions are:

  1. Where should we place the fake Elf64_Rela, Elf64_Sym structures and strings?
  2. How can we write to the addresses determined in 1?

For the first question, we can check the addresses of each section and the process’s memory map using the elfsections and vmmap commands in GDB, which gives the following:

We can see that there is a writable segment at an address higher than the .rela.plt section. Moreover, since PIE is disabled, this address is fixed even when ASLR is enabled. By using a buffer overflow, we can write arbitrary values to this address, so we can place our fake structures here.

For the second question, we can use the following instructions in the main function. Since we can freely control the value of the rbp register via a buffer overflow (remember that the leave instruction pops a value from the stack into rbp), we can use the gets function to write to arbitrary locations. Moreover, because PIE is disabled, these addresses are fixed:

One thing to note is that if we tamper with rbp, rsp will change as well when returning from the gets function (remember that the leave instruction is equivalent to mov rsp, rbp; pop rbp;). Therefore, in the writable segment mentioned above, we need to write not only the fake structure but also the ROP chain correctly.

Exercise

Based on what you have learned so far, write an exploit using ret2dlresolve, and launches a shell. Before you start, make sure to execute the following command to enable ASLR (if you are using a Docker container, run it on the host):

1
sudo sysctl -w kernel.randomize_va_space=2

You can use the template, and the following hints may help. If successful, you should be able to launch a shell like this:

If you have any questions, feel free to leave a comment below. You can see my solution here.

If you have time, try enabling RELRO and see if the attack still works.

Hints

  • Pwntools provides convenient functionality for ret2dlresolve, so you can use it, but don’t become a script kiddie.
  • In the _dl_fixup function, there is a check that make sure ELF64_R_TYPE(r_info) equals 7 (R_X86_64_JUMP_SLOT), so the type needs to be set to this value:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    DL_FIXUP_VALUE_TYPE
    attribute_hidden __attribute ((noinline)) DL_ARCH_FIXUP_ATTRIBUTE
    _dl_fixup (
    # ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
         ELF_MACHINE_RUNTIME_FIXUP_ARGS,
    # endif
         struct link_map *l, ElfW(Word) reloc_arg)
    {
      ....
      /* Sanity check that we're really looking at a PLT relocation.  */
      assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
    
This post is licensed under CC BY 4.0 by the author.

Trending Tags