It sikkerhed og kryptering

Hacker løser udfordring fra Forsvarets Efterretningstjeneste

Da Forsvarets Efterretningstjeneste annoncerede årets hacker-udfordring, tog Robert "Capture the Flag" udfordringen op. Her fortæller han, hvordan han gennemskuede og løste opgaven.

I 2016 oprettede Forsvarets Efterretningstjeneste sit hacker-akademi og lokkede med en udfordring, og i år følger man op med en ny opgave. Lad os tage et kig. Udfordringen består af et billede, som blev trykt i aviser og på FE's hjemmeside: 


Forsvarets Efterretningstjenestes udfordring 2017

Første indtryk

Efter et hurtigt kig ned over assembly-koden til venstre er det klart, at der er tale om en virtuel maskine. Den er ukomplet, for der er symboler, som mangler.

Teksten til højre ligner base64-tegnsættet, så der gemmer sig nok noget binært bag. Måske bytekode til VMen.

Jeg forsøger med et par forskellige OCR-programmer, som kan omdanne tekst i et billede til ren tekst, men der er brugt en OCR-fjendtlig font, så der er mange fejl. Jeg spørger mig selv, om jeg virkelig vil gå gennem teksten for fejl, og svaret er NEJ!

Heldigvis er der en genvej, for avisabonnenter har mulighed for at hente en PDF med teksten, og den får jeg fra en af mine mere belæste bekendte. Der er nogle markerede tegn i base64-teksten, og sætter man dem sammen og base64-dekoder dem, så får man 32hacker557zjkzi.onion, som er en deep web url. Serveren svarer desværre ikke, men jeg får at vide, at man kan downloade de to filer som ren tekst derfra.

Den ukomplette VM

En virtuel maskine (VM) er et stykke software, der opfører sig som en computer, og det er slet ikke så magisk, som det lyder. Den lille stump assembly implementerer en lille BIOS (de fire instruktioner under U5_LE), som indlæser en sektor på 512 bytes fra en virtuel disk til hukommelsen og går derefter ind i en fetch / decode / execute cycle (instruktionerne under SPIN). Derudover implementerer den seks opcodes, men kigger vi i opcode-tabellen (OP_TABLE), er der 17 opcodes, så vi mangler nogle.

Hvis vi læser koden under SPIN, ser vi, at der defineres 64 registre. Registre er en CPUs svar på variable. Nogle er general purpose og kan bruges til hvad som helst, mens andre har en specifik brug. Vi ser i starten, at register 63 bruges til at læse 32 bits fra hukommelsen, hvorefter den inkrementeres med 4. Det er program-counteren, som altså peger på den næste instruktion, som skal udføres, og det, der skete, var, at nuværende instruktion blev læst fra hukommelsen.

Derefter sættes register 0 til værdien 0, og det kender jeg fra MIPS-arkitekturen, som har et register, der altid har værdien 0.

Derefter skilles den indlæste instruktion ad, ved at de øverste fem bits bruges til at vælge operation i opcode-tabellen. De efterfølges af tre gange seks bits, som bruges til at vælge tre registre.

Til sidst springes til implementeringen af den pågældende instruktion. De fleste instruktioner tager op til tre registre som argumenter. Det første bruges som destination for operationen på de to andre registre. En instruktion kunne være ‘MUL r1, r3, r4’, som multiplicerer værdien af r3 og r4 og lægger resultatet i r1-registret. Implementeringerne af ADD, DIV og NOR mangler, men dem kan vi nok gætte ud fra MUL-implementeringen.

Læser og forstår vi de øvrige implementerede instruktioner, kan vi også gætte de resterende manglende instruktioner, så jeg forsøger mig med at implementere en VM i C, mit favoritsprog til den slags.

 

   /* Bring in 512 bytes */
    memcpy(
vm.memory, disk, 512);

    /* Enter execute cycle */
    while (!vm.halted) {
        /* Fetch */
        instruction.machinecode = (vm.memory[vm.registers[REG_PC] + 0] << 24) |
                                  (vm.memory[vm.registers[REG_PC] + 1] << 16) |
                                  (vm.memory[vm.registers[REG_PC] + 2] <<  8) |
                                  (vm.memory[vm.registers[REG_PC] + 3] <<  0);
        vm.registers[REG_PC] += 4; /* Point to next instruction */
        vm.registers[REG_ZERO] = 0; /* Zero register */
        decode(&instruction);
        if (opcode_table[instruction.opcode]) {
            opcode_table[instruction.opcode](&instruction, &vm);
        } else {
            printf("illegal opcode\n");
            exit(1);
        }
    }

