tmxklab

Universal Shell code(x86)원리 및 실습 본문

Security/01 System Hacking

Universal Shell code(x86)원리 및 실습

tmxk4221 2020. 5. 17. 15:58

[윈도우 시스템 해킹 가이드 버그헌팅과 익스플로잇 - 김현민]책을 읽고 실습하면서 몰랐던 내용들이나 실습한 내용들을 나름 정리해보았다.

0. 문제점

쉘 코드에 포함되는 API함수를 사용하기 위해 주소 값을 알아야 하는데 Windows 7이상의 OS부터 부팅할 때마다 kernel32.dll의 상위 2bytes 주소 값이 매번 바뀐다.

 

Example) Python 코드를 이용하여 WinExec함수의 주소 값 확인(환경 : Windows7)

[ 재부팅 전]

[ 재부팅 후 ]

상위 2bytes의 주소 값이 바뀌는 것을 확인할 수 있다.

 

API란?

API(Application Programming Interface)란 OS가 애플리케이션을 위해 제공하는 함수의 집합으로 애플리케이션과 디바이스를 연결해주는 역할을 한다.

OS는 시스템 자원들(메모리, 디바이스, 프로세스, 파일 등)을 관리하는 역할을 한다. 애플리케이션이 이러한 자원들을 사용하기 위해서는 커널에게 자원 사용을 요청해야 하며 유저단에서 커널에게 요청할 수 있도록 인터페이스를 구현한 것이 Win32 API를 사용하여 애플리케이션을 만드는 것이다.

 

kernel32.dll이란?

윈도우에서 실행되는 모든 애플리케이션들은 내부적으로 윈도우 API함수를 호출하는 형태이며 기본적으로 필요한 기능들은 DLL(Dynamic Link Library)형태로 만들어 OS안에 내장되어 있다.
모든 프로세스는 기본적으로 kernel32.dll이 메모리에 로딩되며 kernel32.dll은 ntdll.dll을 로딩한다. ntdll.dll의 역할이 유저단에서 커널에 요청하는 작업을 수행하여 애플리케이션이 시스템 자원에 접근할 수 있는 것이다.

 

 

Win32 API의 핵심 DLL

1) kernel32.dll

부팅시 로드되어 모든 프로그램에 사용되고 커널이 애플리케이션에게 제공할 수 있는 서비스 함수 형태로 존재하며 프로세스, 스레드, 메모리 관리를 한다.

 

1-1) ntdll.dll

- kernel32.dll과 함께 부팅 시 로드되어 모든 프로그램에 사용되며 커널 메모리 영역을 사용할 수 있도록 해주는 역할

- 유저 모드 애플리케이션들에게 커널 API를 연결하는 역할

추가) 모든 프로그램들은 실행되면 항상 ntdll.dll과 kernel32.dll을 사용

 

2) user32.dll

프로그램이 실행될 때 gdi32.dll파일과 드라이버(WIN32K.SYS)를 호출하는 역할(창이나 메뉴 같은 GUI 구성요소들을 구현해주는 역할)

 

3) gdi32.dll

윈도우에서 마우스 움직임, 그림, 화면 등 GUI의 가장 기본이 되는 DLL

 

참고 그림)

출처 : https://kdata.or.kr/info/info_04_view.html?field=&keyword=&type=techreport&page=32&dbnum=178192&mode=detail&type=techreport

 

참고 링크)

 

윈도우 후킹 원리 (1) - User Mode

Intro 리버싱을 하는 데 있어 흔히 "Art of Reversing is an API Hooking"이라는 말과 같이 API 후킹은 리버싱의 꽃이라 일컬어진다. 어떤 윈도우 응용프로그램을 개발하기 위해서 우리는 다양한 종류의 언어

kali-km.tistory.com

 

윈도우 라이브러리 파일 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 둘러보기로 가기 검색하러 가기 이 글은 마이크로소프트 윈도우 라이브러리 파일들에 대한 설명이다. 마이크로소프트 윈도우 운영 체제는 dll이라고 알려진 ��

ko.wikipedia.org

 

결론

시스템 자원에 접근할 수 있는 WinAPI함수를 사용하기 위해서 함수의 주소를 알아야 하며 이러한 함수들은 kernel32.dll에 포함되어 있다. 하지만, Win7이상 부터 kernel32.dll의 상위 2bytes의 주소 값이 부팅될 때마다 변경된다.

따라서, 프로세스 상에서 함수의 주소 값을 동적으로 구해올 수 있는 Universal 쉘 코드를 작성해야 한다.

 


 

1. Universal 쉘 코드

1.1 원리

dll의 시작 주소 값과 dll 시작주소부터 함수까지의 offset을 알면 동적으로 함수의 주소 값을 가져올 수 있다.

 

