[Day 1] 시스템 해킹?

귀찬으니 정리만 한다.


#include <stdio.h>

int main(){
    int i;
    for(i = 0; i < 10; i++){
        printf("Hello, World!\n");
    }
    return 0;
}

간단히 hello world를 10번 출력하는 예제이다.
gcc helloword.c 와같이 컴파일 하게되면 아시다 시피 바이너리로 변해 컴퓨터가 해석을 위해 변환 되게 된다.

이를 기계어로 변하게 하는데 이를 볼 수 있게 해주는 objdump명령어를 통해서 자세히 봐보자.
Command : objdump -D a.out | grep -A20 main.:

위와 같이 입력하면, main 함수의 부분에서 20줄을 보여주게 된다. 실제론 이보다 훠~얼~씬 길지만..

0000000000400526 <main>:
  400526:       55                      push   %rbp
  400527:       48 89 e5                mov    %rsp,%rbp
  40052a:       48 83 ec 10             sub    $0x10,%rsp
  40052e:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
  400535:       eb 0e                   jmp    400545 <main+0x1f>
  400537:       bf e4 05 40 00          mov    $0x4005e4,%edi
  40053c:       e8 bf fe ff ff          callq  400400 <puts@plt>
  400541:       83 45 fc 01             addl   $0x1,-0x4(%rbp)
  400545:       83 7d fc 09             cmpl   $0x9,-0x4(%rbp)
  400549:       7e ec                   jle    400537 <main+0x11>
  40054b:       b8 00 00 00 00          mov    $0x0,%eax
  400550:       c9                      leaveq
  400551:       c3                      retq
  400552:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  400559:       00 00 00
  40055c:       0f 1f 40 00             nopl   0x0(%rax)

0000000000400560 <__libc_csu_init>:
  400560:       41 57                   push   %r15
  400562:       41 56                   push   %r14

각각 열이 의미하는 바에 대해 설명하자면,
제일 처음의 400526:은 메모리 주소를 의미하고 2번째 열은 3~4번째의 어셈블리 코드를 의미한다.

 

++ rbp ebp?? rex ebx??

위 두개는 같은 것을 의미하며, 현재 실행중인 컴퓨터의 환경(x64 or x86)의 따라서 r(x64) 또는 e(x86)이 붙게 된다.

 

위에 코드는 AT&T 문법 인데, 이를 Intel 문법으로 바꾸기 위해선 objdump 에서 -M intel 옵션을 추가해주면 아래와 같은 코드를 볼 수 있다.

0000000000400526 <main>:
  400526:       55                      push   rbp
  400527:       48 89 e5                mov    rbp,rsp
  40052a:       48 83 ec 10             sub    rsp,0x10
  40052e:       c7 45 fc 00 00 00 00    mov    DWORD PTR [rbp-0x4],0x0
  400535:       eb 0e                   jmp    400545 <main+0x1f>
  400537:       bf e4 05 40 00          mov    edi,0x4005e4
  40053c:       e8 bf fe ff ff          call   400400 <puts@plt>
  400541:       83 45 fc 01             add    DWORD PTR [rbp-0x4],0x1
  400545:       83 7d fc 09             cmp    DWORD PTR [rbp-0x4],0x9
  400549:       7e ec                   jle    400537 <main+0x11>
  40054b:       b8 00 00 00 00          mov    eax,0x0
  400550:       c9                      leave
  400551:       c3                      ret
  400552:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
  400559:       00 00 00
  40055c:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]

0000000000400560 <__libc_csu_init>:
  400560:       41 57                   push   r15
  400562:       41 56                   push   r14

 

x86 Debugging

GDB라는 디버깅 도구를 이용해 프로세서 레지스터의 상태를 볼 수 있다.

Command : gdb -q ./a.out

Reading symbols from ./a.out...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x40052a
(gdb) run
Starting program: /home/silnex/hack/a.out

