A CrySys labor idén is csapatot szervezett a 2013-as iCTF versenyre, ami a Santa Barbarai Egyetem éves Capture the Flag játéka. A kihívásból én is kivettem a részem, az alábbiakban a Pesticides pálya (vélt) megoldása következik (nem mindenre emlékszem 100%-ig az esetleges pontatlanságokért előre is elnézést).
Az idei iCTF minden feladata valamilyen kritikus információs infrastrukturát modellező szolgáltatás köré épült: a csapatoknak biztonsági problémákat kellett találniuk a szolgáltatásokban, meg kellett írniuk az ezek kihasználására alkalmas exploitot, valamint ki kellett szűrniük a beérkező hálózati forgalomból a támadásokat. A Pesticides valamiféle vegyi üzem egyik vezérlőjét szimulálta, melyen keresztül lekérdezhetők és módosíthatók egyes kemikáliák összetevői.
Az iCTF feladatokkal kapcsolatban nem megszokott módon a szolgáltatáshoz egy README-t is mellékeltek, melyben leírták a használt protokoll működésének alapjait:
A kommunikáció első lépéseként a csatlakozó kliens egy session kulcsot (szKey) küld a szervernek, a kommunikáció további része RC4-el titkosítva zajlik, a MASTER_KEY || szKey kulccsal, ahol a || a konkatenációt, a MASTER_KEY pedig a mindkét fél számára ismert mesterkulcsot jelenti.
Ez után a kérések az alábbi formátumban küldhetők:
<PROTOCOL>\n
CODE=<COMMAND>\n
<OPT>=<VAL>\n
<OPT>=<VAL>\n
\n
egy érvényes kérés például:
MODBUS-v3\n
CODE=LIST\n
\n
A README egy hiányos példa klienst is tartalmazott, amiben egy egyszerű véletlengeneráló függvény mellett az RC4 inicializáló és titkosító eljárásait kellett implementálni, ennek lényegi része a következő:
class CClient(object):
# ...
def SendCommandGetAnswer(self, szCommand, dicRequest={}):
szData = "MODBUS-v3\nCODE=%s\n" % (szCommand)
for k in dicRequest.keys():
szData += "%s=%s\n" % (k, dicRequest[k])
szData += "\n"
sock.sendall(Rc4Crypt(self._szKey, szData), False)
# Init rc4 to receive data
index = [0,0]
S = range(0,256)
Rc4InitTable(self._szKey, S)
# Receive the data
szData = ""
while szData.find("\n\n") == -1:
d = self._sock.recv(1024)
if d == None or len(d) == 0:
return None
for c in d:
szData += Rc4CryptChar(c, S, index)
return szData
A feladathoz rendelkezésre állt a a kiszolgáló obfuszkált forráskódja is, ez valahogy így nézett ki:
import re
import utils as u
import random as M1
import string as M2
from cpesticide import CPesticide as P1
from output import Output as P3
from constants import PROTOCOL as P
from password import WAW, MASTER_KEY
z = {}
exec("class v0(object):\n\tdef __init__(v6):\n\t\tv6.v1 = {}\n\t\tv6.v2 = \"\"\n\t\tv6.__v9 = \"\"\n\n\tdef A(v6, v3, szValue):\n\t\tv6.v1[v3] = szValue\n\n\tdef S(v6, v4):\n\t\tv6.v2 = v4\n\n\tdef G(v6):\n\t\treturn v6.v2\n\n\tdef R(v6):\n\t\tdef T(c, l):\n\t\t\treturn c.join([chr(x-0x10) for x in l])\n\t\tdef G(c):\n\t\t\treturn T(c, [ord('V'), ord('W')]) + ''.join(M1.choice(M2.ascii_uppercase + M2.digits + M2.ascii_lowercase) for x in range(13))\n\t\tdef Q(l):\n\t\t\treturn \"A\".join([chr(x+0x30) for x in l])\n\n\t\tv3=\"CODE=%s\\n\" % v6.v2\n\t\tfor k in sorted(v6.v1.keys()):\n\t\t\tv3 += \"%s=%s\\n\" % (k, v6.v1[k])\n\n\t\tif v6.v2 == \"OK\":\n\t\t\tif len(v6.__v9) > 0 and z.has_key(v6.__v9) == True:\n\t\t\t\tf = z[v6.__v9]\n\t\t\telse:\n\t\t\t\tf = G(\"L\")\n\t\t\tv3 += \"%s=%s\\n\" % (chr(70)+Q([28,23]), f)\n\n\t\tv3 += \"\\n\"\n\t\treturn v3\n\n\tdef D(v6, v9):\n\t\tv6.__v9 = v9\n\n\ndef handle(o):\n\tP2 = P1()\n\tI = 51\n\n\tv2 = \"\"\n\twhile v2.find(\"\\n\") == -1:\n\t\td = o.request.recv(1)\n\t\tif d == None or len(d) == 0:\n\t\t\treturn\n\t\tv2 += d\n\n\tv1 = False\n\tif len(v2) < 20:\n\t\tP3.debug(\"Client key is too short, closing connection\")\n\t\tv1 = True\n\n\telse:\n\t\tv3 = MASTER_KEY+v2[0:len(v2)-1]\n\n\tI = I<<1\n\twhile v1 == False:\n\t\tAnswer = v0()\n\n\t\tv6 = [0,0]\n\t\tv7 = range(0,256)\n\t\tu.ri(v3, v7)\n\n\t\tv4 = \"\"\n\t\twhile len(v4) < 10:\n\t\t\td = o.request.recv(len(P)+1-len(v4))\n\t\t\tif d == None or len(d) == 0:\n\t\t\t\treturn\n\t\t\tfor c in d:\n\t\t\t\tv4 += u.rec(c, v7, v6)\n\n\t\tif v4 != P+\"\\n\":\n\t\t\tAnswer.S(\"UNKNOW PROTOCOL\")\n\t\t\tv1 = True\n\t\telse:\n\t\t\tb = False\n\t\t\tv8 = \"\"\n\t\t\tv10 = \"\"\n\t\t\twhile v8.find(\"\\n\\n\") == -1:\n\t\t\t\td = o.request.recv(1024)\n\t\t\t\tif d == None or len(d) == 0:\n\t\t\t\t\treturn\n\t\t\t\tfor c in d:\n\t\t\t\t\tv8 += u.rec(c, v7, v6)\n\n\t\t\tif v8.startswith(\"CODE=CTRL\"):\n\t\t\t\tm = re.search(\"S=([^:]+):([^\\n]+):([^\\n]+)\", v8)\n\t\t\t\tif m != None:\n\t\t\t\t\tif m.group(1) != WAW:\n\t\t\t\t\t\tAnswer.S(\"INVALID PASS\")\n\t\t\t\t\telse:\n\t\t\t\t\t\tz[m.group(2)] = m.group(3)\n\t\t\t\t\t\tAnswer.S(\"OK\")\n\n\t\t\t\tm = re.search(\"G=([^\\n]+):([^\\n]+)\", v8)\n\t\t\t\tif m != None:\n\t\t\t\t\tif m.group(1) != WAW:\n\t\t\t\t\t\tAnswer.S(\"INVALID PASS\")\n\t\t\t\t\telse:\n\t\t\t\t\t\td = P2.d\n\t\t\t\t\t\td[d.keys()[0]] += 1810\n\t\t\t\t\t\tb = True\n\t\t\t\t\t\tAnswer.S(\"OK\")\n\t\t\t\t\t\tv10 = m.group(2)\n\n\t\t\tif len(Answer.G()) == 0 and b == False:\n\t\t\t\tm = re.search(\"FGID=([^\\n]+)\\n\", v8)\n\t\t\t\tif m != None:\n\t\t\t\t\tv10 = m.group(1)\n\n\t\t\t\tif o.HandleData(P2, v8, Answer) == False:\n\t\t\t\t\tAnswer.S(\"ERROR\")\n\t\t\t\telse:\n\t\t\t\t\tb = True\n\n\t\t\tif b == True:\n\t\t\t\tfor v in P2.d.values():\n\t\t\t\t\tif v > 1000:\n\t\t\t\t\t\tAnswer.D(v10)\n\n\t\tdef c(v3, v8):\n\t\t\tx = 0\n\t\t\tv1 = range(256)\n\t\t\tfor i in range(256):\n\t\t\t\tx = (x + v1[i] + ord(v3[i % len(v3)])) % 256\n\t\t\t\tv1[i], v1[x] = v1[x], v1[i]\n\t\t\tx = 0\n\t\t\ty = 0\n\t\t\tout = []\n\t\t\tfor c in v8:\n\t\t\t\tx = (x + 1) % 256\n\t\t\t\ty = (y + v1[x]) % 256\n\t\t\t\tv1[x], v1[y] = v1[y], v1[x]\n\t\t\t\tout.append(chr(ord(c) ^ v1[(v1[x] + v1[y]) % 256]))\n\t\t\treturn ''.join(out)\n\n\t\to.request.sendall(c(v3, Answer.R()))\n")
A whitespace-ek megfelelő átalakítása, illetve a program indítószkriptjének tanulmányozása után kiderült, hogy a fent definiált osztályban lényegében a SocketServer.BaseRequestHandler handle() metódusa került megvalósításra, ez az osztály felelős tehát lényegében minden alkalmazáslogikáért, minden más csak körítés. Az RC4-et azért szeretjük, mert alapesetben néhány sorban implementálható, gyakorlott szemmel pedig az ember könnyen felismeri az algoritmust az importált utils csomag ri(), rec() és rea() metódusaiban. Ezeket felhasználva a README-ben található példakód könnyen kiegészíthető volt; a password.py-ban található MASTER_KEY felhasználásával sikeresen meg lehetett szólítani a tesztkiszolgálónkat.
Kisebb meglepetést okozott, hogy egy egyszerű LIST kód elküldése után a vegyianyag szintek mellett rögtön megkaptunk egy pontot érő flaget is. Mint kiderült, a kiszolgáló minden érvényes (megfejthető) kérés mellé csomagolt egy ilyen ajándékot - szépséghiba, hogy a többi csapat mesterkulcsát nem ismertük...
Itt volt az ideje tehát egy kis kriptós gondolkodásnak! Bár az RC4-el kapcsolatban az utóbbi időben nem sok jót hallani, Leventéék felvilágosítottak, hogy a kulcsfolyam megfelelő mértékű megjóslására még a kulcs részleges ismerete esetén sincs reális esély, érdemes viszont végiggondolni a folyamtitkosítók általános konstrukcióját:
a kriptoszöveg a nyíltszöveg és valamilyen, valódi véletlengenerátort közelíteni próbáló kulcsfolyam-generátor bitenkénti XOR összegével áll elő.
Az a példa kliens kódjából egyértelműen látszik, hogy a válaszok megfejtéséhez a kliens mindig újrainicializálja az RC4 kulcsfolyam generátorát, feltételezhető tehát, hogy szerver is ugyanígy tesz, csak a helyes mesterkulcs felhasználásával. Így minden üzenet azonos kucsfolyammal lesz össze XOR-olva, a nyílt szöveg ismeretében pedig a kulcsfolyam megszerezhető és újrahasznosítható. Ennek a ténynek a kihasználásához az alábbi kódrészletre kell felfigyelnünk a handle() metódusban (Néhány változót átneveztem):
while len(v4) < 10:
d = o.request.recv(len(P)+1-len(v4))
if d == None or len(d) == 0:
return
for c in d:
v4 += u.rec(c, range256, v6)
if v4 != P+"\n":
Answer.S("UNKNOW PROTOCOL")
problem = True
Amint az a hibaüzenetből is látszik, P az elvárt protokollazonosító ("MODBUS-v3") - ha a fogadott üzenet nem ezzel a sorral kezdődik, a kiszolgáló visszautasítja a lekérdezést és nem ad flaget sem. Ezzel egyrészt ellenőrizhető, hogy a kliens helyes mesterkulcsot használ-e, másrészt viszont utat enged a kulcsfolyam megismeréséhez: az Answer.S() a session+mesterkulcs felhasználásával titkosítva küldi vissza a paraméter titkosított változatát a kliensnek, ha nem tudja értelmezni az üzenetünket. A kulcsfolyam ezek után például a következő módon kapható meg:
orig="CODE=UNKNOW PROTOCOL\n\n"
sock.send(binascii.unhexlify("00...000a")+("\x31"*21+"\x00")) # session kulcs+szemét
oracle=sock.recv(1024)
keystream=xor(orig,oracle)
A keystream-et az után tetszőleges üzenettel XOR-olva a kiszolgáló számára értelmes üzenetet állíthatunk elő:
lst=bytearray("MODBUS-v3\nCODE=LIST\n\n")
sock.send(binascii.unhexlify("00...000a")+xor(lst,keystream)) # fontos, hogy a session kulcs azonos legyen!
ans=sock.recv(1024)
print xor(ans,keystream)
Ezzel azonban csak a küszöbön tettük be a lábunkat: a kulcsfolyam ismert része ugyanis túl rövid ahoz, hogy a válaszban a Flag értékét is meg tudjuk fejteni (vagy - beleélve magunkat a játékba - komplexebb parancsok kiadásával befolyásoljuk a keverési folyamatot).
A megoldáshoz több lépésben kell megismételnünk a támadást, egyre hosszabb és hosszabb kexstream-ekre szert téve. A végső megoldás valahogy így épül fel:
- Valamilyen szemetet elküldve titkosított UNKNOW PROTOCOL üzenetet kapunk, melyből visszaállítjuk a keystream első 17 byte-ját.
- A 17 byte-os keystream-et felhasználva INFO üzenetet küldünk, melyre a kiszolgáló verzióinformációval válaszol. Mivel a válasz formátuma, illetve a lehetséges verzióértékek ismertek, visszaállíthatjuk a kulcsfolyam további szakaszát.
- A hosszabb kulcsfolyam alkalmas egy INC üzenet kiadására, mellyel növelhetjük az egyik vegyianyag koncentrációját, a válaszban pedig csak a Flag-et kapjuk vissza olyan pozícióban, ameddig a kulcsfolyamunk még elér.
A probléma kihasználásához kulcsfontosságú, hogy a támadó mindig azonos session kulcsot használjon, az adatfolyam további része viszont titkosított, a támadás detektálásához tehát az ismételt session kulcsokat (a szolgáltatáshoz érkező üzenetek első 20 byte-ját) érdemes figyelni. Legalábbis az én elgondolásom szerint - a rendszer erre a megoldásra illetve az exploitra sem adott végül pontot, ami fájdalom, de a megoldás szerintem jól demonstrálja a folyamtitkosítók helytelen használatából eredő kockázatokat.
Az iCTF-en végül a 23. helyen végeztünk nagyjából 100 csapatból, ami csak amiatt kellemetlen, mert a verseny utolsó 3 órájában a pontjaink javát jelentő netflow-ból (amiben a támadásokat kellett detektálni) egyszerűen nem osztott nekünk a rendszer, így kb. 10 helyet csúsztunk vissza :P
Ezzel együtt én nagyon élveztem a játékra szánt órákat, a feladatok sokkal jobban kidolgozottak voltak, mint előző évben, és úgy érzem, hogy a csapat is remekül dolgozott - köszi srácok!
Az iCTF pályákat a nálam lévő teszt/exploit kódokkal mindjárt feltöltöm Gitoriousra (akinél van még kód, az küldjön merge requestet!) - lehet gyakorolni, szinte biztos, hogy a Pesticides-ben is van még hiba! A Defcon CTF-en találkozunk!
domi007 2013.03.30. 22:38:52
synapse · http://www.synsecblog.com 2013.04.01. 13:07:28
Joe80 2013.04.29. 10:49:15
Joe80 2013.04.29. 10:50:38
buherator · http://buhera.blog.hu 2013.04.29. 12:11:31