tmxklab

Universal Shell Code(x64)제작 및 실습 본문

Security/01 System Hacking

Universal Shell Code(x64)제작 및 실습

tmxk4221 2020. 5. 17. 15:58

이번에는 저번에 배웠던 x86환경에서 Universal Shellcode를 작성하는 원리와 실습에서 응용하여 x64환경에서 Universal Shellcode를 작성하여 실행하는데 목표를 두었다. 참고로 저번에는 WinExec함수를 통해 cmd창을 오픈하는 Universal Shellcode를 작성하였지만 응용하여 MessageBoxA함수를 통해 메시지박스를 오픈하도록 작성할 것이다.

 

0. 목표                                                                                           

x64환경에서 MessageBoxA함수를 통해 메시지 박스를 오픈하는 Universal Shellcode작성 및 실행

 


 

1. 실습 환경                                                                                      

  • OS : Windows 10(x64)

  • Debugging Tool : WinDbg Preview

  • IDE : Visual Studio 2017

  • PE파일 분석 툴 : CFF Explorer

 


 

2. 환경 구축                                                                                      

x86환경의 VS 2010에서 인라인 어셈블리가 가능했지만 x64환경에서는 인라인 어셈블리를 사용할 수 없다. 따라서, VS 2017에서 어셈블리어 코드를 작성할 수 있는 환경과 추가로 쉘 코드 작성시 디버깅을 위한 환경을 구성해주도록 한다.

 

 

인라인 어셈블리(Inline Assembly)란?

- 어셈블리 명령들을 Inline함수로 작성하는 것을 뜻하며 이전 실습에서 asm키워드를 사용하여 어셈블리 명령어를 코딩한 것을 말한다.

 

* x64 VS 2017에서 asm키워드를 사용한 경우

 

참고 링크)

 

x64용 MASM (ml64.exe)

x64용 MASM (ml64.exe)MASM for x64 (ml64.exe) 이 문서의 내용 --> Visual Studio에는 x64 코드를 대상으로 하는 32 비트 및 64 비트의 호스팅된 버전의 Microsoft 어셈블러 (MASM)가 모두 포함 되어 있습니다.Visual Studio

docs.microsoft.com

 

 

2.1 어셈블리어 코드 작성 환경 구성                                                                           

masm으로 파일을 만들어서 cpp파일에 링킹시키는 과정으로 작성

 

환경 설정은 다음 링크를 참조하여 적용하였다.

 

window x64 환경에서 Universal shellcode 만들기

window x64 환경에서 Universal shellcode 만들기 Date Person jaeho jung 목차 소개 환경 분석 3.1 쉘코드 작성 3.2 쉘코드 실행 결론 1. 소개 " 윈도우 시스템 해킹 가이드 버그헌팅과 익스플로잇" 개정판이 얼..

wogh8732.tistory.com

 

1) 프로젝트 생성 → [Windows 콘솔 응용 프로그램] 선택

 

2) 빌드에서 x64설정

 

3) [프로젝트 우클릭] → [빌드 종속성] → [사용자 지정 빌드] → [masm]체크

 

4) 이름.asm파일 생성 및 우클릭 → [속성] → [일반] → [항목형식] → [Microsoft Macro Assembler]

 

5) .asm파일에서 함수명 지정

 

6) cpp파일에 해당 asm링킹

 

2.2 쉘 코드 작성시 디버깅을 위한 환경 구성                                                                

 

1) [도구] → [옵션]

 

2) [디버깅] → [일반] → [주소 수준 디버깅 사용]체크

 

3) bp설정 및 마우스 우클릭 → [디스어셈블리로 이동]

 

4) [디버그] → [창/Windows] → [레지스터]

 

 


 

3. 분석                                                                                           

이번에도 지난 번에 배웠던 것(x86 Universal Shellcode)과 동일하게 Universal Shellcode를 작성하기 위해 dll의 시작 주소 값dll 시작주소부터 함수까지의 offset을 구하는 작업을 진행하도록 하겠다.(여기서는 MessageBoxA함수를 사용하기 위해 user32.dll의 Base Address와 offset을 구해야 함)

 

MessageBoxA함수

MessageBoxA함수를 사용하기 위해서 user32.dll파일 필요

 

MessageBoxA function (winuser.h) - Win32 apps

Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message, such as status or error information. The message box returns an integer value that indicates which button the user clicked.

docs.microsoft.com

 

 

1) MessageBoxA()를 사용하여 메시지 박스를 띄우는 코드 작성하여 exe파일 생성

 

2) CFF Explorer툴을 사용하여 MessageBoxA함수 확인

 

