Home Reversing Dreamhack Helloworld
Post
Cancel

Reversing Dreamhack Helloworld

드림핵 강의를 통해 Helloworld.exe를 분석을 해봅시다.


Helloworld Code

간단한 예제인 HelloWorld.exe를 분석해보자 소스 코드는 1초를 대기하고 Hello, world!를 출력하는 프로그램입니다.

1
2
3
4
5
6
7
8
9
10
#include <Windows.h>
#include <stdio.h>
char* str;
int main() {
  int delay = 1000;
  Sleep(delay);  // 1000ms(1초)를 대기합니다.
  str = (char*)"Hello, world!\n";
  printf(str);
  return 0;
}

정적 분석

신뢰할 수 없는 프로그램을 분석할 때는 악성 프로그램일 가능성을 대비하여 정적 분석을 먼저 시도해보는 것이 바람직합니다.


Main 함수 찾기

정적 분석은 주로 main 함수를 찾고 이를 분석하며 시작합니다. 바이너리에서 어떤 함수를 찾는 방법은 크게 두 가지가 존재 합니다.

  1. 프로그램의 시작 지점인 **진입점(Entry Point, EP)**부터 분석을 시작하여 원하는 함수를 찾을때 까지 탐색
  2. 대상 함수의 특성이나 프로그램의 여러 외적인 정보를 이용하여 탐색하는 방법

1의 경우 바이너리 규모가 커지면 분석에 소요되는 시간이 급증합니다.

main 함수


문자열 검색

프로그램을 정적 분석할 때, 많이 사용되는 정보 중 하나가 프로그램에 포함된 **문자열** 입니다.

IDA 문자열 탐색 기능

  1. Shift + F12
  2. Hello, world! 문자열 확인
  3. 문자열 더블 클릭하여 따라감

상호 참조

정적 분석 도구들은 **상호 참조(Cross Reference, XRef)** 기능을 통해 이를 지원합니다.

main 함수를 찾는 일반적인 방법

일반적으로 main 함수는 C계열 언어에서 **프로그래머가 작성한 코드 중 가장 먼저 실행되는 함수** 입니다. 그렇기 때문에 리버싱을 처음 공부할 때 프로그램에서 가장 먼저 실행되는 코드가 main 함수의 코드라고 생각하기 마련입니다.

그러나 운영체제는 바이너리를 실행할 때, 바이너리에 명시된 진입적부터 프로그램을 실행합니다. 진입점이 main 함수인게 불가능한 것은 아니지만, 일반적으로는 그렇지 않습니다.

진입점과 main함수의 사이를 채우는 것은 컴파일러의 몫입니다. 대부분의 컴파일러는 둘 사이에 여러 함수를 삽입하여 바이너리가 실행될 환경을 먼저 구성하고 뒤에 main함수가 호출되게 합니다.

잠시 편의상 진입점에 위치한 함수를 Start 함수라고 정의한다면 main 함수를 쉽게 찾으려면 컴파일러가 작성하는 start 함수에 익숙해져야 합니다.


main 함수 분석

ida에서 디컴파일을 합니다.

1
2
3
4
5
6
7
8
IDA 디컴파일
int __cdecl main(int argc, const char **argv, const char **envp)
{
	Sleep(0x3E8u);
	qword_14001DBE0 = (__int64)"Hello, world!\n";
	sub_140001060("Hello, world!\n");
	return 0;
}

동작 분석

  1. Sleep함수를 호출하여 (0x3E8u) 밀리초 = 1초만큼 대기
  2. qword_14001DBE0에 “Hello, world!\n” 문자열의 주소 넣음
  3. sub_140001060에 “Hello, world!\n” 를 인자로 전달하여 호출

sub_14001060 함수 분석

1
2
3
4
5
6
7
8
9
10
11
__int64 sub_140001060(__int64 a1, ...)
{
  __int64 v1; // rax
  __int64 v3; // [rsp+50h] [rbp+8h]
  va_list va; // [rsp+58h] [rbp+10h]

  va_start(va, a1);
  v3 = a1;
  v1 = sub_140002E28(1i64);
  return (unsigned int)sub_140001010(v1, v3, 0i64, (__int64 *)va);
}

