seccon 4 beginner 2021参加記
ここ数年、secconに出よう出ようと思いつつ、思い出したころにHPを見ると毎回20XX年度の活動は終了していましたが、
今回ついにチームupperVillageで参加しました。
1949ptで71位でした。
4 beginnerといいつつなかなか難しくて、面白い問題が多かったです。
作問者によるwrite upやきれいなwrite upは色々出回っていますので、
この記事ではまともなwriteupというよりも、自分が試行錯誤した内容・感想を書こうと思います。(適当ですみません...)
welcome
welcome
disccordに入って、チャンネルを見に行く。
crypto
simple_RSA
下記inputからcを複合する。
n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283 e = 3 c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613
eが小さいので3乗根を取る。
Logical_SEESAW
下記のproblem.pyコードで、flagとkeyを使って暗号化する。
flag = list(bin(flag)[2:]) key = list(bin(key)[2:]) cipher_L = [] for _ in range(16): cipher = flag[:] m = 0.5 for i in range(length): n = random() if n > m: cipher[i] = str(eval(cipher[i] + "&" + key[i])) cipher_L.append("".join(cipher))
flagとkeyのbitwise andを取っているため、
flagのi bit目が0なら暗号化後も0
flagのi bit目が1なら0と1が混ざる(各bitにつき16個の試行結果が渡されるので)
ことから、暗号化後の各i bit目に一つでも1があれば1,なければ0として文字列にすると解ける。
reversing
only_read
バイナリが渡される。 ghidraでデコンパイルする。
void main(void) { ssize_t sVar1; long in_FS_OFFSET; undefined8 local_28; undefined8 local_20; undefined4 local_18; undefined2 local_14; char local_12; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_28 = 0; local_20 = 0; local_18 = 0; local_14 = 0; local_12 = '\0'; sVar1 = read(0,&local_28,0x17); *(undefined *)((long)&local_28 + sVar1) = 0; if (((((((char)local_28 == 'c') && (local_28._1_1_ == 't')) && (local_28._2_1_ == 'f')) && (((local_28._3_1_ == '4' && (local_28._4_1_ == 'b')) && ((local_28._5_1_ == '{' && ((local_28._6_1_ == 'c' && (local_28._7_1_ == '0')))))))) && (((char)local_20 == 'n' && ((((((local_20._1_1_ == '5' && (local_20._2_1_ == 't')) && (local_20._3_1_ == '4')) && ((local_20._4_1_ == 'n' && (local_20._5_1_ == 't')))) && ((local_20._6_1_ == '_' && ((local_20._7_1_ == 'f' && ((char)local_18 == '0')))))) && (local_18._1_1_ == 'l')))))) && ((((local_18._2_1_ == 'd' && (local_18._3_1_ == '1')) && ((char)local_14 == 'n')) && ((local_14._1_1_ == 'g' && (local_12 == '}')))))) { puts("Correct"); } else { puts("Incorrect"); } if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
children
ファイルを実行すると、fork execしてコプロセスが作られるので、psしながら質問に答える。
psの結果と最後の子プロセス数が合わず5回くらいrejectされたため、psの結果じゃなくて、30%の確率で2個子プロセスが作られることから通るまで13(10+10*0.3)で試した。
please_not_trace_me
バイナリが渡されて、実行中にflagの文字列が作られている。ghidraではよくわからなかったため、gdb-pedaでアタッチすると、タイトル通りの問題となる。
実行中に2回ptrace(PTRACE_TRACEME,0,1,0)が呼ばれていて、正常時は初回0 2回目-1となる。
アタッチされていると初回 2回目ともに-1となる。
そのため初回実行後にset $RAX=0とすると、レジスタにflag文字列が見えた。
と書いているが、初回も2回目もアタッチされていなければ0だと思い込んでいて、かなり時間かかった。
わざわざLD_PRELOADで毎回return 0するコードを書いてもplease_not_trace_meと言ってきたので気づいた。
firmware
firmware.binというdata ファイルが与えられる。
binwalkをするとarm elfが埋まっているので、それを取り出して実行すると、pwが求められる。
わざわざラズパイで実行したが、ghidraで十分。
memcpy(auStack4520,&DAT_00010ea4,0xf4);←ここ sVar2 = strlen((char *)abStack4116); if (sVar2 != 0x3d) { local_10b4 = 0x6f636e49; uStack4272 = 0x63657272; uStack4268 = 0x61702074; uStack4264 = 0x6f777373; local_10a4 = 0xa2e6472; local_10a0 = 0; sVar2 = strlen((char *)&local_10b4); send(local_11d8,&local_10b4,sVar2,0); close(local_11d8); } local_11e0 = 0; while (local_11e0 < 0x3d) { if ((uint)(abStack4116[local_11e0] ^ 0x53) != auStack4520[local_11e0]) {←ここ local_10b4 = 0x6f636e49; uStack4272 = 0x63657272; uStack4268 = 0x61702074; uStack4264 = 0x6f777373; local_10a4 = 0xa2e6472; local_10a0 = 0; sVar2 = strlen((char *)&local_10b4); send(local_11d8,&local_10b4,sVar2,0); close(local_11d8); } local_11e0 = local_11e0 + 1; }
←ここ と書いてあるところが答えにつながる。
&DAT_00010ea4を見に行って、0x53とxorを取るとflagになる。
local_10b4 = 0x6f636e49;この辺の文字列をascii変換すると、incorrect, correctみたいなワードになる。(4 beginnerなので書いとく)
終了5分前に解けて、文字列を慌てて書き写していたため手が震えていた・・・。
pwnable
rewriter
実行するとreturnアドレスを教えてくれる。そこにwin()のアドレスを書き込めばよい。
pwnがほぼできないので過去問を見た。
SECCON Beginners CTF 2020 Beginner's Stack Writeup - Qiita
web
osoba
貰ったファイルを見るとpublic/../../flagにあるので、urlに打ち込むと行ける。
Werewolf
postで送られたデータに対して、
for k, v in request.form.items(): player.__dict__[k] = v
で
class Player: def __init__(self): self.name = None self.color = None self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN']) # :-) # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF']) @property def role(self): return self.__role # :-) # @role.setter # def role(self, role): # self.__role = role
のプロパティを設定している。
roleはgetterがあって、setterはコメントアウトされてるので無理なのかなと思っていたが、
Online PHP/Java/C++... editor and compiler | paiza.IO
で見るとわかるが、
_Player__role
をキーにすれば設定できた。
check_url
getリクエストでurlを渡すとそこにcurlした結果を表示してくれる。
アクセス元ipが127.0.0.1だとflagが表示されるので、自サーバにcurlできればよい。
ただし、ドットと"localhost"は使えない。
色々調べるとドットを使わないipアドレス表示方法があることがわかった。
SSRF payloads. Payloads with localhost | by Pravinrp | Medium
ただし、
http://0177.0.0.1/ http://2130706433/ = http://127.0.0.1 http://3232235521/ = http://192.168.0.1 http://3232235777/ = http://192.168.1.1
この辺の方法だと通らず、
0x7f000001
だと行ける。
solved数高いのに10進数できないで詰まっていてかなり焦っていた。
json
chromeのプラグインを使って、192.168.111.1をx-fowarded-forに設定した。
すると仲が見れて、idを設定するプルダウンがあるが、flagをゲットする2を設定すると弾かれてしまう。
apiの方をみるとIDとして取っていたので{"ID":2,"id":1}というjsonを送るとflagが取れた。
実は大文字小文字ではなくて、idが2つあるときに、最前と最後のどちらをとるかがライブラリによって異なるぽい。
cant_use_db
だんだん適当になってきたが、ファイル読み書きが排他ではないので、同時に書けば行けるだろうと思いつつも、コードを書くのがめんどくさかったので、
急いでクリックすると行けた。
@app.route("/buy_noodles", methods=["POST"]) def buy_noodles(): user_id = session.get("user") if not user_id: return redirect("/") balance, noodles, soup = get_userdata(user_id) if balance >= 10000: noodles += 1 open(f"./users/{user_id}/noodles.txt", "w").write(str(noodles)) time.sleep(random.uniform(-0.2, 0.2) + 1.0) balance -= 10000 open(f"./users/{user_id}/balance.txt", "w").write(str(balance)) return "💸$10000" return "ERROR: INSUFFICIENT FUNDS"
他人のwrite upを読んでいて、sleepがあることに気づいた。
misc
git-leak
diff commitid commitid
をして頑張ると出てくる。