Breakpoint 1, 0x000000000040052a in main ()
(gdb) info registers
rax            0x400526 4195622
rbx            0x0      0
rcx            0x0      0
rdx            0x7ffffffde448   140737488217160
rsi            0x7ffffffde438   140737488217144
rdi            0x1      1
rbp            0x7ffffffde350   0x7ffffffde350
rsp            0x7ffffffde350   0x7ffffffde350
r8             0x4005d0 4195792
r9             0x7fffff410ab0   140737475840688
r10            0x846    2118
r11            0x7fffff050740   140737471907648
r12            0x400430 4195376
r13            0x7ffffffde430   140737488217136
r14            0x0      0
r15            0x0      0
rip            0x40052a 0x40052a <main+4>
eflags         0x246    [ PF ZF IF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0
(gdb) quit
A debugging session is active.

        Inferior 1 [process 565] will be killed.

Quit anyway? (y or n) y

중지점(breakpoint)이 main() 함수에 설정 되어 실행 되기전에  멈추었고, info registers를 통해 실행중에 사용중인 레지스터의 내용들을 볼 수 있다.

첫번째 부터 4개의 레지스터 (eax, ecx, edx, ebx)는 범용 레지스터이고 각각 누산기, 카운터, 데이터, 베이스 레스터라고 부른다.
[위에서는 rax, rcx, rdx, rbx 라고 나왔지만 이는 실행 중인 환경이 x64여서 r이 붙은것일 뿐 의미하는 바는 같다.]

그 다음부터 4개의 (esp, ebp, esi, edi)도 범용 레지스터 이다. 이 레지스터는 가끔 포인터(pointer)나 인덱스(index)라고 부르기도 한다.
각기 스택 포인터, 베이스 포인터, 근원지(source) 인덱스, 목적지(destination) 인덱스라고 불리운다.

위의 레지스터들은 프로그램을 실행하고 메모리를 관리하는데 쓰이는 포인터 이며 매우 중요하게 사용된다.

eip 레지스터는 현재 프로세서가 읽고 있는 명령 포인터 레지스터 이고, 당현히 디버깅시 매우 많이 사용되게 된다.
마지막으로 eflags레지스터는 비교와 메모리 분할을 위한 몇 비트의 플래그로 이루어져있다. 이에 대해선 나중에 알아보자.

 

어셈블리 언어

대게 인텔의 문법 어셈블리어를 많이 사용하므로 (앞으로도 많이쓰게될진 모르겟지만..) 디버깅시 사용하는 툴도 인텔 문법에 맞춰 사용하기 위해서
set disassembly-flavor intel이라고 입력해 역어셈블 표기를 인텔로 정할 수 도 있으며,
명령을 넣은.gdbinit파일을 생성해 GDB를 실행할 때 마다 자동으로 환경설정 되도록 할 수 도 있다.

 

인텔 어셈블리어는 다음과 같은 형식으로 이루어져 있다.
명령 <목적지>, <근원지>

목적지근원지는 레스터나 메모리 주소 값이 될 수 있고 명령은 보통 직관적인 연산기호 이다.
mov명령은 근원지에서 목적지로 값을 이동,
sub는 빼고, inc는 증가시킨다.

예를 들어 다음 명령은 esp에서 ebp로 이동시킨 후 8을 뺀 결과를 esp에 저장하라는 명령이다.
8048375:        89 e5            mov    ebp,esp
8048377:        83 ec 08         sub    esp,0x8

또한 실행의 흐름을 제어하는 명령도 있다.
cmp 명령은 값을 비교하는데 사용되고,
j로 시작하는 명령은 코드를 다음부분으로 점프하는데 사용된다.

만약 gdb에서 소스코드를 볼 수 있는 추가 정보를 포함시키려면, 컴파일 시 gcc -g 플레그를 넣으면 된다.

Reading symbols from a.out...done.
(gdb) list
1       #include <stdio.h>
2
3       int main(){
4           int i;
5           for(i = 0; i < 10; i++){
6               printf("Hello, World!\n");
7           }
8           return 0;
9       }
(gdb) disassemble main
Dump of assembler code for function main:
   0x0000000000400526 <+0>:     push   rbp
   0x0000000000400527 <+1>:     mov    rbp,rsp
   0x000000000040052a <+4>:     sub    rsp,0x10
   0x000000000040052e <+8>:     mov    DWORD PTR [rbp-0x4],0x0
   0x0000000000400535 <+15>:    jmp    0x400545 <main+31>
   0x0000000000400537 <+17>:    mov    edi,0x4005e4
   0x000000000040053c <+22>:    call   0x400400 <puts@plt>
   0x0000000000400541 <+27>:    add    DWORD PTR [rbp-0x4],0x1
   0x0000000000400545 <+31>:    cmp    DWORD PTR [rbp-0x4],0x9
   0x0000000000400549 <+35>:    jle    0x400537 <main+17>
   0x000000000040054b <+37>:    mov    eax,0x0
   0x0000000000400550 <+42>:    leave
   0x0000000000400551 <+43>:    ret
End of assembler dump.
(gdb) break main
Breakpoint 1 at 0x40052e: file firstprog.c, line 5.
(gdb) run
Starting program: /home/silnex/hack/a.out

Breakpoint 1, main () at firstprog.c:5
5           for(i = 0; i < 10; i++){
(gdb) info registers rip
rip            0x40052e 0x40052e <main+8>
(gdb)

중지점이 main() 함수의 시작 점있다. 이때 17번째 줄에 0x000000000040052e <+8>: mov DWORD PTR [rbp-0x4],0x0의 주소값과, 
info registers rip를 통해 얻은 주소값이 동일함을 볼 수 있다.

17번째 줄까지의 명령들을 함수 프롤로그(function prologue)라고 불린다.