va_start 함수를 통해 가변 인자를 처리하는 함수임을 알 수 있습니다. __act_iob_func 함수는 스트림을 가져올때 사용되는 함수인데, 인자로 들어가는 1은 stdout을 의미합니다. 따라서 stdout 스트림을 내부적으로 사용하는 가변 함수임을 알 수 있고 sub_14001060 함수는 printf 함수로 추정할 수 있습니다.

스트림

스트림(Stream)은 데이터가 조금씩 흘러들어온다는 의미입니다. 데이터는 스트림의 한 형식으로 한 프로세스에서 다른 프로세스로, 또는 한 프로세스에서 다른 파일 등으로 이동합니다.

운영체제는 stdin(standard input), stdout(standard output), stderr(standard error) 와 같은 기본 스트림들을 프로세스마다 생성해 줍니다. 일반적으로 사용자 <-> 프로세스 간 연결해주기위해 사용합니다.

printf 함수는 stdout을 통해 출력 데이터를 우리가 볼 수있게 해주며, scanf함수는 우리의 키보드 입력을 stdin으로 받아 프로세스에게 전달합니다.

이런 기본 스트림외에, 프로그래머는 필요에 따라 스트림을 생성할 수 있습니다.


동적 분석

제일 먼저 main 함수 진입이 필요합니다.

중단점 설정(Break Point BP) 및 실행(Run)

중단점은 특정 주소에 설정하고, 실행 명령을 내리면 중단점까지 멈추지 않고 실행됩니다.

  1. main 함수에 중단점 설정 (F2)
  2. 디버깅을 시작하여 main 함수까지 실행, 디버깅 시작 단축키는(F9), 디버거는 Local Windows debugger
  3. 동적분석 준비 완료

한 단계 실행(Step over)

Step Over는 관심이 있는 부분의 코드를 정밀하게 분석하기 위해 사용하는 기능입니다. 단축키는 F8이며, 프로그램의 동작을 분석합니다.

Stepover

  1. sub rsp, 38을 통해 main 함수가 사용할 스택 영역을 확보함
  2. rsp + 0x20에 4바이트 값인 0x000003e8을 저장함
  3. rsp + 0x20에 저장된 값을 ecx에 옮김 -> 함수의 첫 번째 인자를 설정
  4. Sleep 함수를 호출 ecx가 0x3e8 이므로 Sleep(1000)이 실행되어 1초 정지
  5. “Hello, world!\n” 문쟈열의 주소를 rax로 옮김
  6. 메모리 덤프창을 이용하여 0x14001a140 데이터를 보면 실제 해당 문자열 저장된 것을 볼 수 있음
  7. rax의 값을 data 세그먼트의 주소인 0x14001dbe0에 저장
  8. 0x001dbe0에 저장된 값을 rcx에 옮김 -> 다음에 호출할 함수의 첫 번째 인자를 설정
  9. 0x14001060 함수 호출 -> printf 함수
  10. 프로그램을 확인하면 “Hello, world!” 출력 확인
  11. 시작할때 확장한 스택 영역을 add rep, 38을 통해 다시 축소하고, ret으로 원래 실행 흐름으로 돌아감

함수 내부로 진입하기(Step into)

함수가 호출하는 다른 함수까지 정밀하게 분석해야할때 사용하는 기능입니다. 단축키는 F7입니다. printf() 함수에 중단점을 설정하고, F7로 내부로 진입합니다.

Step into

  1. printf를 호출하는 `0x1400110b에 중단점을 설정
  2. Step info (F7) 함수 내부로 진입

Appendix, 실행중인 프로세스 조작

IDA를 이용하면 실행중인 메모리를 조작할 수 있습니다.

Sleep Forever

기존 코드에서는 Sleep(1000)을 호출하여 1초동안 프로세스가 정지되지만 delay을 변조하여 1000초가 멈추도록 바꿔보도록 합니다.

sleepForever

  1. delay를 Sleep 함수의 인자로 전달하는 부분에 중단점 설정
  2. 스택을 보면 rsp + 0x20에 delay값인 0x3e8이 저장됨
  3. 해당 값을 클릭후 F2를 눌러 0xf4240으로 변경 -> F2로 저장
  4. F9를 눌러도 동작을 안함 -> 1000초는 약20분이라, 20분 대기후 프로세스가 제개됨

실제로 어떤 함수의 동작을 모를 때는 인자를 적절히 조작해 봄으로써 함수의 동작과 인자의 역할들을 유추해볼수 있습니다.