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): 어셈블리 코드를 한 줄 실행. 함수 내부로 진입하지 않음!