본문 바로가기

3. Pwnable (포너블)/2) 개념 정리

[2020.05.19] pw.Sly - 달고나 문서 p44~p57

- 달고나 문서 p44~p57까지의 내용

 

<NULL 제거>

char형 문자열은 16진수를 인식하여 1바이트 데이터로 저장해주므로 기계어 쉘 코드에를 char 형으로 전달한다. 그런데 그 중 0x00과 같은 기계어 코드가 매우 많은데, char 형은 0의 값을 만나면 이후 값을 모두 무시해버려 기계어 코드의 NULL 값을 모두 제거해주어야 한다. \0x00 값 말고도 mov $0xb, %eax 코드도 00을 만들어내므로 고쳐주어야 한다.

이전의 코드는 [05.12]의 sh01.c 코드인데, 이를 고쳐서 sh02.c 코드로 만든다.

 

NULL제거 전
NULL제거 후

이제 위 sh02.c 코드를 문자열화 해야한다. char형 배열에 16진수 형태의 바이너리 데이터를 전달하기 위해 \x90형식으로 바꾸어 sh03.c 코드로 만든다.

"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"

위처럼 한 줄에 쓸 수도 있고 줄마다 바꾸어 써도 된다.

 

<쉘 코드 실행>

sh03.c의 main()

위 그림 끝에 }가 잘렸다.

쉘 코드 실행 프로그램인 sh03.c의 동작을 보기 위하여 diassemble한다.

 

diassemble한 모습

위 코드는 main()을 diassemble한 모습이다.

 

먼저 ebp-4 지점의 address를 eax 레지스터에 넣고, eax 레지스터에 8을 저한다. 이 과정은 main()의 ret = (int *)&ret +2; 과정에 해당한다. ret의 포인터 변수의 주소에 8바이트를 더해서 8바이트 상위의 주소로 만든다. 이때, int *형에 1을 더하면 실제로는 4가 더해지는것과 같으므로 2를 더하면 실제로 8을 더하는 것과 같아진다. 

 

다음으로 return address가 들어있는 지점의 주소 값을 ebp-4 지점에 넣어준다. 그리고 ebp-4 지점의 값을 return address가 있는 지점에 넣어준다. 마지막으로 eax 레지스터가 가리키는 지점에 $0x804936c를 넣어준다.

 

위 명령을 모두 수행하면 main()이 종료되고 EIP는 return address가 가리키는 지점의 명령을 수행하는데, return address에는 쉘 코드가 들어있는 지점의 주소가 들어있다. 결과적으로, 시스템은 공격자가 넣은 쉘 코드를 수행한다.

 

char형 말고도 int형으로 만들 수도 있다. 아래는 little endian으로 정렬하여 4바이트 단위(int)로 만든 경우이다.

 

sh04.c

main()은 sh03.c와 동일하다. 하지만 int형 배열보다 char형 배열로 생성하는것이 더 편하다.

 

<setuid와 exit>

buffer overflow 공격이 성공하면 공격자는 쉘을 얻게 되고, 이후 root권한을 얻어야 한다.

이때 setuid 비트가 있는 프로그램을 이용할 수 있는데, 이를 위해 쉘 코드를 수정할 필요가 있다.

 

setuid 추가한 모습

프로그램에 setreuid()함수를 추가했으니 쉘 코드에도 해당 함수의 기계어 코드를 추가해주어야 한다.

 

setreuid()함수의 기계어 코드

execve()함수와 같이 static으로 컴파일하여 setreuid()함수의 기계어 코드를 찾아낼 수 있다. 이제 이 기계어 코드를 이전에 만든 쉘 코드 앞에 붙여 넣어주면 쉘 코드 수정이 완료된다.

 

이제 buffer overflow 공격을 모두 수행했으면 프로그램을 정상적으로 종료할 필요가 있다. 

exit(0)의 기계어 코드는 "x31\xc0\xb0\x01\xcd\x80"이다. 이 코드는 쉘 코드의 맨 뒤에 붙여준다.

 

이렇게 쉘 코드 작성이 완료되었다.

 

<Buffer Overflow 공격>

vul.c 프로그램은 buffer overflow 취약점을 가지고 있다. 

 

vul.c 프로그램 中

위에서 strcpy()함수는 문자열의 길이를 체크하지 않기 때문에 BOF에 취약하다. 쉘 코드를 실행시키려면 이 프로그램의 return address 지점에 쉘 코드가 있는 지점의 address를 넣어주어야 한다. 즉, 쉘 코드가 있는 곳의 address를 찾아야 하며 여기에는 여러 방법이 존재한다.

 

먼저 오로지 추측만으로 address를 찾는 방법이다. 여러번 시행착오를 거치며 쉘이 떨어질때까지 공격 하는 것이다. 쉘 코드가 실행되는 확률을 높이기 위해 buffer를 NOP으로 채우는데, NOP은 보통 0x90값을 사용한다.

 

<NOP>

No Operation -> 아무런 실행을 하지 않는다

 

NOP은 주로 명령어들이 섞이지 않게 하기 위해 명령어를 끊기 위한 목적으로 사용된다. CPU는 NOP을 만나면 아무런 수행을 하지 않고 유효한 명령어를 찾을 때까지 한 바이트씩 이동한다. Buffer Overflow 공격에서는 이러한 특성을 이용하여 CPU가 유효한 명령이 있는 쉘 코드의 시작점이 나올때까지 EIP를 이동시킨다.

 

먼저 쉘 코드 앞을 NOP으로 채우고 return address를 NOP으로 채워져 있는 영역의 어딘가의 주소로 바꾼다.

 

EIP의 이동과정

 

위 그림은 0xbffffa30~0xbffffa4c 영역에 NOP이 채워져 있다. return address를 이 영역 사이의 임의의 주소 값으로 바꾸면, EIP는 NOP을 계속 한 바이트씩 건너 뛰며 쉘 코드가 시작하는 0xbffffa4c 지점까지 이동하게 된다. 그러나 이 방법은 매우 힘들고, 현재에는 쉬운 방법들이 많이 나와있기 때문에 거의 사용하지 않는다.

 

또 다른 방법은 다음 개념에 서술한다.

 

 모든 사진의 출처는 달고나 문서임.