- 달고나 문서 p27~p44까지의 내용
• 버퍼 : 시스템이 연산 작업을 하며 필요한 데이터를 일시적으로 저장하는 메모리 상의 공간으로 주로 스택에 생성된다.
<buffer overflow의 원리>
- 미리 준비된 버퍼에 버퍼의 크기보다 큰 데이터를 쓸 때 발생한다.
예를 들어 위의 그림은 40바이트의 스택이 준비되어 있는 상태이다. 만약 40바이트 이상의 데이터를 쓴다면 아래와 같은 현상이 일어난다.
• 41~44바이트 -> 이전 함수의 base pointer를 수정
• 45~48바이트 -> return address 수정
• 48바이트 이상 -> return address를 넘어 이전에 스택에 저장된 데이터들 수정
즉, buffer overflow 공격은 return address가 저장되어 있는 지점에 공격 코드의 주소를 집어넣어 EIP에 공격자의 코드가 있는 주소를 들어가게 하여 공격을 하는 방법이다. 이때 공격자는 return address의 지점을 정확하게 조작해주어야 한다.
아래 그림은 임의의 공격 코드가 스택 상의 어디에 위치하게 되는지 보여준다.
<Byte order>
- big endian 방식과 little endian 방식이 존재한다.
• big endian 방식 : 높은 메모리 주소에서 낮은 메모리 주소로 저장
• little endian 방식: 낮은 메모리 주소에서 높은 메모리 주소로 저장
ex) 16진수 74E3FF59값을 저장할 때
• big endian -> 74E3FF59
• little endian -> 59FFE374
- little endian 시스템에 return address 값을 넣을 땐 바이트 순서를 뒤집어서 넣어주어야 한다.
<쉘 코드가 지정된 스택 공간을 초과하는 경우>
쉘 코드가 24바이트 공간 안에 들어가야 하는데 24바이트를 초과하게 된다면 스택 상의 다른 공간에 들어가야 한다.
이때 바뀐 스택 공간의 주소를 정확하게 알아내는 것이 매우 어려우므로 간접적으로 명령 수행 지점을 변경해야 한다.
아래는 간접적으로 명령 수행 지점을 변경하는 과정이다.
위 그림에서 쉘 코드가 return address의 아래에 존재하는 경우이다. 함수가 실행을 마치고 return address가 POP되어 EIP에 들어가면 스택 포인터는 4바이트 위로 이동하게 되어 ESP가 return address가 있던 자리 위를 가리키게 된다.
이 때 ESP를 쉘 코드의 시작 지점을 가리키도록 4 + 4 + 40 = 48바이트를 빼주고 jmp %esp instruction을 수행하여 EIP에 ESP가 가리키는 지점의 주소를 넣는다. 이렇게 되면 EIP는 쉘 코드의 시작 지점을 가리키게 된다. ESP 레지스터가 사용자가 수정 가능한 레지스터이므로 가능한 방법이다.
위 방법 말고도 다른 공간을 찾아내어 명령 수행 지점을 변경할 수 있다.
<쉘 실행 프로그램>
• 쉘 : 사용자의 키보드 입력을 받아 실행파일을 실행시키거나 커널에 어떠한 명령을 내릴 수 있는 대화통로
• 쉘 코드 : 쉘을 실행시키는 코드로 주로 바이너리 형태의 기계어 코드
쉘 코드를 만들기 위해 먼저 쉘을 실행시키는 프로그램을 작성하여 어셈블리 코드를 얻어내 일부 수정을 거쳐 바이너리 형태의 데이터를 만들어내야 한다. 쉘을 실행시키려면 '/bin/sh' 명령을 내리면 된다.
위 그림은 쉘을 실행시키기 위해 execve()함수를 사용한 c 코드이다.
execve() 함수는 바이너리 형태의 실행 파일이나 스크립트 파일을 실행시키는 함수로 세 개의 인자가 필요하다.
• 첫 번째 인자 : 파일 이름 -> shell[0]
• 두 번째 인자 : 함께 넘겨줄 인자들의 포인터 -> shell
• 세 번째 인자 : 환경 변수 포인터 -> NULL
이제 바이너리 코드를 얻어야 하는데, execve() 함수가 libc에 들어있으므로 static library 옵션을 주어 컴파일 해야한다.
<동적 링크 라이브러리와 정적 링크 라이브러리>
• 동적 링크 라이브러리 : 운영체제가 많은 응용프로그램들이 공통적으로 사용하는 명령어의 기계어 코드를 가지고 있고 다른 프로그램에게 빌려주는 것
• 정적 링크 라이브러리 : 기계어 코드를 실행파일이 직접 가지고 있는 것
리눅스는 libc라는 라이브러리에 .so 또는 .a라는 확장자 형태로 존재하며 윈도우에서는 DLL 파일로 존재한다. 정적 링크 라이브러리는 운영체제의 영향을 받지 않으며, 실행파일만 가지고 있는 경우가 많다. 하지만 본인이 기계어 코드를 가지고 있는 만큼 실행파일의 크기가 동적 링크 라이브러리 파일에 비해 커질 수 밖에 없다.
sh.c코드를 static library 옵션을 주어 컴파일 하려면 gcc -static -g -o sh sh.c를 입력하면 된다.
<execve()>
아래 기계어 코드는 address | 기계어 코드 | 어셈블리 코드로 구분되어 있다.
execve()함수는 스택에 쌓인 인자값을 검사하고 이상이 없으면 인터럽트를 발생시켜 시스템 콜(system call)을 수행한다. 따라서 인터럽트를 발생시키기 이전에 레지스터에 각 인자들을 넣어야 한다.
위 그림을 보면 mov 명령을 3번 실행하고 있다.
• 1번 mov : ebp 레지스터가 가리키는 곳의 +8 바이트 지점 값을 ebx 레지스터에 넣는다.
• 2번 mov : ebp 레지스터가 가리키는 곳의 +12 바이트 지점 값을 ecx 레지스터에 넣는다.
• 3번 mov : ebp 레지스터가 가리키는 곳의 +16 바이트 지점 값을 edx 레지스터에 넣는다.
ebp+0 지점에는 이전 함수의 base pointer가, ebp+4에는 return address가 있을 것이다. 그리고 위에 나열된 인자들은 연순으로 PUSH되어 스택에 들어가 있다.
int $0x80은 인터럽트 영역으로 시스템 콜을 하라는 뜻인데, 호출 이전에 eax 레지스터에 시스템 콜 벡터를 지정해주어야 한다. execve()함수에 해당하는 값은 11이므로 eax 레지스터에 11(0xb)를 넣는다.
<main()>
main()도 execve() 호출을 위해 처리 과정을 거쳐야 한다.
위 코드를 보면 push가 3번 이루어지는 것을 볼 수 있다. 이는 execve()의 세 인자를 넘겨주는 과정이다.
먼저 '/bin/sh'가 들어있는 곳의 주소(0x8089728)을 ebp-8 지점(0xfffffff8)에 넣고 ebp-4 지점(0xfffffffc)에는 0을 넣는다. 이 과정은 sh.c 코드의 shell[0] = "bin/sh";, shell[1] = NULL;과 같다. 이제 이 값들을 PUSH한다. NULL을 push하고, ebp+8의 주소를 eax 레지스터에 넣은 뒤에 eax 레지스터를 PUSH하고, ebp+8 값을 PUSH하면 세 개의 인자를 모두 전달할 수 있다. 인자를 모두 전달하면 call 804c75c<__execve)로 execve()를 호출한다.
<쉘을 띄우기 위한 과정>
1. 스택에 execve()를 실행하기 위한 인자를 배치한다.
2. NULL과 인자값의 포인터를 스택에 넣는다.
3. 범용 레지스터에 값들의 위치를 지정한다.
4. interrupt 0x80을 호출하여 시스템 콜 11을 호출한다.
<코드 작성>
'/bin/sh'가 저장되어 있는 메모리 공간의 주소를 정확히 찾기 어려우므로, 직접 넣어주는 코드를 작성하여야 한다.
코드는 아래와 같다.
위에서 /sh\0와 /bin은 어셈블리 코드로 만들면 $0x0068732f, 0x6e69622f이다. 이제 위 코드를 main() 함수 내에 작성하여 sh01.c 코드를 작성할 수 있다. 이후 과정은 다음 개념에 서술한다.
• 모든 사진의 출처는 달고나 문서임.
'3. Pwnable (포너블) > 2) 개념 정리' 카테고리의 다른 글
[2020.11.07] 4너블4ever - 달고나 문서 44p~57p (0) | 2020.11.07 |
---|---|
[2020.9.19] 4너블4ever - 리눅스 기초 명령어 + 달고나 문서 ~p12 (0) | 2020.09.20 |
[2020.05.19] pw.Sly - 달고나 문서 p44~p57 (0) | 2020.05.23 |
[2020.04.07] pw.Sly - 달고나 문서 ~p27 (2) (0) | 2020.04.26 |
[2020.03.31] pw.Sly - 달고나 문서 ~p27 (1) (0) | 2020.04.26 |