Maldev Academy Part 1

목차


1. 환영 모듈

환영 모듈

소개

MalDev 아카데미에 오신 것을 환영합니다! 이 모듈은 모듈의 레이아웃에 익숙해지고 학습 경험을 극대화할 수 있도록 도와주는 입문 모듈입니다.

전제 조건

MalDev Academy는 사용자가 멀웨어 개발 경험이 없다고 가정하지만, 프로그래밍의 기초를 철저하게 가르치지는 않습니다. 이 과정은 주로 C 프로그래밍 언어를 다루기 때문에 사용자는 최소한 C의 기본 사항에 익숙해야 합니다.

모듈 난이도

모든 모듈은 세 가지 색상 중 하나로 구분됩니다:

  1. 녹색 – 초급 모듈임을 나타냅니다. 기본 개념과 기술을 배우며, 향후 더 어려운 모듈에 대비하기 위한 것입니다. 초급 모듈은 기본적인 이론 지식과 실제 악성코드 개발 기법 입문 과정을 다룹니다.
  2. 주황색 – 중급 모듈임을 나타냅니다. 설명하는 개념과 기법은 이해와 코딩이 더 어렵지만 실제 상황에서 사용하면 더 나은 결과를 얻을 수 있습니다.
  3. 빨간색 – 고급 모듈임을 나타냅니다. 이 모듈에서 다루는 개념과 기술은 어렵기 때문에 탄탄한 이론적 기반은 물론 Windows 아키텍처와 C 프로그래밍 언어에 대한 깊은 이해가 필요합니다.

모듈 레이아웃

각 모듈에는 사용자의 학습 경험을 극대화하기 위한 여러 가지 속성이 포함되어 있습니다:

  • 왼쪽 상단에는 모듈 번호, 모듈 제목 및 난이도가 앞서 설명한 색상 코딩 스타일을 통해 표시됩니다.
  • 오른쪽 상단에는 4개의 버튼이 있습니다. 왼쪽에서 오른쪽으로 시작합니다:
    1. 화면 – 모듈 화면 크기를 전환합니다.
    2. 목표 – 모든 모듈에는 다음 모듈로 넘어가기 전에 완료할 것을 적극 권장하는 학습 목표 세트가 있습니다.
    3. 터미널 – 임시 메모를 작성하거나 코딩을 할 수 있는 브라우저 내 작업 공간을 엽니다.
    4. 다운로드 – 모듈과 관련된 코드 파일을 다운로드합니다. 코드 샘플이 없는 모듈에는 이 버튼이 없습니다.
  • 화면 하단에는 4개의 버튼이 있습니다:
    • 이전 – 이전 모듈로 돌아갑니다(첫 번째 모듈에서는 표시되지 않음).
    • 모듈 – 사용자를 홈 페이지로 돌아갑니다.
    • 완료/실행 취소 – 모듈을 완료 또는 진행 중으로 표시합니다.
    • 다음 – 다음 모듈로 이동합니다.

디스코드 채널

Discord 채널에 가입하여 질문하고 다른 회원들과 소통하세요.

문제 보고

언제든지 사이트 관련 문제나 버그를 신고하려면 이메일( help@maldevacademy.com)로 보내주세요.

모듈을 완료로 표시

아래의 ‘완료’ 버튼을 클릭하여 이 모듈을 완료한 것으로 표시하고 다음 모듈로 넘어갑니다.


2. 멀웨어 개발 소개

모듈 2 – 멀웨어 개발 소개

멀웨어 개발 소개

멀웨어란 무엇인가요?

멀웨어는 컴퓨터에 무단으로 액세스하거나 컴퓨터에서 중요한 데이터를 훔치는 등의 악의적인 작업을 수행하도록 특별히 설계된 소프트웨어의 일종입니다. ‘멀웨어’라는 용어는 종종 불법 또는 범죄 행위와 관련이 있지만, 침투 테스터나 레드팀과 같은 윤리적 해커가 조직의 공인된 보안 평가를 위해 사용하는 용어로도 사용될 수 있습니다.

말데브 아카데미는 이 과정에 등록한 사용자가 배운 지식을 윤리적이고 합법적인 목적으로만 사용한다고 가정합니다. 다른 용도로 사용할 경우 형사 고발을 당할 수 있으며 MalDev Academy는 이에 대해 책임을 지지 않습니다.

멀웨어 개발을 배우는 이유는 무엇인가요?

멀웨어 개발을 배우려는 이유는 여러 가지가 있습니다. 공격적 보안 관점에서 볼 때 테스터는 종종 클라이언트 환경에 대해 특정 악성 작업을 수행해야 합니다. 테스터는 일반적으로 참여에 사용되는 도구의 유형과 관련하여 세 가지 주요 옵션이 있습니다:

  1. 오픈 소스 도구(OST) – 이러한 도구는 일반적으로 보안 공급업체에서 서명하며, 어느 정도 보호되고 성숙한 조직에서나 탐지됩니다. 공격적인 보안 평가에 참여할 때 항상 신뢰할 수 있는 것은 아닙니다.
  2. 도구 구매 – 예산이 많은 팀은 참여 중 귀중한 시간을 절약하기 위해 도구를 구매하는 경우가 많습니다. 사용자 지정 도구와 마찬가지로 이러한 도구는 일반적으로 비공개 소스이며 보안 솔루션을 회피할 가능성이 더 높습니다.
  3. 맞춤형 도구 개발 – 이러한 도구는 맞춤형으로 제작되기 때문에 보안 공급업체에서 분석하거나 서명하지 않았기 때문에 공격 팀이 탐지에 있어 유리합니다. 따라서 성공적인 공격 보안 평가를 위해서는 멀웨어 개발 지식이 가장 중요합니다.

어떤 프로그래밍 언어를 사용해야 하나요?

기술적으로 말하면 Python, PowerShell, C#, C, C++, Go 등 모든 프로그래밍 언어를 사용하여 멀웨어를 만들 수 있습니다. 그렇지만 멀웨어 개발에 있어 특정 프로그래밍 언어가 다른 언어보다 우세한 몇 가지 이유가 있으며, 이는 일반적으로 다음과 같은 점으로 요약됩니다:

  • 특정 프로그래밍 언어는 리버스 엔지니어링이 더 어렵습니다. 공격자는 항상 방어자가 멀웨어의 동작 방식에 대해 제한적으로만 이해하도록 하는 것이 목표의 일부가 되어야 합니다.
  • 일부 프로그래밍 언어의 경우 대상 시스템에 전제 조건이 필요합니다. 예를 들어, Python 스크립트를 실행하려면 대상 시스템에 인터프리터가 있어야 합니다. 시스템에 파이썬 인터프리터가 없으면 파이썬 기반 멀웨어를 실행할 수 없습니다.
  • 프로그래밍 언어에 따라 생성되는 파일 크기가 달라집니다.

하이레벨과 로우레벨 프로그래밍 언어 비교

프로그래밍 언어는 하이레벨과 로우레벨의 두 가지 그룹으로 분류할 수 있습니다.

  • 고수준 – 일반적으로 운영 체제에서 더 추상화되어 있고 메모리 효율성이 떨어지며 여러 복잡한 함수의 추상화로 인해 개발자에게 전반적인 제어권을 덜 제공합니다. 고수준 프로그래밍 언어의 예로는 Python이 있습니다.
  • 저수준 – 운영 체제와 친밀한 수준에서 상호 작용할 수 있는 방법을 제공하며 개발자가 시스템과 상호 작용할 때 더 많은 자유를 제공합니다. 저수준 프로그래밍 언어의 예로는 C가 있습니다.

앞서 설명한 내용을 고려하면 멀웨어 개발, 특히 Windows 시스템을 대상으로 하는 멀웨어 개발에서 저수준 프로그래밍 언어가 선호되는 이유를 알 수 있을 것입니다.

Windows 멀웨어 개발

지난 몇 년 동안 Windows 멀웨어 개발 환경이 변화하여 이제는 안티바이러스(AV) 및 엔드포인트 탐지 및 대응(EDR)과 같은 호스트 기반 보안 솔루션을 우회하는 데 집중하고 있습니다. 기술이 발전함에 따라 의심스러운 명령을 실행하거나 ‘멀웨어와 유사한’ 동작을 수행하는 멀웨어를 만드는 것만으로는 더 이상 충분하지 않습니다.

MalDev 아카데미에서는 실제 교전에서 사용할 수 있는 회피형 멀웨어를 제작하는 방법을 알려드립니다. 이 모듈은 또한 보안 솔루션이나 블루팀에 의해 멀웨어가 탐지될 가능성이 있는 비보안 조치 또는 동작을 알려줍니다.

멀웨어 개발 수명 주기

기본적으로 멀웨어는 특정 작업을 수행하도록 설계된 소프트웨어입니다. 소프트웨어를 성공적으로 구현하려면 소프트웨어 개발 수명 주기(SDLC)로 알려진 프로세스가 필요합니다. 마찬가지로, 잘 구축된 복잡한 멀웨어는 MDLC(멀웨어 개발 수명 주기)라고 하는 맞춤형 버전의 SDLC가 필요합니다.

MDLC가 반드시 공식화된 프로세스는 아니지만, 독자들이 개발 프로세스를 쉽게 이해할 수 있도록 하기 위해 MalDev 아카데미에서 사용하고 있습니다. MDLC는 5가지 주요 단계로 구성됩니다:

  1. 개발 – 멀웨어 내 기능의 개발 또는 개선을 시작합니다.
  2. 테스트 – 지금까지 개발된 코드 내에서 숨겨진 버그를 발견하기 위해 테스트를 수행합니다.
  3. 오프라인 AV/EDR 테스트 – 개발된 멀웨어를 가능한 한 많은 보안 제품에 대해 실행합니다. 보안 공급업체에 샘플이 전송되지 않도록 테스트는 오프라인으로 수행하는 것이 중요합니다. Microsoft Defender를 사용하면 자동화된 샘플 제출 및 클라우드 제공 보호 옵션을 비활성화하면 이 작업을 수행할 수 있습니다.
  4. 온라인 AV/EDR 테스트 – 인터넷 연결이 가능한 보안 제품에 대해 개발된 멀웨어를 실행합니다. 클라우드 엔진은 종종 AV/EDR의 핵심 구성 요소이므로 보다 정확한 결과를 얻으려면 이러한 구성 요소에 대해 멀웨어를 테스트하는 것이 중요합니다. 이 단계에서는 샘플이 보안 솔루션의 클라우드 엔진으로 전송될 수 있으므로 주의하세요.
  5. IoC(침해 지표) 분석 – 이 단계에서는 위협 헌터 또는 멀웨어 분석가가 됩니다. 멀웨어를 분석하고 멀웨어를 탐지하거나 서명하는 데 잠재적으로 사용될 수 있는 IoC를 추출합니다.
  6. 1단계로 돌아갑니다.

