간단한 예시를 통해 버퍼오버플로우에 대해서 알아보겠습니다.

소스코드 다운로드 : wget https://raw.githubusercontent.com/ICEB3AR/2020_Whois_Pwnable/main/week1/bof_poc.c

컴파일 : gcc -fno-stack-protector -no-pie -z execstack -o bof_poc bof_poc.c

 

name이라는 char형 10byte배열과 target이라는 int형 변수가 선언되어있습니다.

코드상에서 target값은 0으로 초기화 되었고 이후에 수정되는 부분이 없기 때문에 0에서 바뀌지 않아야 합니다.

scanf를 호출할 때 포맷스트링이 "%s"입니다. "%s"는 '\n','\x00', ' '등 문자열 끝을 암시하는 문자가 올때 까지 입력을 받습니다.

따라서 name은 10칸짜리 배열인데, 그 이상 입력을 받을 수 있는 상황입니다.

이처럼 자신에게 할당된 공간보다 더 많은 공간에 데이터를 써지는 취약점을 buffer overflow라고 합니다.

이처럼 10글자 이하로 입력하는 경우 target값은 0으로 정상동작합니다.

위의 입력을 받은 상태를 표현해보면 위와 같습니다.

name배열에는 exd0tpy라는 7글자가 들어가고 target에는 0이 저장되어 있습니다.

11글자이상 입력하게 되면 target값이 변하는 것을 확인할 수 있습니다.

마찬가지로 메모리 상태를 표현해보면 위와 같습니다. 

입력했던 글자중 10글자까지는 name에 잘 저장되지만, 11번째 글자부터는 target의 메모리를 침범하는 것을 볼 수 있습니다.

target에 저장된 문자들의 아스키 코드값은 b: 0x62 l: 0x6c e: 0x65 !: 0x21 입니다.

따라서 위와 같이 target이 수정됩니다. 리틀엔디안 방식으로 숫자가 저장되므로 target은 0x21656c62(560295010)가 됩니다.

target값은 위에서 예측한대로 560295010가 출력됩니다.

간단한 예제를 통해 return address에 대해서 알아보겠습니다.

소스코드 다운로드 : wget https://raw.githubusercontent.com/ICEB3AR/2020_Whois_Pwnable/main/week1/ret_tutorial.c

컴파일 : gcc -fno-stack-protector -no-pie -z execstack -o ret_tutorial ret_tutorial.c

컴파일한 바이너리를 gdb로 실행합니다.

hello 함수는 0x400569에서 호출됩니다.

함수호출 직전에 brake하기위해서 0x400569에 brake point를 설정합니다.

brake point가 설정되면, 실행중 해당 지점에서 멈추게됩니다.

(즉 0x400569에 brake point가 있다면 0x400569수행직전에 멈춤)

r을 입력하면 gdb상에서 바이너리가 실행됩니다.

rip를 보면 이전에 설정한 brake point인 0x40056에서 멈춰있는 것을 확인할 수 있습니다.

si명령으로 hello 함수 호출 내부로 진행합니다.

call hello가 수행되면, stack에 0x40056e가 push되는 것을 확인할 수 있습니다.

0x40056e는 call hello다음에 수행되어야 하는 주소입니다.

즉 해당 push된 값이 return address로 함수의 내용을 모두 실행하고 다시 돌아가야할 주소를 stack에 저장하는 것 입니다.

ni(next instruction)으로 push rbp를 수행하면 return address위에 rbp값이 삽입됩니다.

return address와 비슷하게 hello 함수가 끝난 뒤, 이전에 사용하던 스택을 복구하기 위해서 저장하는 값입니다.

한번더 ni로 mov rbp, rsp를 수행합니다. 

이는 hello 함수에서 이용할 스택프레임을 만들기 위해 rbp를 현재 rsp로 설정합니다.

ni로 sub rsp, 0x10을 수행합니다.

rsp에서 0x10만큼을 빼서 우측 그림처럼 스택에 hello배열이 사용할 0x10만큼의 공간을 할당합니다.

빨간색 박스가 쳐져 있는 부분이 hello배열이 사용할 부분이며 배열을 초기화 하기 전이기 때문에 현재는 이전에 사용하던 값들이 들어있습니다.(쓰레기 값)

ni로 leave수행전까지 실행합니다.

ni로 leave를 수행하면 처음에 push rbp로 저장했던 rbp를 rsp로 복사해 할당했던 스택프레임을 해제합니다.

따라서 현재 스택의 최상단에는 return address가 위치하게됩니다.

ni로 ret을 수행하면 return address가 pop되면서 해당 주소로 jmp됩니다. 

따라서 return address인 0x40056e로 jmp된것을 확인할 수 있습니다.

같은 방법으로 bye에서의 return address의 동작을 확인해보세요.

이처럼 return address는 함수가 호출될 때 생성되며, 함수가 종료되었을 때 돌아올 주소를 담고있습니다.

즉 return address를 조작할 수 있다면, 원하는 위치에 있는 코드를 실행 시킬 수 있습니다.

