ROP 제대로 이해될때까지 공부

2018. 4. 4. 08:570x04 pwnable/윈도우즈 어플리케이션 취약점 분석

728x90

DEP는 스택에 있는 코드가 실행될 수 없게 만드는 기술 


ASLR은 스택, 힙, 모듈 베이스 주소를 랜덤화 : 주소 또는 메모리의 위치를 예측 불가하게 만든다.


ROP 체인 구성 기준 


1. 전체 함수를 사용하는 대신에 명령어의 연속된 작은 덩어리를 이용한다.

2. 명령어 조각은 2개~5개 정도가 적당하다.

3. 모든 명령어 조각은 ret로 끝나야 한다. (핵심)

4. 명령어 조각들은 'gadget'으로 서로 연결 되면서 명령어 덩어리가 된다.

5. gadget은 의도 된 특정 행동을 수행한다.

6. 공격자는 gadget를 조합해서 새로운 공격을 수행한다.


DEP를 우회하지 않고는 스택에서 점프 혹은 명령어 실행을 절대 할 수 없다.


DEP로 보호되고 있는 페이지에서 코드를 실행하려는 시도가 포착되면 접근 위반(STATUS_ACCESS_VIOLATION (0xc0000005))이 발생하게 된다.


스택에 있는 코드를 실행할 수 없기 때문에 로드 된 모듈에서 가져온 CALL 함수 또는 기존 명령을 실행시키고 스택의 데이터를 인자 값으로 주어야 한다.


WinExec와 같은 함수가 발견되면 공격자에겐 완전 땡큐다. 

ret-to-libc가 가능하기 때문이다.


스택에 존재하는 쉘코드를 페이지를 실행 가능하도록 표시 후 쉘코드로 점프 할 수도 있다.

-> VirtualProtect의 0X40옵션 

데이터를 실행 가능 영역으로 복사하고 그 위치로 점프할 수도 있다.

(공격자는 메모리를 할당한 뒤 해당 영역을 실행 가능한 영역으로 먼저 표시해야 하기도 한다.)


쉘코드를 실행하기 전에 무조건 DEP를 변경해야한다.


DEP를 우회하기 위해서는 Native Windows api 또는 함수호출을 사용해 수정할 수 있다.


DEP 우회하겠다고 마음 먹으면 반드시 WIN32 API를 호출해야 한다.

API에 들어가는 인자들은 모두 스택에 PUSH되기 때문이다.


스택의 모양은 무슨일이 있어도 바꾸면 안된다.


DEP를 우회 또는 비활성화 시킬 수 있는 함수들 


1. VirtualAlloc 함수가 DEP를 우회 또는 비활성화 시킬 수 있다.

VirtualAlloc(MEM_COMMIT + PAGE_READWRITE_EXECUTE) + copy memory

실행 가능한 새로운 메모리 영역을 생성하고, 쉘코드를 거기에 복사한 다음 실행할 수 있게 한다.

이 기법은 2개의 API를 서로 체인으로 연결하는 작업이 필요하다.


2. HeapCreate(HEAP_CREATE_ENABLE_EXECUTE) + HeapAlloc() + copy memory 음..copy memory에는 쉘코드가 들어가나??

VirtualAlloc과 비슷하나, 3개의 체인을 사용하는것이 엄연히 다르다.


3. SetProcessDEPPolicy() 

이 함수는 현재 프로세스에 대한 DEP정책을 바꾸어 준다. 

하지만 조건이 있다 OptIn이나 OptOut일 때만 해당된다.


4. NtSetInformationProcess()

현재 프로세스에 대한 DEP정책을 바꾸어 준다.


5. VirtualProcect 

이 함수는 쉘코드를 쓰기와 실행이 허용 된 다른 메모리 위치로 복사하는 기능 


알아두어야 하는게 있다.

API가 호출 될 때 함수의 인자가 스택의 최상위 즉, esp에 위치하게 된다. esp는 계속 변하니까 스택이 쌓이면 esp가 변경되어야 하니까. 