3.1 WinDbg와 CFF Explorer툴을 사용하여 분석                                                              

분석 대상 : UniversalShellcode.exe

 

1) TEB 시작주소 확인 - WinDbg

 

2) PEB 시작주소 확인 - WinDbg

 

3) Ldr멤버 변수 주소 확인 - WinDbg

 

4) 로드된 모듈의 리스트 시작 주소 확인 - WinDbg

x86에서 InMemoryOrderModuleList를 따라갔지만 x64에서는 InLoadOrderModuleList를 따라감

 

5) 첫 번째 모듈 확인(자기자신, "UniversalShellcode.exe") - WinDbg

 

6) 두 번째 모듈 확인("ntdll.dll") - WinDbg

 

7) 세 번째 모듈 확인("kernel32.dll") - WinDbg

 

8) 네 번째 모듈 확인("kernelbase.dll") - WinDbg

 

9) 다섯 번째 모듈 확인("user32.dll") - WinDbg

 

10) user32.dll의 export table확인 - CFF Explorer

 

11) Optional Header확인 - CFF Explorer

- PE파일 분석할 때 배웠듯이 Optional HeaderMagic값에 따라 IMAGE_OPTIONAL_HEADER가 32인지 64인지 구분해준다고 했으므로 WinDbg에서 IMAGE_OPTIONAL_HEADER64라고 적어줘야 제대로 나온다.

- 또한, Optional Header까지 Offset이 0x108이므로 아까 user32.dll의 Base주소를 구한 값에 0x108을 더하면 WinDbg에서 나온 값과 cff Explorer에서 나온 값과 동일할 것이다.

 

12) user32.dll의 IMAGE_OPTIONAL_HEADER64확인 - WinDbg

 

13) user32.dll의 IMAGE_OPTIONAL_HEADER64확인 - CFF Explorer

 

그리고 WinDbg에서 마지막 총 16개의 테이블의 시작 주소가 Optinal Header로부터 0x70옵셋을 가지는 것을 확인했으므로 다시 디버깅하여 확인한다.

 

14) Data Directory확인 - WinDbg

 

15) Data Directory확인 - CFF Explorer

 

WinDbg와 CFF Explorer에서 확인한 offset과 동일함을 알 수 있다. 이제 다시 user32.dll Base주소에 Export Table RVA의 value값을 더하여 디버깅해본다.

 

16) Export Table확인 - WinDbg

 

17) Export Table확인 - CFF Explorer

 

WinDbg와 CFF Explorer로 확인한 값들이 동일함을 알 수 있으며 주요 테이블 3개(AddressOfFunction, Names, NameOrdinals)를 확인해본다.

 

18) Export Table(Function, Names, Ordinals ) - CFF Explorer

 

19) AddressOfNames - WinDbg

(흠 근데 왜 WinDbg에서 첫 번째 함수가 CFF Explorer에서 확인할 떄는 2번째 함수인지 모르겠음 일단 넘어갖 ㅋ)

 

20) AddressOfNameOrdinals - WinDbg

 

21) AddressOfFunctions - WinDbg

 

22) 함수 주소 확인 - WinDbg

 

결론

x86환경에서 WinDbg, PE View를 통해서 PE파일을 분석한 결과와 x64환경에서 분석한 결과가 조금씩 차이를 보인다. 위에 분석을 통해 필요한 작업들을 마쳤으므로 이제 Universal Shellcode를 작성해보기로 한다.

 


 

4. 쉘 코드 작성                                                                                

이제부터 이전 작업을 바탕으로 Universal Shellcode를 작성해보기로 한다.

 

4.1 시행착오                                                                                               

1) PEB Address 구하기 

x86에서는 fs:[30]이었지만 x64에서는 gs:[60]으로 바뀜

 

참고 

 

Thread Environment Block (TEB)

Thread Environment Block (TEB) Thread Environment Block (이하 TEB) 는 프로세스에서 실행되는 thread에 대한 정보를 담고 있는 구조체 입니다. thread 별로 TEB 구조체가 하나씩 할당되고, OS 종류별로 조금씩..

kblab.tistory.com

그리고 gs레지스터를 이용하여 다음과 같이 값을 바로 가져오면 접근 에러가 발생

 

이유)

현재 rax의 값이 0xcccccccc로 세팅되어 있으므로 xor연산을 통해 rax의 값을 0x0으로 세팅해줘야 함

 

2) user32.dll Base Address 구하기 

이제 PEB를 구했으니 user32.dll Base Address를 구하는 도중 다음과 같은 문제점이 발생하였다.

 