간단한 예시를 통해 return address조작에 대해서 알아보겠습니다.

위의 예시 코드는 win함수가 선언은 되어있지만, 호출은 하지 않고 있는 상황입니다.

소스코드 다운로드 : wget https://raw.githubusercontent.com/ICEB3AR/2020_Whois_Pwnable/main/week1/easy_bof.c

컴파일 : gcc -fno-stack-protector -no-pie -z execstack -o easy_bof easy_bof.c

컴파일된 바이너리를 gdb를 이용해 로드합니다. (gdb easy_bof)

그리고 p win명령어를 입력하면 win함수의 주소를 알 수 있습니다. (0x400557)

main이 실행중일 때 스택의 상태는 오른쪽과 같습니다.

buffer가 0x10byte를 차지하고, 그 아래에는 main함수 호출에 의해 생성된 rbp, ret이 위치하고 있습니다.

scanf가 %s로 문자열을 입력받고 있기 때문에, buffer에 0x10이상입력할 수 있습니다.

따라서 buffer에서 overflow가 발생해서 ret을 덮을 수 있는 상황입니다.

buffer에 AAAAAAAAAAAAAAAA(16byte)BBBBBBBB(8byte)\x57\x05\x40\x00\x00\x00\x00\x00를 입력하면 return address가 0x0000000000400557가 됩니다.

이때 \x숫자는 해당 hex값의 문자를 뜻합니다. '\x41'은 A과 같습니다.

또한 return address는 주소값을 포함하고 있기 때문에 little endian으로 저장되어있습니다. 따라서 위와같이 변환해서 입력합니다.

위와같이 스택을 덮어준 뒤, main이 return될 때, win이 실행되게 됩니다.

python과 pipe를 이용해서 \x05, \x00과 같이 문자로 표현할 수 없는 값도 input으로 전달할 수 있습니다.

print는 printf처럼 출력을 위한 함수입니다.

python은 문자열간 연산이 가능해서 + 연산으로 문자열을 연결하고 * 연산으로 반복되는 문자열을 만들 수 있습니다.

| 는 리눅스의 pipe기능으로 앞쪽 실행결과를 뒤쪽의 input으로 전달하는 역할을 합니다. 따라서 앞의 Python에서 print된 결과를 easy_bof의 input으로 전달되기 때문에 scanf로 전달되는 것 입니다.

GOOD JOB!이라는 문자열이 출력되면서 win이 실행된 것을 확인할 수 있습니다.

 

다음으로는 쉘코드에 대해서 알아보도록 하겠습니다.

소스코드 다운로드 : wget https://raw.githubusercontent.com/ICEB3AR/2020_Whois_Pwnable/main/week1/shell.c

컴파일 gcc -o shell shell.c

해당 코드에서 사용된 system함수는 stdlib.h에 정의되어있습니다.

우리가 기본적으로 리눅스 커맨드 라인으로 수행하는 ls, cd, cat, echo등등 명령을 인자로 전달하면 동작결과를 보여주는 함수입니다.

명령어 수행과 더불어 파일명을 전달하면 해당 파일을 실행시킬 수도 있습니다.

system함수의 인자로 전달한 /bin/sh는 리눅스의 쉘프로그램으로 명령어를 입력하고 결과를 출력하는 명령어 입력기 입니다.

기본적으로 리눅스의 터미널을 켜서 명령을 입력하고 하는 프로그램을 쉘이라고 합니다.

직접 /bin/sh를 실행해보세요. (종료는 exit입력)

아까 컴파일했던 ./shell을 실행하게 되면, 쉘이 뜨게됩니다.

쉘코드란 방금처럼 쉘을 실행하는 코드를 기계어로 번역한 결과입니다.

소스코드 다운로드 : wget https://raw.githubusercontent.com/ICEB3AR/2020_Whois_Pwnable/main/week1/shellcode.c

컴파일 : gcc -fno-stack-protector -no-pie -z execstack -o shellcode shellcode.c

간단한 예시를 통해 shellcode를 실행시켜보겠습니다.

위의 코드를 보면 shellcode라는 문자열을 하나 생성하고, 함수포인터로 shellcode문자열의 주소를 전달했습니다.

함수포인터를 실행하게되면, 함수포인터로 지정한 주소를 call하게됩니다.

즉 shellcode 문자열로 jmp하게 됩니다. 그렇게 되면, 쉘코드의 기계어가 수행되면서 쉘을 실행시킵니다.

컴파일한 shellcode를 실행해보면 쉘이 실행되는 것을 볼 수 있습니다.

buffer overflow가 가능한 상황에서는 위와같이 shellcode를 스택에 저장한뒤, ret을 shellcode가 위치한 주소로 지정해주게되면, return이 일어날 때, shellcode가 위치한 주소로 jmp되면서 쉘을 획득할 수 있습니다.

 

'Security > Pwnable' 카테고리의 다른 글

[pwnable] 우분투 설치 후 깔아야 하는 것들 모음  (0) 2020.01.20
복사했습니다!