Min VMs fetch / decode / execute cycle

 

disk.img

Teksten til højre i billedet bliver base64-dekodet og forsøgt fodret til min VM - med succes (efter et par bug fixes). Det ér et program til VM’en, og det beder om et password til dekryptering. Jeg prøver med forskellige ord fra FE-s hjemmeside for akademiet, dog uden held.

Jeg kan udføre programmet cirka 25 gange i sekundet, så et udtømmende brute force-angreb er udelukket. Til brute forcing skal vi gerne op på et par hundredtusinde forsøg pr. sekund, afhængig af den forventede længde af password. Jeg kunne selvfølgelig også forsøge med et dictionary-angreb, hvor jeg bruger en ordbog, men nej, jeg vil hellere gennemskue programmet.


Første forsøg med at eksekvere bytecoden i min VM

Reverse engineering af bytecoden

Jeg modificerer min VM, så jeg kan få den til at dumpe et disassembly af koden, mens den eksekverer. Dette kaldes et runtrace og er en nem måde at få den relevante kode ud på. Jeg kunne have disassemblet hele filen, men jeg ved ikke, hvad der er kode, og hvad der er data. Jeg ved heller ikke, hvilke dele af koden, som er relevant. Et runtrace dumper den kode, som bliver eksekveret!

Der er selvfølgelig løkker, så filen kommer til at fylde et par millioner linjer, men hvis jeg sorterer filen og fjerner dubletter, ender jeg med 345. Det er jo til at overskue.

Jeg går i gang med at læse koden og kan heldigvis bygge på min erfaring med MIPS assembly, for der er brugt mange af de samme tricks.

Når jeg skal løse en opgave som den her, giver det ikke mening at gennemskue hver eneste detalje. Man skal “bare” få nok med til at forstå, hvad der sker, og så kan man gå i detaljer med de dele, som er ekstra interessante. Mens jeg læser koden, skriver jeg masser af kommentarer og forsøger at navngive funktionerne.

Programmet starter med en boot-sektor (som BIOS-delen af VM’en kopierer ind i hukommelsen), der har til opgave at kopiere resten af programmet til hukommelsen og udføre det. Derefter beder programmet om et password, som gives til en krypto initialiserings-rutine, som jeg hurtigt gennemskuer er RC4-algoritmen. Den er meget nem at genkende, for den starter med at oprette et 256 bytes stort array med tallene fra 0-255.

Derefter dekrypteres 56 bytes, som sammenlignes med teksten “Another one got caught today, it's all over the papers.” fra the hackers manifesto. Hvis ikke der er et match, får vi “Bad key!”-beskeden, og programmet afsluttes.

Der er også en funktion, som ikke gør andet end at loope 300.000 gange. Det er derfor, programmet tager så lang tid. Vi kan fjerne kaldene til denne funktion, så programmet vil køre hurtigere, men jeg gør noget andet i stedet.

 

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
   busywait(uint32_t loops)            ;; 
;;   Preconditions:                      ;; 
;;      * r3  : Number of times to loop  ;; 
;;      * r59 : Contains return address  ;; 
;;   Postconditions:                     ;; 
;;                                       ;; 
;;   Does this:                          ;; 
;;                                       ;; 
;;   while (loops--) {}                  ;; 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; r20 = 0  loop counter                  ;;
08f8   ADD   r20, r0, r0                 ;;
; GOTO 0x90c                             ;;
08fc   MOVI  r57, 0x90c                  ;;
0900   ADD   r63, r57, r0                ;;
; r20++                                  ;;
0904   MOVI  r57, 0x1                    ;;
0908   ADD   r20, r20, r57               ;;
; r21 = r3 - r20                         ;;
090c   NOR   r57, r20, r20               ;;
0910   ADD   r21, r3, r57                ;;
0914   MOVI  r57, 0x1                    ;;
0918   ADD   r21, r21, r57               ;;
; if r21 != 0:                           ;;
;    GOTO 0x904                          ;;
091c   MOVI  r57, 0x904                  ;;
0920   ADD   r57, r57, r0                ;;
0924   CMOV  r63, r57, r21               ;;
; Return                                 ;;
0928   ADD   r63, r59, r0                ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