[이거는 kernelbase.dll의 dllbase를 구한거 - windbg랑 디버깅이랑 같은 값을 가짐]

 

[이거는 user32.dll의 dllbase를 구한거 - windbg랑 디버깅이랑 다른 값을 가짐]

 

이유)

kernel32.dll과 ntdll.dll은 부팅시 메모리에 로딩되는데 user32.dll은 필요 있을 때 로딩이 된다.

그래서 왼쪽 windbg는 MessageBoxA함수가 포함된 exe파일이므로 user32.dll이 리스트에 들어가있지만 오른쪽 디버깅에서는 user32.dll이 리스트에 존재하지 않는다.

 

user32.dll 참고)

 

CreateRemoteThread로 DLL Injection하기

CreateRemoteThread로 DLL Injection하기 dll-injection hacking windows 15 Dec 2018 Table of Contents IntroductionBackgroundDebugging APIRegistryCreateRemoteThreadHookMechanismx86_64/WOW64 ProblemResultInjecting DllFuture WorkConclusion ChangeLog 2018/12/1

gwangyi.github.io

 

 

해결방법)

모든 프로세스는 kernel32.dll을 공유하고 동일한 주소 값을 가진다.

x86에서도 설명했듯이 kernel32.dll과 ntdll.dll은 무조건 메모리에 올라온 상태이다. 하지만, 핵심 dll중 하나인 user32.dll은 프로그램이 필요로할 때 dll을 호출하는 형식이다. 따라서, user32.dll의 주소 값을 구하기 위해서는 kernel32.dll에 포함된 LoadLibrary함수를 이용하여 user32.dll을 로드해야한다.

 

loadlibrary 추가 설명)

 

LoadLibraryA function (libloaderapi.h) - Win32 apps

Loads the specified module into the address space of the calling process.

docs.microsoft.com

 

참고로 kernel32.dll의 Export Table에 LoadLibrary함수가 존재하지 않으므로 이와 비슷하게 동작하는 LoadLibraryA함수를 호출하기로 한다.

 

[LoadLibraryA 테스트]

rax값에 kernel32.dll의 dllbase주소 값이 나옴(WinDbg랑 동일)

 

3) MessageBoxA함수 추가 설명 

rcx, rdx, r8d, r9d레지스터를 사용하여 MessageBoxA함수의 파라미터로 들어간다.

 

+) Windows x64 호출 규약

 

[시스템] 윈도우 x64 호출 규약 리뷰 - YoungJin Shin

미세 먼지 하나 없는 새파란 하늘에 햇살 좋은 날, 배터지게 점심 먹고 아메리카노를 한손에 집어 들고, 송도 센팍을 한가롭게 노다니며 사는 인생과 발리에서 서핑이나 하다가 심심하면 웹서핑

www.jiniya.net

파라미터가 첫 번째부터 rcx, rdx, r8d, r9d순서대로 전달되며 홈 공간을 확보한 후 함수를 호출해야 한다.

 

 

4.2 프로세스

이제 쉘 코드 작성하기 위해 진행 절차를 간략하게 정리하면 다음과 같다.

 

① kernel32.dll의 dllbase주소를 구한다.

② kernel32.dll의 dllbase를 통해 Optional Header에서 3개의 테이블을 통해 LoadLibraryA와 ExitProcess를 구한다.

③ "user32.dll"문자열을 LoadLibrary함수의 파라미터로 넘겨 반환 값(user32.dll의 dllbase)을 받는다.

④ user32.dll의 dllbase를 통해 Optional Header에서 3개의 테이블을 통해 MessageBoxA함수의 주소 값을 구한다.

⑤ MessageBoxA함수를 호출하고 ExitProcess함수를 호출하여 프로세스를 종료한다.

 

 

4.3 쉘 코드 작성   

0) 각 함수의 hash값 확인

hash_calc.py 파일은 지난번 x86에서 Universal Shellcode원리 및 실습 때 사용한 것과 동일하다.

확인)

LoadLibraryA : 0x496

MessageBoxA : 0x42f

ExitProcess : 0x479

 

1) 어셈 코드 작성

.code

SHELL PROC
jmp start

get_func_addr:
	xor r9, r9               ; NameTable Index
	xor r10, r10             ; Cal Hash
	xor rcx, rcx 
	xor rdx, rdx
	add rsi, rax
	mov ecx, dword ptr[rsi]
	add rcx, rax             ; rcx : FunctionName Address
	jmp hash

loop_ent:
	xor r10, r10
	inc r9                              ; Inc NameTable Index
	mov ecx, dword ptr[rsi+r9*4]        ; Inc FunctionName Address
	add rcx, rax
	
