# 1日 1rev問のwriteup ## 2022/3 ### 3/13 : picoCTF 2021 Shop - 50点問題 - goで書かれた実行ファイル - 解析でmain.mainを見ると最後にmain.mainがもう一度呼ばれてることが分かる - だからflagを得るまでは一生動く - 適当に買ってみると、お金が減る - ここで持金が負になってもループが続くことに気づく - ならマイナスの判定がなさそう - マイナス個買ったら持金が増えた - flagが変える値段までやる flag : picoCTF{b4d_brogrammer_591a895a} ### 3/14 : picoCTF 2021 gogo - 110点 - Writeupを見てミスに気づく - goで書かれた実行ファイル - main.mainにて、passwordを受け取りmain.checkPasswordに渡す この関数には8つの値があった ``` local_40[0] = 0x38313638; local_40[1] = 0x31663633; local_40[2] = 0x64336533; local_40[3] = 0x64373236; local_40[4] = 0x37336166; local_40[5] = 0x62646235; local_40[6] = 0x39383338; local_40[7] = 0x65343132; ``` 直前にはこんな文が ``` if ((int)param_2 < 0x20 ``` 全部書くのはめんどくさいので軽く書くと、param2はparam1とlocal_40をxorしたものと一致するかを見るもの だから、passは0x20 = 32文字であることは分かる。ただ、local_40が圧倒的に少ない。 よく見ると、2byteずつ分割すれば4 * 8 = 32になるのでこれで試してみる。 ここがつまづきポイント。**リトルエンディアン**なので、そのまま分割して使ってはだめ。 0x38, 0x36 ...とけつから取って新しい配列を作らなければならなかった。 次にparam1を探す。 改めて書くとparam1は内蔵されている値で、param2は入力値。param1はmain.statictmp_4に入っていた。 無事xorするとpassを得る。次のステップはlocal_40(key)を復号するというもの。 32bitのハッシュはmd5が思いつく。無事復号できる。 リトルエンディアンの部分でつまずいたものの、他は問題なかった 各bit数をまとめたもの - https://qiita.com/KEINOS/items/c92268386d265042ea16#32-%E6%A1%81%E3%81%AE%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E5%80%A4 flag : picoCTF{p1kap1ka_p1c001b3038b} ### 3/14 : picoMini by redpwn not-crypto - 150点 - 普通のELF 挙動とentryからmain関数となるのはFUN_00101070 中身を見ていると、隠れるようにfreadが存在する ``` fread(local_198,1,0x40,stdin); # 自分の環境だと131行目 ``` その後、いろんなことされてから ``` iVar24 = memcmp(local_88,local_198,0x40) ``` iVar24の結果によってcorrectかincorrectか決まる 中身を読む気は起きなかったので動的解析することにした。 memcmpをするときには比較対象local_88は生成できているので、ここをbreak pointにしたらなんか見れるかな程度でbreak pointを貼るとビンゴ(あと単純に他に貼れるとこがなかった) flag : picoCTF{c0mp1l3r_0pt1m1z4t10n_15_pur3_w1z4rdry_but_n0_pr0bl3m?} ### 3/15 : picoCTF 2021 easy as GDB - 160 - 普通のELF - 実行すると入力を求める形式 Ghidraで見る。最終的には内蔵データと比較する形式みたい - angrを試す -> コードがだめなのか、駄目だった - gdbを試す -> 何をどうするのかわからん。break point貼って眺めてても内蔵データしか見えん 静的解析をすることに。関数名、変数名を変えることでかなり内容を理解することができた。 そこで、逆の変換を書いたんだけどなんかうまいこと行かない <- forの処理がなんかおかしそう - writeupを見たけど、ちょっとよく分からない。IDAとGhidraでそんなに結果が変わるものなのか gdbでやる場合はファイル名がbruteの通り、総当りをしてチェックが通っているときを保存しつつflagを完成させていく なるほど。 break pointのアドレスは、計算する。gdb上とghidra上での__libc_mainの差に足す まとめ - 静的解析 : 流れは読めたけど、なんかうまく戻せない <- コード拝借して一旦解決 - 動的解析 : 理解 flag : picoCTF{I_5D3_A11DA7_6aa8dd3b} ### 3/22 : picoCTF 2021 powershelly - 180点 - powershellスクリプトとoutput.txtが渡される 〜内容〜 - 上の変数を眺める - 5✕6のブロックが264セットあることが分かる - tは多分、ブロック間の空白 - numLengthがブロックと空白の和 - 始めらへんの条件分岐は無視していい - Random-Gen - この関数はforのiによってしか決まらない - Scramble - この関数でブロックが暗号化?されて、funに流し込まれている - main的なの - funといろいろxor取ったものをoutput.txtに流し込む - seedsもRandom-Genと一緒でforのiによって決まる - 元に戻していく ``` # blockは264個 random = [(((i * 327) % 681 ) + 344) % 313 for i in range(1, 265)] seed = [(i * 127) % 500 for i in range(1,265)] with open("output.txt", "r") as f: output_dat = list(map(int, f.read().splitlines())) # print(output_dat) fun = [] result = 0 for i in range(len(output_dat)): ans = output_dat[i] ^ result ^ random[i] result = ans ^ result ^ random[i] fun.append(bin(ans)[2:]) # print(fun) blocks = [""] * 5 for i in range(len(fun)): tmp = fun[i] raw = [] block = [] for j in range(0, len(tmp), 2): if tmp[j:j+2] == "11": raw.append("1") else: raw.append("0") look = [False] * len(raw) for j in range(len(raw)): y = (j * seed[i]) % len(raw) while look[y] == True: y = (y+1) % len(raw) look[y] = True block.append(raw[y]) for j in range(30): blocks[j // 6] += block[j] for j in range(5): blocks[j] += " " for i in range(0, len(blocks[0]), 7): if blocks[0][i:i+7] == "100001 ": print(0, end = "") else: print(1, end = "") print() ``` - 最後のfor文はblocksの結果を見た時に0と1ぽいのが見えたので後から付け加えた。 - 最終的に2進数をlong_to_bytesを用いて復号 flag : picoCTF{2018highw@y_2_pow3r$hel!} ### 3/23 : picoMini by redpwn breadth - 200 - elfファイルが2つ配られる 〜内容〜 - 実行結果が一緒だから差分を見ればよさそうだなと思う(静的解析の結果も踏まえて) - 違うところを探す -> xorして0じゃないところは違うとこ - 上記を行うコードを書いた - 異なるバイトを文字に変えてみるけど違う - ターミナル上で差分を見るコマンドを調べてみた - diff : これはテキストを見てしまうぽくて結果がイマイチ - cmp : ビンゴ - https://atmarkit.itmedia.co.jp/ait/articles/1704/27/news027.html - オプションから違いが発生しているのは725〜744と610380〜610396であることが分かる それぞれをhexに直して、ghidraの0x100000に足して見てみると後者の値では特定の関数にたどり着いた。 breadth.v2ではputしているが、v1ではしていない。これが答え flag : picoCTF{VnDB2LUf1VFJkdfDJtdYtFlMexPxXS6X} ### 3/24 : picoCTF 2019 reverse_cipher - 300(点数に見合った問題じゃ無い気がする) - ghidraで見て終わり encされたflagが渡されるので、元に戻す。 ``` enc_flag = "picoCTF{w1{1wq8/7376j.:}" dec_flag = "" # flow # for i in range(8): # enc_flag += flag[i] # for i in range(8, 23): # if i & 1 == 0: # enc_flag += chr((ord(flag[i]) + 5)) # else: # enc_flag += chr((ord(flag[i]) - 2)) for i in range(8): dec_flag += enc_flag[i] for i in range(8, 23): if i & 1 == 0: dec_flag += chr((ord(enc_flag[i]) - 5)) else: dec_flag += chr((ord(enc_flag[i]) + 2)) print(dec_flag) ``` flag : picoCTF{r3v3rs312528e05} ### 3/25 : picoCTF 2020 OTP implementation - 300(これも点数に見合って無い気がする) - ghidraで見るだけ - 複数の関数から構成されている - 入力をチェックする関数 - 条件分岐からa <= 入力 <= f、0 <= 入力 <= 9であることが分かる - jumble - 一度ここに入力を渡して変化させる - main関数 - jumbleの結果とか使ってまた変化させる - 最終的に内蔵データと比較 - 比較結果に応じて出力が変わる また、内蔵データと一致したとき入力データとflag.txtをxorすればいいらしい。 ``` def jumble(s): if ord("`") < ord(s): s = chr(ord(s) + ord("\t")) bVar1 = ord(s) >> 11 s = chr(((ord(s) + bVar1 & 0xf) - bVar1) * 2) if 0x0f < ord(s): s = chr(ord(s) + 0x01) return ord(s) inp = "" enc_inp = [] pat = "0123456789abcdef" for i in range(len(match)): for p in pat: cVar1 = jumble(p) if i == 0: bVar2 = cVar1 >> 11 enc = (cVar1 + bVar2 & 0xf) - bVar2 else: bVar2 = cVar1 + ord(enc_inp[i-1]) >> 0x37 enc = (cVar1 + ord(enc_inp[i-1]) + (bVar2 >> 4) & 0xf) - (bVar2 >> 4) if chr((enc + ord("a"))) == match[i]: inp += p enc_inp.append( chr(enc) ) break print(inp) ``` inp = c4a2db52092bc52e6ca24db26f9467cd43d0c636792cefb7639cd085a3768bee7549423e82b35e956abbc9246b2ffc3537ce 始め、これを文字列と見てflagとxorしてたけどなんかうまくいかなかった。 よく考えるとfまでだから16進数かなと思い、intでxorしてlong_to_bytes flag : picoCTF{cust0m_jumbl3s_4r3nt_4_g0Od_1d3A_50869043} ### 3/26 : picoCTF 2021 Rolling My Own - 300 - 自分のレベルを分からされる問題だった - writeup見た 〜自分でできたパート〜 - 多分入力は4✕4 = 16 - 最初に通るfor文にて次の処理を行い、md5に渡す入力を作る - 入力を4個づつに分割してmd5_inputに追加 - saltを追加 - このsaltは前回のように、リトルエンディアンであることに注意すると次のようなデータに **GpLaMjEW pVOjnnmk RGiledp6 Mvcezxls** - このようにして出来た入力の長さは4✕4 + 8✕4 = 48 ↑ 一旦mainの処理終わり、次はちょっと変わったmd5 - まずは、入力の長さが12で割れるか確認 -> 割れますよと - MD5_Init・MD5_Update・MD5_Finalとあるが、多分普通のmd5と見てよさそう? - 前処理みたいな関数で結局は32bitのmd5が出力されるだけかなと思った - 正直、こっからちょっとよく分からなくなった - まず、for文を回してる理由が分からん - この関数の結果を入れるリストの大きさが64だったのが分からん - md5したあとの結果次第でflag.txtが呼び出されるんだけどこれも分からん 〜Writeup見て、確かにとなったパート〜 - for文は48を12で割った値なんだけど、結局この後で"input+salt"*4を4セットづつに分割してる - こうしてmd5にはinput+saltが渡されて無事32bitのhashが出来上がる - ずっと、0x10が不思議だったけどbyte?で見るから0x10なんだと理解した - だからmain部でも0x40だったけど結局は32*4のhashが全部入るだけ **入力 : (input+salt)✕4 = 48 出力 : (32bitのmd5hash)✕4 = 128** - 使い方が分からなかったから求めていなかったけど、出力のどこから取り出すかを示したものがある - 数字を使ってやってみると出来た配列は **[8, 9, 10, 11, 18, 19, 20, 21, 39, 40, 41, 42, 49, 50, 51, 52]** - この位置から取り出す。また、先程と同様bitで見たいからこの値は2倍する **[16, 18, 20, 22, 36, 38, 40, 42, 78, 80, 82, 84, 98, 100, 102, 104]** - 1セットのhashで使用する値は4つだということが分かった - ここからは確かにというか、そうなんだとなった 〜Writeup見て、そうなんだとなったパート〜 - 先程hashから取り出した値はコードとして実行される -> 確かに、変数の前にcodeとついている。これがそれ表しているんかな? - rdiレジスタにフラグ表示用の関数のアドレスが格納されている -> ここらへんか?このflag.txtを展開する関数の引数にrdi渡してるし **00100e0b 48 8d 3d 19 LEA RDI,[FUN_0010102b]** - これを行うためのshellcode、機械語を16バイトで構成する ``` .intel_syntax noprefix .globl _start _start: mov rsi, rdi mov rdi, 0x7b3dc26f1 call rsi ``` 与えられたら理解はできるけど、作るのはできないです。 これをコンパイルしたときのバイト列が\x48\x89\xFE\x48\xBF\xF1\x26\xDC\xB3\x07\x00\x00\x00\xFF\xD6 これが出来上がる入力を総当りする。 <- このコードは書けます flag : picoCTF{r011ing_y0ur_0wn_crypt0_15_h4rd!_dae85416} これ普通に良問では。いろいろ新しいことがありました。前回の反省とかも踏まえれたし、よかったです。 Writeup : https://ctftime.org/writeup/27055 ### 3/27 : Internetwatch CTF 2016 File_checker - 60 picoがきつくなってきたので違うやつやる(前やっていたこれ↓) https://github.com/N4NU/Reversing-Challenges-List - elfを実行するとFatal error: File does not existとでて終了 - .passwordというファイルがあるかまず見ているよう - ファイルの内容を見る関数がある - fgetc : ファイルから1文字だけを読む。この際、返される値はchar -> intへ - feof : ファイルの内容が終了しているか見る - こうして取り出した値にいろいろする - 最終的にいろいろした値が1より小さければOK - このいろいろは余りを論理和とってみたいな感じなので、余りを0にしないといけない - 内蔵データとかを使うと以下のようなソルバになる ``` local_48 = [ 4846, 4832, 4796, 4849, 4846, 4843, 4850, 4824, 4852, 4847, 4818, 4852, 4844, 4822, 4794 ] for l in local_48: remain = 0x1337 - l assert ((l + remain) % 0x1337) == 0 print(chr(remain), end = "") ``` flag : IW{FILE_CHeCKa} ### 3/28 : Nuit du Hack CTF Quals 2016 Matriochka - Step1 〜 3を解いた - それぞれ、50・100・300 **Step1** - 実行時にpassを書かないとだめ ``` $ ltrace ./step1.bin a ``` Much_secure__So_safe__Wow - passが一致してたらstep2のバイナリが配布されるんだけど、もう持ってるからそれはもういい **Step2** - 形式はStep1と同じ - ltraceでは見れなかった(当たり前) - Ghidraで見る - 文字列をリストで見ていて、いろいろしているので元に戻していく - 確実に分かるやつから逆算的にやっていくだけ Pandi_panda **Step3** - 解けたというかなんか出来た - 関数がそれこそマトリョーシカみたいに、ずっとつながっている - 入力とかに対して割り算を行って、それをもとになんかしてる - 多分、デコンパイルのミスだと思う - この割る値は関数によって異なっていたので、この値をまとめてみた - 文字に変換すると解けちゃった Did_you_like_signals? ### 3/29 : Codegate CTF 2018 Preliminary easy_serial - 350 - 昔のやつだからlibがあってなかったりしてその調整を始めにした - 実行すると入力を求められる - easy: Prelude.!!: index too largeと出る - Ghidraで見る - なんかいつものelfと違う - goだったら、fileコマンドのときに出るけど出てなかったのでgoではない - main関数がどれか分からん - 文字列検索で入力を求めるコメントは見つけた - 最初に書いたコメントみたいなのが、実はエラーメッセージだった - haskellらしい Haskellのデコンパイラ https://github.com/gereeter/hsdecomp 実行すると、デコンパイルデータがテキストで返ってくるのでvscodeで見る - なんか読みづらいけど、変数とか調整して雰囲気で読む - 数字、大文字、小文字の文字列が用意されている - 入力は文字列#文字列#文字列みたいな形式で#で区切ってチェックしていく - I#がintでC#がcharなのはなんとなく分かる -> これで添字とかもやっていそう - あとはやるだけ ``` Main_main_closure = >> $fMonadIO (putStrLn (unpackCString# "Input Serial Key >>> ")) (>>= $fMonadIO getLine (\s1dZ_info_arg_0 -> >> $fMonadIO (putStrLn (++ (unpackCString# "your serial key >>> ") (++ s1b7_info (++ (unpackCString# "_") (++ s1b9_info (++ (unpackCString# "_") s1bb_info)))))) (case && (== $fEqInt (ord (!! s1b7_info loc_[0])) (I# 70)) (&& (== $fEqInt (ord (!! s1b7_info loc_[1])) (I# 108)) (&& (== $fEqInt (ord (!! s1b7_info loc_[2])) (I# 97)) (&& (== $fEqInt (ord (!! s1b7_info loc_[3])) (I# 103)) (&& (== $fEqInt (ord (!! s1b7_info loc_[4])) (I# 123)) (&& (== $fEqInt (ord (!! s1b7_info loc_[5])) (I# 83)) (&& (== $fEqInt (ord (!! s1b7_info loc_[6])) (I# 48)) (&& (== $fEqInt (ord (!! s1b7_info loc_[7])) (I# 109)) (&& (== $fEqInt (ord (!! s1b7_info loc_[8])) (I# 101)) (&& (== $fEqInt (ord (!! s1b7_info loc_[9])) (I# 48)) (&& (== $fEqInt (ord (!! s1b7_info (I# 10))) (I# 102)) (&& (== $fEqInt (ord (!! s1b7_info (I# 11))) (I# 85)) (== $fEqInt (ord (!! s1b7_info (I# 12))) (I# 53))))))))))))) of <tag 1> -> putStrLn (unpackCString# ":p"), c1ni_info_case_tag_DEFAULT_arg_0@_DEFAULT -> case == ($fEq[] $fEqChar) (reverse s1b9_info) (: (C# 103) (: (C# 110) (: (C# 105) (: (C# 107) (: loc_7168872 (: loc_7168872 (: (C# 76) (: (C# 51) (: (C# 114) (: (C# 52) [])))))))))) of False -> putStrLn (unpackCString# ":p"), True -> case && (== $fEqChar (!! s1bb_info loc_[0]) (!! pat_Alfa loc_[0])) (&& (== $fEqChar (!! s1bb_info loc_[1]) (!! pat_alfa (I# 19))) (&& (== $fEqChar (!! s1bb_info loc_[2]) (!! pat_Alfa (I# 19))) (&& (== $fEqChar (!! s1bb_info loc_[3]) (!! pat_alfa loc_[7])) (&& (== $fEqChar (!! s1bb_info loc_[4]) (!! pat_num loc_[2])) (&& (== $fEqChar (!! s1bb_info loc_[5]) (!! pat_Alfa (I# 18))) (&& (== $fEqChar (!! s1bb_info loc_[6]) (!! pat_alfa (I# 19))) (&& (== $fEqChar (!! s1bb_info loc_[7]) (!! pat_num loc_[3])) (&& (== $fEqChar (!! s1bb_info loc_[8]) (!! pat_alfa (I# 17))) (== $fEqChar (!! s1bb_info loc_[9]) (!! pat_alfa (I# 18))))))))))) of <tag 1> -> putStrLn (unpackCString# ":p"), c1tb_info_case_tag_DEFAULT_arg_0@_DEFAULT -> putStrLn (unpackCString# "Correct Serial Key! Auth Flag!") ) ) ) pat_num = unpackCString# "1234567890" pat_Alfa = unpackCString# "ABCDEFGHIJKLMNOPQRSTUVWXYZ" pat_alfa = unpackCString# "abcdefghijklmnopqrstuvwxyz" s1b5_info = splitOn $fEqChar (unpackCString# "#") s1dZ_info_arg_0 -- s1dZ_info_arg_0 : 文字#文字#文字の形式 s1b7_info = !! s1b5_info loc_[0] -- [70,108,97,103,123,83,48,109,101,48,102,85,53] : Flag{S0me0fU5 s1b9_info = !! s1b5_info loc_[1] -- [103,110,105,107,48,48,76,51,114,52] : gnik00L3r4 s1bb_info = !! s1b5_info loc_[2] {- [A_0,a_19,A_19,a_7,n_2,A_18,a_19,n_3,a_17,a_18] [A,T,S] [t,h,t,r,s] [3,4] AtTh3St4rs -} -- Flag{S0me0fU5gnik00L3r4AtTh3St4rs loc_[0] = I# 0 loc_[1] = I# 1 loc_[2] = I# 2 loc_[3] = I# 3 loc_[4] = I# 4 loc_[5] = I# 5 loc_[6] = I# 6 loc_[7] = I# 7 loc_[8] = I# 8 loc_[9] = I# 9 loc_7168872 = C# 48 ``` flag : Flag{S0me0fU5gnik00L3r4AtTh3St4rs ### 3/30 : Codegate CTF 2018 Preliminary RedVelvet - 75 - 今回もlibなんか足りてなかった <- 治らなかったので実行せず Ghidraで見る - mainからたくさんの関数(func1〜func15)が呼び出される - not strippedだったので、関数名残ってる - func系は、前の値を使って次の値を求めるみたいな感じでずっと連なっている - 方程式とか建てたら解けるんですかね? - angrぽかったので、それでやりました - angr、正しく理解していなくて前のコード使いまわしているから正しい資料が欲しい ``` import angr project = angr.Project("./RedVelvet", auto_load_libs=False) @project.hook(0x401196) def print_flag(state): print(state.posix.dumps(0)) project.terminate_execution() project.execute() ``` flag : What_You_Wanna_Be?:)_lc_la ### 3/31 : ASIS CTF 2018 baby C - 74 - 解けない - (多分)この時に用意された想定解をすることができない - また、それ以外に方法がない(動的解析ガチ勢でも無理じゃないかな) <- debuggerの検知で実行が止まるのを回避すればいける やったところまで - 入力を求めてくる - ghidraで見ると、明らかに難読化されていそうな感じ - とりあえず、mainで使用されている文字列"Wrong!"を探す - 上にはCorrectが、下にはflagの一部と思われる文字列がある - この文字列、気に止めてなかったけど後からもう一回見たらこの難読化を示していた - **m0vfu3c4t0r!** -> movfuscator - 実際、コードもmovだらけだった movfuscator関連 - https://github.com/xoreaxeaxeax/movfuscator - https://github.com/kirschju/demovfuscator このdemovfuscatorの利用環境を作るのにとても時間がかかった 参考資料 - https://www.jianshu.com/p/d7280c8a1569 - http://meyon.gonna.jp/study/pc/1973/ いざ実行となると、謎のassertionとsigsegvに困らされる - プルリク上のnode.cppを利用することでこの2つは解決 - ただ、難読化を解いたコードが反映されない - コードの流れを示すもの -> ○ - idaで変数いじいじするためのもの -> ○ - 出力のバイナリは元と同じどころか、実行後にSIGSEGVを吐くようになってる - 上記のリポジトリを見てるとissueで書かれている問題がこれ - なら無理です Writeupを見てみると、難読化解除後はflagの文字列がそのまま見えるので簡単ぽい なら、なんの難読化かたどりついた時点で解けたという解釈でよろしいのでは - https://realsung.github.io/ctfs/2020-1-22-asis-babyC ## 2022/4 ### 4/1 : ASIS CTF 2018 Echo - 80 実行すると、引数足りないと言われるのでまずは ` ltrace ./Echo a ` とすると、GIVEMEFLAGと比較しているので第一段階突破 この後は入力をずっと返してくるので、手元でやるのはやめた ` strings Echo ` をすると、brainfuckらしき文字列が見える。Ghidraで改めて確認すると一部のbrainfuckコードはリトルエンディアンで格納されており、for文などを駆使して完璧なコードを作り上げる。 手元の環境ではFUN_00100970がそれにあたる。 ここ、諦めたポイントでforで使う変数とそれを用いて新たに作られる変数。この変数の位置にコードの要素(>,+とか)を埋めていくんだけどどうしてもうまく行かない - これは静的でc -> pythonに出来なかったということ - 動的はこのFUN_00100970のみを実行する方法が分からない - gdb-peda : sigsegv吐かれる <- b貼っていたら - radare2 : -dつけてやると実行がaaaが終わらさすぎて次行けない - gdbでやりたいけど、どうやればいいんですかね そんなこんなでwriteupを見る。-> brainfuckのコードを入手 もう一回、自分でやってみる このbrainfuckのコードはSEXT48というなぞの関数?に渡される。 - http://wapiflapi.github.io/2019/10/10/efficiency-reverse-engineering-with-ghidra.html - brainfuckのコード自体に影響をもたらすものではない 大事なのは、条件分岐を抜けたあとのfor文。switchでコードの要素(>,+とか)を管理している。 おそらくここが実行環境になっているんだろうと思う。 ここで使用される値があって、ちょっと変数の順番がおかしくなっているのを調整してから見てみると、最初がflagになってそのあとがエンコードされた文字列みたいになっている。 brainfuckの役割 - 入力をそのまま返す -> ,[.,]でやってる - エンコードされたflagを復号する -> これ以外のコード 参考 ` > = increases memory pointer, or moves the pointer to the right 1 block. < = decreases memory pointer, or moves the pointer to the left 1 block. + = increases value stored at the block pointed to by the memory pointer ー = decreases value stored at the block pointed to by the memory pointer [ = like c while(cur_block_value != 0) loop. ] = if block currently pointed to's value is not zero, jump back to [ , = like c getchar(). input 1 character. . = like c putchar(). print 1 character to the consol ` はい。これ以上分からなくて、オンラインデバッガーとかで見ながらしてたけど無理でした。 反省点 - gdbでの動的解析もっと使えたらこの問題楽に解けそう - コードを得るパート - そのコードをイジイジするパート <- わざわざオンラインのやつを使わなくていい また、writeupに帰ります。やっぱりgdbでできるっぽい。[+]は復号後のflagが配置されているメモリを削除するためにあったらしい gdbでできるっぽいが、やり方が載っていない。載っていてほしい。そう思うので、自分がwriteup書くときは丁寧に書こうと思った。 ### 4/2 : Atenea CTF Saint Seiya 逃げちゃ駄目なんだけど、Medium Easyですら難しいというか出来ないので違うやつをやってみる。 - 自分の実力よりちょい上が欲しい - gdb、angr系の手順を踏ませてくれる問題が欲しい この問題は常設の問題っぽい - 80 - exeファイルが配られる Ghidraで見る - base64らしき文字列がある - inputをbase64して内蔵データと比較している - この内蔵データが1文字 + 1文字 + 残りの文字って感じで配置されてた - 文字数(==)的にそれは雰囲気で関係あると分かる - base64テーブルが違うので、それと内蔵データ(enc data)を取ってきてcyberchefで治す Parad0x 答えの提出方法はこれをmd5にして提出するという形式 ### 4/4 : Hack The Box Reversing Impossible Password - 30 - Atenea CTF(https://atenea.ccn-cert.cni.es/home) - revいくつかあったのに、elf系の問題が前やったやつだけだった hack the boxにもctf追加されていたので、それを解いていく - 入力を求められる ` ltrace ./impossible_password.bin ` 比較対象が見つかった -> SuperSeKretKey これは1段階目 2段階目 - ltraceで比較対象は見れるが、毎回値が変わる - seed 0x14を元に適当な比較対象を作り出している - ltraceからもrand関数が見えるため分かる - randを無効化するにはオーバーフローさせればいいんだっけ?とか思いつつGhidraで見る - 入力は%20sと制限があるので無理そう? <- あまり分かっていない - strcmpの結果が0の場合flagを出してくれる関数があるっぽい flag出力 - 内蔵データと9をxorしている - 手元で適当にやって終わり flag : HTB{40b949f92b86b18} ### 4/5 : Hack The Box Reversing Bypass - 20(昨日のやつよりむずいよ) - exeが配られる - .Netとあるので、dnspyで見ることに - 32bitでもあったので、x86使わないとdebug出来ない - やり方あってたけど、flag得れなかった - なんでか分からない dnspyで見る - 比較対象であったり、入力時のコメントなどが文字列として残っていない - global::0.1();とかで管理されていたけど、見れなかった - 静的解析は諦めて動的解析することに - 前やったからなんとなくできる 動的 - とりあえず、ifの部分にブレークポイントをはってfalseをtrueにしたらいいんじゃないかと思う - やってみる - 1段階目は文字列とか見当たらなかった - 2段階目は比較対象見つかった - ここで2段階目のifのブレークポイントはずして正しい入力とかを与えるけど - flag出てこないで終わる - 1段階目みたいにfalse -> trueにしても変わらない - ここでwriteup見るけどあってたっぽい - なんで駄目なんだろ - 編集したりして再度コンパイルしたらなんか変わったかな? ### 4/6 : Hack The Box Reversing Exatlon - 20 - elfが配られる - 入力を求められる ``` ltrace ./exatlon_v1 ``` Couldn't find .dynsym or .dynstr in "/proc/5828/exe"と出て大変なことになる - なんで出るのか調べてない Ghidraで見る - 文字列検索の時にupxが目に入る - upx -dでアンパック - やり直す - かなり関数が多い - しかもstd::とかが多くて見づらい -> C++ですかね - 文字列から関数を逆引きする - do whileでくくられている。終了条件はkeyが正しいときでしょう - アスキーアートの表示 - 入力受け取り - encする関数 exatlon - 内蔵データとの比較 - encの関数 exatlonは読んでできる感じじゃない - 動的解析をすることに gdbでの動的解析 - 関数が多すぎて、気持ち悪い - 正しい位置じゃないと思うけど、readにbreak pointを設置 - 入力してnを押し続ける - mainに返ってきた時にencされたデータを確認することができたので、全ての文字に同じことをしようとする - 文字ごとに、数字の差が16であることに気づく - 小文字、大文字、数字の頭(a, A, 0)とけつ(z, Z, 9)をやるだけで済む - encデータを格納する配列を用意して、比較を行う ``` comp_dat = [1152, 1344, 1056, 1968, 1728, 816, 1648, 784, 1584, 816, 1728, 1520, 1840, 1664, 784, 1632, 1856, 1520, 1728, 816, 1632, 1856, 1520, 784, 1760, 1840, 1824, 816, 1584, 1856, 784, 1776, 1760, 528, 528, 2000] dat = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}!_" enc = [768, 784, 800, 816, 832, 848, 864, 880, 896, 912, 1040, 1056, 1072, 1088, 1104, 1120, 1136, 1152, 1168, 1184, 1200, 1216, 1232, 1248, 1264, 1280, 1296, 1312, 1328, 1344, 1360, 1376, 1392, 1408, 1424, 1440, 1552, 1568, 1584, 1600, 1616, 1632, 1648, 1664, 1680, 1696, 1712, 1728, 1744, 1760, 1776, 1792, 1808, 1824, 1840, 1856, 1872, 1888, 1904, 1920, 1936, 1952, 1968, 2000, 528, 1520] # 1968 { # 2000 } # 1520 _ # 528 ! key = "" for n in comp_dat: if n not in enc: print(n) key += " " else: key += dat[enc.index(n)] print(key) ``` 最初はコメントで書いてる数字4つが埋めれていなかった。 ただ、_と{}くらいはイメージがつく。残りの528は場所的に!か?な気がするので試したらあっていたという感じ flag : HTB{l3g1c3l_sh1ft_l3ft_1nsr3ct1on!!} ### 4/7 : Hack The Box Reversing Behind the Scenes - 10 - elfが渡される - 実行する時に入力を渡す必要がある ``` ltrace ./behindthescenes hoge ``` 何も出てこなかった Ghidraで見る 1文字づつ入ってた - _ごとに区切られて、00が入ってるからそれは消す flag : HTB{Itz_0nLy_UD2} ### 4/10 : Winja CTF 2022 César Gandía - elfが配布される - 途中まで出来ていて、writeup見た 実行時に入力を一緒に書く形式。 まずは、入力の長さチェックが入る。ある程度伸ばしても当たらなかったのでGhidraで見る。 いつもとなんか違うので、stringsで見てるとpythonの文字列がいくつか見える。 これはpythonで書かれたプログラムがelf化されているということ。 この場合、次のツールを使うことでpycを抽出できる。 - https://github.com/extremecoders-re/pyinstxtractor これでちゃんと取れました。chall.pycがcoreっぽい。(実行したら分かる) stringsコマンドで見ると、base64とzlibで暗号化・圧縮されたデータがあることが分かる。 (marshalとかもある) pycは以下のツールでpyに戻せる - https://www.toolnb.com/tools-lang-en/pyc.html - uncompyle6 だけど、これpython 3.9で書かれていて上記2つのツールは3.9をサポートしてないので無理だった。 これでCTF期間は解けてなかった。 writeupを見ると、stringsの結果がb64decode -> (文字列) -> marshal -> zlib -> b64decodeになってるからという理由で(半分推測なんだろうけど)、コードに起こしていた。 ``` import marshal, zlib from base64 import b64decode code = "c8gCAABlSnh0VTh1V29qQVUvS0RaOEdqN0RJdGVTQVFNQ0NvUGdld2thRUFTeVRsb04rSHJKK2lvcmNmbFRXN2RxcnFwQUVaUDJERkVDY3hobC9sZW5nWkttZHBkcHFFdVR6RlpSZE1hSGN3R3pZamlwOG1RSHlxMmpIR1BZcXYzWTBzTG1LWHROTHRDZzZ4bnRNczNocG1vUWVYT2tqNCtxc3Q4c1BSTUk0T3JCdnM4Z2g1Z0lVVzFTUXNXZkNNbmFSSHJLeVNhVDBEb0dXWGwvbHBQRGVqMEhPdnIyNzBuNzFtaFE3S05URjdVcHNqVHlSRkZWWWZTajF2UHlZLzY1enEydWtWczhTVnBYWkJPYUtrYXJIRHNZNnJSY3VIOHJzZGV1NFF6aFZ5dzZvbExYSjlGL2IzL1BUOFJmdjB6QkdEVTM3Z0YyK2pYdlpXRjU5Q2RYemQwblB2TVJaOTQ3aHlpNHRrQmNuZmtYYmN1WW9hQVRrTzI2UWZCekQ0akxXbjl3ZStDV2ZMR1QvTzlHTUpYVC9MTWZmWWsvb3FIcHlzR3lGbVliWlF5Yzg4UXdIckIrTGZrYkI5ZU1BZUVjL2xtbDkxaW5RNExaZ3VrNTF6T051RE0rZ1QxbEVCZy9tQkd6eVdvR0tRdXhkbUd5N2tOdE0xSzZqK2hMS1Q0MEpLVmM1bjVCOWFOQjRIRXpZTUpGaE82YzZqaWdmOW4wM2Q4cVBBM0NvZkFHck5CaTdxcWNqMVU4N1RmLzhyUVEwTkR6N2xtQ0tuaElMUHRqSjZoSlhHc1ZMWUFTazkwekx2TVhNWGxFbTRlNzduRWVpaThxemFwdjlUU3dTNjlxQ0dyV0NFdmYrVEZQNm5YZDE2cjl0YWNGdk9BeWpjam96YzhkM2x4WExmUCtxd0sycUhZUmFhSk5Gdm1hOVQzOWZVUDQxWkREZz09" print(b64decode(zlib.decompress(b64decode(marshal.loads(b64decode(code)))))) ``` 分からんでもない。試してなかったのが悪かった。(printはexecです。stringsの結果的に) 返ってくるのは、チェックするためのコード ``` import sys actual_flag = "f0dd841e3b8f971e111166a6{aag1109la_ASSa$51N}Nc17_h3\'S_a" def encode(flag): new_flag = list(flag) for i in range(len(flag)-1,len(flag)-11,-1): new_flag[i],new_flag[i-11] = flag[i-11],flag[i] for i in range(22,34): new_flag[(i^2)%11],new_flag[i] = flag[i],flag[(i^2)%11] for i in range(33,45): new_flag[i//4],new_flag[i//2] = flag[i//2],flag[i//4] return "".join(new_flag) if len(sys.argv) == 1: print(f"Required Parameter: <flag>") sys.exit() if len(sys.argv[1]) != len(actual_flag): print(f"Incorrect Flag Length") sys.exit() if encode(str(sys.argv[1])) == actual_flag: print("Correct!") else: print("Incorrect!! Try Again") ``` encodeの逆を行うコードを書く flag : flag{a09116a971e33bb88f884dd3b1e0dc17_h3'S_aN_ASSa$51N} 多分あってるでしょう ### 4/11 : Winja CTF 2022 Shakir - elf - これも解けなくてWriteup見た。 - まだ解けてない -> なんでか分からん 実行する - まずは入力の長さのチェックが入る - Ghidraで見ると、比較対象らしき数値がある -> 77個 - ここで止まってた(というか解くの忘れてた) Writeup見る - main__encodeがあったらしい - 気づいてなかった - 確認ちゃんとしましょう - 入力を名前の通りエンコードしてるみたい - とりあえず、"A"を77文字入力してみた - なんか返ってくる - 比較対象との距離を表している - ただし、この距離は絶対値 - ここらへんはどうでもよくて、とりあえずencの逆を比較対象にすればいい - はずなんですが、元に戻らない よく分からん 追記 decが出来なかったので、総当りしてみた ``` import string pat = string.printable for i in range(len(dat)): for p in pat: s = ord(p) if s < 0x41 or 0x5a < s: if 0x60 < s and s < 0x7b: s -= 0xb else: s -= 6 if i % 2 == 1: s //= 2 else: s *= 3 if s - dat[i] == 0: print(p, end = "") break ``` flag : fkag{294d404521aa048dc280e649c86fe568iY0uiHFmLEREDjsHDj0327iPAKi$64miiHFcJER} なんかちゃう気するけど ### 4/12 : Winja CTF 2022 matria - 問題名正しくないでしょう - elfが配られます 実行すると、入力が求められます 間違えてたらshellコードが流される <- これなんか関係あるのか見てない Ghidraで見る - flagの一部が入力となっていて、これを用いて出来上がったやつがまたflagの一部と比較 - 比較対象はm4triaRchy - 入力文字列は計算的に 0〜9のみ - 文字を計算して数字に変える - vという配列があってこれの位置を示す値となる(実際は計算いろいろしてから) - だけど、どう計算しても欲しい値が出てこない - 解決できてない関数 basic_stringのせいかも - 型がlongに変わるからかも - 長さ10の使う文字10の10の10乗。総当りできなそうだし Writeupを見たいが無い 追記 for文のiに影響されないので0〜9の計算後の様子を確認すればいいことに気づく gdbを使用 - 全ての関数を確認できる ``` disas main b _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEC1EPKcRKS3_@plt <- basic_string(別につける必要なかった) r 0123456789 を入力 b strcmp@plt <- 最初にやると入力前に止まったので入力後にやった n <- を複数回 ``` すると、"Rr4ctimayh"が出てきた。後はそろえるだけ 入力は"6241570398" flag : flag{97da0d68baf233159832676241570398_L3t_Th3_m4triaRchy_b3g1n} よくないな。よく分からんまま解けてしまった ### 4/13 : NITIC CTF 2 report or repeat - 確か参加してて解けなかった - elfとencのpdfが配布される Ghidraで見る - mainはいつも通りの見かた - ファイルを読み込めるか等の条件分岐がいくつかある - 最初はどうでもいい - 最後らへんのelseが大事 - ファイルを0x100づつ読み込む - これを関数に渡す。引数はファイルデータとそれをencしたデータが入る変数 enc - 内部データ1がある - このデータはリトルエンディアンで合計0x100個ある - この内部データ1とは別に内部データ2がある - 内部データ2は先程のファイルデータの位置を指すものとなる - 内部データ3[file[内部データ2] ^ 内部データ1[forのi]] - ここで注意するのは内部データ3は2段階踏んでデータにたどり着く(Ghidra内での操作の話し) - これがencのデータとなる 結局はこんな感じ(変数は察して) enc[i] = 内部データ3[file[内部データ2[forのi]] ^ 内部データ1[forのi]] xorで戻すというよりかは、結果が1対1なのでfileの部分をencにencをdecにする感じです flag : nitic_ctf{xor+substitution+block-cipher} ### 4/14 : ASIS CTF 2018 Quals Left or Right? - writeup見た - 解法を実践できていないので、明日に持ち越し - 全人類Ghidraを使ってくれ(IDAは金がかかるでしょう) - elfが配られる - 内部文字列からこれはrustで書かれたことが分かる 実行 - 入力が求められる - ltrace駄目 Ghidraで見る - 関数が入れ子になりすぎて分からん - 文字列で欲しい関数を見に行ってみるけど、うーんて感じ - とある関数が入力に近い文字列を多数持っていた - 多数 -> dummyなのは分かる - 関数がどれかメモっておらずこの関数は闇の中 Writeup見る - とある関数が入力チェックになっているらしい - それが見つからん - 明日探す https://ctftime.org/writeup/9939#left-or-right-sces60107 ### 4/16 : 続き - 自分が見ていた関数発見 - FUN_001 -> FUN_0010 -> FUN_00108 -> FUN_001083c0 ここにある文字列 - superkeymysecretkeygivemetheflagasisctfisfun - mysecretkeygivemetheflagasisctfisfun - givemetheflagasisctfisfun - asisctfisfun - therustlanguageisfun ここにある文字列が別の関数に渡されて返ってくる - これもまた入れ子になってる関数 動的解析することに - breakpoint貼ったのは、先程の関数に渡されて返ってきたあと - 0x00005555554086c6に貼った - もう一つはいったか分からないけど、0x0000555555408b0c - 答えありきだから出来たけど - n押してると途中で分裂した比較対象が出てくる - できたとは言いづらいが - keyはsscsfuntnguageisfunsu - 分裂の仕方 - ss - sc - sfunt - eisfunsu - nguageis って感じ flag : ASIS{Rust_!s_Right_i5_rust_!5_rust_but_rust_!s_no7_left} ### 4/17 : ASIS CTF 2018 Quals Density - elfとencの出力が渡される Ghidraで見る - mainはいつもどおり、entryの最初の関数 - 実行時に入力を渡す形式なので、入力数が2の条件分岐を確認 - 最初はrand()が関係している関数 - 最後にcustom base64でenc - ファイルもb64pack出し、大体これがb64テーブルかとなる - output用の関数もある rand関係しているが、出力が渡されているのでcustom base64戻すだけで大丈夫そう 続きは明日 ## 2022/5 ### 5/5 : 続き 長らくサボっていました。記憶を呼び覚ましながらやっていきます。 custom_base64の逆をやる - 最後の操作(decでは最初の操作) - 普通のbase64。テーブルもnormal - base64encodeだと思っててやってたらうまくいかず、decにしたらできちゃった - よくない - 次の操作 - 条件分岐で分かれてるけど、セットで考える strchrという、文字がある文字列のどの位置にあるかを見る関数がある。 pythonでいうと、```"文字列".find("文字")``` これが0の場合がifを通る。ポジティブな通り方じゃないから勘違いするかも 2つ文字列がある。片方を通った場合は++文字、もう片方は+文字と変換される。 ここミスったポイントでデコーダーでは0x7fと0x68で計算している風に見えたが、0x61で計算をするみたい ![](https://i.imgur.com/s0fG6XN.png) なんで? そんなこんなで答えを得た。ソルバ ``` import base64, re str1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" # normal str2 = "[\\]^{|}~`\t" # この位置 + 0x7fした文字(++) str3 = "@$_!\"#%&\'()*+,-./:;<=>?\n" # この文字 - 0x68した文字(+) with open("short", "rb") as f: enc_dat = list(f.read()) # print(enc_dat) enc_dat = base64.b64encode(bytes(enc_dat)).decode() # O++h+b+qcASIS++e01d+c4Nd+cGoLD+cASIS+c1De4+c4H4t+cg0e5+cf0r+cls+d++gdI++j+kM+vb++fD9W+q/Cg== hoge = re.findall(r'\+\+\w', enc_dat) fuga = re.findall(r'\w\+\w', enc_dat) fuga = [s[1:] for s in fuga] # print(hoge) # print(fuga) for s in hoge: target = s[-1] dec = str2[ord(target) - ord("a")] enc_dat = enc_dat.replace(s, dec) for s in fuga: target = s[-1] dec = str3[ord(target) - ord("a")] enc_dat = enc_dat.replace(s, dec) print(enc_dat) ``` flag : ASIS{01d_4Nd_GoLD_ASIS_1De4_4H4t_g0e5_f0r_ls!} ### 5/19 : angstrom CTF 2018 Rev2 - 80 - 数字の入力を求められる ghidraで見る - 最初は直に値が埋め込まれてる - 2個目は計算したらいい - 素数ぽかったので素因数分解した - 正解 flag : actf{4567_47_73} ## 2022/6 ### 6/4 : angstrom CTF 2018 Rev3 - 110 - 入力を求められる ltraceで見る encodeされて、内部で保持されている文字列と比較を行っている ghidraで見る - encodeがある - xorしてる - decodeする ``` flag = "egzloxi|ixw]dkSe]dzSzccShejSi^3q" flag = list(flag) flag = [chr((ord(s) + 3) ^ 9) for s in flag] print(''.join(flag)) ``` flagを得たけど、入力してもokにならない。writeup見るけど、答えぽい。よく分からん。 flag : actf{reversing_aint_too_bad_eh?} ### 6/5 : Seccon Beginners CTF 2022 Recursive - 91 本番中に解いたんですが、解いた問題が少ないのではてブロを書くほどでもないのでここで書きます。 入力を求められるので、ltraceとかしてみるけど直のチェックはしていない。 ghidraで見る。今回の問題、だいたい関数名が残っていたのでありがたい。 最初のチェックで入力は0x26であることが分かる。次にこの入力がcheckという関数に渡され、返り値が1である場合不正解になることが分かる。 checkを見ていく。 中身をある程度読むと、再帰的な構造になっていることが分かる。最終的に、param1(文字列)が1文字のとき内部で保持されているtableという配列のparam2(int)番目が一致しているか見ているよう。 ``` param1 == table[param2] ``` 的な。(本当は!=です) コードを書いてもいいんですが、動的解析でいいかなと思ってgdbでやることにしました。 いろいろガチャガチャします。breakpointは上記のようなコードのif文のところです。 ``` b *0x00005555555552a8 r FLAG : ctf4b{"0x26-len("ctf4b{}")の適当な文字列"} n #rdxの値を確認したら c ``` という風にやっていきます。自分は、ifを通らないようにレジスタのflagを書き直す方法を知らないので一回一回continueしてます。 flag : ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1} ### 6/10 : Seccon Beginners CTF 2022 Ransom 本番中にほぼ解けてた -> 動作をデコンパイル情報からpythonに書き起こしていたが、型の問題でCでしないとだめだった 実際はこの流れがRC4なので、ライブラリを使用してdecryptoするのが正しいよう。 鍵はpcapのtcpstreamより得る。 ``` rgUAvvyfyApNPEYg ``` そしてこれがRC4の流れ ![](https://i.imgur.com/LQx16HA.png) solver ``` from Crypto.Cipher import ARC4 enc = b"\x2b\xa9\xf3\x6f\xa2\x2e\xcd\xf3\x78\xcc\xb7\xa0\xde\x6d\xb1\xd4\x24\x3c\x8a\x89\xa3\xce\xab\x30\x7f\xc2\xb9\x0c\xb9\xf4\xe7\xda\x25\xcd\xfc\x4e\xc7\x9e\x7e\x43\x2b\x3b\xdc\x09\x80\x96\x95\xf6\x76\x10" key = b"rgUAvvyfyApNPEYg" cipher = ARC4.new(key) dec = cipher.decrypt(enc) print(dec) ``` flag : ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1 ## 2022/7 ### 7/2 : buuCTF easyre strings | grep "{" flag : flag{this_Is_a_EaSyRe ### 7/3 : buuCTF reverse2 - reverse1なんか動かないので飛ばした - ghidraがね elfだった。動かすと、flag一致するまで永遠に入力を求める ghidraで見る。関数名残ってるのでmainを見る。 - 内蔵flagがある - これを操作してから - 入力と比べる この操作がiかrだったら、そこを1に変えるというもの 内蔵が{hacking_for_fun}だったので、直す flag : flag{hack1ng_fo1_fun} ### 7/4 : buuCTF 内涵的软件 よく分からんままできた 問題文も問題の意図するところもよく分からん "DBAPP{49d3c93df25caad81232130f3d2ebfad}" っていうのがあって、DBAPPをflagに変えるんかなと思ってやったら合ってた flag : flag{49d3c93df25caad81232130f3d2ebfad} ### 7/7 : buuCTF 新年快乐 upxでパックされてることが、fileコマンドから分かる upx -d 新年快乐.exe ghidraでデコンパイルできない。仕方なくradare2で見る。 mainがあるので、mainを見る - HappyNewYear!という文字がある - scanfした文字と比べてる - 別段これ以外に処理はなさそう - 問題分でもflagの中身を見つけろ的な感じ 雰囲気でやってしまった flag : flag{HappyNewYear!} ### 7/8 : buuCTF xor ちょっとつまった 詰まったポイント - Ghidraが示してる文字列のラベルと実際の文字列が違った - \nとかが_に置き換わってた - これに気付かず問題といてた 問題の中身 入力を受け取り、それをxor。それを内蔵文字列と比較 ``` input[i] = input[i-1] ^ input[i] ``` なので、これを内蔵文字列にやってあげる ソルバ ``` enc = [ 0x66, 0x0A, 0x6B, 0x0C, 0x77, 0x26, 0x4F, 0x2E, 0x40, 0x11, 0x78, 0x0D, 0x5A, 0x3B, 0x55, 0x11, 0x70, 0x19, 0x46, 0x1F, 0x76, 0x22, 0x4D, 0x23, 0x44, 0x0E, 0x67, 0x06, 0x68, 0x0F, 0x47, 0x32, 0x4F, 0x00 ] dec = "" for i in range(1, len(enc)): dec += chr(enc[i-1] ^ enc[i]) print(dec) """ i = (i-1) ^ i """ ``` お気づきの方もいるかもしれませんが、これ添字がちょっとちゃうのでちゃんとしたflagは出てこない flag : flag{QianQiuWanDai_YiTongJiangHu} ### 7/9 : buuCTF helloword apkなので、bytecode-viewerにぶちこむ com -> example -> helloword -> MainActivity.class flagが直ではいってるので見て終わり flag : flag{7631a988259a00816deda84afb29430a} ### 7/11 : buuCTF reverse3 exeファイルです。Ghidraで見ます。 entryからmainを探してると、時間がかかったので文字列からmainを割り出します。 "please enter the flag:" <- mainでしょう(FUN_004156e0) thunk_FUN_00411ab0からできあがった文字列をいろいろして、内蔵文字列と比較しています。 そのいろいろは、local_98という変数にコピー。長さでforを回して ``` local_98[i] = local_98[i] + i ``` です。 で、このthunk_FUN_00411ab0なんですがぱっと見base64です。なぜなら、"A-Za-z0-9+/="があるからというのと、シフト演算をしているからです。 ということで、ソルバ ``` dat = "e3nifIH9b_C@n@dH" local_98 = "" for i in range(len(dat)): s = dat[i] local_98 += chr(ord(s) - i) import base64 as b64 dec = b64.b64decode(local_98) print(dec) ``` flag : flag{i_l0ve_you} ### 7/12 : buuCTF SimpleRev - 詰まった - デコンパイラのデコンパイルミス elfが配布される。ghidraで見る。 mainがちゃんと残ってる。こまんどdで入力を受ける関数(Decry)に行く 見る リトルエンディアンと、内蔵文字列を使って2つのキーが作られる - 1つは比較対象 リトルエンディアンであることに注意して、keyとtextを作成 ``` key = ADSFKNDCLS text = killshadow ``` keyは下記のコードでもう一回変更が加わる ``` for (i = 0; i < iVar3; i = i + 1) { if (('@' < (char)key[j % iVar3]) && ((char)key[j % iVar3] < '[')) { key[i] = key[j % iVar3] + ' '; } j = j + 1; } ``` このコードは大文字を小文字に直すものだったので、key = key.lower()で済ます 少し飛ばすが次がデコンパイルミス ``` iVar2 = ((int)cVar1 - (int)(char)key[j % iVar3]) + 0x3a; str2[k] = (char)iVar2 + (char)(iVar2 / 0x1a) * -0x1a + 'a'; ``` 二行目、絶対おかしいでしょ。しかもこのstr2[k]はtextと比較されるやつです。 過去writeupを見ました(IDAでのデコンパイル) 正解はこちら ``` str2[j] = (c - 39 - key[index++ % v5] + 97) % 26 + 97; ``` 全然違う。以下ソルバ ``` from Crypto.Util.number import long_to_bytes as lb local_28 = 0x776f646168 text = "kills" + lb(local_28).decode()[::-1] # print(text) killshadow local_48 = 0x534c43444e key1 = [ 0x41, 0x44, 0x53, 0x46, 0x4B ] key = "" for i in key1: key += chr(i) key += lb(local_48).decode()[::-1] print(key) """ このfor文が小文字にするためのものであることを確認 for i in range(len(key)): if ('@' < key[i % len(key)] and key[i % len(key)] < '['): print(chr((ord(key[i % len(key)]) + ord(' ')))) """ key = list(key.lower()) # textの比較対象 str2が生成される流れ # input_dat = "" # str2 = ["a"] * len(key) # for i in range(len(key)): # if (input_dat[i] < 'a' or input_dat[i] > 'z'): # # ivar2 = ord(input_dat[i]) - ord(key[j % len(key)]) + 0x3a # # ord(inp) = ivar2 + ord(key[j % len(key)]) + 0x3a # # str2[i] = ivar2 + (ivar2 // 0x1a) * -0x1a + ord("a") # str2[i] = (c - 39 - key[index++ % v5] + 97) % 26 + 97 import string pat = string.ascii_uppercase flag = "" for i in range(len(text)): for c in pat: if ord(text[i]) == ((ord(c) - 39 - ord(key[i % len(key)]) + 97) % 26 + 97): flag += c print(flag) ``` 説明が後になったけれど、if文により大文字のみが対象となっているのでパターンはstring.ascii_uppercaseになっている flag : flag{KLDQCUDFZO} ### 7/13 : buuCTF Java逆向解密 javaのclassが配布される。bytecode-viewerにつっこむ 比較対象(key)がある inputした文字列は、Encrypt関数に渡される ここではencryptと比較を一気に行うがencの部分だけ下に貼る ``` for(int i = 0; i < arr.length; ++i) { int result = arr[i] + 64 ^ 32; Resultlist.add(result); } ``` 足してxorだからxorして引けばいい ソルバ ``` key = [ 180, 136, 137, 147, 191, 137, 147, 191, 148, 136, 133, 191, 134, 140, 129, 135, 191, 65 ] for i in key: s = i ^ 32 s -= 64 print(chr(s), end = "") print() ``` flag : flag{This_is_the_flag_!} ### 7/14 : buuCTF luck_guy - GXYCTF2019の問題 実行すると、数字を求められる Ghidraで見る。 この数字入力はどうでもいい。というか、正しい数値を打つとflag生成に進まない get_flagの処理を見る f1とf2があって、f1には配列がf2には空の配列がある。 f1はflagの前半、f2にはflagの後半が入るよう f2の処理を見る ``` local_38 = 0x7f666f6067756369; strcat(f2,(char *)&local_38); /* 他の処理 */ for (j = 0; j < 8; j = j + 1) { if ((j - (j >> 0x1f) & 1U) + (j >> 0x1f) == 1) { f2[j] = f2[j] + -2; } else { f2[j] = f2[j] + -1; } } ``` local_38はリトルエンディアンのlong_to_bytesでしょう では、ソルバ ``` f1 = [ 0x47, 0x58, 0x59, 0x7B, 0x64, 0x6F, 0x5F, 0x6E, 0x6F, 0x74, 0x5F ] f1_s = "" for i in f1: f1_s += chr(i) print(f1_s) # GXY{do_not_ local_38 = 0x7f666f6067756369 from Crypto.Util.number import long_to_bytes as lb local_38 = lb(local_38).decode()[::-1] local_38 = list(local_38) # print(local_38) for i in range(len(local_38)): if ((i - (i >> 0x1f) & 1) + (i >> 0x1f) == 1): local_38[i] = chr(ord(local_38[i]) - 2) f1_s += local_38[i] else: local_38[i] = chr(ord(local_38[i]) - 1) f1_s += local_38[i] print(f1_s) ``` flag : flag{do_not_hate_me} GXY{}はflag{}に変更します ### 7/15 : buuCTF JustRE - BJDCTF2020の問題 exe。デコンパイルミスあった お気持ちで解決 strigsコマンド時に"BJD{%d%d2069a45792d233ac}"があることに気づく %d%dに残りが入る Ghidraで見る entryから追いかけるとだめそうだったので、stringsから すると、デコンパイルミスにより関数が関数でなくなっていた そのため、かなりだるい ただ、文字列の直前にpushが2回されている それが、0と0x4e1f。この0x4e1fは比較対象にもなっているのでflagかなとguess 順番はスタックの上から取ってみる -> 正解 flag : flag{1999902069a45792d233ac} ### 7/19 : buuCTF 刮开有奖 - 完璧には解けてない - 諦めた exeが配布される。 内蔵文字列がある この内蔵文字列を関数に渡して、その後使用する この関数がめんどくさすぎて諦めた -> IDAの見ると簡単だったのでなぜだろうという感じ。 ここを飛ばして読むと以下のようになる 内蔵文字列の各文字を用いて、入力文字と一致するかを確かめる。 これが前半のflagで、後半flagはbase64encodeした入力と別の内蔵文字列が一致するかを確かめる。 全て一致していたらそれがflagになる。 base64があるのは分かった。で上記のことも分かったが、最初の内蔵文字列の関数に渡すパートが分からなかった。 writeupを見て、解く。 https://blog.csdn.net/fzuim/article/details/113306993 flag : flag{UJWP1jMp} ## 2022/8 ### 8/15 : buuCTF 简单注册器 - apkファイル - bytecode-viewerで見る com/example/flag/MainActivity$1.classを見る flagのデコードをしているように見える ソルバ ``` var5 = "dd2940c04462b4dd7c450528835cca15" var5 = list(var5) var5[2] = chr(ord(var5[2]) + ord(var5[3]) - 50) var5[4] = chr(ord(var5[2]) + ord(var5[5]) - 48) var5[30] = chr(ord(var5[31]) + ord(var5[9]) - 48) var5[14] = chr(ord(var5[27]) + ord(var5[28]) - 97) for i in range(16): var5[i], var5[31 - i] = var5[31 - i], var5[i] print("flag{" + ''.join(var5) + "}") ``` flag : flag{59acc538825054c7de4b26440c0999dd} ### 8/18 : buuCTF pyre - pycが配布される - uncompyle6でpyに直す - python2だったため普通に戻る ``` l = len(input1) for i in range(l): num = ((input1[i] + i) % 128 + 128) % 128 code += num for i in range(l - 1): code[i] = code[i] ^ code[(i + 1)] print(code) code = ['\x1f', '\x12', '\x1d', '(', '0', '4', '\x01', '\x06', '\x14', '4', ',', '\x1b', 'U', '?', 'o', '6', '*', ':', '\x01', 'D', ';', '%', '\x13'] ``` 頭からl-2までをxorしているのとnumがmodを取っているのが分かる。 なので、xorをもう一回してmodを取り直すとflagが得られる - xorはけつ(l-2)からやっていく - modの式は、128 % 128 = 0なので(input1[i]+i) % 128になる - その逆だから(code[i] - i) % 128 ソルバ ``` code = ['\x1f', '\x12', '\x1d', '(', '0', '4', '\x01', '\x06', '\x14', '4', ',', '\x1b', 'U', '?', 'o', '6', '*', ':', '\x01', 'D', ';', '%', '\x13'] l = len(code) for i in range(l-2, -1, -1): code[i] = chr(ord(code[i]) ^ ord(code[i+1])) for i in range(l): num = chr((ord(code[i]) - i) % 128) print(num, end="") ``` flag : flag{Just_Re_1s_Ha66y!} ### 8/19 : imaginary ctf 2022 hidden - 本番解けなかった - https://nanimokangaeteinai.hateblo.jp/entry/2022/07/21/200947#Reversing-100-hidden-129-solves - をちらっと見る 本番意味わかってなかったけど、ダミーフラグの部分には本当のフラグが入るって意味だったんだね ``` uVar2 = uVar2 * uVar2 ^ *param_2; if (uVar2 != *puVar3 ``` puVar3は内臓データでparam_2がフラグ。uvar2は初期値があるけど、計算結果が代入されていく param2を取り出したいため、puVar3の内臓データをけつからとってxorしなおしたらいい また、エンディアンの関係で計算結果で出てきたflagの部分は逆から読む必要がある。 そういえば、from Crypto.Util.number import long_to_bytesって駄目になったみたいなのを友人から聞いた気がする。 import binasciiでこれからはやるべきなのかな? writeupを読んで、& 0xffffffffffffffffをやる意味がよく分からなかった。多分、型の関係なんだろうなと思ってたけどプロが教えてくれた > ここでは```imul```という命令があってこれは桁があふれたら上位ビットを切り取る。pythonはそれしないから、0xffffffffffffffffで上位ビット取り除いてる なるほど。 flag : ictf{h1ddenc0de_1a29d3} ### 8/22 : buuCTF findit - apkファイルが配られる MainActivity.classに明らかにrotされてそうな文字列がある pvkq{m164675262033l4m49lnp7p9mnk28k75} rot16だった flag : flag{c164675262033b4c49bdf7f9cda28a75} ### 8/23 : buuCTF rsa - これはrevなのか - pub.keyが配布される ASN.1の読み方分からなくて、読み方いろいろ調べてたけど出てこないのでツールを調べる 使ったのはこれ - http://tool.chacuo.net/cryptrsakeyparse - http://factordb.com/ 1つ目でnとeを取り出し、2つ目でnを素因数分解しpとqを取り出す 後はコード書くだけ ``` e = 65537 n = 86934482296048119190666062003494800588905656017203025617216654058378322103517 p = 285960468890451637935629440372639283459 q = 304008741604601924494328155975272418463 from Crypto.Util.number import * phi = (p-1) * (q-1) d = inverse(e, phi) with open("output/flag.enc", "rb") as f: enc_flag = f.read() enc_flag = bytes_to_long(enc_flag) flag = pow(enc_flag, d, n) print(long_to_bytes(flag)) ``` flag : flag{decrypt_256} ACN.1の良い資料合ったら教えて下さい ### 8/28 : buuCTF reverse1 - exeをghidraが読み込まないのはバージョンのせいでした 文字列からmainを見つける。FUN_1400118c0がmain {hello_world}という文字列に対し、ループを書けて"o"が見つかった場合それを"0"に変更する。 次に、入力を受け取り{hell0_w0rld}と比較し一致している場合correctが出る。 flag : flag{hell0_w0rld} ### 8/30 : buuCTF 不一样的flag - exe 文字列```*11110100001010000101111#```を、pcVar3及びlocal_39にcopyしている。 その後、1~4を受け取りそれに応じた操作がなされる。複数の条件分岐があり、その中ではexit(1)がある。 "#"に到達したときはexit(0)と。これらから、迷路を抜け出す系だとわかる。また"1"に触れた場合exit(1)なので、0を通る必要がある。 加えて、上記の文字列は25なので5✕5でしょう。 flag : flag{222441144222} ### 8/31 : buuCTF [ACTF新生赛2020]easyre - exe - 最後見た Ghidraで見る。mainがあるので見る。 入力を内蔵文字列と比較する感じ 明らかにデコンパイルミスがある。 ![](https://i.imgur.com/Q5odP29.png) アセンブラとデコンパイルビューを見ながら解釈を進めた - local_3eは比較対象 - 内蔵文字列のindexは入力された値 - 実際は入力された値 - 1 - ```SUB EAX, 0x1```とかから分かる そこで、書いたコードは下 ``` dat = '''~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$# !"''' local_3e = [0]*12 local_3e[0] = 42 local_3e[1] = 70 local_3e[2] = 39 local_3e[3] = 34 local_3e[4] = 78 local_3e[5] = 44 local_3e[6] = 34 local_3e[7] = 40 local_3e[8] = 73 local_3e[9] = 63 local_3e[10] = 43 local_3e[11] = 64 for s in local_3e: print(dat[chr(s) + 1], end = "") ``` 出力は```S7V[/Q[U4>R=```なので、多分ミスってる これ以上は分からないので見た。 ``` for s in local_3e: print(chr(dat.find(chr(s)) + 1), end="") ``` これ以上は分からないと思ってたけど、ここは逆算なのでこうする必要があることに気づいた 元の式が ``` local_3e[i] == dat[ord("input文字列"[i]) - 1] ``` 的な感じなので、```ord("input文字列"[i] - 1)```はindexになる。なら逆算ではfind使うよね。反省。さっきのコードを分割すると ``` for s in local_3e: index = dat.find(chr(s)) print(chr(index + 1)) ``` はい。 flag : flag{U9X_1S_W6@T?} ## 2022/9 ### 9/1 : buuCTF [ACTF新生赛2020]rome - exe - ちょいだめ なんか似た問題あったぞと思いながら説いていく 昨日のと流れはかなり似ているのではしょりつつ 内蔵文字列があり、入力を変更してから比較を行う。 ![](https://i.imgur.com/BgnmfyU.png) コメントの通りで、小文字か大文字か判定がある。デコードしていく。 ``` local_21 = [0]*16 local_21[0] = 81 local_21[1] = 0x73 local_21[2] = 0x77 local_21[3] = 0x33 local_21[4] = 0x73 local_21[5] = 0x6a local_21[6] = 0x5f local_21[7] = 0x6c local_21[8] = 0x7a local_21[9] = 0x34 local_21[10] = 0x5f local_21[11] = 0x55 local_21[12] = 0x6a local_21[13] = 0x77 local_21[14] = 0x40 local_21[15] = 0x6c for i in range(0x10): if (96 < local_21[i] < 123): print(f"lowwer: {i}") n = local_21[i] - ord('a') + 0x4f elif (64 < local_21[i] < 91): print(f"upper: {i}") n = local_21[i] - ord('A') + 0x33 else: print(f"other: {i}") n = local_21[i] print(chr(n)) ``` デバッグを挟んでいるのは今から書く。 実行結果は"Cae3aX_Zh4_GXe@Z"で、これを入力するとはじかれた。 アセンブラにimulがあったことや、上記デコンパイルビューに少しミスがあることからlowwer、uppper、otherのどこを通っているか見てみる。 ``` upper: 0 C lowwer: 1 a lowwer: 2 e other: 3 3 lowwer: 4 a lowwer: 5 X other: 6 _ lowwer: 7 Z lowwer: 8 h other: 9 4 other: 10 _ upper: 11 G lowwer: 12 X lowwer: 13 e other: 14 @ lowwer: 15 Z ``` 5、7、12、15のところがlowwerなのにupperの大文字になっていることが分かる。分かるけど直し方が分からん。 諦めて見てみると、X -> r, Z -> tらしく。差を調べるとどちらも26少なかったみたい。 modを取っていたのでやってみる。そもそも文字にならなかった うーん。条件分岐を中でつけて26の調整をするといいのか?分からん flag : flag{Cae3ar_th4_Gre@t} ### 9/3 : buuCTF CrackRTF ステップ1: sha1のhash値となる文字列を求める 6文字の入力を求められる。また、その値は多分数字である必要がある。 <- 後で調べたらatoi関数という文字を数字に変換する関数に通されてたみたい。 この6文字入力にsaltの"@DBApp"が連結されてsha1関数に通される。 で、sha1とわかるのはCryptCreateHashのALG_IDが0x8001であるから。 https://docs.microsoft.com/en-us/windows/win32/seccrypto/alg-id 6桁の数字ということは100000から1000000の間でfor文を回せばいい ``` import hashlib salt = "@DBApp" for i in range(100000, 1000000): dat = str(i) + salt hash = hashlib.sha1(dat.encode()).hexdigest() if hash == "6e32d0943418c2c33385bc35a1470250dd8923a9": print(dat) break ``` dat: 123321@DBApp ステップ2: md5のhash値となる文字列を求める 同じく6文字の入力を求められる。sha1と違って変な関数(atoi関数)呼ばれてないから大文字・小文字等なんでもありの6文字。 そして、この6文字には先程のデータが連結される。 md5とわかるのはALG_IDが0x8003だから。 hashに通される値の半分以上がわかっていてmd5なら、総当りにも耐えそうだと思ったのでhashcatを使う。 以下のように実行します。 ``` hashcat -m 0 -a 3 27019e688a4e62a649fd99cadaafdb4e -1 '?a' '?1?1?1?1?1?1123321@DBApp' ``` - -m 0でmd5 - -a 3で総当り - -1 '?a'でルール作ってる。?a = 大文字 + 小文字 + 数字 + 記号 - -o オプションは着けたほうがわかりやすいと思う dat: ~!3a@0123321@DBApp このデータはrtfっていうファイルのパスワードになってるんだと思う。このステップの次に呼ばれる関数でrtfとかいろいろあったから。 多分実行したらflag得れるので、終わり。 確認したら合ってたし、謎関数はatoi関数であることがわかった。 flag : flag{N0_M0re_Free_Bugs} ### 9/4 : CakeCTF 2022 nimrev - nimで書かれたelf correct等で関数を探すとNimMainModuleがmainなのがわかる 内蔵文字列と入力を比較して、correctかwrongかが出る。 eqStringsが呼ばれる直前に内蔵文字列に対する処理が終わるぽいのでそこでメモリを見る。 RDIにありそうだったので、次のようにgdbを動かす ``` b *0x000055555555efb4 x/s $rdi ``` 無い。ってなって、もうちょい前で見てたりしてたんだけど何を思ったかsの値を変えてみたら ``` x/-s $rdi ``` 0x7ffff7d040e0: "CakeCTF{s0m3t1m3s_n0t_C}" flag : CakeCTF{s0m3t1m3s_n0t_C}