기초 / / 2020. 2. 6. 02:42

윈도우 쉘코드 작성하기

요즘 다시 공부를 시작하고 있는데 어디 기록해두지 않으면 잊어버린다

그래서 정리 겸 올려보려한다

실행환경은 Windows 10 x64 / Visual Studio 2019 Release x86 기준으로 실행되었다


Windows 에서 프로세스를 실행할때 대표적으로 아래의 Win32 API를 이용한다

WinExec

CreateProcess

ShellExecute

WinExec를 Visual Studio에서 사용하려 하면 Deprecated 되었으니 CreateProcess를 사용하라 하지만

쉘코드 특성상 인자값이 적은게 유리하기 때문에 이번 글에서는 WinExec를 사용할것이다

WinExec의 함수원형은 아래와 같다

UINT WinExec(
  LPCSTR lpCmdLine,
  UINT   uCmdShow
);

LpCmdLine에는 실제로 우리가 실행할 프로세스명이 들어갈것이다.

LPCSTR은 const char * 과 같은 타입이니 일반 배열로 선언해주면 되고

uCmdShow에는 윈도우의 상태를 정하는 상수값을 넣어줄것인데 그냥 화면을 뛰운다는 SW_SHOW 값을 넣을것이다

#include <Windows.h>

int main(int argc, char** argv) {

	char command[] = { 'c', 'm', 'd', '\0' };
	WinExec(command, SW_SHOW);
	ExitProcess(1);

}

간단하게 설명하면 cmd창을 열고 프로세스를 종료하는 코드이다

컴파일하고 실행했을때 cmd창만 열려있다면 반은 성공이다

이제 여기서 중간에 아무곳이나 Break Point 를 걸고 디버깅을 시작한뒤 디스어셈블리 창을 보자

	char command[] = { 'c', 'm', 'd', '\0' };
	WinExec(command, SW_SHOW);
00CF1010 6A 05                push        5  
00CF1012 8D 45 F8             lea         eax,[command]  
00CF1015 C7 45 F8 63 6D 64 00 mov         dword ptr [command],646D63h  
00CF101C 50                   push        eax  
00CF101D FF 15 04 20 CF 00    call        dword ptr [__imp__WinExec@8 (0CF2004h)]  
	ExitProcess(1);
00CF1023 6A 01                push        1  
00CF1025 FF 15 00 20 CF 00    call        dword ptr [__imp__ExitProcess@4 (0CF2000h)]  

내용은 간단하다

5와 "cmd\0" 값을 순서대로 스택에 넣고 WinExec함수를 call 한뒤

1을 스택에 넣고 ExitProcess 함수를 call 한다는 내용이다

여기서 opcode를 바로 쉘코드로 사용하기엔 아래와 같은 제약이 있다

1. command같은 변수를 사용할수 없다

2. 로드된 함수들도 컴퓨터마다 메모리에 적재된 주소가 다르다

 

1번은 스택을 이용하여 문제를 해결할수 있다. 아래의 사진을 보자

32비트 기준으로 RET 4byte, SFP 4byte, 버퍼는 그때마다 다르다

함수가 생성된 뒤로 위의 구조로 스택프레임이 생성된다

쉘코드 작성하는 여러글을 보면 esp를 기준으로 인자값을 넣는것을 확인할수 있는데

여기서는 간단히 스택에 push만 이용하여 인자값들을 넣어봤다

push 00646d63h
push 5
push 0
lea eax, dword ptr [esp+8]
mov dword ptr [esp], eax

위 코드대로 push만 수행하면 스택프레임이 위 사진과 같아진다

0x00646d63은 아스키코드로 'c', 'm', 'd', NULL 문자열을 의미한다. 왜 00 먼저 들어가냐 하면 윈도우의 메모리 순서가 리틀엔디안 방식이기 때문이다

0을 push한 이유는 저기에 cmd 문자열의 주소를 넣어줘야되기 때문이다

그래서 esp 기준 +8을 하면 문자열이 있는곳을 가르키기 때문에 주소를 복사하여 esp주소에 넣어준다

이제 필요한 값들은 다 준비가 되었고 2번문제인 함수주소만 찾아서 call해주면 된다

 

2번같은 경우는 Universal Shellcode 라는 이름을 가진 별도의 방법이 있지만 지금은 단순 쉘코드 작성이므로 주소를 하드코딩하여 call할 생각이다

함수주소를 확인하는 가장 간단한 방법은 컴파일된 실행파일을 OllyDbg 같은 디버거로 실행하여 주소를 확인하는 방법이다

내 컴퓨터 기준 WinExec 함수의 주소는 0x7555DAB0 이다

mov eax, 0x7555dab0 // WinExec 주소 복사
call eax

push 1 // ExitProcess 인자값
mov eax, 0x755258f0 // ExitProcess 주소 복사
call eax

WinExec의 주소를 복사하여 call 한뒤 ExitProcess도 인자값 1을 주고 call 했다

여기까지 잘 왔다면 커맨드창이 정상적으로 실행될 것이다

생성된 opcode

생성된 opcode들을 그대로 잘 복사한뒤 Visual Studio에서 새로운 프로젝트를 만든뒤 아래와 같이 코드를 작성한다

#include <Windows.h>

char shellcode[] = "\x68\x63\x6d\x64\x00\x6a\x05\x6a\x00\x8d\x44\x24\x08\x89\x04\x24\xb8\xb0\xda\x55\x75\xff\xd0\x6a\x01\xb8\xf0\x58\x52\x75\xff\xd0";

int main(int argc, char** argv) {

	int* shell = (int*)shellcode;

	__asm {
		jmp shell
	}

}

일반 문자열에 \x 를 붙여주면 헥사값으로 저장이 된다

링커 - 고급 - DEP(데이터 실행 방지) 항목을 '예' 에서 '아니오' 로 바꿔준뒤 컴파일 해보자

성공적으로 쉘코드 작성이 완료되었다

이 글을 통해 쉘코드의 기본 매커니즘을 알아보았고 실제 공격에 사용하려면 위에서 말한 1번, 2번 등의 여러가지 제약을 해결해야 한다

작성자 기준으로 글이 작성되었으므로 3자가 보기엔 이해가 안될수도 있으니 피드백은 언제나 환영이다

'기초' 카테고리의 다른 글

음수 X 음수 = 양수일 수 밖에 없는 이유가 생각났다  (0) 2022.10.11
Pwnable 기법들  (0) 2020.02.24
함수 프롤로그/에필로그 정리  (0) 2020.02.13
패커 분석  (0) 2019.04.01
함수 호출 규약  (0) 2019.03.31
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유