프로그래밍 #2
Updated:
이 포스팅은 책 “해킹 공격의 예술 개정2판”을 온전히 직접 공부한 것을 기반으로 기록한 페이지 입니다.
[어셈블리 언어]
인텔 문법 어셈블리 언어에 대해 설명하겠습니다.
여기서 사용하는 툴은 인텔 문법에 맞게 설정돼 있습니다.
GDB를 사용할 때 set disassembly intel 이나 줄여서 set dis intel 이라고 입력해 역어셈블( Disassembly )표기를 인텔로 설정할 수 있습니다.
홈 디렉터리의 .gdbinit 파일에 앞의 명령을 입력해 GDB를 실행할 때마다 이런 환경 설정이 되게 할 수 있습니다.
reader@hacking:~/booksrc $ gdb -q
(gdb) set dis intel
(gdb) quit
reader@hacking:~/booksrc $ echo "set dis intel" > ~/.gdbinit
reader@hacking:~/booksrc $ cat ~/.gdbinit
set dis intel
reader@hacking:~/booksrc $
이제 GDB에서 인텔 문법을 사용할 수 있게 설정했습니다.
인텔 문법 어셈블리 명령은 보통 다음과 같은 형식입니다.
명령 <목적지>, <근원지>
목적지(Destination)와 근원지(Source)는 레지스터나 메모리 주소, 값이 될 수 있습니다.
명령은 보통 직관적 연상 기호(Mnemonic)입니다.
mov (Move)명령은 근원지에서 목적지로 값을 이동시키고, sub (Subtraction)는 빼고, inc (Increase)는 증가시킵니다.
예를 들어 다음 명령은 값을 ESP에서 EBP로 이동시킨 후 ESP에서 8을 빼 결과를 ESP에 저장하라는 명령입니다.
8048375: 89 e5 mov ebp,esp
8048377: 83 ec 08 sub esp,0x8
실행 흐름을 제어하는 데 사용되는 명령도 있습니다.
cmp (Compare) 명령은 값을 비교하는 데 사용되고, 기본적으로 j (Jump)로 시작하는 명령은 코드의 다른 부분으로 점프하는 데 사용됩니다.
다음 예제의 첫 번째 줄은 EBP의 4바이트값에서 4를 뺀 값과 9를 비교합니다.
두 번째 줄은 앞의 비교 결과를 보고 작거나 같으면 점프하는 명령입니다.
값이 9보다 작거나 같으면 실행은 0x8048393으로 점프합니다.
아니면 실행 흐름은 무조건 다음 명령으로 넘어갑니다.
값이 9보다 작거나 같지 않으면 실행은 세 번째 줄에 의해 0x80483a6으로 점프합니다.
804838b: 83 7d fc 09 cmp DWORD PTR [ebp-4],0x9
804838f: 7e 02 jle 8048393 <mani+0x1f>
8048391: eb 13 jmp 80483a6 <main+0x32>
이 예제는 앞의 역어셈블에서 나왔습니다. 인텔 문법을 사용하도록 디버거를 설정으니 디버거로 어셈블리 명령 단계에서 첫 번째 프로그램을 살펴보겠습니다.
GDB 컴파일러 사용 시 GDB에서 소스코드를 볼 수 있는 추가 디버그 정보를 포함시키려면 -g 플래그를 사용합니다.
reader@hacking:~/booksrc $ gcc -g firstprog.c
reader@hacking:~/booksrc $ ls -l a.out
-rwxr-xr-x 1 matrix users 11977 Jul 4 17:29 a.out
reader@hacking:~/booksrc $ gdb -q ./a.out
Using host libthread_db library "/lib/libthread_db.so.1".
(gdb) list
1 #include <stdio.h>
2
3 int main()
4 {
5 int i;
6 for(i=0; i < 10; i++)
7 {
8 printf("Hello, world!\n");
9 }
10 }
(gdb) disassemble main
Dump of assembler code for function main():
0x08048384 <main+0>: push ebp
0x08048385 <main+1>: mov ebp,esp
0x08048387 <main+3>: sub esp,0x8
0x0804838a <main+6>: and esp,0xfffffff0
0x0804838d <main+9>: mov eax,0x0
0x08048392 <main+14>: sub esp,eax
0x08048394 <main+16>: mov DWORD PTR [ebp-4],0x0
0x0804839b <main+23>: cmp DWORD PTR [ebp-4],0x9
0x0804839f <main+27>: jle 0x80483a3 <main+31>
0x080483a1 <main+29>: jmp 0x80483b6 <main+50>
0x080483a3 <main+31>: mov DWORD PTR [esp],0x80484d4
0x080483aa <main+38>: call 0x80482a8 <_init+56>
0x080483af <main+43>: lea eax,[ebp-4]
0x080483b2 <main+46>: inc DWORD PTR [eax]
0x080483b4 <main+48>: jmp 0x804839b <main+23>
0x080483b6 <main+50>: leave
0x080483b7 <main+51>: ret
End of assembler dump.
(gdb) break main
Breakpoint 1 at 0x8048394: file firstprog.c, line 6.
(gdb) run
Starting program: /hacking/a.out
Breakpoint 1, main() at firstprog.c:6
6 for(i=0; i < 10; i++)
(gdb) info register eip
eip 0x8048394 0x8048394
(gdb)
먼저 소스코드가 출력되고 main() 함수의 역어셈블이 출력됩니다.
그리고 main()의 시작점에 중지점(Breakpoint)을 설정하고 프로그램을 실행시킵니다.
- 중지점 - 디버거가 해당 지점에 가면 프로그램 실행을 중지하도록 합니다.
중지점이 main() 함수 시작점에 설정됐으므로 프로그램은
main() 함수의 어떤 명령도 실행하기 전에 중지점에 다다르고 멈추게됩니다.
그 후 EIP(명령 포인터)의 값이 출력됩니다.
EIP가 main() 함수의 역어셈블 명령을 가리키는 메모리 주소를 갖고 있는 것을 보면,
<main+1> ~ <main+14>의 명령들은 함수 프롤로그(Function Prologue)라고 불립니다.
그러나 우선 GDB를 알아보겠습니다.
GDB 디버거는 examine을 줄인 명령 x를 사용해 메모리를 조사하는 방법을 제공합니다.
GDB 디버거는 프로그램 실행의 매 순간을 확실하게
조사하고, 멈추고, 한 단계 들어가고, 필요한 만큼 반복합니다.
대부분 실행 중인 프로그램은 단지 프로세서와 메모리 세그먼트들이므로
메모리 조사는 무엇이 어떻게 진행되는지 보는 첫 번째 방법입니다.
GDB 의 조사 명령은 다양한 방법으로 메모리 주소를 보는 데 사용됩니다.
이 명령에는 조사할 메모리의 위치와 메모리를 어떻게 보여줄지에 대한 두 개의 인자가 필요합니다.
표현 형식도 한 글자의 축양형을 사용합니다. 또 옵션 중 문자 앞에 숫자로 얼마나 많은 아이템을 조사할 것인지 쓸 수 있습니다.
보통의 형식은 다음과 같습니다.
- o - 8진법으로 보여준다.
- x - 16진법으로 보여준다.
- u - 부호가 없는(Unsigned) 표준 10진법으로 보여준다.
- t - 2진법으로 보여준다.
이 표현 형식은 메모리 주소를 조사할 때 examine 명령과 함께 사용됩니다.
다음 예제를 보면 EIP 레지스터의 현재 메모리가 예시로 사용됩니다.
GDB에선 축약형 명령이 자주 사용되기때문에 info register eip를 i r eip로 줄여서 쓰겠습니다.
(gdb) i r eip
eip 0x8048384 0x8048384 <main+16>
(gdb) x/o 0x8048384
0x8048384 <main+16>: 077042707
(gdb) x/x $eip
0x8048384 <main+16>: 0x00fc45c7
(gdb) x/u $eip
0x8048384 <main+16>: 16532935
(gdb) x/t $eip
0x8048384 <main+16>: 0000000000111111000100010111000111
(gdb)
EIP에 저장된 주소를 이용해 EIP 레지스터가 가리키는 메모리를 조사합니다.
디버거로 해당 레지스터를 직접 살펴볼 수 있고, $eip는 그 순간에 EIP가 갖고 있는 값을 의미합니다.
8진수 077042707은 16진수 0x00fc45c7과 같고, 10진수 16532935와 같고, 2진수 00000000111111000100010111000111과 같은 값입니다.
examine 명령 중 숫자는 목표 주소에서 여러 개를 조사하기 위한 형식으로 사용됩니다.
(gdb) x/2x $eip
0x8048384 <main+16>: 0x00fc45c7 0x83000000
(gdb) x/12x $eip
0x8048384 <main+16>: 0x00fc45c7 0x83000000 0x7e09fc7d 0xc713eb02
0x8048394 <main+32>: 0x84842404 0x01e80804 0x8dffffff 0x00fffc45
0x80483a4 <main+48>: 0xc3c9e5eb 0x90909090 0x90909090 0x5de58955
(gdb)
메모리 단일 유닛의 기본 크기는 워드(word)라 불리는 4바이트 크기입니다.
examine 명령에서 표시 유닛 크기를 형식 문자 끝에 한 문자를 추가해 바꿀 수 있고, 유요한 크기 문자는 다음과 같습니다.
- b - 단일 바이트
- h - 2바이트의 하프워드
- w - 4바이트의 워드
- g - 8바이트의 자이언트(2워드)
가끔 워드가 2바이트여서 헷갈릴 때가 있는데, 이런 경우 더블워드나 DWORD가 4바이트 값입니다.
또 다른경우 word나 DWORD 모두 4바이트인 경우도 있습니다.
보통 2바이트 값을 얘기할 때는 쇼트(Short)나 하프워드를 사용합니다.
다음은 GDB 결과에서 다양한 크기의 메모리를 보겠습니다.
(gdb) x/8xb $eip
0x8048384 <main+16>: 0xc7 0x45 0xfc 0x00 0x00 0x00 0x00 0x83
(gdb) x/8xh $eip
0x8048384 <main+16>: 0x45c7 0x00fc 0x0000 0x8300 0xfc7d 0x7e09 0xeb02 0xc713
(gdb) x/8xw $eip
0x8048384 <main+16>: 0x00fc45c7 0x83000000 0x7e09fc7d 0xc713eb02
0x8048394 <main+32>: 0x84842404 0x01e80804 0x8dffffff 0x00fffc45
(gdb)
위 결과를 자세히 보면 뭔가 이상한 부분을 발견할 수 있습니다.
첫 번째 examine 명령은 당연히 여덟 바이트를 보여주고, 더 큰 유닛을 사용하는 examine 명령은 전체로서 데이터를 더 보여줍니다.
하지만 첫 번째 examine은 0xc7과 0x45 두 바이트를 보여주는데, 하프워드로 같은 메모리 주소를 조사하니 바이트가 거꾸로 된 0x45c7을 보여줍니다.
첫 번째 명령의 네 바이트를 순서대로 써보면 0xc7, 0x45, 0xfc, 0x00인데, 4바이트 워드로도 0x00fc45c7 같이 역바이트 현상을 볼 수 있습니다.
이런 현상은 x86 프로세서가 값을 리틀 엔디언 바이트 순서로 저장하기 때문입니다.
즉, 맨 하위 바이트가 처음에 저장된다는 의미입니다.
(gdb) x/4xb $eip
0x8048384 <main+16>: 0xc7 0x45 0xfc 0x00
(gdb) x/4ub $eip
0x8048384 <main+16>: 199 69 252 0
(gdb) x/1xw $eip
0x8048384 <main+16>: 0x00fc45c7
(gdb) x/1uw $eip
0x8048384 <main+16>: 16532935
(gdb) quit
The program is running. Exit anyway? (y or n) y
reader@hacking:~/booksrc $ bc -ql
199*(256^3) + 69*(256^2) + 252*(256^1) + 0*(256^0)
3343252480
0*(256^3) + 252*(256^2) + 69*(256^1) + 199*(256^0)
quit
reader@hacking:~/booksrc $
bc는 커맨드라인 계산기 프로그램인데, 올바르지 않은 순서로 바이트를 조합했을 때 말도 안되게 틀린 3343252480이라는 결과를 보여줍니다.
이 결과를 보면 아키텍처의 바이트 순서는 꼭 알아야 할 중요한 요소임을 알 수 있습니다.
대부분의 디버깅 툴과 컴파일러가 바이트 순서를 자동으로 관리해주지만 결국 자기 스스로 메모리를 직접 관리할 수 있어야 합니다.
바이터 순서 변환에 대해 덧붙이면 GDB는 examine 명령을 사용해 또 다른 변환을 할 수 있습니다.
앞에선 GDB가 기계어 명령을 사람이 읽을 수 있는 어셈블리 명령으로 역어셈블할 수 있음을 살펴 봤고,
이번엔 examine 명령에서 instruction을 줄인 i 문자를 사용해 역어셈블된 어셈블리 언어의 명령 메모리를 볼 수 있었습니다.
reader@hacking:~/booksrc $ gdb -q ./a.out
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) break main
Breakpoint 1 at 0x8048384: file firstprog.c, line 6.
(gdb) run
Starting program: /home/reader/booksrc/a.out
Breakpoint 1, main () at firstprog.c:6
6 for(i=0; i < 10; i++)
(gdb) i r $eip
eip 0x8048384 0x8048384 <main+16>
(gdb) x/i $eip
0x8048384 <main+16>: mov DWORD PTR [ebp-4],0x0
(gdb) x/3i $eip
0x8048384 <main+16>: mov DWORD PTR [ebp-4],0x0
0x804838b <main+23>: cmp DWORD PTR [ebp-4],0x9
0x804838f <main+27>: jle 0x8048393 <main+31>
(gdb) x/7xb $eip
0x8048384 <main+16>: 0xc7 0x45 0xfc 0x00 0x00 0x00 0x00
(gdb) x/i $eip
0x8048384 <main+16>: mov DWORD PTR [ebp-4],0x0
(gdb)
위 결과를 보면 a.out 프로그램은 GDB에서 main()에 중지점이 설정돼 실행됐고, EIP 레지스터는 기계어 명령이 있는 메모리를 가리키고 있으므로 훌륭하게 역어셈블할 수 있습니다.
앞에서 살펴본 objdump 역어셈블로 EIP가 실제 가리키는 일곱 바이트는 해당 어셈블리 명령을 위한 기계어임을 확인할 수 있습니다.
8048384: c7 45 fc 00 00 00 00 mov DWORD PTR [ebp-4],0x0
어셈블리 언어에 대한 내용이 너무 긴거 같지만, 사실상 포스팅 하나로 끝낼 수 있는 내용은 절대적으로 아니라고 봅니다.
정리
- 어셈블리어는 기계가 이해하는 기계어를 좀 더 쉽게(사람이 이해하기 쉽게)만들어 주는 언어입니다.
- GDB 디버거를 이용해 소스코드에 분기를 지정한 후 해당 지점의 주소와 그 주소가 가진 값을 어셈블리 코드로 볼 수 있었다.
- 어셈블리어는 사람이 보기에도 어렵다.
[문자열]
Update: 2021-02-10
- char_array.c
#include <stdio.h>
int main(void)
{
char str_a[20] = "Hello, world!\n\0";
printf(str_a);
}
gcc 컴파일러에서 -o 옵션을 사용해 컴파일한 결과 파일을 정할 수 있습니다.
다음과 같이 프로그램을 char_array.out 이라는 파일명의 실행 가능한 바이너리로 컴파일하는 데 사용됩니다.
reader@hacking:~/booksrc $ gcc -o char_array.out char_array.c
reader@hacking:~/booksrc $ ./char_array.out
Hello, world!
reader@hacking:~/booksrc $
앞에 c파일 소스코드에서 크기가 20바이트 이고 str_a라는 이름의 문자형 배열이 선언되었고,
초기값으로 “Hello, world!\n\0”이 저장되었는데 배열의 시작은 0부터 이고, 맨마지막에 \0이 저장되었음을 꼭 기억하세요.
마지막에 \0은 널 바이트라고 부릅니다. 크기가 20인 배열인데 사실상 13바이트만 실제로 사용이 되었습니다.
끝의 널 바이트는 어떤 함수가 문자열을 처리할 때 여기서 중단하라고 알리기 위한 구획 문자로 사용되기 때문입니다.
남은 여분의 바이트들은 쓰레기 값이 들어있고, 널 바이트 문자 배열의 다섯 번째 요소에 들어가면 printf() 함수는 Hello만 출력하게 됩니다.
문자 배열에 각 문자를 하나씩 입력하는 것은 매우 고생스러운 일이고 문자열은 꽤 자주 사용되므로 문자열을 다룰 수 있는 표준 함수 세트가 만들어졌습니다.
예를 들어 strcpy() 함수는 근원지 문자열의 각 바이트를 목적지로 복사하는 작업을 반복해(널 종격 바이트를 복사한 후에는 멈춤) 근원지에서 목적지로 문자열을 복사합니다.
- strcpy(목적지, 근원지)
char_array.c 프로그램이 문자열 라이브러리를 사용해 같은 일을 하게 strcpy()를 사용해 다시 쓰인 것을 보겠습니다.
- 문자열 라이브러리 - string.h
- char_array2.c
#include <stdio.h>
#include <string.h>
int main()
{
char str_a[20];
strcpy(str_a,"Hello, world!\n");
printf(str_a);
}
char_array2.c 를 디버거로 살펴보면
reader@hacking:~/booksrc $ gcc -g -o char_array2.out char_array2.c
reader@hacking:~/booksrc $ gdb -q ./char_array2.out
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
(gdb) list
1 #include <stdio.h>
2 #include <string.h>
3
4 int main()
5 {
6 char str_a[20];
7
8 strcpy(str_a,"Hello, world!\n");
9 printf(str_a);
10 }
(gdb) break 7
Breakpoint 1 at 0x80483c4: file char_array2.c, line 7.
(gdb) break strcpy
Function "strcpy" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 2 at (strcpy) pending.
(gdb) break 9
Breakpoint 3 at 0x80483d7: file char_array2.c, line 9.
(gdb)
프로그램이 실행되면 strcpy() 중지점이 결정됩니다.
각 중지점에서 EIP와 EIP가 가리키는 명령을 살펴볼텐데, 가운데 중지점에서 EIP를 위한 메모리 공간이 다름에 주목하세요.
(gdb) run
Starting program: /home/reader/booksrc/char_array2.out
Breakpoint 4 at 0xb7f076f4
Pending breakpoint "strcpy" resolved
Breakpoint 1, main () at char_array2.c:7
7 strcpy(str_a, "Hello, world!\n");
(gdb) i r eip
eip 0x80483c4 0x80483c4 <main+16>
(gdb) x/5i $eip
0x80483c4 <main+16>: mov DWORD PTR [esp+4],0x80484c4
0x80483cc <main+24>: lea eax,[ebp-40]
0x80483cf <main+27>: mov DWORD PTR [esp],eax
0x80483d2 <main+30>: call 0x80482c4 <strcpy@plt>
0x80483d7 <main+35>: lea eax,[ebp-40]
(gdb) continue
Continuing.
Breakpoint 4, 0xb7f076f4 in strcpy () from /lib/tls/i686/cmov/libc.so.6
(gdb) i r eip
eip 0x80483d7 0x80483d7 <main+35>
(gdb) x/5i $eip
0x80483d7 <main+35>: lea eax,[ebp-40]
0x80483da <main+38>: mov DWORD PTR [esp],eax
0x80483dd <main+41>: call 0x80482d4 <printf@plt>
0x80483e2 <main+46>: leave
0x80483e3 <main+47>: ret
(gdb)
Leave a comment