* gdb를 사용한 실행파일 디버깅하기 *




보통 보안이나 해킹에서 보면 실행파일을 까 보는? 경우가 있습니다. 물론 제가 아직 거기까지 통달하지는 않았지만


역시나 오늘도 기록을 위해서 포스트를 작성해 보도록 하겠습니다.



일단 C언어를 사용한 코드를 하나 작성해 보겠습니다.



> touch shell.c


> nano shell.c



(source)

#include <stdio.h>


int main() {

int a = 10;

int b = 20;

int result = 0;


result = a + b;


return 0;

}



사실상 stdio.h 헤더는 사용하지 않았지만 버릇처럼 넣어버렸습니다...


아주 간단한 코드입니다.


a 에 10을 넣고 b 에 20을 넣고 그 값을 result 라는 4바이트짜리 변수에 넣고 종료


이것을 이제 gcc 를 통하여 실행파일로 컴파일 해보겠습니다.



> gcc -o shell shell.c


-o 옵션은 컴파일한 출력 파일의 이름을 지정해주는 옵션입니다.


단순히 gcc shell.c 로도 할 수 있지만, 이 결과는 a.out 리는 실행파일이 나오게 됩니다.


출력문 printf 를 쓰지 않았기 때문에 당연히 아무 반응도 없습니다.


==============================================================================


이것을 이제 gdb(GNU Debugger) 를 통해 디버깅을 해서, 어셈블리 코드로 바꿔서 결과를 보겠습니다.




> gdb shell


(gdb) > disass main


disass main 의 의미는 disassemble main 의미입니다.


즉 main 메소드를 역어셈블 한다는 뜻이죠.


한줄한줄 살펴볼까요?



==============================================================================



1. <+0>:    push   %rbp


이 1번줄과 2번줄은 어셈블리에서 항상 나오는 표현입니다.


레지스터 rbp를 스택에 넣어두고, rsp를 rbp에 넣어줍니다.


참고로 윈도우와는 다르게 리눅스에서는 source 와 destination 의 위치가 바뀝니다.



즉, add %r1, %r2 가 윈도우에서는


r1 = r1 + r2


이었다면, 리눅스에서는


r2 = r2 + r1


이 됩니다. 헷갈리실 수도 있으니 꼭 참고하세요.




2. <+1>:    mov    %rsp,%rbp


네, 아까 말씀드린 것처럼 어셈블리에서 항상 나오는 표현입니다.


1번줄과 2번줄은 외워두시는 것도 좋습니다. 저는 이미 외웠습니다.




3. <+4>:    movl   $0xa,-0xc(%rbp)


본격적인 main함수의 표현입니다.


movl $0xa, -0xc(%rbp)


언뜻 봐도 0xa(=dec. 10) 의 값을 %rbp 에 넣어주는 표현인 것 같습니다.


그런데 왜 -0xc 를 해줄까요? 분명히 int 는 4바이트(=0x4)인데?




어셈블리어 익숙하신 분들은 눈치채셨을 수도 있습니다.


저는 ARM M1 Cortex 시리즈를 학교에서 공부하면서 ARM 어셈블리 문법을 공부했었는데,


거기서 배운 것 하나가 여기서 도움이 되네요.



역어셈블 과정에서 디버거는 변수의 개수를 셉니다.


main 함수를 보시면 int 변수가


int a

int b

int result


이렇게 3개가 나옵니다.


즉, 4바이트 * 3 = 12바이트 (=hex. 0xc)


라는 공간을 "미리" 할당합니다.




아래 그림을 한번 봅시다. (byte address 로 하지말고 4byte 어드레스라고 가정합니다. 즉 1칸에 4바이트)


               [rbp]

4byte

4byte

4byte

 

 

 

 



이렇게 3칸(12byte)을 할당해 줍니다.


그리고 나서, 변수가 나오는 순서에 따라서 맨 아래서부터 메모리 위치를 배정해줍니다.


int a = 10;


즉, 10의 값(=hex. 0xa) 을 맨 아래인 rbp - 12byte 위치에 넣어줍니다.