3. 필요한 도구

필요한 도구

소개

악성코드 개발 여정을 시작하기 전에 악성코드 개발 및 리버스 엔지니어링 도구를 설치하여 개발 작업 공간을 준비해야 합니다. 이러한 도구는 멀웨어의 개발과 분석에 도움이 되며 모듈 전체에서 사용됩니다.

리버스 엔지니어링 도구

언급된 도구 중 일부는 개발보다는 리버스 엔지니어링에 더 중점을 두고 있습니다. 제작된 멀웨어를 리버스 엔지니어링하여 내부 작동 방식을 완전히 이해하고 멀웨어 분석가가 멀웨어를 검사할 때 무엇을 보게 될지 파악하는 것이 필수적입니다.

설치 도구

다음 도구를 설치합니다:

  • Visual Studio – 코딩 및 컴파일 프로세스가 이루어지는 개발 환경입니다. C/C++ 런타임을 설치합니다.
  • x64dbg – x64dbg는 개발된 멀웨어를 내부적으로 파악하기 위해 모듈 전체에서 사용되는 디버거입니다.
  • PE-Bear – PE-bear는 PE 파일을 위한 멀티플랫폼 리버싱 툴입니다. 또한 개발된 멀웨어를 평가하고 의심스러운 지표를 찾는 데도 사용됩니다.
  • Process Hacker 2 – Process Hacker는 시스템 리소스를 모니터링하고 소프트웨어를 디버깅하며 멀웨어를 탐지하는 데 도움이 되는 강력한 다목적 도구입니다.
  • Msfvenom – Msfvenom은 페이로드를 생성, 조작 및 출력하는 데 사용되는 명령줄 인터페이스 도구입니다.

Visual Studio

Visual Studio는 Microsoft에서 개발한 통합 개발 환경(IDE)입니다. 웹 애플리케이션, 웹 서비스, 컴퓨터 프로그램 등 다양한 소프트웨어를 개발하는 데 사용됩니다. 또한 애플리케이션을 빌드하고 테스트하기 위한 개발 및 디버깅 도구도 함께 제공됩니다. 이 과정에서는 비주얼 스튜디오가 개발에 사용되는 주요 IDE입니다.

x64dbg

x64dbg는 x64 및 x86 Windows 바이너리를 위한 오픈 소스 디버깅 유틸리티입니다. 사용자 모드 애플리케이션과 커널 모드 드라이버를 분석하고 디버깅하는 데 사용됩니다. 사용자가 프로그램 상태를 검사 및 분석하고 메모리 내용, 어셈블리 명령어 및 레지스터 값을 볼 수 있는 그래픽 사용자 인터페이스를 제공합니다. x64dbg를 사용하면 중단점을 설정하고, 스택 및 힙 데이터를 보고, 코드를 단계별로 살펴보고, 메모리 값을 읽고 쓸 수 있습니다.

기본 ‘CPU’ 탭에는 4개의 화면이 있습니다:

  1. 어셈블리(왼쪽 상단): 이 창에는 애플리케이션에서 실행 중인 어셈블리 지침이 표시됩니다.
  2. 덤프(왼쪽 하단): 이 창에는 디버깅 중인 애플리케이션의 메모리 내용이 표시됩니다.
  3. 레지스터(오른쪽 상단): 이 창에는 CPU 레지스터의 값이 표시됩니다.
  4. 스택(오른쪽 하단): 이 창에는 스택의 콘텐츠가 표시됩니다.

나머지 탭도 유용한 정보를 제공하지만 사용 시 모듈에서 설명할 예정입니다.

PE-Bear

PE-Bear는 멀웨어 분석가와 리버스 엔지니어가 Windows 휴대용 실행 파일(PE)을 빠르고 쉽게 분석할 수 있도록 설계된 무료 오픈 소스 도구입니다. PE 파일의 구조를 분석 및 시각화하고, 각 모듈의 가져오기 및 내보내기를 확인하며, 정적 분석을 수행하여 이상 징후와 악성 코드를 탐지하는 데 도움이 됩니다. PE-bear에는 PE 헤더 및 섹션 유효성 검사, 16진수 편집기와 같은 기능도 포함되어 있습니다.

프로세스 해커

프로세스 해커는 Windows에서 프로세스 및 서비스를 보고 조작할 수 있는 오픈 소스 도구입니다. 작업 관리자와 유사하지만 더 많은 정보와 고급 기능을 제공합니다. 프로세스 및 서비스를 종료하고, 자세한 프로세스 정보 및 통계를 보고, 프로세스 우선순위를 설정하는 등의 작업을 수행하는 데 사용할 수 있습니다. 프로세스 해커는 실행 중인 프로세스를 분석하여 로드된 DLL 및 메모리 영역과 같은 항목을 볼 때 유용합니다.

Msfvenom

Msfvenom은 사용자가 다양한 유형의 페이로드를 생성할 수 있는 메타스플로잇 프레임워크 독립형 페이로드 생성기입니다. 이 페이로드는 이 과정에서 생성한 멀웨어에 사용됩니다.


4. 코딩 기초

코딩 기초

소개

앞서 언급했듯이 이 강좌는 C에 대한 기본적인 이해가 전제 조건으로 필요합니다. 그렇기 때문에 이 과정 전반에 걸쳐 중요하기 때문에 언급할 몇 가지 개념이 있습니다.

구조

구조 또는 구조체는 프로그래머가 서로 다른 데이터 유형의 관련 데이터 항목을 단일 단위로 그룹화할 수 있는 사용자 정의 데이터 유형입니다. 구조체는 특정 객체와 관련된 데이터를 저장하는 데 사용할 수 있습니다. 구조체는 대량의 관련 데이터를 쉽게 액세스하고 조작할 수 있는 방식으로 구성하는 데 도움이 됩니다. 구조체 내의 각 항목을 “멤버” 또는 “요소”라고 하며, 이 용어는 코스 내에서 같은 의미로 사용됩니다.

Windows API로 작업할 때 흔히 볼 수 있는 것은 일부 API는 입력으로 채워진 구조를 필요로 하는 반면, 다른 API는 선언된 구조를 가져와서 채운다는 것입니다. 아래는 THREADENTRY32 구조체의 예시이며, 이 시점에서 멤버가 어떤 용도로 사용되는지 이해할 필요는 없습니다.

typedef struct tagTHREADENTRY32 {
  DWORD dwSize; // Member 1
  DWORD cntUsage; // Member 2
  DWORD th32ThreadID;
  DWORD th32OwnerProcessID;
  LONG  tpBasePri;
  LONG  tpDeltaPri;
  DWORD dwFlags;
} THREADENTRY32;

구조 선언

이 강좌에서 사용되는 구조체는 일반적으로 구조체에 별칭을 부여하기 위해 typedef 키워드를 사용하여 선언됩니다. 예를 들어, 아래 구조체는 _STRUCTURE_NAME이라는 이름으로 생성되었지만 typedef는 STRUCTURE_NAME과 *PSTRUCTURE_NAME이라는 두 개의 다른 이름을 추가합니다.

typedef struct _STRUCTURE_NAME {

  // structure elements

} STRUCTURE_NAME, *PSTRUCTURE_NAME;

구조 이름 별칭은 구조 이름을 가리키고, PSTRUCTURE_NAME은 해당 구조에 대한 포인터를 나타냅니다. Microsoft는 일반적으로 P 접두사를 사용하여 포인터 유형을 나타냅니다.

구조 초기화하기

구조체를 초기화하는 것은 실제 구조체 유형을 초기화하는 것인지, 구조체에 대한 포인터를 초기화하는 것인지에 따라 달라집니다. 이전 예시를 계속 이어서, 구조체를 초기화하는 방법은 아래와 같이 _STRUCTURE_NAME 또는 STRUCTURE_NAME을 사용할 때와 동일합니다.

STRUCTURE_NAME    struct1 = { 0 };  // The '{ 0 }' part, is used to initialize all the elements of struct1 to zero
// OR
_STRUCTURE_NAME   struct2 = { 0 };  // The '{ 0 }' part, is used to initialize all the elements of struct2 to zero

구조 포인터인 PSTRUCTURE_NAME을 초기화할 때는 다릅니다.

PSTRUCTURE_NAME structpointer = NULL;

구조체 멤버 초기화 및 액세스하기

구조체의 멤버는 구조체를 통해 직접 초기화하거나 구조체에 대한 포인터를 통해 간접적으로 초기화할 수 있습니다. 아래 예제에서 구조체 구조체1에는 점 연산자(.)를 통해 직접 초기화된 두 개의 멤버 ID와 Age가 있습니다.

