
윈도우에서 Subversion과 Apache, OpenSSL를 연동하여 http와 https 프로토콜을 사용하는 서버를 구성하는 방법을 설명합니다.
이번 문서는 모든 개발 환경을 윈도우에서 구축하고자 하는 분들을 위해 작성하였습니다.
http://www.pyrasis.com/main/SubversionServerForWindows



이번에는 어셈블리 명령어에 대해서 간단히 알아보도록 하겠습니다.
사실 리버스를 하기 위해서는 어셈블리 명령어만 알면 되는 것이 아니라 인텔 CPU의 동작 방식과 리버스하려는 시스템의 운영체제의 내부 구조와 메모리 관리 방식을 모두 알아야 합니다.
어셈블리는 어셈블리어라고도 부르는데 이 어셈블리어는 명령어들의 조합니다. 인텔 CPU 안에는 이 명령어들이 회로로 구현되어 있어서 우리가 작성한 어셈블리 코드를 실행할 수 있습니다. CPU는 2진수로 모든것을 처리하는데 어셈블리 명령어들도 당연히 2진수로 되어 있습니다. 하지만 2진수로 된 것은 우리가 눈으로 읽기 어려우니까 디스어셈블러나 디버거 같은 프로그램에서 mov, add, push 같이 사람이 읽기 좋은 형태로 변환하여 보여줍니다.
어셈블리 명령어를 영어로는 instruction이라고 합니다. 그리고 명령어 다음에 오는 레지스터 이름이나 값들은 operand라고 합니다. mov eax, 0x100에서 eax와 0x100이 오퍼랜드 입니다.
리버스할때 자주 등장하는 명령어들 부터 알아보도록 하겠습니다.
mov : move, 이름 그대로 데이터를 이동하는 명령어입니다. mov a, b라고 한다면 b의 값을 a에 대입합니다. a <- b 형태죠. 메모리와 레지스터(CPU 안의 기억공간, 모든 연산은 이 레지스터에 저장한 뒤 이루어집니다.) 사이의 데이터 이동, 레지스터와 레지스터 사이의 데이터 이동이나 값을 메모리나 레지스터에 대입할 때 사용합니다. 단 메모리와 메모리 사이의 데이터 이동은 할 수 없고 세그먼트 레지스터와 세그먼트 레지스터 사이의 데이터 이동에도 사용할 수 없습니다. 메모리와 메모리 사이의 데이터 이동은 movs (string move) 명령어를 사용합니다.
cmp : compare, 비교 명령어입니다. cmp eax, 1 형식으로 사용합니다. cmp 명령은 혼자 사용되지 않고 언제나 조건 점프 명령어나 조건 이동(mov) 명령어와 함께 사용됩니다. 조건 점프 명령어는 밑에서 설명하겠습니다.
jmp : jump, 점프 명령어입니다. 프로그램의 흐름을 바꿀때 사용합니다. 이 점프계열 명령어들을 이용해서 C언어의 if, for, while, goto 등을 구현합니다. jmp 00403000 이라면 00403000의 주소로 가서 그곳의 명령어를 실행합니다. 이 점프 명령어는 다음에 설명할 call 명령어와는 달리 되돌아 오지 않고 뛰어넘어간 주소의 다음 명령어를 계속 실행해 나갑니다. jmp 00403000 형태 처럼 주소를 직접 지정하는 방법과 jmp eax 처럼 레지스터에 저장된 주소로 점프하는 방법이 있습니다.
조건 점프 명령어 : 조건 점프 명령어는 위의 cmp 명령어의 결과에 따라 점프하는 명령어입니다.
이라면 eax에 저장된 값과 1이 같다면 00403000의 주소로 점프 합니다. 같지 않다면 je 다음에 오는 명령어를 실행합니다.
Unsigned 계열 (부호가 없는 값)
je : jump equal - 비교 결과가 같을 때 점프
jne : jump not equal - 비교 결과가 다를 때 점프
jz : jump zero - 결과가 0일 때 점프, je와 같음. (cmp 명령에서 결과가 같으면 0을 출력합니다.)
jnz : jump not zero - 결과가 0이 아닐 때 점프
ja : jump above - cmp a, b에서 a가 클 때 점프
jae : jump above or equal - 크거나 같을 때 점프
jna : jump not above - 크지 않을 때 점프
jnae : jump not above or equal - 크지 않거나 같지 않을 때 점프
jb : jump below - cmp a, b에서 a가 작을 때 점프
jbe : jump below or equal - 작거나 같을 때 점프
jnb : jump not below - 작지 않을 때 점프
jnbe : jump not below or equal - 작지 않거나 같지 않을 때 점프
jc : jump carry - 캐리 플래그가 1일 때 점프
jnc : jump not carry - 캐리 플래그가 0일 때 점프
jnp/jpo : jump not parity / parity odd - 패리티 플래그가 0일 때 / 홀수일 때 점프
jp/jpe : jump parity / parity even - 패리티 플래그가 1일 때 / 짝수일 때 점프
jecxz : jump ecx zero - ecx 레지스터가 0일때 점프
Signed 계열 (부호가 있는 값)
jg : jump greater - cmp a, b에서 a가 클 때 점프
jge : jump greater or equal - 크거나 같을 때 점프
jng : jump not greater - 크지 않을 때 점프
jnge : jump not greater or equal - 크지 않거나 같지 않을 때 점프
jl : jump less - cmp a, b에서 a가 작을 때 점프
jle : jump less or equal - 작거나 같을 때 점프
jnl : jump not less - 작지 않을 때 점프
jnle : jump not less or equal - 작지 않거나 같지 않을 때 점프
jo/jno : jump overflow / not overflow - 오버플로 플래그가 1일 때 / 0일 때 점프
js/jns : jump sign / not sign - 사인(부호) 플래그가 1일 때(음수) / 0일 때(양수) 점프
조건 점프 명령을 조합하여
if ( a > b ) if ( a >= b ) if ( a < b ) if ( a == b )
for, while ... 등의 조건문을 구현합니다.
push : 메모리상에 설정된 스택이라는 공간에 데이터를 저장합니다. 스택의 자료구조는 접시를 쌓는 것과 같다고 생각하면 됩니다. 접시는 순서대로 쌓고, 절대 중간에 있는 접시를 빼낼 수 없습니다. 맨위에 쌓인 접시부터 차례 차례 빼내는 것입니다. push 명령어는 스택의 가장 윗부분에 자료를 추가합니다. 추가하면서 스택 포인터도 가장 나중에 추가된 쪽을 가리키게 합니다. 일반적으로 접시를 쌓는 동작은 아래에서 위로 스택이 커지지만 x86 아키텍쳐에서는 스택이 위에서 아래로 커집니다.
pop : 스택에서 자료를 빼냅니다. pop eax 이면 스택에서 가장 나중에 추가된 데이터를 eax에 저장합니다. 그리고 스택 포인터를 그 바로 전에 추가된 데이터 쪽을 가리키게 합니다.
call : 함수 호출 명령어입니다. 점프 명령어는 한번에 점프한 곳에서 돌아오지 않지만, 이 call 명령어는 점프했던 곳에서 명령어들을 실행한 뒤 ret 명령어를 사용하여 call 명령어를 사용한 곳으로 되돌아옵니다. call 명령어가 실행되면 복귀주소를 스택에 저장(push)합니다 그래서 ret(return) 명령어가 실행되면 다시 되돌아 올 수 있습니다. 우리가 함수로 구현한 부분을 호출할 때 사용합니다.
ret : 스택에 저장된 복귀 주소로 돌아갑니다. ret 10과 같이 오퍼랜드가 같이 있는 경우가 있는데 이것은 스택 포인터를 지정된 오퍼랜드 만큼 위로 올리는 것입니다. push 등으로 스택을 사용한 뒤 초기화 할 때 사용합니다.
sum(1, 2)를 어셈블리 명령어로 표현하면 대략 다음과 같습니다.
00403010:
... ; 1과 2를 더함
ret ; sum 함수를 호출한 곳으로 되돌아갑니다.
앞에 올렸던 리버스 엔지니어링이란? 1부에서 이어지는 글입니다.
윈도우용 바이너리를 리버스 한다고 하면 윈도우 API를 알아야 하고 기타 리눅스나 BSD라면 해당 OS의 API를 알아야 합니다. 그리고 당연히 인텔 어셈블리를 알고 있어야 하고, 인텔 CPU가 돌아가는 방식을 알아야 합니다. 프로그램은 단순히 로직만으로 이루어져 있지 않고 시스템의 API를 호출하여 여러가지 동작을 하기 때문에 각 API의 사용방법과 동작결과를 알고 있어야 어셈블리 코드에서 C소스 코드로 옮기고 다시 소스를 재구현할 수 있기 때문입니다. 어플리케이션을 리버스하는 경우 윈도우 커널에 대한 지식이 없어도 리버스가 가능하지만 커널 레벨로 동작하는 드라이버나 기타 서비스는 윈도우 커널에 대한 지식이 필요합니다.
어셈블리에서 C소스 코드로 옮기는 작업은 한마디로 단순 반복작업입니다. 디스어셈블된 코드에서 call은 함수이고 각종 점프 명령어들은 if, for, while, switch, goto 등의 C언어 제어문입니다. mov 명령어는 변수에 값을 대입한다거나 하는 것이고 push, pop 명령은 함수를 호출하면서 인자값을 넘겨줄 때 사용합니다. 이 어셈블리 명령의 조합을 읽어 C코드로 구현을 하면 됩니다.
각종 도구들
리버스 엔지니어링에서는 디스어셈블러라는 도구가 매우 필수적입니다.
자주 쓰이는 것들로는 리버스계에서 아주 유명한 소프트아이스(SoftICE, 이하 소아)라는 프로그램이 있습니다. 소아의 특징으로는 윈도우가 돌고 있는 상태에서 Ctrl+D를 누르면 윈도우가 멈추고 소아 창이 떠서 현재 실행되고 있는 어셈블리 코드를 보여줍니다. 정말 강력하고 편리한 기능입니다. 이 기능이 소아를 쓰는 이유이기도 합니다. 물론 그냥 바이너리를 열어서 디스어셈블도 가능하며 요즘은 비주얼 소프트아이스라고 나와서 원격 디버깅도 가능합니다. 단 비쌉니다.
WinDBG(Windows Debugger)는 MS에서 배포하는 무료 디버거인데 윈도우 내부를 분석하는데 매우 유용한 도구입니다. 심볼 서버에서 심볼 파일을 받아와서 윈도우 내부 DLL들의 함수이름과 구조체 등을 볼 수 있습니다. 물론 이것도 그냥 바이너리를 열어 디스어셈블이 가능합니다.
OllyDBG, 아주 편리한 도구입니다. 리버스하기에는 딱 알맞은 도구가 아닌가 싶습니다. 디스어셈블 뿐만 아니라 이 디버거가 어셈블리 코드를 분석하여 사용자에게 많은 정보(함수 이름, 인자값 이름, 서브루틴끼리 묶어주는 기능, 점프명령의 도착점 표시 기능 등)를 제공해 줍니다.
W32dasm는 OllyDBG처럼 인터렉티브 하지는 않지만 상당히 쓸만한 도구입니다. 기타 IDA나 PE Browse등의 프로그램이 있는데 사용자 취향에 따라 골라 쓰면 되겠습니다.
거의 모든 디버거에서 레지스터, 스택, 메모리 상태 등을 표시해 주고 있으므로 코드 분석에 많은 도움이 됩니다.
마지막으로 주의할 점은 같은 바이너리를 디스어셈블한다고 하더라도 디스어셈블러마다 분석해 내는 코드가 조금씩 다른 경우가 있습니다. 그래서 한가지 디버거만 쓰다 보면 엉뚱한 코드를 보기 쉽습니다. 여러가지 디버거를 돌려 가면서 코드를 분석하는 것도 좋은 방법입니다.
리버스 엔지니어링이란?
리버스(reverse)라는 말은 반대, 역(逆)의 뜻을 가지고 있는데, 리버스 엔지니어링을 역공학이라고 쓰기도 합니다. 리버스 엔지니어링은 목표가 되는 프로그램이나 프로토콜을 분석하여 똑같은 동작을 만들어 내는 것을 말합니다.
리버스 엔지니어링의 종류
통상적으로 컴파일된 바이너리(EXE, DLL, SYS 등)를 디스어셈블러라는 도구를 이용하여 어셈블리 코드를 출력한 후 그것을 C언어 소스형태로 다시 옮겨 적고 적당한 수정을 통해 리버스하고 있는 파일과 동일한 동작을 하는 프로그램을 만드는 것이 있습니다.
모든 어셈블리 코드를 소스 형태로 옮기지 않고 그냥 동작 방식만을 알아낸다거나 일정 부분만 수정하는 것들도 리버스 엔지니어링이라고 할 수 있습니다. 예를 들면 바이러스를 분석하는 일은 모든 코드를 알아낼 필요가 없기 때문에 동작 방식만 알아내면 됩니다. 그리고 크랙처럼 일정 부분만 수정하여 사용제한을 푸는 것 등도 이에 해당됩니다.
실행파일을 디스어셈블 하지 않고도 그 실행파일이 만들어내는 데이터 파일이나 패킷등을 분석하여 똑같이 재구현하는 것도 리버스 엔지니어링입니다. 예를 들면 오래전 PC 게임에서 많이 하던일인데, HEX 에디터 등으로 세이브 파일을 분석하여 에디트를 만들거나 게임자체를 조작하는 것이 있고, 당나귀와 호환되는 이뮬 같은 프로그램은 당나귀 프로토콜의 패킷을 분석하여 동일한 동작을 하도록 만들어낸 것입니다.
리버스 엔지니어링에서 가장 많이 사용되는 방식은 첫번째로 이야기 했던 바이너리를 디스어셈블 하여 코드를 얻어내는 것입니다. 이것을 하기 위해서는 먼저 인텔 어셈블리를 배워야 하고, 물론 C언어도 알아야 됩니다.
그런데 여기서 컴파일된 바이너리가 VC나 gcc등으로 컴파일한 것이 대부분이지만 비주얼 베이직으로 컴파일 한 것도 있고 델파이(파스칼)로 컴파일 한 것도 있을 것입니다. 이 바이너리들은 모두 CPU에서 직접 실행되는 것들이기 때문에 디스어셈블 해보면 모두 똑같은 방식으로 되어 있습니다. 그래서 VC, VB, 델파이(파스칼)등으로 컴파일 된 것도 디스어셈블 한 뒤 C 소스 코드로 옮길 수 있습니다. 물론 리버스 하는 사람이 VB나 파스칼로 옮겨 적을 수도 있을 것입니다. 하지만 C언어로 하는 것이 가장 간편합니다.
대부분 리버스해서 얻어내는 것들은 프로그램의 로직(알고리즘)이기 때문에 어느 언어로 표현하든 결과는 똑같기 때문입니다.
자바나 닷넷으로 컴파일 된 바이너리는 CPU에서 직접 실행되지 않고 자바 가상머신이나 닷넷 프레임워크를 통해서 실행됩니다. 그래서 자바로 컴파일된 바이너리를 열어보면 자바 바이트코드 문법으로 되어 있고 닷넷으로 컴파일 된 것은 MSIL이라는 문법으로 되어 있습니다. 이런 것을은 인텔 어셈블리와 문법이나 명령어가 다르므로 따로 자바 바이트코드나 MSIL을 공부해서 리버스 하면 되겠습니다.

댓글을 달아 주세요
감사합니다.
http 로는 사용중이었지만 https를 적용하려고 SSL 모듈을 설치하면 자꾸 오류가 나서 고생을 했는데
모두 삭제하고 이곳에 정리하신 순서대로 처음 설치부터 다시 수행해 보니 잘 되는군요.
덕분에 짐 하나를 덜었습니다.
감사합니다.
이글은 아니지만 서브버전 하우투 잘 보았습니다. 그리고 제 블로그에 주소를 인용하였습니다.
svnserve설정을 잘 몰랐는데 리파지토리 프로젝트가 있을 디렉토리를 지정하는것을 저는 프로젝트 디렉토리를 지정해서 svn+ssh만 된다고 투덜거렸습니다.
잘 몰랐던 사실에 대해 깨우치게 해 주셔서 감사합니다.