main에서부터 명령어를 하나하나 보면서 스택과 레지스터에 들어있는 값이 어떻게 변환되고, 프로그램이 어떻게 작동하는지를 하나하나 파악해 보도록 하겠다. 하나하나라고는 하지만 정말 완전히 한 명령어씩만 하지는 않고, 연관성이 있는 명령어들은 묶어서 설명한다.


프로그램 소스는 매우 간단하다. 조금만 프로그래밍을 해본 사람이라면 한 번쯤은 짜봤을 법한 소스이다.

<소스>

#include <stdio.h>

int main()
{
	int i;
	int sum = 0;
	for(i=1; i<=100; i++) sum+=i;
	printf("sum : %d\n", sum);
	return 0;
}


필자는 컴파일과 분석을 우분투 14.04 64bit 버전에서 수행했다.

64비트 환경이므로 32비트로 컴파일하고 분석하기 위하여 컴파일시 뒤에 -m32 를 붙여주자.

또한 분석을 용이하게 하기 위해 뒤에 -mpreferred-stack-boundary=2 도 붙여주도록 하자.


여기까지 했으면 간단한 분석의 준비가 끝났다.

바로 GDB를 이용해 하나하나 분석해 보도록 하자.


gdb를 실행할 때 뒤에 붙인 -q 옵션은 gdb 실행 시 나타나는 긴 버전 및 도움말 등을 스킵하는 옵션이다.

disas main 명령어를 쳐서 메인을 보면 위와 같이 AT&T 문법으로 나타나는 메인의 어셈블리어들을 볼 수 있다.

혹시나 리버싱을 먼저 했거나, Intel의 문법이 더 익숙한 사람들은 ' set disassembly-flavor intel ' 명령을 이용하여 문법의 변경이 가능하다.

우선 ' b *main '으로 main의 첫 부분에 BreakPoint를 걸고 실행시켜 main에서 멈추게 한 뒤 ' info reg '로 레지스터들의 초기값들을 확인해보자.

eax, ecx, edx, ebx는 지금 당장 눈여겨 볼 필요는 없고, 현재 ebp 값이 0이라는 점을 보고 넘어가자.


이제 한줄씩 실행해 가며 분석을 시작할 텐데, 한 번 할때마다 계속 값을 확인하기는 귀찮으므로 display 명령을 이용하여 자동으로 값이 출력되도록 설정하도록 하겠다. 봐야 하는 것은 다음에 실행할 명령어들 일부와 main에서 사용되는 레지스터들의 값이다.

main에서 실질적으로 사용하는 범용 레지스터는 eax밖에 없으므로 esp, ebp, eax 및 esp을 기준으로 스택에서 0x20바이트, eip를 기준으로 명령어 5개를 display하겠다.

이제 si나 ni 등으로 한 명령어씩 수행할 때마다, 혹은 BP에 걸릴 때마다 display된 값들을 자동으로 출력해 준다.


ni를 이용하여 명령을 수행하자.

우선 push %ebp와 mov %esp, %ebp이다.

mov    %ebp와 mov    %esp, %ebp라는 명령이었다.

이 명령을 통해 esp에 +4를 한 후 ebp값이 esp에 저장되고, esp의 값이 ebp에 들어갔으므로, 둘은 0xffffd128이라는 같은 주소를 가리키게 되었다.


다음 명령이다.

sub    $0x10, %esp 이라는 명령이었다.

esp에서 0x10만큼 뺀다는 것인데, 스택은 주소상 위에서 아래로 자라므로 스택에 0x10 bytes만큼을 할당했다고 볼 수 있다. 0xffffd128을 가리키던 esp가 0xffffd118을 가리키게 되었다.

다음 명령이다.

movl    $0x0, -0x4(%ebp) 라는 명령이었다.

ebp-0x4가 가리키는 위치에 0을 넣는다는 뜻인데, 

변한 게 없는 것 같지만 이미 0x00000000이라는 값을 가진 위치가 0x0으로 바뀐 것이기 때문에 변화가 없는 게 당연하다. 지역변수에 0을 할당하였다.

다음 명령이다.

mov    $0x01, -0x8(%ebp) 였다.

지역변수2에 1이라는 값을 할당하였다.

메모리상 ebp-0x8 위치가 1로 바뀐 것을 확인할 수 있다.

다음 명령이다.

jmp    0x804843d 라는 명령.

아래로 점프하였다.

eip의 값이 0x804843d로 변경되었다.

다음 명령이다.


이번에는 연관이 있는 명령이라 한번에 두 개의 명령을 동시에 수행했다.

cmpl $0x64, -0x8(%ebp) 였는데, 이후 jle 0x8048433 명령이 수행되었다.

이는 $0x64라는 값과 ebp-0x8에 들어 있는 값을 비교하여 *(ebp-0x8)이 같거나 작을 경우 지정한 주소로 점프하는 명령어이다.

반복문의 시작점이라고 볼 수 있겠다.

연재 ebp-0x8에는 1이 들어 있고, 이는 아마도 반복문의 카운터가 되는 값이다.

계속하자.


두 개의 명령을 한번에 실행했다.

mov    -0x8(%ebp), %eax  와  add    %eax, -0x4(%ebp) 이었다.

