🔐 Security/System

[시스템보안] 스택 동작 이해 및 셸 코드 생성 및 실행

CODE CATCHER 2024. 9. 13. 13:58

위 글은 2022년도 목포대학교 학점교류 '시스템보안' 수업에서 수행한 과제를 작성한 내용입니다.
교재 : 시스템 해킹과 보안: 정보 보안 개론과 실습 3판, 양대일 (2018)

 

1. 프로그램 실행 과정에 따른 스택 동작 이해하기

디렉터리를 만들고 어셈블리어를 테스트할 sample.c 코드를 vi로 작성하고 리눅스에서 컴파일한다.
(ppt 자료에서 디렉터리는 chapter2로 정의했으나, 코드 오류와 캡처하지 못하여 부득이하게 chapter5로 작성)


gcc S o sample.a sample.c로 컴파일한 후에 vi 에디터로 sample.a 파일을 열어준다.

pushl %ebp로 최초 프레임 포인터 값을 스택에 저장하며 ebp는 함수 시작 전 기준점이 된다.
ebp
전엔 ret이 저장되며 함수 종료 시 점프할 값을 저장한다.
movl %esp, %ebp로 현재 esp값을 EBP 레지스터에 저장한다.
subl $4, %esp esp값에서 4바이트만큼 뺀다(int c이므로, 4 bite만큼 할당하기 위함이다.)
pushl $2, push $1, call functionfunction(1, 2) 코드로 스택에 인자 2, 1, 그리고 function 함수의 ret를 저장하는데, ret이 연산 결과인 c가 되어 함수 호출 후 다시 돌아가야 한다.
pushl %ebp 현재 레지스터의 ebp값을 스택에 저장한다.
movl %esp,%ebp function 함수에 대한 스택을 생성한다.
subl $12, %esp esp 값에서 12바이트만큼 뺀다. 4 bite 단위로 할당하므로, espbuffer[10]의 할당 값이니 12 bite를 할당하게 된다.
movl 12(%ebp). %eax ebp12바이트를 더한 주솟값 내용을 eax에 복사한다.
addl %eax, 8(%ebp) ebp8바이트를 더한 주솟값 내용에 eax값을 더한다. a+b 결과를 저장하는 내용이다.
movl 8(%ebp), %edxebp8바이트를 더한 주솟값 내용을 edx에 저장한다. a=a+b에 관한 내용이다.
jmp .L2로 점프한 후에 ret으로 function 함수를 마친다. 이후 ebp값을 제거한 뒤에 main 함수의 원래 ebp 값으로 EBP 레지스터값을 변경한다.
addl $8, %espesp8바이트를 더해 esp를 올려주고, 그다음 명령어는 eax값을 eax로 복사하는 것이나 사실상 의미는 없다.
movl %eax, -4(ebp)ebp에서 4바이트를 뺀 주솟값에 eax값을 복사한 수 leave로 함수 종료, ret로 프로그램을 종료한다.
pushl %ebp로 최초 프레임 포인터 값을 스택에 저장하며 ebp는 함수 시작 전 기준점이 된다. ebp 전엔 ret이 저장되며 함수 종료 시 점프할 값을 저장한다.
movl %esp, %ebp로 현재 esp값을 EBP 레지스터에 저장한다.
subl $4, %esp esp값에서 4바이트만큼 뺀다(int c이므로, 4 bite만큼 할당하기 위함이다.)
pushl $2, push $1, call functionfunction(1, 2) 코드로 스택에 인자 2, 1, 그리고 function 함수의 ret를 저장하는데, ret이 연산 결과인 c가 되어 함수 호출 후 다시 돌아가야 한다.
pushl %ebp 현재 레지스터의 ebp값을 스택에 저장한다.
movl %esp,%ebp function 함수에 대한 스택을 생성한다.
subl $12, %esp esp 값에서 12바이트만큼 뺀다. 4 bite 단위로 할당하므로, espbuffer[10]의 할당 값이니 12 bite를 할당하게 된다.
movl 12(%ebp). %eax ebp12바이트를 더한 주솟값 내용을 eax에 복사한다.
addl %eax, 8(%ebp) ebp8바이트를 더한 주솟값 내용에 eax값을 더한다. a+b 결과를 저장하는 내용이다.
movl 8(%ebp), %edxebp8바이트를 더한 주솟값 내용을 edx에 저장한다. a=a+b에 관한 내용이다.
jmp .L2로 점프한 후에 ret으로 function 함수를 마친다. 이후 ebp값을 제거한 뒤에 main 함수의 원래 ebp 값으로 EBP 레지스터값을 변경한다.
addl $8, %espesp8바이트를 더해 esp를 올려주고, 그다음 명령어는 eax값을 eax로 복사하는 것이나 사실상 의미는 없다.
movl %eax, -4(ebp)ebp에서 4바이트를 뺀 주솟값에 eax값을 복사한 수 leave로 함수 종료, ret로 프로그램을 종료한다.

 

