프로세스(Process)
July 2, 2019
목표
- 프로세스란 무엇인가?
- 프로세스 관리를 위한 프로세스 커널 오브젝트는 어떻게 생성되는가?
- 프로세스 커널 오브젝트가 프로세스를 어떻게 사용하는가?
- 프로세스의 속성과 특성
- 프로세스 관련 함수
- 프로세스를 시스템에서 생성하는 방법
프로세스란 무엇인가?
수행 중인 프로그램의 인스턴스(instance)
- 프로세스 관리를 위한 프로세스 커널 오브젝트
- 데이터 주소 공간(실행 모듈, DLL, 코드, 스택, 힙 등을 위한)
프로세스는 자력으로 수행될 수 없으며 프로세스가 무언가를 수행하기 위해서는 반드시 스레드(thread)가 필요하다.
- 프로세스가 생성되면 스레드 하나를 자동 생성하며 이를 메인 스레드(main thread)라 부른다.
- 하나의 프로세스는 다수의 스레드를 가질 수 있다.
- 스레드들은 프로세스 내부 주소 공간에서 ‘동시에’ 코드를 수행한다.
- 각 스레드는 레지스터 집합과 스택을 갖는다.
- 각 스레드가 동시에 수행될 수 있도록 운영체제는 round-robin 방식으로 CPU 시간 할당 (단일 CPU기준)
윈도우 애플리케이션의 두 형태
- GUI(Graphical User Interface)
- 링커스위치: /SUBSYSTEM:WINDOWS
- 진입 함수: _tWinMain(WinMain)
- CUI(Console User Interface)
- 링커스위치: /SUBSYSTEM:CONSOLE
- 진입 함수: _tmain(main)
링커스위치: 어플리케이션을 수행하면 운영제체의 로더(loader)는 실행 파일의 헤더를 확인하여 서브시스템 값을 가져와서 이 실행파일이 GUI인지 CUI인지를 구분하는데 이 때 사용하는 것.
진입함수: C/C++의 런타임 시작 함수 이후에 바로 호출될 함수
프로세스의 실행과 종료
운영체제는 어플리케이션을 실행하면 C/C++ 런타임 시작 함수를 호출하고 런타임 시작 함수는 C/C++ 런타임 라이브러리에 대한 초기화를 수행한다.(malloc, free 같은 함수가 실행 가능하도록) 초기화 과정은 기본적으로 다음과 같다.
- 새로운 프로세스의 전체 명령행을 가리키는 포인터 획득
- 새로운 프로세스의 환경 변수를 가리키는 포인터 획득
- C/C++ 런라임 라이브러리의 전역변수를 초기화.
- C/C++ 런타임 라이브러리의 메모리 할당 함수(malloc, free)와 저수준 입력 루틴이 사용할 힙 초기화.
- 모든 전역 오브젝트와 static c++ 클래스 오브젝트들의 생성자 호출.
이러한 초기화 과정 완료 후에 C/C++ 시작 함수는 진입 함수를 호출한다. 진입 함수로부터 프로그래머가 작성한 프로그램의 주 코드가 실행될 것이며 이러한 코드가 실행된 이후에는 c/c++ 런타임 라이브러리의 exit 함수를 호출한다. 이는 다음과 같은 작업을 수행한다.
- _onexit 함수를 이용해 등록된 종료 루틴들을 실행하고
- 모든 전역 클래스 오브젝트와 static c++ 클래스의 소멸자 호출
- ExitProcess() 함수 호출. 이 함수가 호출되면 운영체제는 프로세스를 종료함.
프로세스 인스턴스 핸들
- 모든 실행 파일과 DLL 파일은 프로세스의 메모리 공간에 로드될 때 고유의 인스턴스 핸들을 할당 받는다. (실행파일의 시작 메모리 주소값)
- 실행 파일이 로드될 시작 주소는 링커에 의해 결정되는데 Visual studio의 링커는 전통적을 0x00400000을 사용함.(win98에서 실행 파일 로드 가능한 최하단)
- WinMain의 경우 파라메터로 프로세스 인스턴스 핸들을 받는다.
- GetModuleHandle, GetModuleHandleEx 함수를 통해 모듈의 시작주소 획득 가능.
프로세스의 명령행
- 새로운 프로세스가 실행되면 프로세스의 명령행 전달
- GetCommandLine() 으로 전체 명령행 획득 가능
- 아래 함수로 파싱 가능
LPWSTR * CommandLineToArgvW(
LPCWSTR lpCmdLine,
int *pNumArgs
);
#include <windows.h>
#include <stdio.h>
#include <shellapi.h>
int __cdecl main()
{
LPWSTR *szArglist;
int nArgs;
int i;
szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if( NULL == szArglist )
{
wprintf(L"CommandLineToArgvW failed\n");
return 0;
}
else for( i=0; i<nArgs; i++) printf("%d: %ws\n", i, szArglist[i]);
// Free memory allocated for CommandLineToArgvW arguments.
LocalFree(szArglist);
return(1);
}
프로세스의 환경 변수
- 프로세스는 자기 자신과 연관된 환경블록(environment block)을 가지고 있다.
- 프로세스 정보를 담고 있는 구조체
- 이는 다음과 같은 형태로 일련을 문자열을 갖는다.
Name1=Value1\0 Name2=Value2\0 Name3=Value3\0 ... \0
- 마이크로소프트에 의해 문서화된 PEB의 필드들
- BeingDebugged: 프로세스가 디버깅 당하는 중인지
- Ldr: 로드된 모듈에 대한 정보를 제공하는 PEB_LDR_DATA 구조체를 가리키는 포인터
- ProcessParameters: 로드된 모듈에 대한 정보를 제공하는 RTL_USER_PROCESS_PARAMETERS 구조체를 가리키는 포인터
- PostProcessInitRoutine: DLL 초기화 이후, 메인 실행 코드가 발동되기 이전에 호출되는 콜백 함수에 대한 포인터
- SessionId: 프로세스를 부분으로 갖는 터미널 서비스 세션의 세션 ID
- GetEnvironmentStrings()으로 환경변수 획득 가능
- CUI에서 _tmain의 인자인 TCHAR *env[] 활용도 가능
- GetEnvironmentVariable()/SetEnvironmentVariable() 은 특정 값을 획득/수정
프로세스의 에러 모드
- 프로세스별로 심각한 에러에 어떻게 대응할지 설정 가능
- SetErrorMode()로 설정 가능
- Child process가 상속받음
프로세스의 현재 드라이브와 디렉터리
- 시스템은 내부적으로 프로세스의 현재 드라이브와 디렉터리를 저장해 둠.
- 프로세스 단위로 저장. 모든 스레드가 공유
- GetCurrentDirectory()/ SetCurrentDirectory()로 디렉토리를 얻거나 설정 가능
시스템 버전
- 어플리케이션이 윈도우 버전을 알아야 하는 경우도 있음
- GetVersionEx()사용
- VerifyVersionInfo()를 통해 필요 버전 검증
CreateProcess 함수
CreateProcess 함수를 사용하면 새로운 프로세스를 생성할 수 있다.
BOOL CreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
CreateProcess 함수를 호출하면 일어나는 일
- CreateProcess를 호출하면 시스템은 usage count 1인 프로세스 커널 오브젝트를 생성.
- 이는 OS가 프로세스를 관리하기 위해 할당하는 조그마한 데이터 구조체로. 프로세스에 대한 각종 정보를 갖는 작은 데이터 구조체라 생각하자.
- 프로세스 커널 오브젝트가 생성된 이후에 시스템은 프로세스를 위한 가상 주소 공간을 생성하고, 실행 파일의 코드와 데이터 및 수행에 필요한 추가 DLL 파일들을 프로세스의 주소 공간에 로드한다.
- 프로세스의 메인 스레드를 위한 스레드 커널 오브젝트를 생성한다.
- 링커에 의해 진입점으로 지정된 C/C++ 런타임 시작 코드를 실행한다. -> winmain/main 등으로 이동
- 시스템이 성공적으로 프로세스를 실행하면 TRUE를 반환한다.
CreateProcess의 매개변수
- lpApplicationName: 파일명 지정
- lpCommandLine: 커맨드라인 파라메터 직접 전달 가능.(읽기 전용 const가 아님을 주의)
- 첫 토큰은 프로그램 이름으로 간주
- 잘 실행되면 winmain등에 pszCommandLine값을 전달
- lpProcessAttributes: 생성될 프로세스 커널 오브젝트의 보안 특성 지정
- lpThreadAttributes: 생성될 스레드 커널 오브젝트의 보안 특성 지정
- bInheritHandles: 커널 오브젝트 핸들 상속 지정
- dwCreationFlags: 새로운 프로세스를 어떻게 생성할지 지정하는 bit flag
- lpEnvironment: 환경변수 지정.
- lpCurrentDirectory: 현재 디렉토리
- psiStartInfo: 프로세스 시작 정보 설정
- lpProcessInformation: 프로세스 정보 반환을 위함
- struct LPPROCESS_INFORMATION(processHandle, threadHandle, processId, threadId)
프로세스의 종료
프로세스는 다음 4가지 방법으로 종료될 수 있다.
- 메인스레드 반환 (강추)
- 프로세스 내의 스레드에서 ExitProcess() 호출 (비추)
- 다른 프로세스의 스레드에서 TerminateProcess() 호출 (비추)
- 프로세스 내의 모든 스레드가 각자 종료된다.
1. 메인스레드 반환(강추)
위 4가지 방법 중에서 유일하게 모든 리소스가 적절히 해제되는 것을 보장하는 방법이다. 메인 스레드가 반환되면 다음과 같은 작업을 수행한다.
- 메인 스레드에 의해 생성된 C++ 오브젝트들이 소멸자를 이용하여 적절하게 소멸
- 운영체제는 스레드 스택의 용도로 할당한 메모리 공간을 적절히 해제
- 시스템은 진입 함수의 반환 값으로 프로세스 종료 코드를 설정
- 시스템은 프로세스 커널 오브젝트의 usage count를 감소시킨다.
2. ExitProcess 함수
ExitProcess 함수는 어떠한 값도 반환하지 못한다!!. 정말로 프로세스가 종료되어 버리기 때문! ExitProcess() 함수 호출 이후의 코드는 실행되지 않는다. C++ 오브젝트의 소멸자도 호출되지 않는다. 비추
- 프로세스가 사용하던 리소스는 어떤 방식으로 종료해도 제거된다.
3. TerminateProcess 함수
이 함수는 ExitProcess와 한 가지 차이점이 있다. TerminateProcess 함수는 자신 뿐만 아니라 다른 프로세스까지 종료시킬 수 있다. 매개변수로 프로세스 핸들만 전달하면 된다.
- 다른 방법으로 프로세스를 종료할 수 없는 경우에만 사용하기를 권장.
4. 프로세스 내의 모든 스레드가 종료되는 경우
만일 ExitThread 또는 TerminateThread를 호출하여 프로세스 내의 모든 스레드가 종료되면 운영체제는 더 이상 프로세스의 주소 공간을 유지할 필요가 없다고 판단한다. 이는 프로세스 주소 공간의 코드를 실행할 스레드가 없기 때문에 적절한 판단이다.
프로세스가 종료되면
프로세스가 종료되면 다음의 작업이 이루어진다.
- 프로세스 내의 남아 있는 스레드가 종료된다.
- 프로세스에 의해 할당되었던 모든 사용자 오브젝트와 GDI 오브젝트가 삭제되며 모든 커널 오브젝트는 파괴된다.(다른 프로세스가 해당 커널 오브젝트 핸드를 소유하지 않는 경우)
- 프로세스 종료 코드는 ExitProcess나 TerminateProcess 호출시 설정한 코드로 변경된다.
- 프로세스 커널 오브젝트 상태가 시그널 상태(다음에 설명)가 된다.
- 프로세스 커널 오브젝트의 usage count가 1 감소한다.
프로세스 커널 오브젝트는 프로세스보다는 오랫동안 유지된다. 만약 프로세스 종료 이후에도 프로세스 커널 오브젝트의 usage count가 0이 아니라면 다른 프로세스에서 종료된 프로세스의 핸들을 소유하고 있다는 의미가 된다. 이런 경우 다른 프로세스에서 종료된 프로세스에 관심이 없다면 CloseHandle을 호출하여 usage count를 감소시켜 프로세스 커널 오브젝트가 파괴되도록 하자.
차일드 프로세스
어플리케이션이 다른 코드 블록을 실행해야 하는 경우
- 싱글테스킹 동기화(single tasking synchronization)
- 함수나 서브 루틴 사용
- 동시 작업 불가능
- 멀티스레드
- 프로세스 내에서 동시 작업 수행 가능
- 동기화 문제 발생
- 차일드 프로세스 생성
- 프로세스는 독립된 메모리 공간을 가짐(어차피 동기화 필요하지만…)
- 프로세스간 자료 전송 방법존재(DDE, OLE, 파이프, 메일슬롯, 메모리 맵)
동기화 - CreateProcess로 차일드 생성, WaitForSingleObject로 차일드 프로세스의 종료를 기다릴 수 있음
동기화 - CreateProcess 이후에 전달받은 차일드 프로세스의 커널 핸들을 사용하지 않는다면 바로바로 Close 처리하자.
보안 수준
사용자 계정 컨드롤(UAC)
대부분의 윈도우 사용자는 관리자 권한으로 로그인한다. 관리자 권한을 가진 사용자는 시스템 리소스에 제한없이 접근 가능하다. 로그인 할 때 관리자 권한 토큰이 전달되고 프로세스를 실행하면 동일한 토큰을 프로세스에 전달하게 되는데 이는 모든 프로세스가 수행되기만 하면 관리자의 높은 권한을 갖게 되며 결국 악성 프로그램에 취약하게 된다.
비스타부터 관리자 권한의 유저가 로그인하면
- 관리자 보안 토큰과 더불어 표준 사용자 토큰을 생성
- 표준 사용자 토큰은 프로세스 실행시 자동 전달
- 권한이 제한된 프로세스는 높은 권한의 리소스에 접근할 수 없다!
- 프로세스가 더 높은 권한을 갖고 싶다면 실행 전에 사용자에게 동의를 얻어야 함(관리자 권한으로 실행)
프로세스의 자동 권한 상승
- manifest 파일에서 <trustInfo> 부분을 수정
- 필요에 따라 권한 상승 요청 다이얼로그를 띄우게 됨
- 운영체제의 호환성 규칙에 알맞는 설치 파일로 판단되는 경우에도 권한 상승 요청
프로세스의 수동 권한 상승
- CreateProcess 함수는 권한 상승 관련 매개변수나 플래그가 없음
- 상승된 권한으로 실행하고 싶다면 ShellExecuteEx 함수를 사용
BOOL ShellExecuteExA(
SHELLEXECUTEINFOA *pExecInfo
);
typedef struct _SHELLEXECUTEINFOA {
DWORD cbSize;
ULONG fMask;
HWND hwnd;
LPCSTR lpVerb; // 권한 상승 필드
LPCSTR lpFile; // 경로
LPCSTR lpParameters;
LPCSTR lpDirectory;
int nShow;
HINSTANCE hInstApp;
void *lpIDList;
LPCSTR lpClass;
HKEY hkeyClass;
DWORD dwHotKey;
union {
HANDLE hIcon;
HANDLE hMonitor;
} DUMMYUNIONNAME;
HANDLE hProcess;
} SHELLEXECUTEINFOA, *LPSHELLEXECUTEINFOA;
- 사용자가 권한 상승을 거절하는 경우 FALSE 반환
- 한번 권한을 상승하면 차일드 프로세스도 같은 권한을 얻음
- 대부분의 기능을 표준 권한에서도 작동하도록 코드를 작성해야 호환성이 좋다.
현재의 권한 정보
- GetProcessElevation 함수 사용
시스템에서 수행 중인 프로세스의 나열
- ToolHelp API의 Process32First, Process32Next 사용. 혹은 EnumProcesses사용