본문 바로가기
IT/프로그래밍

hacking에서 프로그래밍

by 모르면 모른다고 해 2013. 4. 10.
반응형

로그래밍
프로그래밍은 매우 자연스럽고 직관적인 개념이다. 프로그램은 특정 언어로 작성된 문장들에 지나지 않는다. 프로그램은 어디에나 있으며 기술을 싫어하는 사람들도 매일 프로그램을 사용한다. 교통 안내, 조리법 축구 경기, DNS는 이상생활에서 볼 수 있는 프로그램들이다. 전형적인 교통 안내 프로그램은 다음과 같다.
-----------------------------------------------------------------------------
동쪽에 있는 큰 도로로 출발하라. 오른쪽에 편의점이 보일 때까지 큰 도로로 직진하라.
현재 거리가 공사 중이라면 15번지에서 우회전, 중앙대로에서 좌회전, 16번지에서 우회
전하라. 아니면 그냥 직진한 다음 16번지에서 우회전하라. 16번지를 쭉 가다가 연수로
방향으로 좌회전하라. 그 길로 약 4km 정도 가면 오른쪽에 집이 보일 것이다. 그집 주소
가 우리가 찾는 도로다.
------------------------------------------------------------------------------
우리말을 아는 사람은 위 안내문을 이해하고 그대로 따라할 수 있다. 하지만, 컴퓨터는 '0'과 '1'로 된 기계어만을 이해할 뿐 우리말을 이해하지 못한다. 즉, 컴퓨터에게 뭔가 지시하려면 명령을 기계어로 작성해야 한다.
그렇지만 기계어는 난해하며 다루기 어렵다. 기계어는 비트와 바이트로 구성돼 있으며, 기계 아키텍처별로 다르다.

즉, 인텔 x86 프로세서용 기계어가 스팍(sparc) 프로세서용 기곙와 다른 것처럼, x86 어셈블리 언어도 스팍 어셈블리 언어와 다르다.

이런 문제를 해결하려고 나온 것이 또 다른 변환기인 컴파일러다. 컴파일러는 하이레벨 언어를 기계어로 변환한다.
하이레벨 언어(고급언어)는 어셈블리 언어보다 훨씬 직관적이며 여러 프로세서 구조의 기계어로 변환될 수 있다. 즉, 어떤 프로그램을 하이레벨 언어로 작성했다면 코드 하나를 컴파일해 다양한 아키텍처의 기계어로 만들 수 있다. C, C++, VB, 포트란이 하이레벨 언어이다. 흔히 하이레벨 언어로 작성된 프로그램은 어셈블리 언어나 기계어보다 훨씬 읽기 쉽고 우리말과 비슷하다.

직접 해보기
예를 들어 보자 . (firstprog.c)
이 프로그램은 워낙 유명하다. "hello world!" 를 10번 출력하는 간단한 C코드다.  
--------------------------------------------------------
#include <stdio.h>

int main()

{   
    int i;
    for(i=0; i<10; i++)      //10번 반복
    { 
            printf("hello world!\n");      //문자열을 출력한다. 
    }
    return 0;                    //오류 없이 종료.
}                
-------------------------------------------------------
gcc로 컴파일 하면 아래와 같다.

큰 그림 이해하기
해커들은 큰 그림 안에서 각 조각들이 어떻게 상호작용하는지 알고 있다. 프로그래밍의 큰 그림을 이해하려면 C 코드는 컴파일 된다는 사실을 깨달아야 한다. 코드는 컴파일 되어 실행 가능한 바이너리 파일이 될 때까지는 아무것도 할 수 없다.
C 소스코드 자체가 프로그램이라 생각하는 것은 가장 일반적인 오해다.

컴파일된 프로그램이 실행되는 동안 보통의 프로그래머는 소스코드만 신경쓸 뿐이다. 하지만 해커는 컴파일된 프로그램이 실제로 무엇을 실행하는지 알고 있다. CPU가 어떻게 동작하는지 더 잘 이해하는 해커는 실행되는 프로그램을 잘 다룰 수 있다. 위에서 살펴 본 것 처럼 첫 번째 프로그램의 소스코드를 살펴봤고 컴파일해 x86 아키텍처에서 실행 가능한 바이너리 파일(a.out)을 만들었다. 그런데 이 실행 가능한 바이너리 파일은 어떻게 생겼을까??
바이너리 파일을 살펴볼 수 있는 objdump 가 있다. 변환된 main()함수를 기계어로 살펴보자. (objdump -M  inel 옵션을 사용해 인텔 문법으로 같은 코드를 볼 수 있다.)

 여기서 보면 기계어 명령 ret가 0xc3이나 11000011보다 기억하고 이해하기 쉽다.

