개요


첫 번째 문서에서 취약점을 찾고, 이를 이용해 공격 코드를 만들어서 공격을 하여 공격자가 원하는 행동을 하였다. 공격자가 원하는 행동을 하는 코드로 이동하기 위해서 스택에 삽입하고 '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.


+ Recent posts