Denne funktion er grunden til at dekrypteringen er langsom

Bryd kryptoen

RC4 er en stream cipher. Det vil sige, at den genererer en strøm af bytes, som man bruger til at kryptere sin plaintext, en byte ad gangen med XOR-operationen. Man gør det samme for at dekryptere, og output fra en god stream cipher skal se helt tilfældig ud. Men RC4 har en svaghed. Den “foretrækker” 1 bits i de første cirka 100 bytes. Det hjælper mig dog ikke, så jeg forsøger mig med et brute force-angreb. Jeg koder det i C igen, og det giver hurtigt et resultat. Det korrekte password er “agent”, og nu føler jeg mig lidt dum, for ikke alene står det i en ordbog, det står tæt på starten. Det tog mig et par aftener at forstå maskinkoden, men jeg kunne nok have fundet det rigtige password med en ordbog og 25 forsøg i sekundet på et par minutter.

 


På 26 sekunder finder jeg det korrekte password

   


Giver man programmet det rigtige password omskriver det sig til en web-server

 

Web-serveren

Ja, jeg er åbenbart ikke færdig. Programmet har omskrevet sig til en web-server og leveret xinetd-konfiguration. xinetd er et smart program, der omdanner et simpelt program, som læser fra standard in og skriver til standard out til en netværks server.

Jeg kigger lidt i det nye program med en hex editor og opdager et par stier, som jeg vil forsøge mig med. En af dem giver mig beskeden, at jeg har løst opgaven!

Dog spørger den også, om jeg har fundet alt, så det har jeg nok ikke.

 

Reverse engineering af web-serveren

Jeg fortsætter strategien med at runtrace web-serveren og langsomt opbygge en forståelse af den udførte maskinkode.

Den starter med at læse en linje og splitte den op i request og sti.

Kort inde i koden begynder den at tjekke stien, et tegn ad gangen, om den matcher en “hemmelig” sti. Hvis ikke den gør det, indlæser den fra et array af lovlige stier, som man tydeligt kan se, hvis man læser i bytecoden (som jeg gjorde først). Den hemmelige sti ligger i kode, så den kan ikke ses uden at læse koden.

FE har virkelig oppet sig og lavet en god og underholdende opgave i år.

 

; if (request_path[11] != 'e')
;    goto 0x4b8
0370   MOVI  r57, 0x14a0
0374   LOAD  r20, BYTE [r57+r0]
0378   MOVI  r57, 0x65
037c   NOR   r57, r57, r57
0380   ADD   r20, r20, r57
0384   MOVI  r57, 0x1
0388   ADD   r20, r20, r57
038c   MOVI  r57, 0x4b8
0390   ADD   r57, r57, r0
0394   CMOV  r63, r57, r20

Sekvenser som denne tjekkede hvert tegn i den forespurgte sti


FE siger både, at jeg har og ikke har løst opgaven.

 


Jeg var blandt de første 20, som løste opgaven og indkasserede en t-shirt fra FE

 

 


Man er i mål, når man får denne besked

 Boks: Robert ""Capture the Flag" Larsen

Robert Larsen, 38 år, er til daglig software udvikler hos CEGO A/S og har i knap 10 år muntret sig med online hackerspil og -udfordringer. Fem gange har han afholdt PROSACTF, som er en hackerkonkurrence primært rettet mod begyndere og har også afholdt PROSA workshops i binære hackerteknikker.

URLs:

https://github.com/RobertLarsen/FE-2017 - Min løsning til denne opgave

https://fe-ddis.dk/hackerakademi/Pages/2017.aspx - FE’s side for hacker akademiet og 2017-udfordringen