pwndbg로 C 소스파일 분석하기 - Ubuntu Linux

2021. 5. 26. 05:18Layer7/Reverse Engineering

728x90

- 사전 준비

 

#include <stdio.h>

int test(int num){
        if (num == (100%260)){
                return 1;
        }
        return 0;
}

void win(int a, int b, int c, int d, int e){
        printf("good\n");
}

void lose(){
        printf("bad\n");
}

int main(){
        int num;
        int res;

        printf("Enter Num : ");
        scanf("%d", &num);

        res = test(num);
        if (res == 1){
                win(1,2,3,4,5);
        }
        else if (res == 0){
                lose();
        }
        return 0;
}

5개의 파일들

우선은 vim을 사용하여 위와 같은 내용의 소스파일을 만들고 ESC -> :wq로 저장을 해줍니다.

그리고 컴파일을 해서 저렇게 5종류의 test 파일을 만들어줍니다.

 

 

그리고 file [파일명]으로 파일을 지정해줍니다.

b main으로 Breakpoint도 만들어주면 실행 전까지의 준비 과정은 모두 끝났습니다!

 

run 명령어 실행 후
main 함수의 전체 소스코드

run 명령어를 실행하니 어셈블리 코드가 잘린 것처럼 나와있었습니다.

이상함을 느끼고 명령어를 좀 찾아보니 전체 코드를 볼 수 있게 해주는 명령어가 있었습니다.

 

u [해당 함수의 주소 값] [명령어 개수]를 입력해주면 전체 코드가 나옵니다.

이제 덩어리별로 분석을 해보겠습니다.

 

 

- 첫 번째 부분

 

  • push rbp : 
    어셈블리에서 항상 나오는 문장입니다.
    스택에서 rsp 값에 rbp 값을 넣어주고 rsp는 한 칸 올라갑니다.
    SFP를 스택 프레임에 넣기 위한 과정입니다.
    (SFP = Stack Frame Pointer)
  • mov rbp, rsp : 
    rsp의 값을 rbp에 복사합니다.
    그 말인 즉슨, rbp와 rsp의 위치가 동일해진다는 뜻입니다.

  • sub rsp, 0x10:
    rsp의 메모리 주소에 있는 값에서 0x10을 뺍니다.
    여기서 0x10은 16과 같으므로 rsp의 위치는 16만큼 올라갑니다.
    메모리 공간 하나당 차지하는 bit 수는 8bit이므로 총 2칸을 올라가게 됩니다.
  • mov rax, qword ptr fs:[0x28] : 
    qword ptr fs:[0x28]의 값을 rax에 복사합니다. 
    이 부분은 오버플로우 공격에 대한 보안을 목적으로 하는 부분이라
    스택에는 영향을 미치지 않습니다.
  • mov qword ptr [rbp-8], rax : 
    rax의 값을 qword ptr [rbp-8]에 복사합니다.
    여기서 rbp-8이라는건 rbp보다 8bit 위에 있는 공간을 뜻합니다.
    즉, rax의 값을 rbp보다 한 칸 위에 복사한다는 뜻입니다.
  • xor eax, eax : 
    eax, eax에 xor 연산을 한 뒤에 eax에 저장합니다.
    (xor 연산 : 두 피연산자의 비트가 다를 경우 1, 같은 경우 0)
    그러므로 xor eax, eax는 mov eax, 0과 같습니다.

  • lea rdi, [rip+0xde1] : 
    [rip+0xde1]의 주소를 rdi에 복사합니다.
    여기서 [rip+0xde1]은 rip보다 0xde1 떨어진 곳을 말합니다.
    여기서 rdi에 들어가는 주소에는
    위 사진에서 보시다시피 "Enter Num : "이라는 문자열이 들어가 있습니다.
  • mov eax, 0 : 
    eax에 0을 넣습니다.
    (eax : rax와 같은 역할, 32bit)
    eax의 역할은 함수의 return 값을 저장해주는 것입니다.
    eax에 0을 넣는다는 건 return 값을 0으로 초기화한 것입니다.
  • call printf@plt : 
    printf 함수를 호출합니다.

 

첫 번째 부분에 있는 코드들은 간략하게 말하자면

printf를 호출하기 위한 준비 과정들을 수행하는 부분이었습니다.

 

 

- 두 번째 부분

 

  • lea rax, [rbp - 0x10] : 
    [rbp - 0x10]의 주소를 rax에 넣습니다.
    여기서 rbp - 0x10을 계산한 주소를 rax에 복사해야 합니다.
    이 부분은 주소 값이기도 하고, 위치를 보고 유추를 해보자면
    scanf에서 &num을 인자로 주는 부분인 것 같습니다.

  • mov rsi, rax : 
    rax의 값을 rsi에 복사합니다.
    rax와 rsi의 값이 같은 것을 확인할 수 있습니다.

  • lea rdi, [rip + 0xdd6] :
    rip+0xdd6의 주소 값이 rdi에 복사됩니다.

  • mov eax, 0 :
    다음 실행될 함수의 return 값을 0으로 초기화시킵니다.
  • call __isoc99_scanf@plt : 
    scanf 함수를 호출합니다.

 

두 번째 부분은 scanf 함수를 호출하기 위한

준비 과정들을 수행하는 부분이었습니다.

 

 

