Whois 정보보안기초 과제 3

소프트웨어학과 설규원

이번 정보보안기초 과제는 gdb를 이용해 주어진 바이너리를 분석하는 것이다.

alt text

우선 분석할 바이너리는 GitHub로 공유되었다. Clone 했더니, tar로 압축된 파일이 있어 압축 해제해 주었다.

alt text

분석해야하는 바이너리는 Handray 폴더 속의 forif이다. file로 확인해보면 두 파일 모두 리눅스 32bit 바이너리라는 것을 알 수 있다. 따라서 분석을 위해 KVM상에서 LMDE 6 32bit를 돌려 작업환경을 옮겼다.

for

alt text

peda로 for를 열어보고 i fu로 함수 목록을 확인해보았다. main 함수부터 분석해 보기로 했다.

main 구조

alt text

pd main으로 보니 위와 같은 결과가 나왔다. 하나하나 분석해보도록 하자.

main +3 ~ +13

   0x080484a5 <+3>:	sub    esp,0x4
   0x080484a8 <+6>:	mov    DWORD PTR [ebp-0x4],0x0
   0x080484af <+13>:	lea    eax,[ebp-0x4]

+3에서. esp에서 4를 뺌으로서 4바이트의 빈 공간을 스택에 만든다.

+6에서, 만들어진 4바이트의 공간을 0x0으로 초기화한다.

그리고 +13에서, 그 공간의 주소를 eax에 저장한다.

이 부분을 C언어로 표현하면 아래와 같다.

int var = 0;

main +16 ~ +27

   0x080484b2 <+16>:	push   eax
   0x080484b3 <+17>:	push   0x804856e
   0x080484b8 <+22>:	call   0x8048350 <__isoc99_scanf@plt>
   0x080484bd <+27>:	add    esp,0x8

이 부분을 보면, push 후 scanf를 호출하고, add로 스택을 정리하는 것을 볼 수 있다.

특히 add에서 8을 더해주기 때문에, 인자들의 크기의 합이 8바이트라는 것을 알 수 있다. 이때, call 전에 push한 값의 크기의 합 또한 8바이트므로, scanf의 인자로 0x804856eeax가 들어간다는 사실을 알 수 있다.

그럼 두 인자는 뭘 의미할까?

첫번째 인자인 0x804856e는 주소값이다. 해당 주소에 해당하는 값을 문자열로 확인해보면:

alt text

첫번째 인자는 "%d"라는 사실을 알 수 있다. 이는 scanf의 포맷 스트링이다.

두번째 인자인 eax는 아까 만들어진 4바이트의 공간의 주소이다.

그러니, 이 부분을 C언어로 표현하면 아래와 같다.

scanf("%d", &var);

main +30 ~ +39

   0x080484c0 <+30>:	mov    eax,DWORD PTR [ebp-0x4]
   0x080484c3 <+33>:	push   eax
   0x080484c4 <+34>:	call   0x804846b <func>
   0x080484c9 <+39>:	add    esp,0x4

+30에서, eaxvar의 값을 저장한다. lea가 아닌 mov이기 때문에 var의 실제 값이다.

그리고, 다시 push, call, add의 과정이 나타난다. 마찬가지로 add에서 4를 더해주기 때문에, call 전에 push한 eax(var의 값)가 func 함수의 인자라는 사실을 알 수 있다.

따라서 이를 C언어로 나타내면:

func(var);

func 구조

alt text

func 함수에 대해 알아보기 위해 pd func를 한 결과이다. jmpcmp, jle를 보아 반복문이 있다는 것을 짐작할 수 있다.

func +0 ~ +1

   0x0804846b <+0>:	push   ebp
   0x0804846c <+1>:	mov    ebp,esp

func 함수의 프롤로그 부분이다. 이 부분에서 중요한 점은, 이제 ebp의 값이 바뀌었다는거다. 근데 그러면, 인수로 넘겨준 var의 값에는 이제 어떻게 접근해야 할까?

생각해보면 아까 push로 var의 값을 스택의 최상단으로 올렸다. 그러니, 이제 ebp를 이용해 var의 값에 접근할 수 있다. 현재 스택의 구조는 아래와 같다.

[ebp] : ebp
[ebp+4] : ebp의 이전 값 (`func +0`에서 push해줬음)
[ebp+8] : var의 값