이것을 어셈블리로 표현하면


movl    $0xa, -c(%rbp)


가 되는 것입니다.




4. <+11>:    movl   $0x14,-0x8(%rbp)


위의 3번줄에서 설명한 것 처럼, 다음 변수는 -12 바이트 위치가 아닌,


4바이트가 줄어든 -8 바이트 위치에 할당해줍니다.


다음 변수는


int b = 20;


입니다. 즉, 20의 값을 -0x8(%rbp) (=%rbp - 8byte) 에 넣어줍니다.


20은 hexa 값으로 0x14 입니다. (1 * 16 + 4 * 1)




5. <+18>:    movl   $0x0,-0x4(%rbp)


똑같습니다. 마지막 변수는 int result = 0; 이므로,


0의 값을 -4바이트 줄어든 위치에 넣어줍니다.




6. <+25>:    mov    -0xc(%rbp),%edx


이 것은 계산을 위해서 값을 옮기는 과정입니다.


계산 식은


result = a + b;


해당 인수들의 주소는


&a = -0xc(%rbp)


&b = -0x8(%rbp)




여기서는 %rbp - 0xc 위치에 있는 값(=10)을 edx 레지스터에 넣어주었습니다.




rbp 에서 직접적으로 계산을 하지 않고 edx 와 eax 같은 범용 레지스터에 임시적으로 옮겨놓고


계산을 한 후 그 결과 값을 다시 rbp에 가져오게 됩니다.


쉽게 생각해서, 값을 보존하기 위해서? 라고 생각하시면 되겠습니다.




ARM 어셈블리를 배울 때도, 메모리에서 직접적으로 계산을 할 수가 없기 때문에


값들은 메모리에 넣어 놓고 필요한 값만 r1, r2, r3 등의 범용 레지스터에 옮겨서 계산 후 다시 메모리에 넣었던 기억이 납니다.




7. <+28>:    mov    -0x8(%rbp),%eax


6번줄과 마찬가지로 rbp -8byte 의 위치에 있는 값(=20)을 eax 레지스터에 넣어줍니다.


8. <+31>:    add    %edx,%eax


계산입니다.


eax = eax + edx


eax 에는 20이 들어가 있었고, edx 에는 10이 들어가 있었죠.


최종적으로


eax = 20 + 10


과 동일합니다.


eax 에 결과 값 30이 들어가게 되겠군요.




9. <+33>:    mov    %eax,-0x4(%rbp)


계산이 끝났으니 값을 옮겨줘야겠죠?


eax 레지스터에 있는 값을 -0x4(%rbp), 즉 %rbp - 4byte 위치에 넣어줍니다.


저희는 이미 위의 과정들을 통하여 어떤 변수(값)가 어느 위치에 있는지 알고 있습니다.


rbp에서 4byte 줄어든 위치는 result 변수가 할당되어 있는 자리입니다. (5번줄 참고)


즉, result 변수에 eax 레지스터의 값, 30이 들어가게 됩니다.




10. <+36>:    mov    $0x0,%eax


사용한 도구는 닦아줘야죠.


eax 레지스터를 초기화 해줍시다. 다음에 있을 연산이나 작업을 위해서요.


항상 초기화는 습관을 들여줍시다.




11. <+41>:    pop    %rbp


모든 프로그램이 끝났습니다. 1번줄에서 stack memory 에 save 해 두었던 값을 rbp 에 다시 옮겨줍시다.


12. <+42>:    retq  


return 0; 의 의미입니다.





==============================================================================




굉장히 쉬운 어셈블리 였습니다. 실제로 보안업무를 진행하시는 분들은 몇 백 줄의 C언어 코드를 역어셈블 해서 보실텐데, 이런 코드는 정말 맛보기일 뿐입니다.


그래도 gdb 를 사용하고, 역어셈블과 어셈블리 언어를 관찰했다는 것에 의의를 두었습니다.



>>> gdb


>>> 리눅스 어셈블리


>>> 리눅스 역어셈블


>>> GNU Debugger

Posted by NDC :