발자취

[Reverse Engineering] #06-2. IDA 실습 본문

Dreamhack Study/Reverse Engineering

[Reverse Engineering] #06-2. IDA 실습

해린 2024. 6. 25. 05:32

본 게시물은 Dreamhack의 Reverse Engineering 로드맵 과정을 학습한 기록이다.

https://dreamhack.io/lecture/courses/572

 

#01. 정적 분석

1. IDA로 파일 열기

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

 

아무것도 건들지 말고 OK 버튼을 누른다

 

 

2. main 함수 찾기

정적 분석에서는 주로 main 함수를 먼저 찾고 이를 분석하면서 시작된다.

이 예제에서는 IDA가 main 함수를 자동으로 찾아주지만, 이번에는 셀프로 찾아보도록 한다.

 

바이너리에서 어떤 함수를 찾는 방법은 크게 두 가지가 있다. 

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

1번 방식은 바이너리 규모가 조금만 커져도 분석에 소요되는 시간이 급증하므로 일반적으로는 사용하기 어렵다. 따라서 2번 방식을 사용해 풀어보도록 하겠다.

 

 

1) 문자열 검색

Shift + F12를 누르면 위와 같이 바이너리에 포함된 문자열을 보여주는 Strings 창이 나타난다.

쭉 내리다 보면 "Hello, world!\n"라는 문자열을 발견할 수 있는데, 이 문자열은 컴파일 과정에서 삽입된 것이 아니라 프로그래머가 추가했을 것으로 추측할 수 있다.

 

해당 문자열을 더블 클릭하여 따라가 볼 수 있다.

 

 

2) 상호 참조

정적 분석을 하다가 어떤 수상한 값이나 함수를 찾으면 이를 참조하는 함수를 분석하고 싶어진다. 이때 상호 참조(Cross Reference, XRef)라는 기능으로 추적해 볼 수 있다.

 

"aHelloWorld"를 클릭한 뒤, 단축키 X를 누르면 xrefs 창이 나타나며, 해당 변수를 참조하는 모든 주소가 출력된다.

 

첫 번째 항목을 더블 클릭해서 따라가 보면, 위와 같이 main 함수를 찾을 수 있다.

 

 

3) main 함수 분석

main 함수를 찾았으므로, F5를 눌러 디컴파일한다. 

위 코드가 IDA에 의해 디컴파일된 main 함수 코드이다.

 

[1] argc, argv, envp로 3개의 인자를 받고 있다.

 

[3] Sleep 함수를 호출하여 1초 대기한다.

참고로, 16진수로 0x3E8은 10진수로 1000이고, 해당 부분을 클릭한 후 h키를 눌러보면 10진수로 바뀐다.

 

[4] qword_14001DBE0에 "Hello, world!\n" 문자열의 주소를 넣는다.

[5] sub_140001060에 "Hello, world!\n"를 인자로 전달하여 호출한다.

[6] 0을 반환한다.

 

💡 생각해보기

Q1. main()에서 사용되는 qword_14001DBE0 변수와 "Hello, world!\n" 문자열이 data, rodata, text 중 어떤 섹션에 존재할지 예측해 보자.

A1.

qword_14001DBE0은 값이 변경될 수 있는 전역변수이므로, data 섹션에 위치한다.
"Hello, world!\n" 문자열은 실행 도중 값이 변경될 일이 없는 상수이므로, rodata 섹션에 위치한다.
→ 이는 해당 변수 및 문자열을 더블클릭해 보면 확인할 수 있다.

Q2. sub_140001060은 어떤 함수일지 예측해 보자.
A2. sub_140001060 함수는 printf() 함수일 것으로 예상된다.

분석
sub_140001060 함수가 어떤 함수인지 알아내기 위해 이 함수의 디컴파일 결과를 살펴볼 수 있다 (더블클릭)

우선, va_start 함수는 가변 인수 리스트를 처리하는 대표적인 함수로, 이를 통해 sub_140001060 함수가 가변 인자를 처리하는 함수임을 알 수 있다.
__acrt_iob_func 함수는 스트림을 가져올 때 사용되는 함수로, 인자로 들어가는 1은 stdout을 의미한다. stdout은 출력을 위한 스트림이다.
→ 문자열 인자를 받고 stdout 스트림을 내부적으로 사용하는 가변 함수이다. 이를 통해 sub_140001060 함수는 printf() 함수일 것으로 추정할 수 있다.

 

 

 

#02. 동적 분석

1. main 함수 진입

1) 중단점 설정(Break Point, F2) 및 실행(Run, F9)

중단점을 특정 주소에 설정한 뒤 실행하면 프로그램은 중단점까지 멈추지 않고 실행된다.

 

main 함수에 F2를 눌러 중단점을 설정한 뒤, F9를 눌러 디버깅을 시작하여 main 함수까지 실행하도록 한다.

 

위 화면은 IDA 동적 디버깅 화면으로, 이와 같은 화면이 나오면 동적 분석을 위한 준비를 모두 끝마친 것이다.

 

 

2. 한 단계 실행(Step Over, F8)

F8은 코드를 한 줄씩 실행하는 기능이다.

 

1. sub rsp, 38h

기존 rsp 값에서 38을 뺀다.

(왼) 실행 전 / (오) 실행 후

main 함수가 사용할 스택 영역을 확보했다.

 

 