여기서 어렘블리 언어의 명령은 연산(Operation)과 연산의 목적지(destination)나 근원지(Source)를 표현하는 추가 인자로 구성된다. 연산은 메모리를 돌아다니며 간단한 수학 함수를 수행하거나 프로세서가 다른 일을 하게 인터럽트(Interrupt)를 건다. 결국 컴퓨터 프로세서가 하는 일은 이것이 전부다. 하지만 몇 개의 알파벳만으로 수백만 권의 책을 쓰는 것처럼 적은 수의 기계어로 무한한 개수의 프로그램을 만들 수 있다. 

※ 컴퓨터 프로세서 : 8086 CPU는 첫 x86 프로세서. 그 후 80186, 80286, 80386, 80486.. 으로 발전 (8,90 년대 사람들이 386, 486 프로세서라 말하던 것이 바로 80386, 80486을 가르킨다)
 
프로세서는 레지스터라는 자신만의 특별한 변수 세트를 갖고 있다. 대부분의 명령은 데이터를 읽거나 쓰기 위해 레지스터를 사용한다. 그래서 프로세서의 레지스터를 이해하는 것은 명령을 이해하는 데 중요하다.

x86 프로세서
프로그래머는 컴파일된 프로그램을 한 단계씩 보며 프로그램 메모리를 검사하고 프로게서 레지스터를 보기 위해 디버거를 사용한다. 프로그램 내부 동작을 보기 위해 디버거를 사용하지 않는 프로그래머는 현미경을 사용하지 않는 17세기 의사와 같다.
하지만 디버거는 현미경에 비유하는 것 이상으로 강력하다. 현미경과는 달리 디버거는 모든 시점에서 실행을 볼 수 있고, 멈출 수도 있으며, 어떤 것이든 바꿀 수도 있다.
GDB가 프로그램이 시작되기 바로 전 프로세서 레지스터의 상태를 보여준다.

어셈블리 언어
인텔 문법 어셈블리 명령은 보통 다음과 같은 형식이다.
--------------------------------------------
명령 <목적지>, <근원지>
--------------------------------------------
목적지(Destination)와 근원지(Soucrce)는 레지스터나 메모로 주소, 값이 될 수 있다. 명령은 보통 직관적 연상 기호(Mnemonic)다. 간단히 살펴보면
mov  : 근원지에서 목적지로 값을 이동시킨다.
sub  :                   "          값을 뺀다.
inc   :                   "          값을 증가시킨다.
cmp :   값을 비교하는 데 사용

ex)
다음 명령은 값을 esp에서 ebp로 이동시킨 후, esp에 8을 뺀후 그 결과를 esp에 저장하는 명령
--------------------------------------------------
8048375:              89    e5               mov    ebp,esp
8048377:              83    ec    08        sub    esp,0x8
--------------------------------------------------

다음 아래 예제의 첫 번째 줄은 ebp의 4바이트 뺀 값에서 4를 뺀 값과 9를 비교한다. 다음 명령은 결과를 보고 작거나 같으면 점프하는 명령이다. 값이 9보다 작거나 같으면 실행은 0x8048393으로 점프한다. 아니면 실행 흐름은 무조건 다음 명령으로 넘어간다. 값이 9보다 작거나 같지 않으면 실행은 0ㅌ80483a6으로 점프한다.
----------------------------------------------------------------
804838b:       83    7d    fc    09        cmp    DWORD PTR [ebp-4],0x9
804838f:        7e    02                      jle      8048393    <main+0x1f>
8048391:       eb    13                      jmp    80483a6    <main+0x32>
-----------------------------------------------------------------
GDB컴파일러 사용 시 GDB에서 소스코드를 볼 수 있는 추가 더버그 정보를 포함시키려면 -g 플래그를 사용한다.

 

먼저 소스코드가 출력되고(list) main() 함수의 역어셈블이 출력된다. 그리고 main()의 시작점에 중지점(break main)을 설정하고 프로그램을 실행시킨다. 이 중지점은 디버거가 해당 지점에 가면 프로그램 실행을 중지하도록 한다. 중지점이 main() 함수 시작점에 설쟁됐으므로 프로그램은 main()의 어떤 명령도 실행하기 전에 중지점에 다다르고 멈춘다. 그리고 EIP(명령 포인터)의 값이 출력된다. eip(명령 포인터)가 main() 함수의 역어셈블 명령을 가리키는 메모리 주소를 갖고 있음을 주의 깊게 봐야한다.
이 메모리 주소 전에 기울임꼴로 표기된 명령(글씨체가 작은 것들)들은 함수 프롤로그라 불리고, main() 함수의 나머지 전역 변수를 위한 메모리를 할당하려고 컴파일러가 생성한 것이다.
GDB 디버거는 examine을 줄인 명령 x를 사용해 메모리를 조사하는 방법을 제공한다. 해커에게 메모리 조사는 아주 중요한 기술이다. GDB의 메모리 조사 명령은 다양한 방법으로 메모리 주소를 보는 데 사용된다.
■ o 8진법
■ x 16진법
■ u 부호 없는 표준 10진법
■ t 2진법