따라서 이제부터는, ebp+8 에 있는 값을 이용해, 인수로 넘겨준 var의 값에 접근할 수 있다.

(사실 브레이크포인트를 찍어서 확인해보면 더 쉽게 알 수 있다. 그러나, 실행하지 않고도 바이너리를 분석하는 연습을 하고 싶었다.)

func +3 ~ +6

   0x0804846e <+3>:	sub    esp,0x4
   0x08048471 <+6>:	mov    DWORD PTR [ebp-0x4],0x1

스택에 4바이트의 공간을 만들고, 그 공간에 1을 넣는 부분이다. 이 함수에 반복문이 있다고 짐작했으므로, 이 부분이 반복문에 사용되는 변수일 것이라고 생각해볼 수 있다. 따라서, 변수의 이름을 i로 지어주도록 하자.

C언어로는:

int i = 1;

func +13, +46 ~ +50

   0x08048478 <+13>:	jmp    0x8048499 <func+46>

jmpfunc +46으로 이동한다.

   0x08048499 <+46>:	cmp    DWORD PTR [ebp-0x4],0x9
   0x0804849d <+50>:	jle    0x804847a <func+15>

cmp를 이용해 i9를 비교한다. 만약 i9보다 작거나 같다면(less or equal), func +15로 이동한다.

이렇듯, 이는 반복문의 형태를 띄고 있다. 따라서 이를 C언어로 나타내면 아래와 같다.

while (i <= 9) {
  // do something
}

func 반복문 내부: +15 ~ +39

   0x0804847a <+15>:	mov    eax,DWORD PTR [ebp+0x8]
   0x0804847d <+18>:	imul   eax,DWORD PTR [ebp-0x4]
   0x08048481 <+22>:	push   eax
   0x08048482 <+23>:	push   DWORD PTR [ebp-0x4]
   0x08048485 <+26>:	push   DWORD PTR [ebp+0x8]
   0x08048488 <+29>:	push   0x8048560
   0x0804848d <+34>:	call   0x8048330 <printf@plt>
   0x08048492 <+39>:	add    esp,0x10

마찬가지로 call 전후로 push와 add가 있다. add를 보면, 0x10 즉 16바이트를 정리한다. 역시, call 전에 push가 네번 되므로 딱 맞아떨어진다.

printf의 인자를 하나하나 해석해보자.

우선 첫번째 인자인 0x8048560은 주소값이다. 해당 주소에 해당하는 값을 문자열로 확인해보면:

alt text

첫번째 인자는 "%d * %d = %d\n"라는 사실을 알 수 있다.

두번째 인자인 ebp+0x8는 앞서 언급한 것처럼, func에 인자로 주어졌던 var의 값이다.

세번째 인자인 ebp-0x4i의 값이다.

네번째 인자인 eax의 값은 +15+18에서 결정된다. +15에서 eaxvar의 값으로 초기화하고, +18에서 i와 곱해준다. 따라서 네번째 인자는 var * i의 값이다.

따라서 이 부분을 C언어로 나타내면 아래와 같다.

printf("%d * %d = %d\n", var, i, var * i);

func 반복문 내부: +42

   0x08048495 <+42>:	add    DWORD PTR [ebp-0x4],0x1

func 반복문의 가장 끝부분이다. i를 1 증가시킨다.

따라서, 이를 C언어로 나타내면 아래와 같다.

i++;

func +52 ~ +54

   0x0804849f <+52>:	nop
   0x080484a0 <+53>:	leave
   0x080484a1 <+54>:	ret

func 함수의 에필로그 부분이다. nop로 끝나기에 리턴 값이 없는 void 함수임을 알 수 있다.

main +42 ~ +48

   0x080484cc <+42>:	mov    eax,0x0
   0x080484d1 <+47>:	leave
   0x080484d2 <+48>:	ret

main 함수의 에필로그 부분이다. eax0을 넣고 끝나기에 리턴 값이 0int 함수임을 알 수 있다.

전체를 C언어로 표현하면?

#include <stdio.h>

void func(int var) {
  int i = 1;
  while (i <= 9) {
    printf("%d * %d = %d\n", var, i, var * i);
    i++;
  }
}

