angstromctf 2020 writeup
angstromctf 2020 writeup
チーム Little Twoosとして参戦して、最終的に66位でした
開催時間長いため、普段やらないreversingとmiscなどにもてだして、なんとかメンバーと協力して何問を解くことができた
misc
ws1 (misc 30pts)
recording
というpcapファイルが渡された。ちょっとスクロールしたら、HTTPのパケットが発見し、クリックしたらflagはそこにあった。flag=xxx
ws2 (misc 80pts)
recording.pcapng
の中にスクロールしながら探してみたら、flag.jpg
らしき文字列が発見、follow->TCP streamで確認する。
すると一個下にJFIF
というJPEGと関わる文字列があり、後にマジックナンバーも一致した。HTTPパケットのフィールドを詳しく見てみると、MIMEの下にJPEG File Interchange Format
というのがありexportしたらflag.jpg
があった
ws3 (misc 180pts)
相変わらずpcapが渡されて、見てみたら、どうやらgitのやり取りでした、恐らくgit push
とかのコマンドによる通信だと思う(間違えたらごめん)。そこで、PACK
という文字列が目に入り、gitと合わせて検索したらgitのパッケージ的なことが判明、それをexportして、git (repository)の復元作業を行った。git packについて詳しい説明は1こちらの解説を参考にした。
git unpack-objects < xxx.pack #git objectを復元、最終的に git cat-file -p {hash} #復元
clam clam clam (misc 70pts)
問題サーバーに接続してみたら、ばーとclamclamみたいな文字列が出てきて、しかもflag形式で出現したから、恐らく大量のclamの中に混じっているじゃないかと推測してとにかく大量に受信してctrl-fで探したら、ヒットした
clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam} malc{malc_malc_malc_malc_malc} clam{clam_clam_clam_clam_clam}
# coding: utf-8 from pwn import * io = remote('misc.2020.chall.actf.co',20204) io.sendline('clamclam') print io.recv(1000000) print io.recv(1000000) print io.recv(1000000) print io.recv(1000000) print io.recv(1000000) print io.recv(1000000) print io.recv(1000000)
Shifter (misc 160pts)
問題サーバーにつないでみたらどうやらフィボナッチ数列の1-50番目の値分シーザー暗号に打ち込めばOKでしたので、先にフィボナッチの50番目に計算しといて、シーザー暗号で復号化した。
Solve 50 of these epic problems in a row to prove you are a master crypto man like Aplet123! You'll be given a number n and also a plaintext p. Caesar shift `p` with the nth Fibonacci number. n < 50, p is completely uppercase and alphabetic, len(p) < 50 You have 60 seconds! -------------------- Shift IVSKGTCJETZVRWFRHMNSTPQYHMBXZXHKHI by n=16 :
#!/usr/bin/env python # -*- coding: utf-8 -*- import sympy as sym #sympyのインポート from pwn import * from m1z0r3 import * def Fib(n): x = sym.symbols('x',nonnegative=True,integer=True) #変数xの宣言(非負の整数) Fib=1/sym.sqrt(5)*(((1+sym.sqrt(5))/2)**(n-1)-((1-sym.sqrt(5))/2)**(n-1)) #一般項の式 result=Fib.subs(x,n) #xにnを代入 result=result.evalf() #式を浮動小数点数として評価 return int(result) Fiblist=[] for n in range(1,51): Fiblist+=[Fib(n)] def rotx(x,s): r = '' for i in s: if ord('z') >= ord(i) and ord(i) >= ord('a'): r += chr((ord(i)-ord('a')+x)%26+ord('a')) elif ord('Z') >= ord(i) and ord(i) >= ord('A'): r += chr((ord(i)-ord('A')+x)%26+ord('A')) else: r += i return r io = remote('misc.2020.chall.actf.co',20300) print io.recvuntil('--------------------') for i in range(50): io.recvuntil('Shift') task = io.recvuntil('\n') print task n = int(task.split('=')[-1]) ct = task.split(' ')[1] pt = rotx(Fiblist[n],ct) print pt io.recvuntil(':') io.sendline(pt) io.interactive()
msd (misc 140pts)
LSBのステガノグラフィー方法のMSD(digit)バージョン。
まず問題スクリプトを見てみると、単純に各RGB値の最初の桁にflagのASCII配列をすり替えただけに見えるが、実際1->0
の桁落ちや百の位で1/2->3/4/5...
に発生するオーバーも考慮しなきゃいけない、だから元の画像が配布されたと思う。解決策としては、エンコードされた画像と元の画像を比較しながら、桁の違いによってそれぞれ対処する感じ。
具体的に:
1. 1,2桁のときかつ桁数一致->最大の位を抽出
2. 3桁かつ元の画像RGBの値は255じゃない->最大の位を抽出、255->とりあえずx
で埋める
3. ほかのときは桁落ちなので0で埋める
そしてその出力からactf{
のASCII配列9799116102123
を検索すればflagがでるはず
from PIL import Image im = Image.open('output.png') im2 = Image.open('breathe.jpg') width, height = im.size def encode(i, d): i = list(str(i)) i[0] = d return int(''.join(i)) msb = [] for j in range(height): for i in range(width): for idx,a in enumerate(im.getpixel((i,j))): _n = len(str(a)) b = im2.getpixel((i,j))[idx] _n2 = len(str(b)) if _n == 2 and _n == _n2: msb.append(str(a)[0]) elif _n == 3 and a == 255: if im2.getpixel((i,j))[idx] == 255: msb.append(str(a)[0]) else: msb.append('x') elif _n == 1 and _n == _n2: msb.append(str(a)[0]) else: msb.append('0') print(''.join(msb))
crypto
Keysar(crypto 40pts)
どうやらkey ANGSTROMCTF
を使ってflagを暗号化した->agqr{yue_stdcgciup_padas}
key付きのシーザー暗号自分聞いたことないので、key caesarで検索したらそれのonline toolが出てきて打ち込んだら一発に出た
Reasonably Strong Algorithm (crypto 70pts)
n = 126390312099294739294606157407778835887 e = 65537 c = 13612260682947644362892911986815626931
問題分見たらnが小さくて、素因数分解できると思ってfactordb使ってp,qを吐かせて、そのまま復号化のスクリプトを書いた。
from Crypto.Util.number import * p = 9336949138571181619 q = 13536574980062068373 phi = (p-1) * (q-1) d = inverse(65537,phi) ct = 13612260682947644362892911986815626931 n = 126390312099294739294606157407778835887 print long_to_bytes(pow(ct,d,n))
Wacko Images (crypto 90pts)
問題スクリプトとその暗号化された画像が配布された。
見てみると
画像[index](RGB値) * key[index] % 251
のような簡易な暗号化でしたので、それの逆算スクリプトを書いて実行したらflagの画像が出た
key = [41, 37, 23] for x in range (0, a): for y in range (0, b): pixel = img[x, y] for i in range(0,3): pixel[i] = pixel[i] * key[i] % 251 img[x][y] = pixel
from Crypto.Util.number import * from PIL import Image from numpy import * ct = Image.open('enc.png') img = array(ct) key = [41,37,23] n = 251 a,b,c = img.shape for x in range(0,a): for y in range(0,b): pixel = img[x,y] for i in range(0,3): pixel[i] = pixel[i] * inverse(key[i],n) % n img[x][y] = pixel pt = Image.fromarray(img) pt.save('flag.png')
Confused Streaming (crypto 100pts)
急に思い出せなくなったのでちゃんと合ってるのか自信がないです...まず問題文を見てみる。注目するのはkeystreamという関数、keyは他の処理で二次元関数の解になる。最後のyield int((ret//1)%2)
によって0か1が出力する感じでした。もしここでretは小数だったら全部0になるはず。そこでkeyをできるだけ小さくしてe,dをどう変換してもretを小数になれるkeyを探してサーバーに送ればflag ^ ¥x00*len(flag)
のような2進数が降って来ると思う。
ちなみに、自分はa=1 b=4 a=-2
にしたはず...
def keystream(key): random.seed(int(os.environ["seed"])) e = random.randint(100,1000) while 1: d = random.randint(1,100) ret = Decimal('0.'+str(key ** e).split('.')[-1]) for i in range(d): ret*=2 yield int((ret//1)%2) e+=1
one time bad (crypto 100pts)
問題文を見てみると、randomによってp,s,xが生成され、xだけが見せられるような挙動でした。ゴールとしては正しいpを送ることですが、sがわからないとうまくできない。
そこでrandom.seed(int(t))
というrandomの初期設定が接続たびに行われて、t
もその時のunixtimeであることも判明。そこで接続する時のtimeを記録すれば、random
をサーバーと同じ初期設定ができることによって同じp,s,xが手元に生成することが可能になった。そのp
を送ればflagがでると思う、もしできなかったら、unixtimeが多少早かったかもしれないので、微調整すればOKのはず
def genSample(): p = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(random.randint(1, 30))]) k = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(len(p))]) x = otp(p, k) return x, p, k # 省略 t = time.time() random.seed(int(t))
import random, time import string import base64 import os from pwn import * def otp(a, b): r = "" for i, j in zip(a, b): r += chr(ord(i) ^ ord(j)) return r def genSample(): p = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(random.randint(1, 30))]) k = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(len(p))]) x = otp(p, k) return x, p, k #base_time = int(time.time())-1000000 #base_time = 1584147600 now_time = int(time.time())-10 io = remote('misc.2020.chall.actf.co',20301) #io = remote('localhost',10001) print(io.recvuntil('> ')) io.sendline('2') recv_x = io.recvline().strip() print(recv_x) io.recvuntil('answer: ') # for t in range(base_time,now_time): while True: random.seed(now_time) x,p,k = genSample() x_pre = base64.b64encode(x.encode()) print(x_pre) if x_pre == recv_x: print('bingo') print(p) io.sendline(p) io.interactive() break else: pass #print 'wrong' # io.sendline(str(10)) # ans = io.recvline() #print base_time now_time += 1
RSA-OTP (crypto 210pts)
他のメンバーのアイディアで協力して解けた問題、ここでは自分の理解のもとに書きます、あってなかったらごめんなさい。
まず問題をみてみると、以下の内容がわかる
1. enc(flag)が降ってくる
2. 暗号化関数は普通のRSA
3. decryptがやってくれる
4. ↑の結果はgetrandbits(1)に邪魔される
そこでRSAのLSBが容易に思いつくが、getrandbits(1)によって使えないですが、decrypt(input)
のbit数は把握できる。そこで以下の試みをしました(と思う):
1. flagを左シフトできるようにpow(2,e,n)
をどんどんかけていく
python
for i in range(464):
inp = pow(2,x*e,n)*enc(flag)
io.sendline(inp)
当たり前のようにxの増加につれてエられた2進数のbit数は増えます
- 仮にx1=460,x2=461にして、その中間値をかけて送ったらどうなるか
python 1020 1019 1020 ...
1020と1019の間に徘徊することがわかりました、ですので
(flag * a % n).bit_length() => [1019,1020]
上の式のa部分は二分探索によってある程度絞られる flagは近似的にlong_to_bytes(pow(2,1019,n)/a)
に等しいといえる よってaを二分探索するようなスクリプトを書いてみた
from pwn import * from Crypto.Util.number import * from fractions import Fraction n = 136018504103450744973226909842302068548152091075992057924542109508619184755376768234431340139221594830546350990111376831021784447802637892581966979028826938086172778174904402131356050027973054268478615792292786398076726225353285978936466029682788745325588134172850614459269636474769858467022326624710771957129 e = 0x10001 # Encrypted flag: ct = 17482644844951175640843255713372869422739097498066773957636359990466096121278949693816080016671592558403643716793132479255285512907247513385850323834210899918531077167485767118313722022095603863840851451191536627814100144146010392752308431038754246815068245448456643024387011488032896209253644172833489422733 io = remote('crypto.2020.chall.actf.co',20600) # left = pow(2,460,n) # right = pow(2,461,n) # twomod = 1 bounds = [Fraction(pow(2,463,n)),Fraction(pow(2,464,n))] while True: # middle = (left + right)//2 middle = sum(bounds)/2 middle = middle.numerator / middle.denominator # print long_to_bytes(pow(2, 1019, n) / (middle)) print long_to_bytes(pow(2, 1022, n) / (middle)) print middle if middle == 0: print middle break io.recvuntil('Enter message to sign: ') io.sendline(str(ct * pow(middle,e,n) % n )) io.recvline() revflag = io.recvline().strip() print len(revflag) # if len(revflag) == 1020: if len(revflag) == 1023: bounds[1] = sum(bounds)/2 #right = middle else: bounds[0] = sum(bounds)/2 #left = middle
これによってflagの58文字分はわかるようになった
actf{this_is_not_what_i_meant_when_i_told_you_to_use_rsa_w
残りの12文字はメンバーのプロパワーによってguessingできた
最後flagはactf{this_is_not_what_i_meant_when_i_told_you_to_use_rsa_with_padding}
もっと良い方法があればぜひ教えてくれたら幸いです。
reversing
普段あんまりreversingやらないので手探り状態で簡単な何問しか解けなかった
Revving up (rev 50pts)
とりあえず実行して、支持に従ってgive flag
を入力したら、「Now run the program with a command line argument of "banana" and you'll be done!」ということで、./revving up banana
で実行し、give flag
入力したらflagが出た。
root@ae548dfbc2cd:/CTF/2020/actf/rev# ./revving_up Congratulations on running the binary! Now there are a few more things to tend to. Please type "give flag" (without the quotes). give flag Good job! Now run the program with a command line argument of "banana" and you'll be done! root@ae548dfbc2cd:/CTF/2020/actf/rev# ./revving_up banana Congratulations on running the binary! Now there are a few more things to tend to. Please type "give flag" (without the quotes). give flag Good job! Well I think it's about time you got the flag!
Windos Opportunity (rev 50pts)
IDAとかで開いたらflagはそこに書いてあった...
Taking Off (rev 70pts)
ghidraデコンパイルしたら、mainは↓のようになった
undefined8 main(int iParm1,long lParm2) { int iVar1; undefined8 uVar2; size_t sVar3; long in_FS_OFFSET; uint local_b4; uint local_b0; uint local_ac; int local_a8; int local_a4; char *local_a0; byte local_98 [136]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("So you figured out how to provide input and command line arguments."); puts("But can you figure out what input to provide?"); if (iParm1 == 5) { string_to_int(*(undefined8 *)(lParm2 + 8),&local_b4,&local_b4); string_to_int(*(undefined8 *)(lParm2 + 0x10),&local_b0,&local_b0); string_to_int(*(undefined8 *)(lParm2 + 0x18),&local_ac,&local_ac); iVar1 = is_invalid((ulong)local_b4); if (iVar1 == 0) { iVar1 = is_invalid((ulong)local_b0); if (iVar1 == 0) { iVar1 = is_invalid((ulong)local_ac); if ((iVar1 == 0) && (local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4)) { iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken"); if (iVar1 == 0) { puts("Well, you found the arguments, but what\'s the password?"); fgets((char *)local_98,0x80,stdin); local_a0 = strchr((char *)local_98,10); if (local_a0 != (char *)0x0) { *local_a0 = 0; } sVar3 = strlen((char *)local_98); local_a4 = (int)sVar3; local_a8 = 0; while (local_a8 <= local_a4) { if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8]) { puts("I\'m sure it\'s just a typo. Try again."); uVar2 = 1; goto LAB_00400bc7; } local_a8 = local_a8 + 1; } puts("Good job! You\'re ready to move on to bigger and badder rev!"); print_flag(); uVar2 = 0; goto LAB_00400bc7; } } } } puts("Don\'t try to guess the arguments, it won\'t work."); uVar2 = 1; } else { puts("Make sure you have the correct amount of command line arguments!"); uVar2 = 1; } LAB_00400bc7: if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return uVar2; }
そこからわかった事としては
1. iParm1 == 5
=> 引数は4 ./taking_off a1 a2 a3 a4
2. local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4
↓
a1*10+a2*100+a3 == 932
↓
./taking_off 3 9 2 a4
3. iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken")
=> a4 = "chicken"
4. if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8])
さらなるpasswordが求められ、passwordの文字のそれぞれ0x20
とxorとった結果をdesired
と比較してるみたい
最後はdesiredに保存される5a464f4b594f0a4d435c4f0a4c464b4d2a
を↑の方法でxorを取るとpasswordはgive me flag\x00
と判明最後向こうのサーバーで実行したらflagを取れた
# coding: utf-8 from pwn import * from Crypto.Util.number import * des = 0x5a464f4b594f0a4d435c4f0a4c464b4d2a des = long_to_bytes(des) print xor(des,chr(0x2a)*len(des))
Autorev,Asseble! (rev 125pts)
ghidraデコンパイルしたらすごい分岐の多いスクリプトが出てきた、自力で読める自信がなく、一か八かで先日インストールしたangrでやってみた。
いくつのチュートリアルを参考して、とりあえず基本のスクリプトを書いた。そこでfind
は正解した時表示する関数のアドレスで、avoid
は間違えた時表示する関数のアドレスになる
import angr project = angr.Project('./autorev_assemble',auto_load_libs=False) state = project.factory.entry_state() simgr = project.factory.simulation_manager(state) simgr.explore(find=0x0408953,avoid=0x0408961) state = simgr.found[0] print(state.posix.dumps(0))
↑のスクリプトを回して、flagそのまま出た
Patcherman (rev 100pts)
if ((_DAT_00201054 != 0xffffffff) && (_DAT_00201050 == 0x1337beef))
この分岐判断の結果によってプログラムがフリーズするかどうかがわかる。
後に_DAT_00201054
はptrace(0,1337,4919,1337)
の返り値だと判明し成功したら0
エラーしたら-1
を返す
_DAT_00201050
は既存のやつで0x1337beef
と違う値が入ってる
そこで↑の条件全部を仮に成立させて、次のスクリプトをそのままコピペして実行したらflagが出た
(solverは汚くてごめん...)
#include <stdio.h> int dword_601050 = 322420463; int dword_6010AC = 322420463; int dword_601054 = 0; int dword_601060 [] = {0x5cd883ea,0xef6680c0,0xafcc7086,0x7e3ab87f,0x7dc5773a,0x38208035,0x0456c3f5}; int sub_4006FF(int a1, int a2) { return a1 + a2; } int sub_400672() { int v0; // ebx int v1; // ebx int v2; // ebx int v3; // eax v0 = sub_400657(dword_6010AC, 0); v1 = (unsigned int)sub_400657(dword_6010AC, 2) ^ v0; v2 = (unsigned int)sub_400657(dword_6010AC, 3) ^ v1; v3 = sub_400657(dword_6010AC, 5); dword_6010AC = (unsigned int)dword_6010AC >> 1; dword_6010AC |= (v2 ^ v3) << 31; return (unsigned int)dword_6010AC; } int sub_400657(int a1, int a2) { return (a1 >> a2) & 1; } int main(){ int dword_601090 [7]; for ( int i = 0; i <= 6; ++i ) { int v3 = sub_400672(); dword_601090[i] = sub_4006FF(dword_601060[i] ^ (unsigned int)dword_601054, v3); } printf("Here have a flag:\n%s\n", dword_601090); }
Califrobnication (rev 120pts)
ディレクトリにあるflag.txt
を↓にあるスクリプトを経てエンコードされた文字列が表示される
memfrob(local_48,__n); strfry(local_48); printf("Here\'s your encrypted flag: %s\n",local_48);
memfrob
はただ文字ごと42とxorを取るだけそんな気にしないstrfry
はshuffle関数で配列を撹乱する たが、他のメンバーによるとstrfryのrandomstateという初期設定は時間とpid依存であることが判明
そこでホームディレクトリに以下のpythonスクリプトを置き、問題ディレクトリに入り、pythonスクリプトを実行して、暗号文を獲得する同時にtimeとpidを入手する
import time import subprocess from subprocess import Popen import os import signal cmd = "/problems/2020/califrobnication/./califrobnication" proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) print( "process id = %s" % proc.pid ) out = proc.stdout.readline().strip() with open('/home/team5469/out-hex.txt','w') as f: f.write(out.encode('hex')) print(time.time())
そこから配布されたc言語ファイルを基づいて、strfryを書き換え、pidとtimeをすり替え,flagのところにstring.printable[:48]
を変えて実行
#include <string.h> #include <stdlib.h> #include <stdio.h> #include <time.h> #include <unistd.h> // typedef struct random_data { // int *fptr; // int *rptr; // int *state; // int rand_type; // int rand_deg; // int rand_sep; // int *end_ptr; // } RANDOMD; // time -= 1 char * strfry_mod (char *string) { static int init; static struct random_data rdata; int t = 1584547877; if (!init) { static char state[32]; rdata.state = NULL; initstate_r (t ^ 7269, state, sizeof (state), &rdata); init = 1; } size_t len = strlen (string); if (len > 0) for (size_t i = 0; i < len - 1; ++i) { int32_t j; random_r (&rdata, &j); j = j % (len - i) + i; char c = string[i]; string[i] = string[j]; string[j] = c; } return string; } int main() { FILE *f; char flag[50]; f = fopen("flag.txt", "r"); fread(flag, 50, 1, f); strtok(flag, "\n"); //memfrob(&flag, strlen(flag)); strfry_mod(&flag); printf("Here's your encrypted flag: %s\n", &flag); }
これでprintable[:48]
をどのようにshuffleされるのがわかるようになって、このパターンを基づいて組み直したらflagが出た
# coding: utf-8 from string import printable ct = '48657265277320796f757220656e6372797074656420666c61673a2043474543574b75754f18124e4e4c1e4c4c484b4b5845441b4e465e44494b1f131e1f511b1a494c755e49451c58434b49'.decode('hex') ct = ct.split(' ')[-1] t = '' for i in range(0, len(ct)): t += chr(ord(ct[i]) ^ 42) flag = [0 for _ in range(48)] a = printable[:48] pattern = 'n9bhL0du7IBH5iKw3l8Gjstvyg2mefJDzA4ECocaqFkx6rp1' for x,y in zip(pattern,t): idx = a.index(x) flag[idx] = y print ''.join(flag)
感想
5日開催マジで長すぎて、結構疲れた...また色々他ジャンルの問題に挑戦できてすごくいい経験になった