공격자는 어떤 방법으로 스택에 있는 어떠한 코드도 실행시키지 않고 스택 값들을 바꾸냐 이것을 잘하면 버그바운티 성공한다...


VirtualAlloc : 새로운 메모리를 할당하게 된다. 

함수에 사용되는 인자 중 하나는 새로 할당되는 메모리영역의 실행 및 접근 수준을 명시한다.

그러므로 이 값을 EXECUTE_READWRITE로 설정한다.


VirtualAlloc의 원형이다

LPVOID WINAPI VirtualAlloc(LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)


VirutalAlloc은 함수 이므로 이 함수가 끝나고 돌아갈 주소를 담을 retrun address 주소가 설정되어 있을 것이다.

lpAddress라는 인자 값은 할당하려는 영역의 시작 주소(==메모리에 할당하고 싶은 "새로운"위치)가 주소로 쓰인다.

보통 여기에는 하드코딩을 한다. 

dwSize는 바이트 단위로 할당하려는 영역의 크기 표시인데 ROP를 이용하지 않는다면 "널 값이 공격코드에 포함되어 무용지물이 되어버린다"

flAllocationType은 0x1000(EXECUTE_READWRITE)로 설정해줘야 한다. 그래야 공격코드를 스택에 삽입할 수 있다.

flProtect는 0x40(EXECUTE_READWRITE)로 설정해줘야 한다. 이 값을 생성 및 스택에 쓰기 위해 ROP를 사용해야 할 수도 있기 때문이다 


VirtualAlloc함수 호출에 성공하게 되면 정해둔 크기만큼 새로운 메모리가 할당되고, 그 주소 값은 EAX 레지스터에 저장된다.


VirtualAlloc을 실제로 사용하려면 해당 바이너리를 열어서 어디 부분이 빈 공간인지 볼 필요가 있을 거라 판단된다.

VirtualAlloc의 리턴주소는 쉘코드를 새롭게 할당한 영역에 복사하고 그곳으로 점프하는 역할을 하는 ROP 체인을 가리키고 있어야 한다 


메모리를 할당했다면 이제 메모리를 복사하는 memcpy와 메모리에 직접 값을 쓸 수 있는 WriteProcessMemory함수를 적극 이용해야 한다.


HeapCreate : 공격 코드에 사용할 수 있는 힙 영역을 생성한다. 공간은 프로세스의 가상 주소 공간에 "예약"된다.

HeapCreate함수는 인자로 DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize가 있다 

flOptions 인자가 0x00040000(HEAP_CREATE_ENABLE_EXECUTE)로 세트되면, 이 힙에 할당 된 모든 메모리 블럭이 DEP 활성화 여부에 상관없이 실행 가능하도록 바뀌어 버린다.

개 좋다... 0x00040000(HEAP_CREAT_ENABLE_EXECUTE)기억하자.


dwInitialSize 인자는 힙의 시작 크기를 나타내는 값 (바이트형식)이다. 

해당 인자를 0으로 설정하게 되면 "하나의 페이지"만 할당되게 된다. 

dwMaximumSize는 힙의 최대 크기를 바이트 혀익으로 나타낸다.


HeapCreate함수는 자체 힙만 할당하여 실행 가능한 영역으로 표시를 하게 한다.

이 함수를 써도 추가적으로 HeapAlloc이란 함수를 이용해서 힙 영역의 메모리를 할당해야하고 쉘코드를 해당 힙 위치에 복사해야 하는 MEMCPY를 사용해야 한다.


HeapCreate함수가 리턴될 때 새롭게 생성된 힙을 가리키는 포인터가 EAX에 저장되게 된다. 

후에 HeapAlloc을 호출할 때 반드시 필요한 포인터이다.


VirtualProtect함수 : 호출 프로세스에서 메모리의 접근 보호 수준을 변경한다.

VirtualProtect함수의 인자는 5개이다.

원형은 다음과 같다. 

BOOL WINAPI VirtualProtect(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect)

이 함수도 호출 후 되돌아갈 주소가 있어야 하므로 return address를 설정해줘야하고 익스플로잇에서는 그 주소가 쉘코드의 주소가 된다.