typedef struct _STRUCTURE_NAME {
  int ID;
  int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { 0 }; // initialize all elements of struct1 to zero
struct1.ID   = 1470;   // initialize the ID element
struct1.Age  = 34;     // initialize the Age element

멤버를 초기화하는 또 다른 방법은 구조체의 어떤 멤버를 초기화할지 지정할 수 있는 지정 초기화자 구문을 사용하는 것입니다.

typedef struct _STRUCTURE_NAME {
  int ID;
  int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { .ID   = 1470,  .Age  = 34}; // initialize both the ID and the Age elements

반면에 포인터를 통해 구조체에 액세스하고 초기화하는 작업은 화살표 연산자(->)를 통해 수행됩니다.

typedef struct _STRUCTURE_NAME {
  int ID;
  int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { .ID   = 1470,  .Age  = 34};

PSTRUCTURE_NAME structpointer = &struct1; // structpointer is a pointer to the 'struct1' structure

// Updating the ID member
structpointer->ID = 8765;
printf("The structure's ID member is now : %d \n", structpointer->ID);

화살표 연산자는 점 형식으로 변환할 수 있습니다. 예를 들어, 구조포인터->ID는 (*구조포인터).ID와 동일합니다. 즉, 구조 포인터가 참조 해제된 다음 직접 액세스됩니다.

가치 전달

값으로 전달은 인수가 객체 값의 복사본인 함수에 인수를 전달하는 방법입니다. 즉, 값으로 인수를 전달하면 객체의 값이 복사되고 함수는 원래 객체 자체가 아니라 객체 값의 로컬 복사본만 수정할 수 있습니다.

int add(int a, int b)
{
   int result = a + b;
   return result;
}

int main()
{
   int x = 5;
   int y = 10;
   int sum = add(x, y); // x and y are passed by value

   return 0;
}

참조로 전달

참조 전달은 함수에 인수를 전달할 때 인수가 객체 값의 복사본이 아닌 객체에 대한 포인터인 경우 인수를 전달하는 방법입니다. 즉, 참조로 인수를 전달하면 객체의 값 대신 객체의 메모리 주소가 전달됩니다. 그러면 함수는 객체의 로컬 복사본을 만들지 않고도 객체에 직접 액세스하고 수정할 수 있습니다.

void add(int *a, int *b, int *result){
  int A = *a; 
  // A is now the same value of a passed in from the main function
 int B = *b; 
 // B is now the same value of b passed in from the main function
 *result = B + A;}
 
int main()<{
  int x = 5;
  int y = 10;  
  int sum = 0;
  add(&x, &y, &sum);  // 'sum' now is 15<br><br> 
  return 0;}
  
  

5. Windows 아키텍처

소개

이 모듈에서는 Windows 아키텍처와 Windows 프로세스 및 애플리케이션의 내부에서 일어나는 일에 대해 설명합니다.

Windows 아키텍처

Windows 운영 체제를 실행하는 컴퓨터 내부의 프로세서는 두 가지 모드에서 작동할 수 있습니다: 사용자 모드와 커널 모드. 애플리케이션은 사용자 모드에서 실행되고 운영 체제 구성 요소는 커널 모드에서 실행됩니다. 애플리케이션이 파일 생성과 같은 작업을 수행하고자 할 때, 애플리케이션은 자체적으로 작업을 수행할 수 없습니다. 작업을 완료할 수 있는 유일한 엔티티는 커널이므로 애플리케이션은 특정 함수 호출 흐름을 따라야 합니다. 아래 다이어그램은 이러한 흐름의 높은 수준을 보여줍니다.

  1. 사용자 프로세스 – 메모장, 구글 크롬, 마이크로소프트 워드 등 사용자가 실행하는 프로그램/애플리케이션입니다.
  2. 하위 시스템 DLL – 사용자 프로세스에서 호출하는 API 함수를 포함하는 DLL입니다. 예를 들어 CreateFile Windows API(WinAPI) 함수를 내보내는 kernel32.dll이 있으며, 다른 일반적인 하위 시스템 DLL로는 ntdll.dlladvapi32.dll 및 user32.dll이 있습니다.
  3. Ntdll.dll – 사용자 모드에서 사용할 수 있는 최하위 계층인 시스템 전체 DLL입니다. 사용자 모드에서 커널 모드로의 전환을 생성하는 특수 DLL입니다. 이를 네이티브 API 또는 NTAPI라고도 합니다.
  4. 이그제큐티브 커널 – Windows 커널로 알려진 이 커널은 커널 모드에서 사용 가능한 다른 드라이버와 모듈을 호출하여 작업을 완료합니다. Windows 커널은 부분적으로 “C:\Windows\System32” 아래의 ntoskrnl.exe라는 파일에 저장됩니다.

함수 호출 흐름

아래 이미지는 파일을 생성하는 애플리케이션의 예를 보여줍니다. 이 예제는 사용자 애플리케이션이 kernel32.dll에서 사용할 수 있는 CreateFile WinAPI 함수를 호출하는 것으로 시작됩니다. Kernel32.dll은 애플리케이션을 WinAPI에 노출하는 중요한 DLL이므로 대부분의 애플리케이션에서 로드되는 것을 볼 수 있습니다. 다음으로 CreateFile은 ntdll.dll을 통해 제공되는 동등한 NTAPI 함수인 NtCreateFile을 호출합니다. 그러면 Ntdll.dll은 실행을 커널 모드로 전환하는 어셈블리 sysenter (x86) 또는 syscall (x64) 명령어를 실행합니다. 그런 다음 요청된 작업을 수행하기 위해 커널 드라이버 및 모듈을 호출하는 커널 NtCreateFile 함수가 사용됩니다.

함수 호출 흐름 예시

이 예는 디버거를 통해 발생하는 함수 호출 흐름을 보여줍니다. 이 작업은 CreateFileW Windows API를 통해 파일을 생성하는 바이너리에 디버거를 연결하여 수행됩니다.

사용자 애플리케이션이 CreateFileW WinAPI를 호출합니다.

네이티브 API(NTAPI) 직접 호출하기

애플리케이션은 Windows API를 거치지 않고도 시스템 호출(예: NTDLL 함수)을 직접 호출할 수 있다는 점에 유의해야 합니다. Windows API는 단순히 네이티브 API의 래퍼 역할을 합니다. 즉, 네이티브 API는 Microsoft에서 공식적으로 문서화하지 않았기 때문에 사용하기가 더 어렵습니다. 또한, Microsoft는 언제든지 경고 없이 변경될 수 있으므로 네이티브 API 함수를 사용하지 말 것을 권장합니다.

향후 모듈에서는 네이티브 API를 직접 호출할 때의 이점에 대해 살펴볼 예정입니다.


6. Windows 메모리 관리

Windows 메모리 관리

소개

이 모듈에서는 Windows 메모리의 기본 사항을 살펴봅니다. Windows가 메모리를 처리하는 방식을 이해하는 것은 지능형 악성코드를 개발하는 데 매우 중요합니다.

가상 메모리 및 페이징

최신 운영 체제의 메모리는 물리적 메모리(즉, RAM)에 직접 매핑되지 않습니다. 대신, 가상 메모리 주소는 실제 메모리 주소에 매핑된 프로세스에서 사용됩니다. 여기에는 몇 가지 이유가 있지만 궁극적으로 물리적 메모리를 최대한 절약하는 것이 목표입니다. 가상 메모리는 물리적 메모리에 매핑될 수도 있지만 디스크에 저장될 수도 있습니다. 가상 메모리 어드레싱을 사용하면 여러 프로세스가 고유한 가상 메모리 주소를 가지면서 동일한 물리적 주소를 공유할 수 있습니다. 가상 메모리는 메모리를 “페이지”라고 하는 4KB의 청크로 분할하는 메모리 페이징 개념을 사용합니다.

아래 이미지를 Windows 내부 7판 – 1부 책에서 확인하세요.

페이지 상태

프로세스의 가상 주소 공간 내에 있는 페이지는 다음 세 가지 상태 중 하나에 속할 수 있습니다:

  1. 무료 – 페이지가 커밋되거나 예약되지 않았습니다. 프로세스에서 페이지에 액세스할 수 없습니다. 예약, 커밋 또는 동시에 예약 및 커밋할 수 있습니다. 자유 페이지에서 읽거나 쓰기를 시도하면 액세스 위반 예외가 발생할 수 있습니다.
  2. 예약됨 – 나중에 사용할 수 있도록 페이지가 예약되었습니다. 주소 범위는 다른 할당 함수에서 사용할 수 없습니다. 페이지에 액세스할 수 없으며 페이지와 연결된 물리적 저장소가 없습니다. 커밋할 수 있습니다.
  3. 커밋됨 – 디스크의 전체 RAM 및 페이징 파일 크기에서 메모리 요금이 할당되었습니다. 이 페이지에 접근할 수 있으며 메모리 보호 상수 중 하나에 의해 접근이 제어됩니다. 시스템은 커밋된 각 페이지를 초기화하고 해당 페이지를 처음 읽거나 쓰려고 시도하는 동안에만 실제 메모리에 로드합니다. 프로세스가 종료되면 시스템은 커밋된 페이지에 대한 저장 공간을 해제합니다.

페이지 보호 옵션

페이지가 커밋되면 보호 옵션을 설정해야 합니다. 메모리 보호 상수 목록은 여기에서 확인할 수 있지만 몇 가지 예는 아래에 나열되어 있습니다.

  • PAGE_NOACCESS – 페이지의 커밋된 영역에 대한 모든 액세스를 비활성화합니다. 커밋된 영역에서 읽기, 쓰기 또는 실행을 시도하면 액세스 위반이 발생합니다.
  • PAGE_EXECUTE_READWRITE – 읽기, 쓰기 및 실행을 활성화합니다. 메모리가 쓰기와 실행이 동시에 가능한 경우는 드물기 때문에 일반적으로 이 기능은 사용하지 않는 것이 좋으며 일반적으로 IoC입니다.
  • PAGE_READONLY – 페이지의 커밋된 영역에 대한 읽기 전용 액세스를 활성화합니다. 커밋된 영역에 쓰기를 시도하면 액세스 위반이 발생합니다.

메모리 보호

최신 운영 체제에는 일반적으로 익스플로잇과 공격을 막기 위한 메모리 보호 기능이 내장되어 있습니다. 이러한 메모리 보호 기능은 멀웨어를 빌드하거나 디버깅할 때 발생할 수 있으므로 염두에 두어야 할 중요한 사항입니다.

  • DEP(데이터 실행 방지) – DEP는 Windows XP 및 Windows Server 2003부터 운영 체제에 내장된 시스템 수준의 메모리 보호 기능입니다. 페이지 보호 옵션이 PAGE_READONLY로 설정되어 있으면 DEP가 해당 메모리 영역에서 코드가 실행되는 것을 방지합니다.
  • 주소 공간 레이아웃 무작위화(ASLR) – ASLR은 메모리 손상 취약점의 악용을 방지하는 데 사용되는 메모리 보호 기법입니다. ASLR은 실행 파일의 베이스와 스택, 힙 및 라이브러리의 위치를 포함하여 프로세스의 주요 데이터 영역의 주소 공간 위치를 무작위로 배열합니다.

x86 대 x64 메모리 공간

Windows 프로세스로 작업할 때는 해당 프로세스가 x86인지 x64인지 확인하는 것이 중요합니다. x86 프로세스는 메모리 공간이 4GB(0xFFFFFFFF)로 더 작은 반면, x64는 메모리 공간이 128TB(0xFFFFFFFFFFFF)로 훨씬 더 큽니다.

메모리 할당 예제

이 예제에서는 C 함수 및 Windows API를 통해 Windows 메모리와 상호 작용하는 방법을 더 잘 이해할 수 있도록 작은 코드 조각을 살펴봅니다. 메모리와 상호 작용하는 첫 번째 단계는 메모리를 할당하는 것입니다. 아래 코드 조각은 기본적으로 실행 중인 프로세스 내부에 메모리를 예약하는 몇 가지 메모리 할당 방법을 보여줍니다.

// Allocating a memory buffer of *100* bytes

// Method 1 - Using malloc()
PVOID pAddress = malloc(100);

// Method 2 - Using HeapAlloc()
PVOID pAddress = HeapAlloc(GetProcessHeap(), 0, 100);

// Method 3 - Using LocalAlloc()
PVOID pAddress = LocalAlloc(LPTR, 100);

메모리 할당 함수는 할당된 메모리 블록의 시작 부분에 대한 포인터인 기본 주소를 반환합니다. 위의 스니펫을 사용하면 pAddress는 할당된 메모리 블록의 기본 주소가 됩니다. 이 포인터를 사용하여 읽기, 쓰기, 실행 등 여러 가지 작업을 수행할 수 있습니다. 수행할 수 있는 작업의 유형은 할당된 메모리 영역에 할당된 보호 기능에 따라 달라집니다.

아래 이미지는 디버거에서 pAddress가 어떻게 보이는지 보여줍니다.

메모리가 할당될 때 메모리가 비어 있거나 임의의 데이터가 포함될 수 있습니다. 일부 메모리 할당 함수는 할당 프로세스 중에 메모리 영역을 0으로 만드는 옵션을 제공합니다.

메모리에 쓰기 예제

메모리 할당 후 다음 단계는 일반적으로 해당 버퍼에 쓰는 것입니다. 메모리에 쓰는 데는 여러 가지 옵션을 사용할 수 있지만 이 예제에서는 memcpy를 사용합니다.

PVOID pAddress	= HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);

CHAR* cString	= "MalDev Academy Is The Best";

memcpy(pAddress, cString, strlen(cString));

HeapAlloc은 할당된 메모리가 0으로 초기화되도록 하는 HEAP_ZERO_MEMORY 플래그를 사용합니다. 그런 다음 memcpy를 사용하여 할당된 메모리에 문자열을 복사합니다. memcpy의 마지막 매개변수는 복사할 바이트 수입니다. 그런 다음 버퍼를 다시 확인하여 데이터가 성공적으로 쓰였는지 확인합니다.

할당된 메모리 확보

할당된 버퍼를 사용하여 애플리케이션이 완료되면 메모리 누수를 방지하기 위해 버퍼를 할당 해제하거나 해제하는 것이 좋습니다.

메모리를 할당하는 데 사용된 함수에 따라 해당 메모리 할당 해제 함수가 있습니다. 예를 들어

  • malloc으로 할당하려면 무료 기능을 사용해야 합니다.
  • HeapAlloc으로 할당하려면 HeapFree 함수를 사용해야 합니다.
  • LocalAlloc으로 할당하려면 LocalFree 함수를 사용해야 합니다.

아래 이미지는 주소 0000023ADE449900에서 할당된 메모리를 해제하는 HeapFree가 작동하는 모습을 보여줍니다. 0000023ADE449900 주소가 프로세스 내에 여전히 존재하지만 원래 내용이 임의의 데이터로 덮어씌워진 것을 알 수 있습니다. 이 새 데이터는 프로세스 내부에서 OS가 수행한 새 할당으로 인한 것일 가능성이 높습니다.


7. Windows API 소개

Windows API 소개

소개

Windows API는 개발자에게 애플리케이션이 Windows 운영 체제와 상호 작용할 수 있는 방법을 제공합니다. 예를 들어 애플리케이션에서 화면에 무언가를 표시하거나, 파일을 수정하거나, 레지스트리를 쿼리해야 하는 경우 이러한 모든 작업은 Windows API를 통해 수행할 수 있습니다. Windows API는 Microsoft에서 매우 잘 문서화되어 있으며 여기에서 확인할 수 있습니다.

Windows 데이터 유형

Windows API에는 잘 알려진 데이터 유형(예: int, float) 외에도 많은 데이터 유형이 있습니다. 이러한 데이터 유형은 문서화되어 있으며 여기에서 확인할 수 있습니다.

몇 가지 일반적인 데이터 유형은 다음과 같습니다:

  • DWORD – 32비트 및 64비트 시스템 모두에서 0에서 (2^32 – 1까지의 값을 나타내는 데 사용되는 32비트 부호 없는 정수입니다.)
DWORD dwVariable = 42;
  • size_t – 객체의 크기를 나타내는 데 사용됩니다. 32비트 시스템에서는 32비트 부호 없는 정수로 0에서 (2^32 – 1)까지의 값을 나타냅니다. 반면에 64비트 시스템에서는 64비트 부호 없는 정수로 0에서 (2^64 – 1까지의 값을 나타냅니다.)
SIZE_T sVariable = sizeof(int);
  • 무효 – 특정 데이터 유형이 없음을 나타냅니다.
void* pVariable = NULL; // PVOID와 동일합니다.
  • PVOID – 32비트 시스템에서 모든 데이터 유형의 32비트 또는 4바이트 포인터입니다. 또는 64비트 시스템에서 모든 데이터 유형의 64비트 또는 8바이트 포인터입니다.
PVOID pVariable = &SomeData;
  • HANDLE – 운영 체제에서 관리 중인 특정 객체(예: 파일, 프로세스, 스레드)를 지정하는 값입니다.
HANDLE hFile = CreateFile(...);
  • HMODULE – 모듈에 대한 핸들입니다. 이것은 메모리에 있는 모듈의 기본 주소입니다. 모듈의 예는 DLL 또는 EXE 파일일 수 있습니다.
HMODULE hModule = GetModuleHandle(...);
  • LPCSTR/PCSTR – 널로 끝나는 8비트 Windows 문자(ANSI)로 구성된 상수 문자열에 대한 포인터입니다. “L”은 16비트 Windows 프로그래밍 시대에서 파생된 “long”을 의미하며, 현재는 데이터 유형에 영향을 미치지 않지만 명명 규칙은 여전히 존재합니다. “C”는 “상수” 또는 읽기 전용 변수를 나타냅니다. 이 두 데이터 유형은 모두 const char*와 동일합니다.
LPCSTR  lpcString   = "Hello, world!";
PCSTR   pcString    = "Hello, world!";
  • LPSTR/PSTR – LPCSTR 및 PCSTR과 동일하지만, 유일한 차이점은 LPSTR 및 PSTR이 상수 변수를 가리키지 않고 읽기 및 쓰기가 가능한 문자열을 가리킨다는 점입니다. 이 두 데이터 유형은 모두 char*와 동일합니다.
LPSTR   lpString    = "Hello, world!";
PSTR    pString     = "Hello, world!";
  • LPCWSTR\PCWSTR – 16비트 Windows 유니코드 문자(유니코드)의 널로 끝나는 상수 문자열에 대한 포인터입니다. 이 두 데이터 유형은 모두 const wchar*와 동일합니다.
LPCWSTR     lpwcString  = L"Hello, world!";
PCWSTR      pcwString   = L"Hello, world!";
  • PWSTR\LPWSTR – LPCWSTR 및 PCWSTR과 동일하지만, 유일한 차이점은 ‘PWSTR’과 ‘LPWSTR’이 상수 변수를 가리키지 않고 읽기 및 쓰기가 가능한 문자열을 가리킨다는 점입니다. 이 두 데이터 유형은 모두 wchar*와 동일합니다.
LPWSTR  lpwString   = L"Hello, world!";
PWSTR   pwString    = L"Hello, world!";
  • wchar_t – 와이드 문자를 표현하는 데 사용되는 wchar와 동일합니다.
wchar_t     wChar           = L'A';
wchar_t*    wcString        = L"Hello, world!";
  • ULONG_PTR – 지정된 아키텍처에서 포인터와 같은 크기의 부호 없는 정수를 나타냅니다. 즉, 32비트 시스템에서는 ULONG_PTR의 크기가 32비트이고 64비트 시스템에서는 64비트입니다. 이 강좌에서는 포인터가 포함된 산술 표현식(예: PVOID)을 조작할 때 ULONG_PTR을 사용합니다. 산술 연산을 실행하기 전에 포인터는 ULONG_PTR로 타입 캐스팅됩니다. 이 접근 방식은 컴파일 오류를 유발할 수 있는 포인터의 직접적인 조작을 피하기 위해 사용됩니다.
PVOID Pointer = malloc(100);
// Pointer = Pointer + 10; // not allowed
Pointer = (ULONG_PTR)Pointer + 10; // allowed

데이터 유형 포인터

Windows API를 사용하면 개발자가 데이터 유형을 직접 선언하거나 데이터 유형에 대한 포인터를 선언할 수 있습니다. 이는 데이터 유형 이름에 반영되어 있는데, ‘P’로 시작하는 데이터 유형은 실제 데이터 유형에 대한 포인터를 나타내고 ‘P’로 시작하지 않는 데이터 유형은 실제 데이터 유형 자체를 나타냅니다.

이 기능은 나중에 데이터 유형에 대한 포인터인 매개변수가 있는 Windows API로 작업할 때 유용하게 사용할 수 있습니다. 아래 예는 “P” 데이터 유형이 포인터가 아닌 데이터 유형과 어떻게 관련되는지 보여줍니다.

  • PHANDLE은 HANDLE*과 동일합니다.
  • PSIZE_T는 SIZE_T*와 동일합니다.
  • PDWORD는 DWORD*와 동일합니다.

ANSI 및 유니코드 함수

대부분의 Windows API 함수는 “A” 또는 “W”로 끝나는 두 가지 버전이 있습니다. 예를 들어 CreateFileA와 CreateFileW가 있습니다. “A”로 끝나는 함수는 “ANSI”를 나타내고, “W”로 끝나는 함수는 유니코드 또는 “와이드”를 나타냅니다.

염두에 두어야 할 주요 차이점은 ANSI 함수는 해당되는 경우 ANSI 데이터 유형을 매개변수로 받는 반면, 유니코드 함수는 유니코드 데이터 유형을 받는다는 점입니다. 예를 들어 CreateFileA의 첫 번째 매개변수는 8비트 Windows ANSI 문자로 구성된 상수 널로 끝나는 문자열에 대한 포인터인 LPCSTR입니다. 반면 CreateFileW의 첫 번째 매개 변수는 16비트 유니코드 문자로 구성된 상수 널로 끝나는 문자열에 대한 포인터인 LPCWSTR입니다.

또한 필요한 바이트 수는 사용하는 버전에 따라 달라집니다.

char str1[] = "maldev"; // 7바이트(maldev + null 바이트).

wchar str2[] = L"maldev"; // 14바이트, 각 문자는 2바이트입니다(널 바이트도 2바이트입니다).

입력 및 출력 매개변수

Windows API에는 인/아웃 매개변수가 있습니다. IN 매개변수는 함수에 전달되어 입력에 사용되는 매개변수입니다. 반면 OUT 매개변수는 함수 호출자에게 값을 반환하는 데 사용되는 매개변수입니다. 출력 매개변수는 포인터를 통해 참조로 전달되는 경우가 많습니다.

예를 들어 아래 코드 스니펫은 정수 포인터를 받아 값을 123으로 설정하는 함수 HackTheWorld를 보여줍니다. 이 매개변수는 값을 반환하므로 아웃 매개변수로 간주됩니다.

BOOL HackTheWorld(OUT int* num){

    // Setting the value of num to 123
    *num = 123;

    // Returning a boolean value
    return TRUE;
}

int main(){
    int a = 0;

    // 'HackTheWorld' will return true
    // 'a' will contain the value 123
    HackTheWorld(&a);
}

OUT 또는 IN 키워드를 사용하는 것은 개발자가 함수가 기대하는 것과 이러한 매개변수로 수행하는 작업을 더 쉽게 이해할 수 있도록 하기 위한 것입니다. 그러나 이러한 키워드를 제외해도 해당 매개변수가 출력 매개변수로 간주되는지 입력 매개변수로 간주되는지 여부에는 영향을 미치지 않는다는 점을 알아두세요.

Windows API 예제

이제 Windows API의 기본 사항에 대해 설명했으므로 이 섹션에서는 CreateFileW 함수의 사용법을 살펴봅니다.

API 참조 찾기

함수의 기능이나 필요한 인수가 확실하지 않은 경우 항상 설명서를 참조하는 것이 중요합니다. 항상 함수에 대한 설명을 읽고 함수가 원하는 작업을 수행하는지 평가하세요. CreateFileW 문서는 여기에서 확인할 수 있습니다.

반환 유형 및 매개변수 분석

다음 단계는 반환 데이터 유형과 함께 함수의 매개변수를 확인하는 것입니다. 문서에 따르면 함수가 성공하면 반환 값은 지정된 파일, 장치, 명명된 파이프 또는 메일 슬롯에 대한 열린 핸들이므로 CreateFileW는 생성된 지정된 항목에 대한 HANDLE 데이터 유형을 반환합니다.

또한 함수 매개변수가 모두 매개변수 안에 있다는 점에 유의하세요. 즉, 매개변수가 모두 매개변수 안에 있기 때문에 함수가 매개변수에서 어떤 데이터도 반환하지 않습니다. 대괄호 안의 키워드(예: inoutoptional)는 순전히 개발자의 참조용이며 실제로는 아무런 영향을 미치지 않는다는 점에 유의하세요.

HANDLE CreateFileW(
  [in]           LPCWSTR               lpFileName,
  [in]           DWORD                 dwDesiredAccess,
  [in]           DWORD                 dwShareMode,
  [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  [in]           DWORD                 dwCreationDisposition,
  [in]           DWORD                 dwFlagsAndAttributes,
  [in, optional] HANDLE                hTemplateFile
);

기능 사용

아래 샘플 코드는 CreateFileW의 사용 예시입니다. 이 코드는 현재 사용자의 데스크톱에 maldev.txt라는 이름의 텍스트 파일을 생성합니다.

// This is needed to store the handle to the file object
// the 'INVALID_HANDLE_VALUE' is just to intialize the variable
Handle hFile = INVALID_HANDLE_VALUE;

// The full path of the file to create.
// Double backslashes are required to escape the single backslash character in C
LPCWSTR filePath = L"C:\\Users\\maldevacademy\\Desktop\\maldev.txt";

// Call CreateFileW with the file path
// The additional parameters are directly from the documentation
hFile = CreateFileW(filePath, GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// On failure CreateFileW returns INVALID_HANDLE_VALUE
// GetLastError() is another Windows API that retrieves the error code of the previously executed WinAPI function
if (hFile == INVALID_HANDLE_VALUE){
    printf("[-] CreateFileW Api Function Failed With Error : %d\n", GetLastError());
    return -1;
}

Windows API 디버깅 오류

함수가 실패하면 자세한 설명이 없는 오류를 반환하는 경우가 많습니다. 예를 들어 CreateFileW가 실패하면 파일을 만들 수 없음을 나타내는 INVALID_HANDLE_VALUE가 반환됩니다. 파일을 만들 수 없는 이유에 대한 자세한 정보를 얻으려면 GetLastError 함수를 사용하여 오류 코드를 검색해야 합니다.

코드가 검색되면 Windows의 시스템 오류 코드 목록에서 해당 코드를 조회해야 합니다. 몇 가지 일반적인 오류 코드가 아래에 번역되어 있습니다:

  • 5 – 오류_액세스_거부
  • 2 – 오류_파일_못_찾음
  • 87 – 오류_부정확한_파라미터

Windows 네이티브 API 디버깅 오류

Windows 아키텍처 모듈에서 기억해 두세요. NTAPI는 대부분 ntdll.dll에서 내보내집니다. Windows API와 달리 이러한 함수는 GetLastError를 통해 오류 코드를 가져올 수 없습니다. 대신 NTSTATUS 데이터 유형으로 표시되는 오류 코드를 직접 반환합니다.

NTSTATUS는 시스템 호출 또는 함수의 상태를 나타내는 데 사용되며 32비트 부호 없는 정수 값으로 정의됩니다. 시스템 호출이 성공하면 0인 STATUS_SUCCESS 값이 반환됩니다. 반면에 호출이 실패하면 0이 아닌 값을 반환하므로 문제의 원인을 자세히 조사하려면 NTSTATUS 값에 대한 Microsoft의 설명서를 확인해야 합니다.

아래 코드 스니펫은 시스템 호출에 대한 오류 검사가 수행되는 방법을 보여줍니다.

NTSTATUS STATUS = NativeSyscallExample(...);
if (STATUS != STATUS_SUCCESS){
    // printing the error in unsigned integer hexadecimal format
    printf("[!] NativeSyscallExample Failed With Status : 0x%0.8X \n", STATUS);
}

// NativeSyscallExample succeeded

NT_SUCCESS 매크로

NTAPI의 반환값을 확인하는 또 다른 방법은 여기에 표시된 NT_SUCCESS 매크로를 사용하는 것입니다. 이 매크로는 함수가 성공하면 TRUE를 반환하고 실패하면 FALSE를 반환합니다.

#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)

다음은 이 매크로를 사용하는 예시입니다.

NTSTATUS STATUS = NativeSyscallExample(...);<br>if (!NT_SUCCESS(STATUS)){<br>    // printing the error in unsigned integer hexadecimal format   
printf("[!] NativeSyscallExample Failed With Status : 0x%0.8X \n", STATUS);<}
// NativeSyscallExample succeeded

8. 휴대용 실행 파일 형식

휴대용 실행 파일 형식

소개

휴대용 실행 파일(PE)은 Windows에서 실행 파일을 위한 파일 형식입니다. PE 파일 확장자의 몇 가지 예로는 .exe, . dll, . sys 및 . scr이 있습니다. 이 모듈에서는 멀웨어를 빌드하거나 리버스 엔지니어링할 때 알아야 할 중요한 PE 구조에 대해 설명합니다.

이 모듈과 향후 모듈에서는 종종 실행 파일(예: EXE, DLL)을 “이미지”라는 용어로 혼용하여 지칭합니다.

PE 구조

아래 다이어그램은 휴대용 실행 파일의 단순화된 구조를 보여줍니다. 이미지에 표시된 모든 헤더는 PE 파일에 대한 정보를 담고 있는 데이터 구조로 정의됩니다. 각 데이터 구조는 이 모듈에서 자세히 설명합니다.

도스 헤더(이미지_도스_헤더)

PE 파일의 첫 번째 헤더에는 항상 0x4D와 0x5A라는 두 바이트가 접두사로 붙는데, 이 바이트는 일반적으로 MZ라고 합니다. 이 바이트는 파싱 또는 검사 중인 파일이 유효한 PE 파일인지 확인하는 데 사용되는 DOS 헤더 서명을 나타냅니다. DOS 헤더는 다음과 같이 정의되는 데이터 구조입니다:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // Offset to the NT header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

구조체에서 가장 중요한 멤버는 e_magic과 e_lfanew입니다.

e_magic은 2바이트이며 고정 값은 0x5A4D 또는 MZ입니다.

e_lfanew는 NT 헤더의 시작 부분에 대한 오프셋을 포함하는 4바이트 값입니다. e_lfanew는 항상 0x3C의 오프셋에 위치한다는 점에 유의하세요.

DOS 스텁

NT 헤더 구조로 넘어가기 전에, 프로그램이 DOS 모드 또는 “디스크 작동 모드”에서 로드된 경우 “이 프로그램은 DOS 모드에서 실행할 수 없습니다”라는 오류 메시지를 출력하는 DOS 스텁이 있습니다. 이 오류 메시지는 컴파일 시 프로그래머가 변경할 수 있다는 점에 주목할 필요가 있습니다. 이것은 PE 헤더는 아니지만 알아두는 것이 좋습니다.

NT 헤더(IMAGE_NT_HEADERS)

NT 헤더는 PE 파일에 대한 많은 양의 정보를 포함하는 두 개의 다른 이미지 헤더인 FileHeader와 OptionalHeader를 통합하므로 필수적입니다. DOS 헤더와 마찬가지로 NT 헤더에는 이를 확인하는 데 사용되는 서명 멤버가 포함되어 있습니다. 일반적으로 서명 요소는 0x50 및 0x45 바이트로 표시되는 “PE” 문자열과 동일합니다. 그러나 서명은 데이터 유형이 DWORD이므로 두 개의 널 바이트가 추가된다는 점을 제외하면 서명은 0x50450000으로 표시되며 여전히 “PE”입니다. NT 헤더는 DOS 헤더 내부의 e_lfanew 멤버를 사용하여 도달할 수 있습니다.

NT 헤더 구조는 머신의 아키텍처에 따라 다릅니다.

32비트 버전:

typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

64비트 버전:

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD                   Signature;
    IMAGE_FILE_HEADER       FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

유일한 차이점은 OptionalHeader 데이터 구조인 IMAGE_OPTIONAL_HEADER32 및 IMAGE_OPTIONAL_HEADER64입니다.

파일 헤더(이미지_파일_헤더)

이전 NT 헤더 데이터 구조에서 액세스할 수 있는 다음 헤더로 이동합니다.

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

가장 중요한 구조체 멤버는 다음과 같습니다:

  • NumberOfSections – PE 파일에 있는 섹션의 수입니다(나중에 설명).
  • 특성 – 실행 파일에 대한 특정 속성(예: 동적 링크 라이브러리(DLL) 또는 콘솔 애플리케이션)을 지정하는 플래그입니다.
  • SizeOfOptionalHeader – 다음 선택적 헤더의 크기입니다.

파일 헤더에 대한 추가 정보는 공식 문서 페이지에서 확인할 수 있습니다.

선택적 헤더(이미지_옵션적_헤더)

선택적 헤더는 중요하며 “선택적”이라고 불리지만 PE 파일을 실행하는 데 필수적입니다. 일부 파일 유형에는 이 헤더가 없기 때문에 선택적이라고 합니다.

선택적 헤더에는 32비트 및 64비트 시스템용 버전의 두 가지 버전이 있습니다. 두 버전 모두 데이터 구조가 거의 동일하지만 주요 차이점은 일부 멤버의 크기입니다. 64비트 버전에서는 ULONGLONG이, 32비트 버전에서는 DWORD가 사용됩니다. 또한 32비트 버전에는 64비트 버전에 없는 일부 멤버가 있습니다.

32비트 버전:

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

64비트 버전:

typedef struct _IMAGE_OPTIONAL_HEADER64 {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  ULONGLONG            ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  ULONGLONG            SizeOfStackReserve;
  ULONGLONG            SizeOfStackCommit;
  ULONGLONG            SizeOfHeapReserve;
  ULONGLONG            SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

선택적 헤더에는 사용할 수 있는 수많은 정보가 포함되어 있습니다. 다음은 일반적으로 사용되는 구조체 멤버 중 일부입니다:

  • 마법 – 이미지 파일(32비트 또는 64비트 이미지)의 상태를 설명합니다.
  • 주요 운영 체제 버전 – 필요한 운영 체제의 주요 버전 번호(예: 11, 10)
  • MinorOperatingSystemVersion – 필요한 운영 체제의 부 버전 번호(예: 1511, 1507, 1607)
  • SizeOfCode – .text 섹션의 크기(나중에 설명)
  • AddressOfEntryPoint – 파일의 진입점에 대한 오프셋(일반적으로 주요 함수)
  • BaseOfCode – .text 섹션의 시작 부분으로 오프셋합니다.
  • SizeOfImage – 이미지 파일의 크기(바이트)
  • ImageBase – 애플리케이션이 실행될 때 메모리에 로드할 기본 주소를 지정합니다. 그러나 ASLR(주소 공간 레이아웃 무작위화)과 같은 Windows의 메모리 보호 메커니즘으로 인해 Windows PE 로더가 파일을 다른 주소에 매핑하기 때문에 기본 주소에 매핑된 이미지를 볼 수 있는 경우는 드뭅니다. Windows PE 로더에 의해 수행되는 이러한 무작위 할당은 일정하다고 간주되는 일부 주소가 변경되기 때문에 향후 기술 구현에 문제를 일으킬 수 있습니다. 그런 다음 Windows PE 로더는 이러한 주소를 수정하기 위해 PE 재배 치를 수행합니다.
  • DataDirectory – 선택적 헤더에서 가장 중요한 멤버 중 하나입니다. 이것은 PE 파일의 디렉터리를 포함하는 IMAGE_DATA_DIRECTORY의 배열입니다(아래 설명 참조).

데이터 디렉토리

데이터 디렉토리는 옵션의 헤더 마지막 멤버에서 액세스할 수 있습니다. 이는 다음과 같은 데이터 구조를 가진 데이터 유형 IMAGE_DATA_DIRECTORY의 배열입니다:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

데이터 디렉토리 배열의 크기는 16이라는 상수 값인 IMAGE_NUMBEROF_DIRECTORY_ENTRIES입니다. 배열의 각 요소는 PE 섹션 또는 데이터 테이블(PE에 대한 특정 정보가 저장되는 위치)에 대한 일부 데이터를 포함하는 특정 데이터 디렉토리를 나타냅니다.

특정 데이터 디렉토리는 배열에서 해당 인덱스를 사용하여 액세스할 수 있습니다.

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

아래 두 섹션에서는 내보내기 디렉토리와 주소 테이블 가져오기라는 두 가지 중요한 데이터 디렉터리에 대해 간략하게 설명합니다.

내보내기 디렉토리

PE의 내보내기 디렉토리는 실행 파일에서 내보내는 함수 및 변수에 대한 정보를 포함하는 데이터 구조입니다. 내보내기 디렉터리에는 내보낸 함수와 변수의 주소가 포함되어 있으며, 다른 실행 파일에서 해당 함수와 데이터에 액세스하는 데 사용할 수 있습니다. 내보내기 디렉터리는 일반적으로 함수를 내보내는 DLL에서 찾을 수 있습니다(예: CreateFileA를 내보내는 kernel32.dll ).

주소 테이블 가져오기

가져오기 주소 테이블은 다른 실행 파일에서 가져온 함수의 주소에 대한 정보를 포함하는 PE의 데이터 구조입니다. 이 주소는 다른 실행 파일의 함수 및 데이터에 액세스하는 데 사용됩니다(예: Application.exe가 kernel32.dll에서 CreateFileA를 가져오는 경우).

체육 섹션

PE 섹션에는 실행 프로그램을 만드는 데 사용되는 코드와 데이터가 포함되어 있습니다. 각 PE 섹션에는 고유한 이름이 지정되며 일반적으로 실행 코드, 데이터 또는 리소스 정보가 포함됩니다. 컴파일러마다 구성에 따라 섹션을 추가, 제거 또는 병합할 수 있기 때문에 PE 섹션의 수는 일정하지 않습니다. 일부 섹션은 나중에 수동으로 추가할 수도 있으므로 동적이며 IMAGE_FILE_HEADER.NumberOfSections가 그 수를 결정하는 데 도움이 됩니다.

다음 체육 섹션은 가장 중요한 섹션이며 거의 모든 체육에 존재합니다.

  • .text – 작성된 코드인 실행 코드를 포함합니다.
  • .data – 코드에서 초기화된 변수인 초기화된 데이터를 포함합니다.
  • .rdata – 읽기 전용 데이터를 포함합니다. 접두사 const가 붙은 상수 변수입니다.
  • .idata – 가져오기 테이블을 포함합니다. 코드를 사용하여 호출된 함수와 관련된 정보 테이블입니다. 이 테이블은 Windows PE 로더에서 프로세스에 로드할 DLL 파일과 각 DLL에서 사용되는 함수를 결정하는 데 사용됩니다.
  • .reloc – 프로그램이 오류 없이 메모리에 로드될 수 있도록 메모리 주소를 수정하는 방법에 대한 정보가 포함되어 있습니다.
  • .rsrc – 아이콘 및 비트맵과 같은 리소스를 저장하는 데 사용됩니다.

각 PE 섹션에는 해당 섹션에 대한 중요한 정보가 포함된 IMAGE_SECTION_HEADER 데이터 구조가 있습니다. 이러한 구조는 PE 파일의 NT 헤더 아래에 저장되며, 각 구조가 섹션을 나타내는 서로 위에 쌓여 있습니다.

이미지 섹션 헤더 구조는 다음과 같습니다:

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

요소들을 살펴보면 하나하나가 매우 가치 있고 중요합니다:

  • 이름 – 섹션의 이름입니다. (예: .text, .data, .rdata).
  • 물리적 주소 또는 가상 크기 – 섹션이 메모리에 있을 때의 크기입니다.
  • 가상 주소 – 메모리에서 섹션 시작 부분의 오프셋입니다.

추가 참조 자료

특정 섹션에 대한 추가 설명이 필요한 경우 0xRick의 블로그에 있는 다음 블로그 게시물을 적극 권장합니다.

결론

PE 헤더를 처음 접하면 이해하기 어려울 수 있습니다. 다행히도 기본 모듈 중 PE 구조에 대한 심층적인 이해가 필요한 모듈은 없습니다. 하지만 멀웨어가 더 복잡한 기술을 수행하려면 일부 코드에서 PE 파일의 헤더와 섹션을 파싱해야 하므로 더 많은 이해가 필요합니다. 이는 중급 및 고급 모듈에서 볼 수 있습니다.


9. 동적 링크 라이브러리

동적 링크 라이브러리(DLL)

소개

.exe와 .dll 파일 형식은 모두 휴대용 실행 파일 형식으로 간주되지만 두 파일 형식 간에는 차이점이 있습니다. 이 모듈에서는 두 파일 형식의 차이점에 대해 설명합니다.

DLL이란 무엇인가요?

DLL은 여러 애플리케이션에서 동시에 사용할 수 있는 실행 가능한 함수 또는 데이터의 공유 라이브러리입니다. 프로세스에서 사용할 함수를 내보내는 데 사용됩니다. EXE 파일과 달리 DLL 파일은 자체적으로 코드를 실행할 수 없습니다. 대신 코드를 실행하려면 다른 프로그램에서 DLL 라이브러리를 호출해야 합니다. 앞서 언급했듯이 CreateFileW는 kernel32.dll에서 내보내므로 프로세스에서 해당 함수를 호출하려면 먼저 kernel32.dll을 주소 공간에 로드해야 합니다.

일부 DLL은 프로세스가 제대로 실행되는 데 필요한 기능을 내보내므로 기본적으로 모든 프로세스에 자동으로 로드됩니다. 이러한 DLL의 몇 가지 예로는 ntdll.dllkernel32.dll 및 kernelbase.dll이 있습니다. 아래 이미지는 현재 explorer.exe 프로세스에 의해 로드되는 몇 가지 DLL을 보여줍니다.

시스템 전체 DLL 기본 주소

Windows OS는 메모리 사용량을 최적화하고 시스템 성능을 개선하기 위해 시스템 전체 DLL 기본 주소를 사용하여 특정 컴퓨터의 모든 프로세스의 가상 주소 공간에 있는 동일한 기본 주소에 일부 DLL을 로드합니다. 다음 이미지는 실행 중인 여러 프로세스 중 동일한 주소(0x7fff9fad0000)에 로드되는 kernel32.dll을 보여줍니다.

왜 DLL을 사용하나요?

Windows에서 DLL이 자주 사용되는 데에는 몇 가지 이유가 있습니다:

  1. 코드 모듈화 – 전체 기능을 포함하는 하나의 거대한 실행 파일이 아닌, 코드를 여러 개의 독립적인 라이브러리로 나누고 각 라이브러리가 특정 기능에 집중하도록 합니다. 모듈화를 통해 개발자는 개발 및 디버깅을 더 쉽게 할 수 있습니다.
  2. 코드 재사용 – DLL은 하나의 라이브러리를 여러 프로세스에서 호출할 수 있으므로 코드 재사용을 촉진합니다.
  3. 효율적인 메모리 사용 – 여러 프로세스에 동일한 DLL이 필요한 경우, 해당 DLL을 프로세스의 메모리에 로드하는 대신 공유하여 메모리를 절약할 수 있습니다.

DLL 진입점

DLL은 프로세스가 DLL 라이브러리를 로드할 때와 같이 특정 작업이 발생할 때 코드를 실행하는 진입점 함수를 선택적으로 지정할 수 있습니다. 엔트리 포인트가 호출될 수 있는 가능성은 4가지입니다:

  • DLL_PROCESS_ATTACHED – 프로세스가 DLL을 로드 중입니다.
  • DLL_THREAD_ATTACHED – 프로세스가 새 스레드를 생성 중입니다.
  • DLL_THREAD_DETACH – 스레드가 정상적으로 종료됩니다.
  • DLL_PROCESS_DETACH – 프로세스가 DLL을 언로드합니다.

샘플 DLL 코드

아래 코드는 일반적인 DLL 코드 구조를 보여줍니다.

BOOL APIENTRY DllMain(
    HANDLE hModule,             // Handle to DLL module
    DWORD ul_reason_for_call,   // Reason for calling function
    LPVOID lpReserved           // Reserved
) {

    switch (ul_reason_for_call) {
        case DLL_PROCESS_ATTACHED: // A process is loading the DLL.
        // Do something here
        break;
        case DLL_THREAD_ATTACHED: // A process is creating a new thread.
        // Do something here
        break;
        case DLL_THREAD_DETACH: // A thread exits normally.
        // Do something here
        break;
        case DLL_PROCESS_DETACH: // A process unloads the DLL.
        // Do something here
        break;
    }
    return TRUE;
}

함수 내보내기

DLL은 호출하는 애플리케이션이나 프로세스에서 사용할 수 있는 함수를 내보낼 수 있습니다. 함수를 내보내려면 extern 및 __declspec (dllexport) 키워드를 사용하여 함수를 정의해야 합니다. 내보낸 함수 HelloWorld의 예는 다음과 같습니다.

////// sampleDLL.dll //////

extern __declspec(dllexport) void HelloWorld(){
// Function code here
}

동적 연결

LoadLibraryGetModuleHandle 및 GetProcAddress WinAPI를 사용하여 DLL에서 함수를 가져올 수 있습니다. 이를 동적 링크라고 합니다. 이는 링커 및 가져오기 주소 테이블을 사용하여 컴파일 시 링크하는 대신 런타임에 코드(DLL)를 로드하고 링크하는 방법입니다.

동적 링크를 사용하면 몇 가지 이점이 있으며, 이러한 이점은 Microsoft에서 여기에 문서화되어 있습니다.

이 섹션에서는 DLL 로드, DLL 핸들 검색, 내보낸 함수의 주소 검색 및 함수 호출 단계를 안내합니다.

DLL 로드

애플리케이션에서 MessageBoxA와 같은 함수를 호출하면 Windows OS가 호출 프로세스의 메모리 주소 공간(이 경우 user32.dll)으로 MessageBoxA 함수를 내보내는 DLL을 강제로 로드합니다. user32.dll 로드는 코드가 아니라 프로세스가 시작될 때 OS가 자동으로 수행합니다.

그러나 sampleDLL.dll의 HelloWorld 함수와 같은 일부 경우에는 DLL이 메모리에 로드되지 않을 수 있습니다. 애플리케이션이 HelloWorld 함수를 호출하려면 먼저 해당 함수를 내보내는 DLL의 핸들을 검색해야 합니다. 애플리케이션에 sampleDLL.dll이 메모리에 로드되어 있지 않은 경우 아래와 같이 LoadLibrary WinAPI를 사용해야 합니다.

HMODULE hModule = LoadLibraryA("sampleDLL.dll"); // 이제 hModule에 sampleDLL.dll의 핸들이 포함됩니다.

DLL의 핸들 검색

sampleDLL.dll이 이미 애플리케이션의 메모리에 로드되어 있는 경우 LoadLibrary 함수를 활용하지 않고 GetModuleHandle WinAPI 함수를 통해 해당 핸들을 검색할 수 있습니다.

HMODULE hModule = GetModuleHandleA("sampleDLL.dll");

함수 주소 검색하기

DLL이 메모리에 로드되고 핸들이 검색되면 다음 단계는 함수의 주소를 검색하는 것입니다. 이 작업은 함수를 내보내는 DLL의 핸들 및 함수 이름을 가져오는 GetProcAddress WinAPI를 사용하여 수행됩니다.

PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");

함수 호출하기

헬로월드의주소가 pHelloWorld 변수에 저장되면 다음 단계는 이 주소를 헬로월드의함수 포인터로 타입 캐스팅하는 것입니다. 이 함수 포인터는 함수를 호출하기 위해 필요합니다.

// Constructing a new data type that represents HelloWorld's function pointer
typedef void (WINAPI* HelloWorldFunctionPointer)();

void call(){
    HMODULE hModule = LoadLibraryA("sampleDLL.dll");
    PVOID pHelloWorld = GetProcAddress(hModule, "HelloWorld");
    // Type-casting the 'pHelloWorld' variable to be of type 'HelloWorldFunctionPointer'
    HelloWorldFunctionPointer HelloWorld = (HelloWorldFunctionPointer)pHelloWorld;
    HelloWorld();   // Calling the 'HelloWorld' function via its function pointer
}

동적 연결 예시

아래 코드는 MessageBoxA가 호출되는 동적 연결의 또 다른 간단한 예시를 보여줍니다. 이 코드에서는 해당 함수를 내보내는 DLL인 user32.dll이 메모리에 로드되지 않은 것으로 가정합니다. DLL이 메모리에 로드되지 않은 경우 해당 DLL을 프로세스의 주소 공간에 로드하기 위해 LoadLibrary를 사용해야 한다는 점을 기억하세요.

typedef int (WINAPI* MessageBoxAFunctionPointer)( // Constructing a new data type, that will represent MessageBoxA's function pointer
  HWND          hWnd,
  LPCSTR        lpText,
  LPCSTR        lpCaption,
  UINT          uType
);

void call(){
    // Retrieving MessageBox's address, and saving it to 'pMessageBoxA' (MessageBoxA's function pointer)
    MessageBoxAFunctionPointer pMessageBoxA = (MessageBoxAFunctionPointer)GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
    if (pMessageBoxA != NULL){
        // Calling MessageBox via its function pointer if not null
        pMessageBoxA(NULL, "MessageBox's Text", "MessageBox's Caption", MB_OK);
    }
}

함수 포인터

이 과정의 나머지 부분에서는 함수 포인터 데이터 유형에 “함수 포인터”를 의미하는 fn이 앞에 붙은 WinAPI 이름을 사용하는 명명 규칙이 적용됩니다. 예를 들어, 위의 MessageBoxAFunctionPointer 데이터 유형은 fnMessageBoxA로 표시됩니다. 이는 코스 전반에 걸쳐 단순성을 유지하고 명확성을 높이기 위해 사용됩니다.

Rundll32.exe

프로그래밍 방법을 사용하지 않고 내보낸 함수를 실행하는 방법에는 몇 가지가 있습니다. 한 가지 일반적인 방법은 rundll32.exe 바이너리를 사용하는 것입니다. Rundll32.exe는 DLL 파일의 내보낸 함수를 실행하는 데 사용되는 내장된 Windows 바이너리입니다. 내보낸 함수를 실행하려면 다음 명령을 사용합니다:

rundll32.exe <dllname>, <function exported to run>

예를 들어, User32.dll은 머신을 잠그는 LockWorkStation 함수를 내보냅니다. 이 함수를 실행하려면 다음 명령을 사용합니다:

rundll32.exe user32.dll,LockWorkStation

Visual Studio로 DLL 파일 만들기

DLL 파일을 만들려면 Visual Studio를 시작하고 새 프로젝트를 만듭니다. 프로젝트 템플릿이 제공되면 동적 링크 라이브러리(DLL) 옵션을 선택합니다.

다음으로 프로젝트 파일을 저장할 위치를 선택합니다. 완료되면 다음과 같은 C 코드가 나타납니다.

제공된 DLL 템플릿에는 미리 컴파일된 헤더로 알려진 framework.hpch.h 및 pch.cpp가 함께 제공됩니다. 이러한 파일은 대규모 프로젝트의 프로젝트 컴파일 속도를 높이는 데 사용되는 파일입니다. 이 상황에서는 이러한 파일이 필요하지 않을 가능성이 높으므로 이러한 파일을 삭제하는 것이 좋습니다. 삭제하려면 파일을 강조 표시하고 삭제 키를 누른 다음 ‘삭제’ 옵션을 선택합니다.

미리 컴파일된 헤더를 삭제한 후에는 컴파일러의 기본 설정을 변경하여 프로젝트에서 미리 컴파일된 헤더가 사용되지 않도록 해야 합니다.

C/C++ > 고급 탭으로 이동

‘미리 컴파일된 헤더’ 옵션을 ‘미리 컴파일된 헤더 사용 안 함’으로 변경하고 ‘적용’을 누릅니다.

마지막으로 dllmain.cpp 파일을 dllmain.c로 변경합니다. Maldev Academy에서 제공하는 코드 스니펫은 C++ 대신 C를 사용하므로 이 작업이 필요합니다. 프로그램을 컴파일하려면 빌드 > 솔루션 빌드를 클릭하면 컴파일 구성에 따라 릴리스 또는 디버그 폴더 아래에 DLL이 생성됩니다.


10. 탐지 메커니즘

탐지 메커니즘

소개

보안 솔루션은 악성 소프트웨어를 탐지하기 위해 여러 가지 기술을 사용합니다. 보안 솔루션이 소프트웨어를 악성 소프트웨어로 탐지하거나 분류하는 데 어떤 기술을 사용하는지 이해하는 것이 중요합니다.

정적/서명 탐지

시그니처는 악성코드를 고유하게 식별하는 악성코드 내의 바이트 또는 문자열 수입니다. 변수 이름이나 가져온 함수 등 다른 조건도 지정할 수 있습니다. 보안 솔루션이 프로그램을 검사하면 알려진 규칙 목록과 일치시키려고 시도합니다. 이러한 규칙은 미리 작성하여 보안 솔루션에 푸시해야 합니다. YARA는 보안 공급업체에서 탐지 규칙을 작성하는 데 사용하는 도구 중 하나입니다. 예를 들어 셸코드에 FC 48 83 E4 F0 E8 C0 00 00 00 41 51 41 50 52 51로 시작하는 바이트 시퀀스가 포함된 경우, 이를 사용하여 페이로드가 Msfvenom의 x64 실행 페이로드임을 탐지할 수 있습니다. 파일 내의 문자열에 대해서도 동일한 탐지 메커니즘을 사용할 수 있습니다.

시그니처 탐지는 우회하기 쉽지만 시간이 많이 소요될 수 있습니다. 구현을 고유하게 식별하는 데 사용할 수 있는 값을 멀웨어에 하드코딩하지 않는 것이 중요합니다. 이 과정 전체에 걸쳐 제시된 코드는 하드코딩될 수 있는 값의 하드코딩을 피하고 대신 값을 동적으로 검색하거나 계산하려고 시도합니다.

해싱 탐지

해싱 탐지는 정적/시그니처 탐지의 하위 집합입니다. 이는 매우 간단한 탐지 기법이며, 보안 솔루션이 멀웨어를 탐지할 수 있는 가장 빠르고 간단한 방법입니다. 이 방법은 알려진 멀웨어에 대한 해시(예: MD5, SHA256)를 데이터베이스에 저장하는 것만으로 수행됩니다. 멀웨어의 파일 해시를 보안 솔루션의 해시 데이터베이스와 비교하여 일치하는 항목이 있는지 확인합니다.

해싱 탐지를 회피하는 방법은 매우 간단하지만, 그 자체로는 충분하지 않을 수 있습니다. 파일에서 최소 1바이트만 변경하면 모든 해싱 알고리즘에 따라 파일 해시가 변경되므로 해당 파일은 고유한 파일 해시를 갖게 됩니다.

휴리스틱 탐지

악성 파일을 조금만 변경해도 시그니처 탐지 방법을 쉽게 우회할 수 있기 때문에 기존 멀웨어의 알려지지 않은 새 버전이나 수정된 버전에서 발견할 수 있는 의심스러운 특징을 찾아내기 위해 휴리스틱 탐지가 도입되었습니다. 보안 솔루션에 따라 휴리스틱 모델은 다음 중 하나 또는 둘 다로 구성될 수 있습니다:

  • 정적 휴리스틱 분석 – 의심스러운 프로그램을 디컴파일하고 코드 조각을 이미 알려져 있고 휴리스틱 데이터베이스에 있는 알려진 멀웨어와 비교하는 작업을 수행합니다. 소스 코드의 특정 비율이 휴리스틱 데이터베이스에 있는 항목과 일치하면 해당 프로그램이 플래그가 지정됩니다.
  • 동적 휴리스틱 분석 – 가상 환경 또는 샌드박스 내에 프로그램을 배치한 다음 보안 솔루션에서 의심스러운 동작이 있는지 분석합니다.

동적 휴리스틱 분석(샌드박스 탐지)

샌드박스 탐지는 샌드박스가 적용된 환경에서 파일을 실행하여 파일의 동작을 동적으로 분석합니다. 파일을 실행하는 동안 보안 솔루션은 의심스러운 동작이나 악성으로 분류되는 동작을 찾습니다. 예를 들어 메모리를 할당하는 행위가 반드시 악성 행위는 아니지만 메모리를 할당하고, 인터넷에 연결하여 셸코드를 가져오고, 셸코드를 메모리에 쓰고, 그 순서대로 실행하는 것은 악성 행위로 간주됩니다.

멀웨어 개발자는 샌드박스 환경을 감지하기 위해 안티 샌드박스 기술을 내장합니다. 멀웨어가 샌드박스에서 실행되는 것을 확인하면 정상 코드를 실행하고, 그렇지 않으면 악성 코드를 실행합니다.

행동 기반 탐지

멀웨어가 실행되면 보안 솔루션은 실행 중인 프로세스에서 수행되는 의심스러운 동작을 계속 찾습니다. 보안 솔루션은 DLL 로드, 특정 Windows API 호출, 인터넷 연결과 같은 의심스러운 지표를 찾습니다. 의심스러운 동작이 감지되면 보안 솔루션은 실행 중인 프로세스의 인메모리 스캔을 수행합니다. 프로세스가 악의적인 것으로 판단되면 해당 프로세스는 종료됩니다.

특정 작업은 인메모리 검사를 수행하지 않고 프로세스를 즉시 종료할 수 있습니다. 예를 들어, 멀웨어가 notepad.exe에 프로세스 삽입을 수행하고 인터넷에 연결하면 악성 활동일 가능성이 높기 때문에 프로세스가 즉시 종료될 수 있습니다.

행동 기반 탐지를 피하는 가장 좋은 방법은 프로세스를 최대한 정상적으로 동작하게 만드는 것입니다(예: cmd.exe 하위 프로세스 생성 방지). 또한 메모리 암호화를 통해 인메모리 검사를 우회할 수 있습니다. 이는 향후 모듈에서 다룰 고급 주제입니다.

API 후킹

API 후킹은 주로 EDR과 같은 보안 솔루션에서 프로세스나 코드 실행에서 악성 행위를 실시간으로 모니터링하는 데 사용하는 기술입니다. API 후킹은 일반적으로 악용되는 API를 가로챈 다음 해당 API의 파라미터를 실시간으로 분석하는 방식으로 작동합니다. 이는 보안 솔루션이 난독화 해제 또는 복호화된 후 API로 전달된 콘텐츠를 확인할 수 있기 때문에 강력한 탐지 방법입니다. 이러한 탐지는 실시간 탐지와 행동 기반 탐지의 조합으로 간주됩니다.

아래 다이어그램은 높은 수준의 API 후킹을 보여줍니다.

DLL 언훅 및 직접 시스템 호출과 같은 API 훅을 우회하는 방법에는 여러 가지가 있습니다. 이러한 주제는 향후 모듈에서 다룰 예정입니다.

IAT 확인

PE 구조에서 설명한 구성 요소 중 하나는 주소 테이블 가져오기 또는 IAT입니다. IAT의 기능을 간략히 요약하면, 런타임에 PE에서 사용되는 함수 이름이 포함되어 있습니다. 또한 이러한 함수를 내보내는 라이브러리(DLL)도 포함되어 있습니다. 이 정보는 실행 파일이 어떤 WinAPI를 사용하는지 알 수 있기 때문에 보안 솔루션에 유용합니다.

예를 들어, 랜섬웨어는 파일을 암호화하는 데 사용되므로 암호화 및 파일 관리 함수를 사용할 가능성이 높습니다. 보안 솔루션에서 CreateFileA/W, SetFilePointer, Read/WriteFile, CryptCreateHash, CryptHashData, CryptGetHashParam과 같은 유형의 함수가 포함된 IAT를 발견하면 해당 프로그램에 플래그를 지정하거나 추가 조사를 수행합니다. 아래 이미지는 바이너리의 IAT를 검사하는 데 사용되는 dumpbin.exe 도구를 보여줍니다.

IAT 스캔을 회피하는 한 가지 솔루션은 향후 모듈에서 설명할 API 해싱을 사용하는 것입니다.

수동 분석

앞서 언급한 모든 탐지 메커니즘을 우회하더라도 블루팀과 멀웨어 분석가는 여전히 멀웨어를 수동으로 분석할 수 있습니다. 멀웨어 리버스 엔지니어링에 정통한 방어자는 멀웨어를 탐지할 수 있을 것입니다. 또한 보안 솔루션은 추가 분석을 위해 의심스러운 파일의 사본을 클라우드로 전송하는 경우가 많습니다.

멀웨어 개발자는 리버스 엔지니어링 프로세스를 더 어렵게 만들기 위해 안티 리버싱 기술을 구현할 수 있습니다. 일부 기술에는 향후 모듈에서 설명하는 디버거 탐지 및 가상화 환경 탐지가 포함됩니다.

Share