다음 소개되는 내용은 쉘 코드 원리를 이해하기 위해 추가적인 배경지식이다.

 

1) TEB(Thread Environment Block, TIB라고도 불림)

- 현재 실행되고 있는 쓰레드에 대한 정보를 담은 구조체

- 세그먼트 레지스터 FS(x86) 또는 GS(x64)의 오프셋을 통해 TEB에 접근가능

- TEB를 통해 Win32 API를 호출하지 않고 TEB구조체 안에 PEB를 가리키는 포인터를 통해 프로세스에 대한 많은 정보를 얻을 수 있다.

 

1-1) TEB 구조체 확인(windbg사용)

일부만 가져왔으며 0x30위치에 PEB를 가리키는 포인터가 존재

 

1-2) TEB 시작 주소 확인(windbg 사용)

self필드에 teb의 주소 값 확인

 

2) PEB(Process Environment Block)

- 현재 실행되고 있는 프로세스에 대한 정보를 담은 구조체

- 프로세스에 로드된 PE Imgae(EXE, DLL 등)에 대한 정보도 기록

 

2-1) PEB 구조체 확인(windbg사용)

peb구조체 안에 ldr변수가 존재하며 ldr은 프로세스에 로드된 모듈에 대한 정보를 담고 있는 구조체(_LDR_TABLE_ENTRY)를 가리키는 포인터

 

2-2) PEB 시작 주소 확인(windbg 사용)

 

참고)

프로세스가 생성되면 커널 메모리에 EPROCESS구조체가 생성되어 모든 프로세스는 각각 EPROCESS구조체를 하나씩 갖게 되며 사용하는 쓰레드의 개수만큼 ETHREAD구조체를 갖는다.

 

그리고 EPROCESS와 ETHREAD내부에 각각 PCB(Process Control Block), TCB(Thread Control Block)란 이름의 KPROCESSKTHREAD라는 구조체가 존재한다.

 

EPROCESS, ETHREAD는 커널 메모리에서만 접근 가능하다. 하지만 이러한 구조는 커널 메모리에만 접근해야 프로세서의 정보를 수정할 수 있으므로 유저 메모리에서 접근할 수 있도록 만들어진 것이 PEBTEB이다.

 

3) IMAGE_EXPORT_DIRECTORY

DLL은 자신이 어떤 함수들을 Export하고 있는지에 대한 정보를 PE헤더에 저장해둔다.

  • IAT(Export Address Table) : 프로그램이 어떤 라이브러리에서 어떤 함수를 사용하고 있는지 기술한 테이블
  • EAT(Export Address Table) : 라이브러리 파일에서 제공하는 함수를 다른 프로그램에서 가져다 사용할 수 있도록 해주는 역할

 

여기서 Export Table은 IMAGE_EXPORT_DIRECTORY구조체로 저장

 

3-1) IMAGE_EXPORT_DIRECTORY구조체

typedef struct _IMAGE_EXPORT_DIRECTORY{
	...
	DWORD AddressOfFuncitons 
	DWORD AddressOfName
	DWORD AddressOfNameOrdinals
}

 

- AddressOfFunctions : export 함수들의 시작 주소까지 offset

- AddressOfName : 함수 이름 배열 주소

- AddressOfNameOrdinals : 함수의 서수 배열

 

1.2 동적으로 Win32 API함수 주소 찾기 실습

실습은 "iexplore.exe"를 대상으로 한다.

 

1) dll의 시작 주소 값 구하기(DLLBase)

1-1) 순서

 

1-2) 실습

① TEB 주소 찾기

 

② PEB 주소 찾기

 

③ ldr 주소 찾기

 

④ LDR_DATA_TABLE_ENTRY 구조체 확인

참고로 LDR_DATA_TABLE_ENTRY구조체는 더블 링크드 리스트로 저장됨

따라서, 노드가 서로 가리키므로 0X2e1ac0에서 8bytes를 뺀 만큼 확인

 

⑤ 첫 번째로 로드된 모듈 확인("iexplorer.exe" - 자기 자신)

 

⑥ 두 번째로 로드된 모듈 확인("ntdll.dll")

 

⑦ 세 번째로 로드된 모듈 확인("kernel32.dll")

DllBase Address : 0x774b0000

 

 

2) 함수의 offset 구하기(IMAGE_EXPORT_DIRECTORY)

1-1) 순서

① DllBase주소를 통해 IMAGE_NT_HEADERSIMAGE_OPTIONAL_HEADERIMAGE_DATA_DIRECTORY의 주소를 구함

AddressOfName배열에서 원하는 함수 명을 찾아 인덱스 값을 구함