lpAddress는 접근 보호 속성을 바꿔야 할 페이지 영역의 베이스 주소를 가리키는 포인터이다. 쉽게 말하면 이 주소는 스택에 위치한 쉘코드의 베이스 주소가 된다.

dwSize는 바이트 의 수를 정하는데 전체 쉘코드가 실행될 크기만큼 설정해야 한다. 그렇다면 페이로드 작성할 때 마다 유동적으로 바뀌어야 할 것이다. 

만약 쉘코드가 디코딩 작업과 같은 특정 이유로 인해 확장되게 되면, 이 추가 바이트를 이용하게 될 것이다 .


flNewProtect는 새로운 보호 속성을 명시한다. 보통적으로 사용하는 옵션은 PAGE_EXECUTE_READWRITE 0x00000040이다. 

lpflOldProtect는 이전에 가지고 있던 접근 보호 속성 값을 받을 변수를 가리키는 포인터이다. 이 부분이 제일 이해가 되지 않았던 부분인데 어떤 블로그에선 0x01010101이렇게 설정했기

때문이다.


WriteProcessMemory함수 

원형 : BOOL WINAPI WriteProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfByteWritten)

메모리 변조할 때 요즘 자주 쓰고 있는 API인데 익스플로잇에서는 쉘코드를 실행 가능한 다른 위치로 복사하는 역할로 사용된다. 결국 쉘코드를 실행할 수 있게 되는 것이다.

복사가 진행되는 동안 WPM(WriteProcessMemory)은 복사되는 메모리 위치를 쓰기 가능하도록 만든다. 공격자는 복사되는 위치가 실행 가능 한지 확인해야 한다.


이 함수 역시 return address가 있어야 하고, 중요한 사실은 lpBaseAddress와 return address의 주소는 동일해야 한다(쉘코드의 시작주소가 되겠다)

그리고 hProcess는 현재 프로세스를 가리키기 위해 0xFFFFFFFF 정적 값을 유지해야 한다. 

파이썬에서 hProcess = "0xFFFFFFFF" 라고 표현 할 수 있겠다


WPM기법은 2가지가 있다.


1. full WPM() call 기법

해당 기법은 공격자가 쉘코드를 실행가능한 위치에 복사해서 그리로 점프하게 한다.

해당 기법이 동작하기 위해서는 WPM()인자들이 제대로 세팅되어야 한다.

문서에서는 Oleaut32.dll을 예시로 들고 있다.

Oleaut32.dll은 .text섹션이 R,E를 가지고 있다. 우리는 W권한이 있어야 쓸 수 있다는 것을 안다. 

R+E만 적용된 영역에 쉘코드를 삽입(쓰게)하게 되면 여기에 쓰인 쉘코드는 스스로를 변경하지 못하게 된다.

중요한 사실이 하나 있다 WriteProcessMemory함수는 해당 위치를 임의로 쓰기 가능하게 할순 있지만, 곧 다시 원래의 레벨을 찾게 된다.

이는 공격자가 인코딩 된 쉘코드를 쓰면 결국 정상적으로 동작하지 않을것이라는 의미이다. -> 오염 문자를 야기할 수도 있다.


여기서 이제 드디어 lpNumberOfByteWritten이 어떻게 쓰이는지 알게 되었다.

쉘코드가 목적지 주소에 복사 된 뒤 오염되는 것을 피하기 위해 목적지 주소보다 이전에 배치되어야 한다.

가령 쉘코드의 시작주소를 0x7FF20103에 했다고 가정하면, 0x7FF20103보다 이전 주소에 배치를 해야한다. 


디코더를 사용하는 쉘코드(hyunmini님강의에서 들음)를 사용하고 싶다면, 인코딩된 쉘코드를 실행하기 전에 현재 메모리 영역을 W/R 가능 영역으로 만들기 위해 

VirtualProtect 호출 코드를 쉘코드에 첨가해야한다. 그렇다면 쉘코드 만들 때 단순히 계산기 코드만 넣는게 아니라는 말이 된다.


오늘은 여기까지.