Uczestniczyliśmy (@msm oraz @Rev ) jako p4 w CONFidence CTF (w sumie głównie przez/dzięki @Gynvael Coldwindowi) i wrzucamy opis tego co udało nam się zrobić - tym razem dużo tego nie ma, całe 5 zadań. Był to swoja droga pierwszy CTF offlinowy (na miejscu, nie przez internet) w którym braliśmy udział.
Tym razem (i następne będa) z konta @p4, żeby móc spokojnie pisać rozwiazania bezosobowo.
A, i ten writeup przeleżał (jako googledoc) dobre kilka miesięcy zanim go odgrzebaliśmy i wrzuciliśmy w końcu. No ale lepiej późno niż wcale.
<font size="6">Turbo CrackMe (100p)</span>
Najbardziej standardowe zadanie z tych które rozwiązaliśmy na CTFie, i najprostsze (dla nas).
Nazwa sugeruje że program był napisany w Turbo Pascalu. W sumie nie weryfikowaliśmy tego, ale Pascal Stringi w binarce popierają tą teorię.
Po uruchomieniu pokazuje się następujący widok (animowanego smoka niestety screen nie oddaje):
Szybkie przejrzenie źródła i miejsca gdzie smok jest rysowany na ekran pozwoliło zlokalizować funkcję sprawdzającą podane hasło. Był to długi, ale bardzo prosty w rewersowaniu ciąg opcodów, idący mniej więcej tak:
xor ax, ax
mov [bp+valid_chars], ax
mov [bp+var_1], 0
cmp [bp+var_102], 19
jz short loc_106B7
jmp loc_109B2
loc_106B7:
mov al, [bp+char0]
xor al, 'D'
or al, al
jnz short loc_106C5
inc [bp+valid_chars]
loc_106C5:
mov al, [bp+char0]
xor al, [bp+char1]
cmp al, '6'
jnz short loc_106D5
inc [bp+valid_chars]
loc_106D5:
mov al, [bp+char1]
xor ah, ah
mov dx, ax
mov al, [bp+char0]
xor ah, ah
sub ax, dx
cmp ax, -46
jnz short loc_106EE
inc [bp+valid_chars]
loc_106EE:
mov al, [bp+char1]
xor al, [bp+char2]
cmp al, 15h
jnz short loc_106FE
inc [bp+valid_chars]
loc_106FE:
mov al, [bp+char2]
xor ah, ah
mov dx, ax
mov al, [bp+char1]
xor ah, ah
sub ax, dx
cmp ax, 0Bh
jnz short loc_10717
inc [bp+valid_chars]
loc_10717:
mov al, [bp+char2]
xor al, [bp+char3]
cmp al, 9
jnz short loc_10727
inc [bp+valid_chars]
loc_10727:
mov al, [bp+char3]
xor ah, ah
mov dx, ax
mov al, [bp+char2]
xor ah, ah
sub ax, dx
cmp ax, 0FFF9h
jnz short loc_10740
inc [bp+valid_chars]
Dla niemówiących po asemblerowemu, to odpowiednik mniej więcej takiego C:
if (pass[0] == 'D') { valid_chars++; }
if (pass[0] ^ pass[1] == 0x36) { valid_chars++; } // 0x36 == '6'
if (pass[1] ^ pass[2] == 0x15) { valid_chars++; }
if (pass[2] ^ pass[3] == 0x9) { valid_chars++; }
// ...
Na końcu było porównanie czy valid_chars == 19, i jeśli tak, hasło było uznane za prawidłowe.
Idąc znak po znaku i xorując sobie ręcznie uzyskaliśmy flagę:
DrgnS{TextModeFTW!}
Autor zadania: gynvael coldwind
<font size="6">Encryption (200p)</span>
Ciekawe zadanie w którym dostajemy zaszyfrowany plik (o którym wiemy że jest obrazem png) i mamy oczywiście zadanie przywrócenia go do oryginalnej postaci.
Kod za pomoca którego zaszyfrowano obraz wyglądał tak (po drobnych moich modyfikacjach, nie zmieniających działania):
def split_by(data, cnt):
return [data[i : i+cnt] for i in xrange(0, len(data), cnt)]
def str_to_int(s):
return int(s.encode('hex'), 16)
def int_to_str(num, bytes):
s = '{0:x}'.format(num)
while len(s) < bytes*2:
s = '0' + s
return s.decode('hex')
def egcd(a,b):
xa,xb = 1,0
ya,yb = 0,1
while ya*a + yb*b > 0:
cnt = (xa*a + xb*b) / (ya*a + yb*b)
xa -= ya*cnt
xb -= yb*cnt
xa,ya = ya,xa
xb,yb = yb,xb
return xa, xb
def inv_mod(x, mod):
return (egcd(x, mod)[0]) % mod
def encrypt(data, key_start, key_base, BLOCK_SIZE):
mod = 1 << BLOCK_SIZE*8
k = key_start
res = []
for block in split_by(data, BLOCK_SIZE):
int_block = str_to_int(block)
assert inv_mod(k, mod) * k % mod == 1
int_block = ((int_block * k % mod) ^ k) * inv_mod(k, mod) % mod
res.append(int_to_str(int_block, BLOCK_SIZE))
k = (k * key_base) % mod
return ''.join(res)
def main():
if len(sys.argv) != 3:
print 'Usage: %s [input file] [output file]' % sys.argv[0]
return
with open(sys.argv[1], 'rb') as in_file, open(sys.argv[2], 'wb') as out_file:
data = in_file.read()
out_file.write(encrypt(data, KEY_START, KEY_BASE, BLOCK_SIZE))
print 'Done!'
Jak się do tego zabrać? Rozpoczęliśmy od zauważenia, że header png jest zawsze stały (pierwsze ~~16 bajtów, może trochę więcej). Zapisaliśmy więc sobie pierwsze bajty plain- i ciphertextu:
const_hdr = '89504E470D0A1A0A0000000D49484452'.decode('hex')
encr_hdr = 'A75EF713A0460B235AAD1C91C5493C60'.decode('hex')
Szyfr jest blokowy (nie znamy długości bloku, jeszcze), i każdy blok jest szyfrowany niezależnie - piewszy blok z kluczem k, drugi z km, trzeci z km*m, itd.
Więc pierwszy krok który zrobiliśmy to było odzyskanie k
. Patrząc na to jak szyfrowane są bloki:
for block in split_by(data, BLOCK_SIZE):
int_block = str_to_int(block)
int_block = ((int_block * k % mod) ^ k) * inv_mod(k, mod) % mod
res.append(int_to_str(int_block, BLOCK_SIZE))
mamy równanie modularne w rodzaju
CIPH_BLOCK === ((PLAIN_BLOCK * k) ^ k) * (1/k) (mod m)
albo zapisując inaczej
CIPH_BLOCK * k === (PLAIN_BLOCK * k) ^ k (mod m)
Niestety, równanie to jest (wg. mojej wiedzy) w ogólności nierozwiązywalne w żaden sensowny sposób (jakiś matematyk tutaj mnie wyprowadzi z błędu?), nawet mimo tego że jedyną niewiadomą tutaj jest 'k'.
Z drugiej strony, 'm' nie jest takie zupełnie dowolne - wiemy że jest potęgą dwójki.
Jeśli przyjrzymy sie jeszcze raz temu równaniu:
CIPH_BLOCK * k === (PLAIN_BLOCK * k) ^ k (mod m)
można zauważyć że dla danego plain_block, na najniższy bit ciph_block wpływa tylko najniższy bit k. Później na drugi najniższy bit ciph_block wpływają tylko dwa najniższe bity k, etc, etc. W ten sposób możemy odzyskać k idąc od najniższych bitów:
def nth_bit(x, n):
return int(('0' * 256 + bin(x)[2:])[-1 - n])
def get_k_when_a_times_k_is_b_times_k_xor_k_mod_m(a, b, n):
result = []
reverse(0, a, 0, b, n, 0, '', result)
return result
def reverse(current_a, a, current_b, b, n, bit, k, result):
if bit == n:
ki = int(k, 2)
if inv_mod(ki, (1 << n)) * ki % (1 << n) == 1:
if (a * ki) % (1 << n) != ((b * ki) ^ ki) % (1 << n):
print ':('
result.append(ki)
else:
# sprawdz dla bit = 1
current_a2 = current_a + (a << bit)
current_b2 = current_b + (b << bit)
if nth_bit(current_a2, bit) == nth_bit(current_b2, bit) ^ 1:
reverse(current_a2, a, current_b2, b, n, bit + 1, '1' + k, result)
# sprawdz dla bit = 0
if nth_bit(current_a, bit) == nth_bit(current_b, bit):
reverse(current_a, a, current_b, b, n, bit + 1, '0' + k, result)
Ta funkcja szczerze mówiąc bardzo mnie zaskoczyła, bo zadziałała już za pierwszym razem.
Co teraz - uzbrojeni w funkcję odwracającą (o bardzo opisowej nazwie) atakujemy zaszyfrowany obrazek - musimy spróbować każdej możliwej długości bloku, bo nie wiemy w zasadzie jakie jest block_size:
for i in range(1, 16):
cst = const_hdr[:i]
enc = encr_hdr[:i]
mod = 1 << (8 * i)
a = str_to_int(cst)
b = str_to_int(enc)
ks = get_k_when_a_times_k_is_b_times_k_xor_k_mod_m(a, b, 8 * i)
print i, ks
Okazało się że dla każdej długości bloku poza '7' nie ma żadnego pasującego k, więc jeden problem z głowy - wiemy że block_size jest równe 7, oraz mamy możliwe 'k'.
Drugi krok był podobny do pierwszego - robimy to samo, tylko dla drugiego bloku:
block_size = 7
cst = const_hdr[7:14]
enc = encr_hdr[7:14]
a = str_to_int(cst)
b = str_to_int(enc)
print a, b
ks = get_k_when_a_times_k_is_b_times_k_xor_k_mod_m(a, b, block_size * 8)
print ks
Oraz trzeci i ostatni krok - bruteforce po możliwych kluczach i sprawdzenie czy któryś z nich tworzy działający png. Jako że możliwych k w pierwszym i drugim kroku było więcej niż 1 a bliżej 100 (na 90% dało się wyeliminować większość z nich, ale okazało się to niekonieczne, bo sprawdzenie wszystkich ~~10k możliwości zadziałało równie dobrze):
possible1 = [...] # k z pierwszego kroku
possible2 = [...] # k z drugiego kroku
data = open('flag.png.enc', 'rb').read()
block_size = 7
mod = 1 << (8 * block_size)
for p1 in possible1:
for p2 in possible2:
key_start = p1
key_base = p2 * inv_mod(p1, mod)
dec = decrypt(data[:21], key_start, key_base, block_size)
if 'IHDR' in dec: # czy to poprawny png? (heurystyka)
print 'ok'
dec = decrypt(data, key_start, key_base, block_size)
open('out.' + str(p1) + '.' + str(p2) + '.png', 'wb').write(dec)
Autor zadania: redford (pozdrawiamy!)
<font size="6">Internet of Booze (200p)</span>
Ciekawe zadanie (bo hardwarowe), i musiało być pracochłonne w tworzeniu (szacunek dla autora za chęci - swoja drogą kod został po CTFie udostępniony na githubie: https://github.com/q3k/internet-of-booze) - tym bardziej robiło wrażenie.
Otóż na stole organizatorów (dragonsectoru tzn) stała taka diabelska maszyna:
Działała tak, że przyjmowała banknoty 50 zł i wydawała wódkę (podobno, nikt z nas nie próbował zapłacić 50zł za kieliszek :P).
Ale dostaliśmy też firmware do zreversowania, i okazało się że maszyna przyjmuje SMSy (i nawet odpisuje na nie).
Zreversowaliśmy kod odpowiadający za czytanie odbieranych SMSów, i doszliśmy do czegoś takiego:
int main() {
char *i32u89xn = "\xBC\xED\xE5\xB5"; // c29j
char a2[] = "WYSŁANA WIADOMOSC";
int v8 = strlen(a2 + 4);
char *v7 = a2 + 4;
char v22[20] = "000000000000000000000000";
for (int i = 0; i != v8; ++i ) {
char v15 = *(v7++);
uint8_t v16 = ("93lf4s"[i % 6] + i) ^ v15;
if ( i > 3 ) {
printf("%x ", v16);
*(v22 + i - 20) = v16;
}
else if ( v16 != "c29j"[i]) {
puts("fail");
}
}
if (v22[0] == 1) { DbgSendStatus(); } // ta linijka z pamięci
}
(+ sprawdzenie na początku czy wiadomość zaczyna się od CTRL).
Jak widać wiadomość była "deszyfrowana" poprzez xorowanie ze stałym napisem, pierwsze cztery bajty były dodatkowo sprawdzane przez porównanie z jakimś magic stringiem. Kiedy to rozwiązaliśmy, wystarczyło wysłać odpowiedni SMS i gotowe.
Była jeszcze druga część zadania której nie zdążyliśmy wykonać, a konkretnie zmuszenie maszyny do wydania darmowego napoju - sposób był bardzo podobny i byliśmy na dobrym tropie, ale nie starczyło nam czasu (mieliśmy trochę ponad 40 minut po skończeniu pierwszego etapu) - należało pójść dalej z SMSem, i za pomocą buffer overflowa nadpisać adres powrotu funkcji tak żeby skoczyła do miejsca odpowiedzialnego za nalewanie.
Autor zadania: q3k
<font size="6">PHP Core (300p)</span>
Nasze ulubione zadanie z całego CTFa (z tych które rozwiązaliśmy przynajmniej, na temat reszty nie mamy zdania). Tak bardzo nam się spodobało że aż (za namową autora) napisaliśmy o nim profesjonalny artykuł do "Programisty". Pdf z artykułem będzie wrzucony na forum jak tylko upewnimy się co do formalności.
Autor zadania: gynvael coldwind
Poza własnym kontem na 4programmers, p4 uzyskało też konto na githubie: https://github.com/p4-team/ctf
Zapraszamy do śledzenia jeśli ktoś jest chętny. Writeupy tworzymy teraz na githubie, więc jest dużo aktualniejszy (dwa writeupy ciągle zrestą oczekują na skład i wrzucenie na 4p). No i dzięki samozaparciu @Shaloma są nawet wersje angielskie ;).
- a71438d0c1.png (15 KB) - ściągnięć: 172
- 0503c75fa2.png (12 KB) - ściągnięć: 190
- 8896512a64.png (391 KB) - ściągnięć: 279