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秒以上経過させればよい。
よくわからんから適当に長い文字列入れたらいけた。


以上!

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