AddressOfNameOrdinals배열에서 찾은 인덱스를 통해 서수 인덱스 값을 구함

AddressOfFunctions배열에서 서수 인덱스 값을 통해 offset을 찾음

 

1-2) 실습

IMAGE_OPTIONAL_HEADERS : DllBase(0x774b0000) + IMAGE_NT_HEADERS(0xf0) + 0x18

참고 : 0xf0값은 파일 및 환경에 따라 달라지므로 직접 확인

DllBase로부터 0xf0떨어진 곳에 IMAGE_NT_HEADERS확인

 

DllBase로부터 0x108(0xf0 + 0x18)떨어진 곳에 IMAGE_OPTIONAL_HEADER확인

 

전에 windbg에서 구한 offset인 0x60을 다시 더한다.

비교 : 왼쪽 PEView, 오른쪽 windbg

 

EXPORT Table 주소 : 0xb5a34

- windbg와 PEView를 통해 서로 매칭되는 것을 확인

- 함수를 찾는데 필요한 3개의 테이블 offset을 찾을 수 있음

- Address Table RVA → AddressOfFunctions배열의 offset

- Name Pointer Table RVA → AddressOfName배열의 offset

- Ordinal Table RVA → AddressOfNameOrdinals배열의 offset

 

AddressOfName테이블에 저장된 함수명 확인

"AddAtomA"함수는 4번째에 위치하므로 "인덱스 3"

 

AddressOfNameOrdinals테이블에 저장된 서수 확인

인덱스 0일 때 저장된 서수는 "0x5"

 

AddressOfFunctions테이블에 저장된 함수 주소 확인

서수가 5이므로 6번째에 저장된 함수의 주소 값은 0x3759d

 

PEView를 통해 확인한 결과 동일한 주소 값을 갖음을 알 수 있다.

 

확인

 


 

2. Universal Shell Code 실습

2.0 실습 환경

  • OS : Win7(x86, VMWare )

  • IDE : Visual Studio 2010

 

2.1 Universal Shell Code

쉘 코드는 간단하게 WinExec함수를 통해 cmd창을 키는 쉘 코드이다.

(유니버셜 쉘 코드는 현미니님의 블로그에 [윈도우 시스템 해킹 가이드 자료실]에서 구하여 사용하였다.)

 

secuholic : 네이버 카페

해킹&보안 스터디 까페 SecuHolic 입니다 ^^

cafe.naver.com

 

2.2 Universal Shell Code 실행

실행시키면 cmd창이 켜지는 것을 확인할 수 있다.

 

2.3 참고사항

- [프로젝트 속성] → [구성속성] → [링커] → [고급] 에서 "DEP(데이터 실행 방지)"의 선택사항에 "아니오"라고 체크해야 정상적으로 돌아간다.

- 쉘 코드에서 AddressOfName테이블에 존재하는 함수 명을 일일히 찾기에는 성능이 않좋으므로 해쉬함수를 통해 모든 함수들의 함수명을 해쉬 값으로 만들어 비교하는 방법을 사용함

 

함수의 hash값을 찾기 위해 다음 파이썬 파일을 사용할 것이다.

(해당 파일은 현미니님의 블로그에서 구해서 사용하였다.)

 

[hash_calc.py]

import sys
import pefile

def usage():
        print " Usage) %s [dll]" % sys.argv[0]
        print " ex) %s kernel32.dll" % sys.argv[0]

def get_hash(srcstr):
        hashstr = 0
        for i in srcstr:
                hashstr += ord(i)
        return hex(hashstr)

print " # Hash Caculator for Universial Shellcode v1.0"

if len(sys.argv) < 2:
        usage()
        sys.exit()

pe = pefile.PE(sys.argv[1])
print "%-10s\t%-35s\t%-5s\t%-6s" % ("Address", "Name", "Ordinal", "Hash")
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
   print "%-10s\t%-35s\t%-5s\t%-6s" % (hex(pe.OPTIONAL_HEADER.ImageBase + exp.address), exp.name, exp.ordinal, get_hash(exp.name))

 


 

3. 느낀점

책을 보면서 유니버셜 쉘 코드의 필요성과 작동원리를 알게 되었고 직접 제작(은 사실 카페에 있는 코드를 가져왔지만ㅠ)하여 실습하면서 다른 공부들? 지식들이 필요하다는 것을 알게 되었다.

(Win32 API, 쉘 코드 인코딩, PE파일 등등.. 아직 더 공부해야할 것들이 많다...)

이번에는 x86환경에서 진행하였지만 이를 기반으로 다음에는 x64환경에서 쉘 코드를 직접 커스텀하여 제작하는 실습을 진행하도록 하겠다.

Comments