2. 셸 실행 과정을 이해하고, 셸 코드 생성하기

(2) 셸 기본 코드 분석

셸을 c언어로 실행시키는 shell.c 컴파일하여 어셈블리어 코드를 획득한다.

gdp shell 명령으로 디버깅한다. disass 명령은 스택 동작을 살펴보고 소스 코드를 어셈블리어 파이로 변경한 것과 결과가 비슷하다.

앞의 두 코드는 설명을 생략하고,
0x804819b <main+2>: sub $0x8, %espchar* name[2]을 나타내며 포인터 값 두 개를 위한 8바이트의 저장 공간을 스택에 마련한다.
0x804819e <main+6>: movl $0x8071548, 0xfffffff8(%ebp)ebp에서 -8 (0xfffffff8) 거리에 있는 주소에 0x08071548 값을 저장하게 된다. 또한 name[0] (“/bin/sh”;)의 포인터이다.
0x80481a5 <main+13>: movl $0x0, 0xfffffffc(%ebp) ebp에서 -4 (0xfffffffc) 거리에 있는 주소에 0x0 값을 저장한다. name[1] = 0을 나타낸다.
0x80481ac <main+20>: push 0x0execve(name[0], name, NULL)의 마지막 인수 값 (NULL)을 스택에 저장한다.
0x80481ae <main+22>: lea 0xfffffff8(%ebp), %eaxlea (Load Effective Address) 명령을 한다, 이는 -8(%ebp)(0x08071548) 주소(0xbfffe830)%eax 값에 저장함을 의미한다.
0x80481b1 <main+25>: push %eaxeax (0xbfffe830)을 스택에 저장한다. execve의 두 번째 인수 name의 값에 해당한다.
0x80481b2 <main+26>: mov 0xfffffff8(%ebp), %eax eaxebp에서 -8 (0xfffffff8) 거리에 있는 주솟값을(0x08071548) 저장한다.
0x80481b5 <main+29>: push %eax은 스택에 eax 값 저장하게 된다. /bin/sh 문자열이 저장된 주소가 된다.
스택에 EDI 레지스터와 EBX 레지스터값을 저장하고, ebp에서 8바이트 상위 주솟값을 edi에 저장한다.
0x804d050 <__execve+36>: int $0x80로 시스템 호출을 하여 셸을 실행하게 된다.
이후 quit로 종료한다.

 

3. 셸 코드를 작성한 후 실행 결과

셸 실행

asmshell.c에 대한 c언어 코드를 작성하였다.

셸 코드의 각 명령 크기에 맞춰 실제 주소를 입력한다. Call 명령의 49바이트에서 jmp 2바이트를 뺀 47(16진수 0x2f)을 참고해 popl이 저장된 값으로 함수를 호출한다.

asmshell.c를 컴파일하여 셸 코드를 획득한 후 기계어를 추출한다.
실제 코드는 <main+3>이므로 여기부터 기계어를 추출하는데, x/bx main+3 명령어를 사용한다.

획득한 기계어는 다음과 같다.

 

획득한 셸 코드로 실제 셸을 얻을 수 있는지 확인해보도록 한다.

셸 획득을 위한 shelltest.c 코드를 작성하기 위해 vi code를 사용한다.

shelltest.c 코드는 단순하게 기계어 코드를 main 함수의 RET 주소에 작성한다. 기계어 코드는 0x00을 공백 문자로 인식하여 오류가 생길 수 있으므로 없애고 EAX나 EBX같 은 레지스터를 사용한다.

ls 명령어로 쉘 코드가 잘 만들어졌는지 확인한다.

컴파일하고 실행해보면 다음과 같이 셸을 얻을 수 있는 기계어 코드를 획득하는 데 성공하게 된다.