프로그래밍 #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 eipi 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진수 07704270716진수 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