hash:
	mov dl, byte ptr [rcx]            ; rdx : 1byte AsciiCode
	inc rcx
	add r10, rdx
	test dl, dl
	jnz hash
	cmp rdi, r10                      ; cmp Find Func, FunctionTable Element Hash
	jne loop_ent
	mov ecx, dword ptr [rbx+24h]      ; rbx :  AddressOfNameOrdinals[Export Table]
	add rcx, rax
	mov dx, word ptr [rcx+2*r9+2]
	mov r9, rdx
	mov ecx, dword ptr [rbx+1ch]      ; rcx :  AddressOfFunctions[Export Table]
	add rcx, rax
	mov edx, dword ptr [rcx+4*r9-4]
	add rdx, rax                      ; rdx : Real Function Address
	ret

start:

	xor rax, rax                      ; Setting '0'
	xor rbx, rbx

	mov rax, gs:[rax+60h]           ; PEB Address
	mov rax, [rax+18h]              ; PEB_LDR_DATA
	mov rax, [rax+10h]              ; .exe
	mov rbx, [rax]                  ; ntdll.dll
	mov rax, [rbx]                  ; kernel32.dll
	mov rax, [rax+30h]              ; rax : kernel32.dll Base Address

	mov rbx, rax
	add rbx, 7ah                    ; PE Header Offset / 0x100 : Optional Header / 0x70 : Data Directory(First Export Table)
	add rbx, 7bh                    ; 170h = 7ah + 7bh + 7bh 
	add rbx, 7bh
	mov ebx, dword ptr [rbx]        ; rbx : Export Table RVA
	add rbx, rax                    ; rbx : Export Table Address
	mov esi, dword ptr [rbx+20h]    ; rsi : AddressOfNames[Export Table]

	xor rdi, rdi                    ; Setting '0'
	mov di, 479h                    ; 0x479 : ExitProcess()의 hash값
	call get_func_addr				
	mov [rbp+48h], rdx              ; [rbp+10h] : ExitProcess() 

	mov rbx, rax
	add rbx, 7ah                    ; PE Header Offset / 0x100 : Optional Header / 0x70 : Data Directory(First Export Table)
	add rbx, 7bh                    ; 170h = 7ah + 7bh + 7bh 
	add rbx, 7bh
	mov ebx, dword ptr [rbx]        ; rbx : Export Table RVA
	add rbx, rax                    ; rbx : Export Table Address
	mov esi, dword ptr [rbx+20h]    ; rsi : AddressOfNames[Export Table]

	xor rdi, rdi                    ; Setting '0'
	mov di, 496h                    ; 0x496 : LoadLibraryA()의 hash값
	call get_func_addr

	sub rsp, 8h
	xor rax, rax                    ; Setting '0'
	mov qword ptr[rbp+20h], rax	
	mov qword ptr[rbp+28h], rax
	mov byte ptr[rbp+20h], 75h      ; u
	mov byte ptr[rbp+21h], 73h      ; s
	mov byte ptr[rbp+22h], 65h      ; e
	mov byte ptr[rbp+23h], 72h      ; r
	mov byte ptr[rbp+24h], 33h      ; 3
	mov byte ptr[rbp+25h], 32h      ; 2
	mov byte ptr[rbp+26h], 2eh      ; .
	mov byte ptr[rbp+27h], 64h      ; d
	mov byte ptr[rbp+28h], 6ch      ; l
	mov byte ptr[rbp+29h], 6ch      ; l
	lea rcx, [rbp+20h]
	call rdx                        ; call LoadLibrary() / ret -> rax : user32.dll
	add rsp, 10h

	mov rbx, rax
	add rbx, 7dh                    ; PE Header Offset / 0x108 : Optional Header / 0x70 : Data Directory(First Export Table)
	add rbx, 7dh
	add rbx, 7eh
	mov ebx, dword ptr [rbx]        ; rbx : Export Table RVA
	add rbx, rax                    ; rbx : Export Table Address
	mov esi, dword ptr [rbx+20h]    ; rsi : AddressOfNames[Export Table]

	xor rdi, rdi                    ; Setting '0'
	mov di, 42fh                    ; 0x42f : MessageBoxA()의 hash값
	call get_func_addr
	mov rbx, rdx
	
	sub rsp, 20h
	xor rdx, rdx
	xor rax, rax
	mov qword ptr[rbp+28h], rax
	mov qword ptr[rbp+30h], rax
	mov byte ptr[rbp+28h], 68h      ; h
	mov byte ptr[rbp+29h], 69h      ; i
	
	xor r9d, r9d  
	lea r8, [rbp+28h]  
	lea rdx, [rbp+28h]  
	xor ecx, ecx
	call rbx                        ; call MessageBoxA()
	add rsp, 28h

	xor ecx, ecx
	sub rsp, 8h
	call qword ptr [rbp+48h]        ; call ExitProcess()
	add rsp, 8h