int main() {
  int var;
  scanf("%d", &var);
  func(var);
  return 0;
}

구구단 코드가 나왔다.

실제 컴파일에 사용된 코드

#include <stdio.h>

void func(int a){
	for(int i=1;i<10;i++){
		printf("%d * %d = %d\n",a,i,a*i);
	}
}

int main(){

	int a = 0;
	scanf("%d",&a);

	func(a);
	return 0;
}

두 코드를 비교해보면, for문과 while문의 차이 빼고는 거의 동일하다는 것을 알 수 있다.


if

alt text

for과 마찬가지로 i fu로 함수 목록을 출력했다. main 함수부터 분석해보기로 했다.

main 구조

alt text

pd main으로 본 결과이다. for과 비슷하니, 설명상의 중복을 줄이기 위해 간략히 표현되는 부분들이 있을 것이다.

main +3 ~ +6

   0x080484a5 <+3>:	sub    esp,0x4
   0x080484a8 <+6>:	mov    DWORD PTR [ebp-0x4],0x0

4바이트의 빈 공간을 만들고, 그 값은 0으로 초기화한다. 이를 C언어로 나타내면 아래와 같다.

int var = 0;

main +13 ~ +23

   0x080484b6 <+13>:	push   0x8048580
   0x080484bb <+18>:	call   0x8048330 <printf@plt>
   0x080484c0 <+23>:	add    esp,0x4

printf 함수를 호출한다. 인자로 0x8048580을 넘겨준다. 해당 주소에 해당하는 값을 문자열로 확인해보면:

alt text

"점수를 입력하세요 : "라는 사실을 알 수 있다. 따라서 var 변수는 점수를 입력받는 변수인 것 같다.

따라서 이 부분을 C언어로 나타내면 아래와 같다.

printf("점수를 입력하세요 : ");

main +26 ~ +40

   0x080484c3 <+26>:	lea    eax,[ebp-0x4]
   0x080484c6 <+29>:	push   eax
   0x080484c7 <+30>:	push   0x804859d
   0x080484cc <+35>:	call   0x8048350 <__isoc99_scanf@plt>
   0x080484d1 <+40>:	add    esp,0x8

scanf 함수를 호출한다.

첫번째 인자인 0x804859d가 가리키는 문자열은 "%d" 이다.

두번째 인자는 var의 주소이다.

따라서 이 부분을 C언어로 나타내면 아래와 같다.

scanf("%d", &var);

main +43 ~ +52

   0x080484d4 <+43>:	mov    eax,DWORD PTR [ebp-0x4]
   0x080484d7 <+46>:	push   eax
   0x080484d8 <+47>:	call   0x804846b <func>
   0x080484dd <+52>:	add    esp,0x4

func 함수에 var의 값(점수)을 인자로 넘겨준다.

따라서 이 부분을 C언어로 나타내면 아래와 같다.

func(var);

func 구조

alt text

수많은 cmp가 보인다. 특정 조건을 만족하면 eax에 리턴값을 저장하고, +60으로 이동하여 함수를 종료한다.

func +0 ~ +1

   0x0804846b <+0>:	push   ebp
   0x0804846c <+1>:	mov    ebp,esp

for 문제와 마찬가지로, func 함수에 주어진 인자인 var의 값은 이제 ebp+0x8에 있다.

func +3 ~ +14

   0x0804846e <+3>:	cmp    DWORD PTR [ebp+0x8],0x59
   0x08048472 <+7>:	jle    0x804847b <func+16>
   0x08048474 <+9>:	mov    eax,0x41
   0x08048479 <+14>:	jmp    0x80484a7 <func+60>

var의 값이 0x59(89)보다 작거나 같으면 +16으로 이동한다. 그렇지 않으면 eax0x41('A')를 저장하고, +60으로 이동해 리턴한다.

C언어로 나타내면 아래와 같다.

if (var <= 89) {
  // 다음 조건
}
else {
  return 'A';
}

조건 var <= 89 만족 시: func +16 ~ +27

   0x0804847b <+16>:	cmp    DWORD PTR [ebp+0x8],0x4f
   0x0804847f <+20>:	jle    0x8048488 <func+29>
   0x08048481 <+22>:	mov    eax,0x42
   0x08048486 <+27>:	jmp    0x80484a7 <func+60>

