요즘 다시 공부를 시작하고 있는데 어디 기록해두지 않으면 잊어버린다
그래서 정리 겸 올려보려한다
실행환경은 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번은 스택을 이용하여 문제를 해결할수 있다. 아래의 사진을 보자
함수가 생성된 뒤로 위의 구조로 스택프레임이 생성된다
쉘코드 작성하는 여러글을 보면 esp를 기준으로 인자값을 넣는것을 확인할수 있는데
여기서는 간단히 스택에 push만 이용하여 인자값들을 넣어봤다
push 00646d63h
push 5
push 0
lea eax, dword ptr [esp+8]
mov dword ptr [esp], eax
0x00646d63은 아스키코드로 'c', 'm', 'd', NULL 문자열을 의미한다. 왜 00 먼저 들어가냐 하면 윈도우의 메모리 순서가 리틀엔디안 방식이기 때문이다
0을 push한 이유는 저기에 cmd 문자열의 주소를 넣어줘야되기 때문이다
그래서 esp 기준 +8을 하면 문자열이 있는곳을 가르키기 때문에 주소를 복사하여 esp주소에 넣어준다
이제 필요한 값들은 다 준비가 되었고 2번문제인 함수주소만 찾아서 call해주면 된다
2번같은 경우는 Universal Shellcode 라는 이름을 가진 별도의 방법이 있지만 지금은 단순 쉘코드 작성이므로 주소를 하드코딩하여 call할 생각이다
함수주소를 확인하는 가장 간단한 방법은 컴파일된 실행파일을 OllyDbg 같은 디버거로 실행하여 주소를 확인하는 방법이다
mov eax, 0x7555dab0 // WinExec 주소 복사
call eax
push 1 // ExitProcess 인자값
mov eax, 0x755258f0 // ExitProcess 주소 복사
call eax
WinExec의 주소를 복사하여 call 한뒤 ExitProcess도 인자값 1을 주고 call 했다
여기까지 잘 왔다면 커맨드창이 정상적으로 실행될 것이다
생성된 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 |