SHELL ENDP
End

 

2) 어셈 코드 테스트

 

3) 쉘 코드 변환

쉘 코드 변환 과정은 디버깅모드로 디스 어셈블리 창에서 추출하였다.

솔직히 엄청난 노가다를 하였다....

또한 디버깅을 통해 널 값과 bad char을 직접 제거해주는 방법으로 인코딩하여 쉘 코드를 제작하였다.

 

#include "stdafx.h"
#include <stdio.h>
#include <Windows.h>

unsigned char shellcode[353] = "\xEB\x52\x4D\x33\xC9\x4D\x33\xD2\x48\x33\xC9\x48\x33\xD2\x48\x03\xF0\x8B\x0E\x48\x03\xC8\xEB\x0D"
"\x4D\x33\xD2\x49\xFF\xC1\x42\x8B\x0C\x8E\x48\x03\xC8\x8A\x11\x48\xFF\xC1\x4C\x03\xD2\x84\xD2\x75\xF4"
"\x49\x3B\xFA\x75\xE2\x8B\x4B\x24\x48\x03\xC8\x66\x42\x8B\x54\x49\x02\x4C\x8B\xCA\x8B\x4B\x1C\x48\x03\xC8"
"\x42\x8B\x54\x89\xFC\x48\x03\xD0\xC3\x48\x33\xC0\x48\x33\xDB\x65\x48\x8B\x40\x60\x48\x8B\x40\x18\x48\x8B\x40\x10"
"\x48\x8B\x18\x48\x8B\x03\x48\x8B\x40\x30\x48\x8B\xD8\x48\x83\xC3\x7A\x48\x83\xC3\x7B\x48\x83\xC3\x7B\x8B\x1B"
"\x48\x03\xD8\x8B\x73\x20\x48\x33\xFF\x66\xBF\x79\x04\xE8\x6E\xFF\xFF\xFF\x48\x89\x55\x48\x48\x8B\xD8\x48\x83\xC3\x7A"
"\x48\x83\xC3\x7B\x48\x83\xC3\x7B\x8B\x1B\x48\x03\xD8\x8B\x73\x20\x48\x33\xFF\x66\xBF\x96\x04\xE8\x47\xFF\xFF\xFF"
"\x48\x83\xEC\x08\x48\x33\xC0\x48\x89\x45\x20\x48\x89\x45\x28\xC6\x45\x20\x75\xC6\x45\x21\x73\xC6\x45\x22\x65\xC6\x45\x23\x72"
"\xC6\x45\x24\x33\xC6\x45\x25\x32\xC6\x45\x26\x2E\xC6\x45\x27\x64\xC6\x45\x28\x6C\xC6\x45\x29\x6C\x48\x8D\x4D\x20"
"\xFF\xD2\x48\x83\xC4\x10\x48\x8B\xD8\x48\x83\xC3\x7D\x48\x83\xC3\x7D\x48\x83\xC3\x7E\x8B\x1B\x48\x03\xD8\x8B\x73\x20"
"\x48\x33\xFF\x66\xBF\x2F\x04\xE8\xE3\xFE\xFF\xFF\x48\x8B\xDA\x48\x83\xEC\x20\x48\x33\xD2\x48\x33\xC0\x48\x89\x45\x28"
"\x48\x89\x45\x30\xC6\x45\x28\x68\xC6\x45\x29\x69\x45\x33\xC9\x4C\x8D\x45\x28\x48\x8D\x55\x28\x33\xC9\xFF\xD3\x48\x83\xC4\x28"
"\x33\xC9\x48\x83\xEC\x08\xFF\x55\x48\x48\x83\xC4\x08";


int main()
{
	printf("%zd\n", sizeof(shellcode));
	void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	memcpy(exec, shellcode, sizeof shellcode);
	((void(*)())exec)();
	
	return 0;
}

 

하지만, IDA에 LazyIDA플러그인을 설치하면 좀 더 편리하고 쉽게 쉘 코드를 뽑아올 수 있다.

 

L4ys/LazyIDA

Make your IDA Lazy! Contribute to L4ys/LazyIDA development by creating an account on GitHub.

github.com

 


 

5. 실행 결과                                                                                     

Comments