- 세 번째 부분

 

  • mov eax, dword ptr [rbp - 0x10] :
    dword ptr [rbp - 0x10]의 값을 eax에 복사합니다.
    rbp - 0x10은 입력을 받는 변수가 위치한 주소를 뜻합니다.
    이 변수의 입력값이 eax에 저장됩니다.
    그렇기에 eax 레지스터의 값을 출력해보면 1이라는 값이 들어있는 걸 알 수 있습니다.
    (값이 1인 이유는 아까 키보드로 아무 숫자나 눌렀는데 그 숫자가 1이었기 때문...!)
  • mov edi, eax :
    eax의 값을 edi에 복사합니다.
    edi는 함수의 매개변수로 사용됩니다.
  • call test : 
    test 함수를 호출합니다.

 

세 번째 부분은 num이 100인지 아닌지 판별하는 test 함수를 호출하는 부분이었습니다.

 

 

- 네 번째 부분

 

  • mov dword ptr [rbp - 0xc], eax : 
    eax값을 dword ptr [rbp - 0xc]에 복사합니다.
    여기서 eax는 return 값일 거고, rbp - 0xc는 rbp에서 12bit 떨어진 부분을 뜻합니다.

  • cmp dword ptr [rbp - 0xc], 1 : 
    rbp에서 12bit만큼 떨어진 부분 (바로 위에서 참조했던 부분)에다가 1을 복사합니다.
    직역하자면 위처럼 해석이 되지만, 아까 위에서 eax = 0이었으므로
    eax와 1을 비교하여 Flag Register를 설정하는 부분입니다.
  • jne main+120 : 
    jne라는 명령어는 비교한 값이 A!=B이면 지정된 위치로 점프를 하는 명령어입니다.
    이 부분에서는 두 가지 경우로 나뉩니다.
    A==B, A!=B, 이렇게 두 가지 경우로 말이죠.
    전자의 경우에는 별 다른 반응 없이 다음 명령어를 실행하고
    후자의 경우에는 main+120의 위치로 이동합니다.

 

네 번째 부분은 test 함수의 return 값(eax)을 활용하여 비교 연산을 하고,
eax가 1이냐 아니냐에 따라 main+120의 위치로 이동할 것인지를 따지는 부분이었습니다.

 

- 다섯 번째 부분

 

  • mov r8d, 5:
    r8d에 5를 넣는다.
    r8d는 함수의 매개변수 중 하나로, 다섯 번째 인자의 값을 저장하는 매개변수입니다.
  • mov ecx, 4:
    ecx는 네 번째 인자의 값을 저장하는 매개변수이고, ecx에 4를 넣습니다.

  • mov edx, 3 :
    edx는 세 번째 인자의 값을 저장하는 매개변수이고, edx에 3을 넣습니다.

  • mov esi, 2 :
    esi는 두 번째 인자의 값을 저장하는 매개변수이고, esi에 2를 넣습니다.

  • mov edi, 1 : 
    edi는 첫 번째 인자의 값을 저장하는 매개변수이고, edi에 1을 넣습니다.

  • call win : 
    win 함수를 호출합니다.

다섯 번째 부분은 win 함수를 호출하기 위해 5개의 매개변수에 값을 넣어주고, win 함수를 호출합니다.

 

 

- 여섯 번째 부분

 

  • jmp main+136 : 
    main+136의 위치로 무조건 점프합니다.

  • cmp dword ptr [rbp - 0xc], 0 :
    rbp - 0xc의 값이 0인지 1인지 판별합니다.
    (rbp - 0xc의 안에는 test 함수의 return값이 들어있습니다)

  • jne main+136 : 
    위 명령어의 반환 값이 1이면 실행합니다.
    main+136의 위치로 점프합니다.

  • mov eax, 0 : 
    eax를 0으로 초기화해줍니다.
    eax에는 lose의 return값이 들어갈 예정입니다.

  • call lose :
    lose 함수를 실행합니다.

 

여섯 번째 부분은 test의 return값에 따라 main+136으로 점프할지, lose 함수를 호출할지 정하는 부분이었습니다.

 

- 마지막, 일곱 번째 부분

 

  • mov eax, 0 : 
    main 함수의 return값을 0으로 설정합니다.

  • mov rdx, qword ptr [rbp - 8] : 
    rbp - 8의 위치에 있는 값을 rdx에 복사합니다.
    스택 오버플로우가 일어났는지 검증하기 위해 들어간 구문 같습니다.

  • xor rdx, qword ptr fs:[0x28] : 
    메모리와 코드를 보호하기 위한 부분입니다.

  •  je main+161 : 
    main + 161로 점프합니다.

  • call __stack_chk_fail@plt :
    스택 오버플로우가 났을 때 실행되는 함수인 것 같습니다.

  • leave : 
    mov rsp, rbp와 pop rbp를 수행합니다.
    즉, 현재까지 썼던 메모리 스택을 깔끔히 비우고,
    자신을 호출했던 메모리의 베이스 주소를 ebp에 다시 채우는 과정입니다.
  • ret :
    함수가 끝나면 스택에 저장되어있던 eip 주소를 원래대로 되돌려 놓습니다.

 

일곱 번째 부분은 코드 실행을 끝마친 후에 스택과 메모리들을 원래대로 돌려놓거나
깔끔하게 마무리를 하며 동시에 보안도 유지하는 부분입니다.

728x90