dl 표현 형식은 메모리 주소를 조사할 때 examine 명령과 함께 사용된다. 다음 예제에서 eip 레지스터의 현재 메모리가 사용된다. GDB에서는 축약형 명령이 자주 사용되고 info register eip는 i r eip로 줄여 쓸 수 있다.

위의 결과를 자세히 보면 뭔가 이상한 부분을 발견할 수 있을 것이다. 첫번째 examine 명령은 당연히 8바이트를 보여주고, 더 큰 유닛을 사용하는 examine 명령은 전체로서 데이터를 더 보여준다. 하지만 첫 번재 examine은 0xc70x45 두 바이트를 보여주는데, 하프워드로 같은 메모리 주소를 조사하니 바이트가 거꾸로 된 0x45c7을 보여준다.
첫 번재 명령의 네 바이트를 순서대로 써보면 0xc7, 0x45, 0xfc, 0x00 인데 , 4바이트 워드로도 0x00fc45c7 처럼 같은 역바이트 현상을 볼 수 있다.
이런 현상은 x86 프로세서는 값을 리틀 엔디언(Little-endian)바이트 순서로 저장하기 때문이다. 즉, 맨 하위 바이트가 처음에 저장되고 예를들어 네 바이트가 단일 값일 때 바이트는 반드시 역순으로 사용된다.

이것은 C에서 변수 i가 메모리 어디에 저장되었는지를 나타낸다. i는 x86 프로세서의 메모리 4바이트를 사용해 정수로 선언했다. 기본적으로 이 명령은 for 루프에서 변수 i를 0으로 만든다. 그 메모리를 바로 조사한다면 메모리에는 아무것도 안 들어있는 것이 아니라 임의의 쓰레기 값이 들어있을 것이다. 이 취이의 메모리를 여러 방법으로 조사할 수 있다. 

 

EBP 레지스터는 주소 0xbfff818 임을 볼 수 있다. 그리고 어셈블리 명령은 오프셋 4만큼 작은 0xbffff414에 씌여질 것이다.
print 명령은 간단한 계산을 할 때 사용될 수 있고, 결과는 디버거의 임시 변수에 저장된다.

(안경그림) 첫 번째 명령의 cmp는 C의 변수 i가 사용하는 메모리의 값과 9를 비교하는 비교 명령이다. 다음 명령 jle는 작거나 같으면 점프하라는 명령이다. jle명영은 앞의 비교 연산의 목적지가 근원지보다 작거나 같으면 코드의 다른 부분으로 EIP를 점프하라는 데 앞의 비교 결과를 이용한다. 이 경우에는 C 변수 i를 위한 메모리에 저장된 값이 9보다 작거나 같으면 0x8048393 주소로 점프한다. 그리고 여기서 무조건 적인 점프(즉 조건이 만족하면) EIP가 0x80483a6  주소로 점프하게 된다. 이 세개의 명령이 결합되어 if-then-else  즉 for문의 조건,제어구문이 된다.
"i가 9보다 작거나 같으면 명령을 0x8048393 주소로 이동히켜라. 그렇지 않으면 0x80483a6 주소로 이동시켜라."
9와 비교할 메모리 위치에 0이 저장돼 있고 0은 9보다 작거나 같다는 것을 알기 때문에 다음 두 명령이 실행된 이후에 EIP가 0x8048393을 가르킬 것이다.

예상대로 앞의 두 명령 이후에 프로그램 주소는 0x8048393이 됐고(x/i $rip) 다음 두 명령을 실행 할 수 있게 됐다.
(별 그림) 첫 번째 명령은 0x8048484 주소를 ESP 레지스터의 메모리 주소에 쓰라는 mov 명령이다. 그렇다면 지금 ESP가 가르키고 있는 것은 무엇일까? 바로 0xbfff810 주소를 가르킨다. 이제 mov 명령이 실행되면 0x8048393 주소를 지금 ESP의 메모리 주소에(0xbfff810) 쓰일 것이다. Why 0x8048484 ??
이 명려은 0x8048484 메모리 주소에 데이터 문자열 "hello world!"이 저장돼 있음을 알게 해준다. 이 문자열은 print()  함수의 인자다. 문자열의 주소를 ESP(0x8048484)에 저장된 주소로 옮기는 것이 printf() 함수와 관련된다는 점을 나타낸다.

main+16 ~ 29 , main+43 ~ 48  까지가 for루프문이다.
위와 같이 역어셈을 다시 봤을 때 C코드의 어떤 부분이 기계어로 어떻게 컴파일되는지 알 수 있도록 꾸준히 익히고 자주 접해보아야 할 것이다.

 

반응형

댓글