초보자를 위한 리버스 엔지니어링 상식 - 어셈블리 명령어

저작권 안내
  • 책 또는 웹사이트의 내용을 복제하여 다른 곳에 게시하는 것을 금지합니다.
  • 책 또는 웹사이트의 내용을 발췌, 요약하여 강의 자료, 발표 자료, 블로그 포스팅 등으로 만드는 것을 금지합니다.

이번에는 어셈블리 명령어에 대해서 간단히 알아보겠습니다.

사실 리버스를 하기 위해서는 어셈블리 명령어만 알면 되는 것이 아니라 인텔 CPU의 동작 방식과 리버스하려는 시스템의 운영체제의 내부 구조메모리 관리 방식을 모두 알아야 합니다.

어셈블리는 어셈블리어라고도 부르는데 이 어셈블리어는 명령어들의 조합니다. 인텔 CPU 안에는 이 명령어들이 회로로 구현되어 있어서 우리가 작성한 어셈블리 코드를 실행할 수 있습니다. CPU는 2진수로 모든것을 처리하는데 어셈블리 명령어들도 당연히 2진수로 되어 있습니다. 하지만 2진수로 된 것은 우리가 눈으로 읽기 어려우므로 디스어셈블러나 디버거 같은 프로그램에서 mov, add, push 같이 사람이 읽기 좋은 형태로 변환하여 보여줍니다.

어셈블리 명령어를 영어로는 instruction이라고 합니다. 그리고 명령어 다음에 오는 레지스터 이름이나 값들은 operand라고 합니다. mov eax, 0x100에서 eax0x100이 오퍼랜드 입니다.

리버스할때 자주 등장하는 명령어들 부터 알아보겠습니다.

  • 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 다음에 오는 명령어를 실행합니다.

cmp  eax, 1
je   00403000
mov  ebx, 1
  • 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 아키텍쳐에서는 스택이 위에서 아래로 커집니다.
스택이             실행 전        push 20000000       실행 후
커지는 방향        n      00000000                    n      00000000
↓                 n - 4  00000001 ← esp              n - 4  00000001
                                                     n - 8  20000000 ← esp
  • pop: 스택에서 자료를 빼냅니다. pop eax이면 스택에서 가장 나중에 추가된 데이터를 eax에 저장합니다. 그리고 스택 포인터를 그 바로 전에 추가된 데이터 쪽을 가리키게 합니다.
스택이             실행 전          pop eax           실행 후
커지는 방향        n      00000000                    n      00000000
↓                 n - 4  00000001                    n - 4  00000001 ← esp
                  n - 8  20000000 ← esp
  • call: 함수 호출 명령어입니다. 점프 명령어는 한번에 점프한 곳에서 돌아오지 않지만, 이 call 명령어는 점프했던 곳에서 명령어들을 실행한 뒤 ret 명령어를 사용하여 call 명령어를 사용한 곳으로 되돌아옵니다. call 명령어가 실행되면 복귀주소를 스택에 저장(push)합니다 그래서 ret(return) 명령어가 실행되면 다시 되돌아 올 수 있습니다. 우리가 함수로 구현한 부분을 호출할 때 사용합니다.

  • ret: 스택에 저장된 복귀 주소로 돌아갑니다. ret 10과 같이 오퍼랜드가 같이 있는 경우가 있는데 이것은 스택 포인터를 지정된 오퍼랜드 만큼 위로 올리는 것입니다. push 등으로 스택을 사용한 뒤 초기화 할 때 사용합니다.

C언어의 sum(1, 2) 함수를 어셈블리 명령어로 표현하면 대략 다음과 같습니다.

push 2         ; 두번째 인자를 스택에 저장합니다.
push 1         ; 첫번째 인자를 스택에 저장합니다.
call 00403010  ; 함수 sum의 시작 주소로 이동합니다.
00403010:
...            ; 1과 2를 더함
ret            ; sum 함수를 호출한 곳으로 되돌아갑니다.

관련글


저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.