2. mov [rsp+38h+dwMilliseconds], 3E8h

rsp 스택 포인터로부터 0x38 만큼 떨어진 위치에 있는 dwMilliseconds 오프셋 위치(rsp+0x20)에 0x3E8 (즉, 1000)을 저장한다.

(왼) 실행 전 / (오) 실행 후

rsp의 주소는 이전 단계에서 000000000014FEB0인 것을 확인했고, 그렇다면 rsp+0x20은 000000000014FED0임을 알 수 있다.

Stack View에서 000000000014FED0 주소의 값을 확인해 보면, 3E8이 제대로 들어간 것을 확인할 수 있다.

 

 

3. mov ecx, [rsp+38h+dwMilliseconds]

rsp+0x20에 저장된 값, 즉 3E8을 ecx에 옮긴다. 이는 함수의 첫 번째 인자를 설정하는 것이다.

(왼) 실행 전 / (오) 실행 후

실행 후 ecx 부분을 보면 값이 들어간 것을 확인할 수 있다.

*참고: ecx는 32비트 레지스터로, rcx의 하위 32비트를 사용한다.

 

 

4. call cs:Sleep

코드 세그먼트(cs)에서 Sleep 함수를 호출한다.

ecx가 0x3e8이므로, Sleep(1000)이 실행되어 1초간 멈춘다.

 

 

5. lea rax, aHelloWorld

"Hello, world!\n" 문자열의 주소를 rax에 옮긴다.

(왼) 실행 전 / (오) 실행 후

rax가 "Hello, world!\n" 문자열이 저장되어 있는 주소인 000000014001A140으로 바뀌었다.

 

메모리 덤프 창을 보면 해당 주소에 해당 문자열이 저장되어 있는 것을 확인할 수 있다.

 

 

6. mov cs:qword_14001DBE0, rax

rax의 값을 코드 세그먼트의 주소 14001DBE0에 저장한다.

*참고: 'qword_14001DBE0'에서 qword는 64비트 값을 의미, 뒤에 있는 숫자는 주소를 의미.

 

(왼) 실행 전 / (오) 실행 후

'000000014001A140'이 리틀 엔디안 방식으로 저장된 것을 확인할 수 있다.

 

 

7. mov rcx, cs:qword_14001DBE0

14001DBE0에 저장된 값 '000000014001A140'을 rcx에 저장한다. 이는 다음 호출할 함수의 첫 번째 인자로 사용될 것이다.

(왼) 실행 전 / (오) 실행 후

 

8. call sub_140001060

'140001060' 함수를 호출한다. 앞서 정적 분석 과정에서 추측했듯, 이 함수는 printf일 것이다.

실행해 보면 이처럼 "Hello, world!" 문자열이 출력되는 것을 확인할 수 있다.

 

 

9. xor eax, eax

eax를 초기화한다.

 

 

10. add rsp, 38h

시작할 때 확장한 스택 영역을 다시 축소한다.

 

 

11. ret

원래 실행 흐름으로 돌아간다.

 

 

3. 함수 내부로 진입하기(Step Into, F7)

F8은 함수 내부로 진입하지 않는다. 때에 따라 함수 내부까지 정밀 분석해야 하는 경우가 있기 때문에 F7를 사용한다.

 

 

1. 디버깅 중단(Ctrl+F2) 후 printf에 중단점을 설정한다.

 

 

2. 디버깅을 다시 시작하고 Continue(F9)를 클릭하여 printf 함수에 도달한다.

printf 함수가 호출되기 직전

 

 

3. F7 단축키로 함수 내부로 진입한다. 함수 내부로 RIP가 이동한 것을 확인할 수 있다.

 

 

 

4. Appendix, 실행 중인 프로세스 조작하기

1) Sleep Forever

Sleep 함수의 인자 값을 1000000으로 조작하여 1000초 동안 프로세스를 정지시키도록 만들어본다.

 

1. delay를 Sleep의 인자로 전달하는 부분에 중단점을 설정 후 프로세스 재시작

 

 

2. 스택의 rsp+0x20에 0x3E8이 저장되어 있는 것을 확인

 

 

3. 해당 값 클릭 후, F2를 누른 뒤 0xf4240(=1000000)을 입력하고 다시 F2를 눌러 값을 저장한다.

 

 

4. F9를 눌러 Sleep 함수를 호출한다. 한참을 기다려도 프로세스가 재개되지 않는다. 

 

 


💎 IDA의 단축키 정리 💎

1. BreakPoint(F2)
: 중단점 설정
2. Restart(Ctrl + F2): 디버깅 중단
3. Run(F9): 프로그램 계속 실행, 혹은 디버깅 시작
4. Step Into(F7): 어셈블리 코드 한 줄 실행. 함수의 호출이라면 함수 내부로 진입
5. Step Over(F8): 어셈블리 코드를 한 줄 실행. 함수 내부로 진입하지 않음!

 


 

이전에 IDA Pro를 가지고 실습을 진행해 본 경험이 있는데,

그때는 진도를 따라가느라 바빠서 차마 완전히 이해하지 못했던 기억이 있다.

 

기초부터 다시 차근차근 학습해 나가니까 이해하기에도 좋고,

이전에 실습했던 내용이 새록새록 떠오르면서 이해가 되는 것 같기도 하다 ㅎㅎ