var의 값이 0x4f(79)보다 작거나 같으면 +29으로 이동한다. 그렇지 않으면 eax0x42('B')를 저장하고, +60으로 이동해 리턴한다.

C언어로 나타내면 아래와 같다.

if (var <= 79) {
  // 다음 조건
}
else {
  return 'B';
}

조건 var <= 79 만족 시: func +29 ~ +40

   0x08048488 <+29>:	cmp    DWORD PTR [ebp+0x8],0x45
   0x0804848c <+33>:	jle    0x8048495 <func+42>
   0x0804848e <+35>:	mov    eax,0x43
   0x08048493 <+40>:	jmp    0x80484a7 <func+60>

var의 값이 0x45(69)보다 작거나 같으면 +42으로 이동한다. 그렇지 않으면 eax0x43('C')를 저장하고, +60으로 이동해 리턴한다.

C언어로 나타내면 아래와 같다.

if (var <= 69) {
  // 다음 조건
}
else {
  return 'C';
}

조건 var <= 69 만족 시: func +42 ~ +53

   0x08048495 <+42>:	cmp    DWORD PTR [ebp+0x8],0x3b
   0x08048499 <+46>:	jle    0x80484a2 <func+55>
   0x0804849b <+48>:	mov    eax,0x44
   0x080484a0 <+53>:	jmp    0x80484a7 <func+60>

var의 값이 0x3b(59)보다 작거나 같으면 +55으로 이동한다. 그렇지 않으면 eax0x44('D')를 저장하고, +60으로 이동해 리턴한다.

C언어로 나타내면 아래와 같다.

if (var <= 59) {
  // 다음 조건
}
else {
  return 'D';
}

조건 var <= 59 만족 시: func +55

   0x080484a2 <+55>:	mov    eax,0x46

지금까지의 조건을 모두 만족하면, eax0x46('F')을 저장한다.

func +60 ~ +62

   0x080484a7 <+60>:	pop    ebp
   0x080484a8 <+61>:	ret

함수의 에필로그 부분이다. 위의 조건 중 하나에 해당되는 값을 리턴하고 함수를 종료한다.

main +55

   0x080484e0 <+55>:	movsx  eax,al

다시 main으로 돌아오자마자 실행되는 부분이다. 사실 이 부분은 무슨 뜻인지 잘 모르겠다. 단편적으로 안 사실은, alahax의 하위 8비트와 상위 8비트를 나타낸다. 그리고 이 ax를 확장한 것이 eax라는 것이다. 일종의 전처리같기는 하지만 이게 무슨 의미인지 정확히는 모르겠고, 브레이크포인트를 찍어서 확인해보아도 eax의 값이 달라지지 않아서 잘 모르겠다. 추후 알게 된다면 글을 수정해야겠다.

main +58 ~ +69

   0x080484e3 <+58>:	push   eax
   0x080484e4 <+59>:	push   0x80485a0
   0x080484e9 <+64>:	call   0x8048330 <printf@plt>
   0x080484ee <+69>:	add    esp,0x8

printf 함수를 호출한다. 첫번째 인자인 0x80485a0이 가리키는 문자열은 "%c 등급입니다.\n" 이다. 두번째 인자인 eax는 앞서 func 함수에서 리턴한 값이다.

따라서 이 부분을 C언어로 나타내면 아래와 같다.

printf("%c 등급입니다.\n", func(var));

main +72 ~ +78

   0x080484f1 <+72>:	mov    eax,0x0
   0x080484f6 <+77>:	leave
   0x080484f7 <+78>:	ret

main 함수의 에필로그 부분이다. eax0을 넣고 끝나기에 리턴 값이 0int 함수임을 알 수 있다.

전체를 C언어로 표현하면?

#include <stdio.h>

char func(int var) {
  if (var <= 89) {
    if (var <= 79) {
      if (var <= 69) {
        if (var <= 59) {
          return 'F';
        }
        else {
          return 'D';
        }
      }
      else {
        return 'C';
      }
    }
    else {
      return 'B';
    }
  }
  else {
    return 'A';
  }
}

int main() {
  int var;
  printf("점수를 입력하세요 : ");
  scanf("%d", &var);
  printf("%c 등급입니다.\n", func(var));
  return 0;
}