eax에 -0x8(%ebp)를 집어넣는데, 이는 아까 가져온 카운터 값이다. 그리고 처음에 0으로 초기화한 -0x4(%ebp)에 집어넣는다. 아마 이 부분이 sum 변수일 것으로 생각된다.

다음 명령이다.


addl $0x1, -0x8(%ebp) 이었다.

카운터라고 추측했던 -0x8(%ebp)에 상수 1을 더한다.

그리고 cmpl과 jle 문을 지나면서 반복문을 계속한다.

-0x8(%ebp)가 0x64보다 작거나 같을 때까지 반복하는 반복문이므로 C언어로 표현하면

for(i=1; i<=100; i++) sum += i;

이런 구문이라고 볼 수 있겠다.

반복문 다음의 mov, 즉 main+38에 BP를 걸고 c로 계속 진행시키자.


-0x8(%ebp)의 위치에는 0x65가 들어가 반복문을 빠져나온 직후이며, 결과인 sum은 0x13ba라는 값인 것을 알 수 있다.

다음은 함수의 인자를 지정해 주는 단계이다. 계속하자.


-0x4(%ebp)에 들어 있는 결과값을 eax에 집어넣고, 이를 또 0x4(%esp)에 넣는다.

마지막으로 문자열의 주소가 저장되어 있을 것으로 예상되는 0x80484f0이라는 주소를 (esp)에 넣는다.

보통 함수의 인자를 넣을 때 인자를 거꾸로 넣으며 push를 이용해 스택에 넣어준다고 알고 있는 사람이 많은데,

이렇게 esp를 미리 함수 인자만큼 할당 후 (esp)에 첫번째 인자, (esp+4)에 두번째 인자 등으로 넣어 주어도 관계는 없다.

어차피 push reg 라는 명령이 { sub    $0x04, %esp  /   mov    reg, (%esp) } 와 같은 의미이기 때문이다.

이제 call 명령을 수행한다.


명령을 수행하자마자 " sum : 5050 " 이라는 문자열이 커맨드라인에 출력되었다.

첫 번째 인자로 들어갔던 0x80484f0의 주소에 들어 있는 값을 확인하니 "sum : %d\n"라는 문자열임을 확인할 수 있다.

다음으로 return 0을 수행하기 위해 %eax에 0을 넣는다.


이제 마지막으로 남은 것은 함수를 끝내는 함수 에필로그 뿐이다.

이 부분은 조금 자세히 보자.


leave 명령을 수행한 직후의 모습이다.

leave는 저장해 두었던 main 이전 함수의 %ebp, 즉 SFP(Saved Frame Pointer)를 원래대로 되돌리는 명령이다.

이 명령은 { mov %ebp, %esp  /  pop %ebp } 로 이루어져 있다.

%ebp의 값을 %esp에 넣게 되므로 %esp와 %ebp는 같은 위치를 가리키게 되고, %ebp는 main 함수의 프롤로그에서 { push %ebp  /  mov %esp, %ebp } 를 한 후 고정되어 있었으므로 당연히 SFP를 가리키고 있다.

pop 명령은 %esp를 기준으로 진행되는 명령이므로 pop %ebp를 하게 되면 SFP가 ebp로 들어가고, 이렇게 원래의 ebp가 복원되었다.

위 이미지에서 가장 처음에 ebp가 갖고 있던 초기값인 0x0이 복원된 것을 볼 수 있다.

다음으로 ret 명령이다.


leave 명령이 %ebp를 복원하는 명령이었다고 한다면, ret 명령은 %eip를 원래 있어야 할 장소로 돌려보내는 명령이다.

call로 함수를 수행하게 되면 자동으로 RET을 스택에 push 하는데, 이는 이 함수 이후 수행할 명령어가 저장된 주소이다.

예를 들어 이 프로그램에서라고 하면 call printf로 함수를 수행한 뒤에도 eip는 return 0을 수행하기 위해 그 다음의 주소로 정상적으로 돌아왔다.

실제 함수의 주소는 main이 아닌 libc에 있기 때문에 %eip가 달라져 다른 주소로 점프하게 되는데, 점프하기 전에 다시 돌아오기 위해 스택에 값을 저장해 두는 것이다.

ret 명령은 { pop %eip  /  jmp %eip } 이다. 원래 %eip는 mov나 pop 같은 명령어로는 변조가 불가능한 매우 중요한 레지스터이기 때문에 뒤에 jmp %eip를 통해 정당성을 확보해 준 것이다.

esp는 leave 명령을 수행한 이후 pop 때문에 SFP+4의 위치를 가리키고 있는데, 여기에서 pop을 하게 되면 SFP 직전에 저장된 RET가 eip에 들어가게 되어 eip가 정상적으로 복구되는 것이다.

RET가 스택에 저장된다는 취약점을 이용하여 버퍼 오버플로우 등을 이용해 이 부분을 원하는 값으로 덮어씌우면 원하는 주소의 명령을 수행하게 할 수 있다.





블로그 이미지

__미니__

E-mail : skyclad0x7b7@gmail.com 나와 계약해서 슈퍼 하-카가 되어 주지 않을래?

댓글을 달아 주세요