본 포스팅은 DreamHack 사이트의 x64 기초 강의 내용을 요약한 것입니다.

자세한 내용은 사이트에 들어가셔서 보시기 바랍니다.

https://dreamhack.io/

 

해커들의 놀이터, DreamHack

해킹과 보안에 대한 공부를 하고 싶은 학생, 안전한 코드를 작성하고 싶은 개발자, 보안 지식과 실력을 업그레이드 시키고 싶은 보안 전문가까지 함께 공부하고 연습하며 지식을 나누고 실력 향상을 할 수 있는 공간입니다.

dreamhack.io


 

intel 구조의 64bit 버전 명령어 집합에서 쓰이는 x64 명령어 집합에 대해서 배웁니다..

 

Instruction Cycle

기계 코드가 실행되는 한 번의 과정을 말합니다.

기본적으로 다음 실행할 명령어 읽기(Fetch) -> 읽어온 명령어 해석(Decode) ->해석한 결과 실행(Excute)의 과정입니다.

 

레지스터(Register)

CPU 동작에 필수적인 저장 공간의 역할을 하는 CPU 구성 요소

 

종류

범용 레지스터: 용도를 특별히 정해두지 않고 다양하게 쓸 수 있는 레지스터

원칙적으로는 용도가 정해져 있진 않지만, 관행적으로 쓰임새가 정해져 있는 경우도 있습니다.

rax rcx rdx r8 r9 r10 r11 rbx rsi rdi rbp r12 r13 r14 r15 rsp

이렇게 16개가 범용 레지스터에 속합니다.

이 중 함수가 실행될 때 필요한 인자들을 저장하는 용도로 사용하는 함수 호출 규약으로 사용되는 레지스터도 있습니다.

예를 들어 Windows 64bit에서는 rcx, rdx, r8, r9가 함수 호출 규약으로 사용됩니다.

또한 rsp는 범용 레지스터로 분류되지만, 스택의 가장 위쪽을 가리키는 stack pointer로 용도가 정해져 있습니다.

 

명령어 포인터: 범용 레지스터와는 달리 용도가 엄격히 정해져 있는 레지스터

rip가 대표적인데요. 다음에 실행될 명령어가 위치한 주소를 가리키고 있는 레지스터입니다.

 

 

Data Size

 

CPU가 사용하는 값의 크기 단위를 WORD라고 합니다. 

16bit CPU에서는 ax, cx, dx, bx

32bit CPU에서는 eax, ecx, edx, ebx

64bit CPU에서는 rax, rcx, rdx, rbx

이런식으로 사용됩니다.

 

각 CPU에서 지정된 것만 사용하는 것이 아닌, 64bit에서는 32bit, 16bit에서 사용하는 방식으로도 쓸 수 있으며, 32bit에서는 16bit에서 사용하는 방식으로 사용할 수도 있습니다.

 

상태 레지스터(FLAGS)

현재 상태나 조건을 0과 1로 나타내는 레지스터입니다. 64비트로 구성되어있다면 이 각각의 비트들이 서로 다른 의미를 가집니다.

몇 가지 중요한 플래그에 대해서만 조금씩 알고 가면 됩니다.

 

CF(Carry Flag): 산술 연산이나, 비트이동 등의 연산이 일어났을 때 자리 올림(carry)이 발생한 경우 CF이 1이 됩니다. 연산에 사용된 값들에 부호가 없다는 특징이 있습니다.

ZF(Zero Flag): 연산 결과가 0일 때 ZF는 1이 됩니다. 예를 들어 두 값을 비교할 때 값의 차를 구해서 비교를 하는데 0이라면 ZF가 1이 되어 같은 값인지 확인할 수 있게 됩니다.

SF(Sign Flag): 부호가 있는 연산에서 쓰여 결과가 음수인지 양수인지를 가리킵니다. 최상위 비트(MSB)가 0일 때(양수일 때) SF는 0이 되고, 1일 때(음수일 때) SF는 1이 됩니다.

OF(Overflow Flag): 부호가 있는 연산에서의 CF 입니다. 부호 있는 값을 대상으로 자리 올림이 발생했다는 것은 Overflow가 발생했다는 것을 의미합니다.

 

Opcode, Operand

Opcode: 명령 코드, 명령어에서 실제로 어떤 동작을 할지 나타내는 부분입니다.

nesquitto.tistory.com/173

위의 링크에서 Opcode의 내용이 문제 풀이와 함께 있으니 참고하시면 좋을 것 같습니다.

 

Operand: 피연산자, 명령 코드(Opcode)가 연산할 대상입니다. Opcode를 함수라고 하면 Operand는 인자의 관계와 비슷합니다. 이 피연산자는 어떤 상수일 수도 있으며, 레지스터에 들어있을 수도 있고, 어떤 주소에 들어있을 수도 있습니다.

push	rbp
mov 	rbp, rsp

다음과 같은 어셈블리 코드가 있다고 했을 때 push , mov를 Opcode라고 하며, rbp, rsp를 Operand라고 합니다.

해당 어셈블리에서는 Operand가 레지스터에 들어있는 값을 사용한 경우입니다.

 

Operand로 올 수 있는 값을 더 자세히 알아보도록 하죠.

1. 상수값: 일반적인 상수(0x1337, 0xbeef)의 값이 사용됩니다.

2. 레지스터: 레지스터(rax, rbx...)에 들어있는 값이 사용됩니다.

3. 주소값: 레지스터에 저장된 메모리 주소에 들어있는 값이 사용됩니다. C언어의 포인터 개념과 비슷합니다.

레지스터 -> 메모리 주소 -> 참조값  과 같은 형식입니다.

mov   [rcx],rax                   ; *rcx = rax
-mov의 결과로 rax에 들어있는 값을 rcx 레지스터에 들어있는 주소값의 메모리에 저장합니다.
mov   byte ptr [rcx],al           ; *rcx = al
-mov의 결과로 al에 들어있는 값을 rcx 레지스터에 들어있는 주소값의 메모리에 1바이트만 저장합니다.
-al은 ax레지스터가 16bit일 때 뒷부분 8bit를 가리킵니다. 앞부분 8bit는 ah라고 표현합니다.
mov   dword ptr [rbp-1Ch],eax      
-mov의 결과로 eax에 들어있는 값을 rbp에서 1Ch만큼 떨어진 위치의 메모리 주소에 4바이트만 저장합니다.
mov   byte ptr [rdi+rcx*4+3],0xFF
-mov의 결과로 rdi+rcx*4+3의 위치의 메모리에 1바이트 값인 0xff가 저장됩니다.

지금까지 모르고 살았는데 대괄호로 감싼것이 포인터와 비슷한 의미였군요..! ㅎㅎ

 

 

어셈블리 명령어 몇가지...

Movement

mov a, b
b를 a에 옮깁니다.

lea a, b
b의 주소를 a에 저장합니다.

 

Arithmetic

-Unary

inc a
++a와 같은 의미입니다.

dec a
--a와 같은 의미입니다.

neg a
-a와 같이 부호를 바꿉니다.

not a
~a와 같이 비트를 반전합니다.

-Binary

add a, b
a에 b의 값을 더합니다.

sub a, b
a에 b의 값을 뺍니다.

imul a, b
a에 b의 값을 곱합니다.

and a, b
a와 b를 and연산한 결과를 a에 저장합니다.

or a, b
a와 b를 or 연산한 결과를 a에 저장합니다.

xor a, b
a와 b를 xor 연산한 결과를 a에 저장합니다.

-Shift

shl a, b
a<<b의 비트 연산을 수행합니다.
a를 b만큼 왼쪽으로 이동합니다.

shr a, b
a>>b의 비트 연산을 수행합니다.
a를 b만큼 오른쪽으로 이동합니다.

sal a, b
a<<b의 연산을 수행하며, 부호가 보전됩니다.(최상위비트는 바뀌지 않습니다.)

sar a, b
a>>b의 연산을 수행하며, 부호가 보전됩니다.(최상위비트는 바뀌지 않습니다.)

 

Conditional

test a, b
and 논리연산을 하지만 결과값을 저장하지 않습니다.
연산 결과가 음수라면 SF가 1이 되고, 연산 결과가 0이라면 ZF가 1이 됩니다.

cmp a, b
sub 논리연산을 하지만 결과값을 저장하지 않습니다.
a, b가 같을 때에는 ZF가 1이 되고, 같지 않을 때에는 0이 됩니다.
a<b일 때에는 CF가 1이 되고, a>b일 때에는 0이 됩니다.
jmp a
a의 주소로 점프합니다.

jcc(je, jne, jg, jge, jl, jle, ja, jb, js, jns) a
특정 조건이 만족되면 a로 점프합니다.

 

Stack

프로그램이 동작하는 함수 내에서는 지역 변수를 사용할 때가 많습니다.

이 지역 변수는 함수 내에서 사용되고 이후 함수가 종료되면 더 이상 사용되지 않기 때문에 쉽게 쓰고 지울 수 있는 스택에 데이터를 저장합니다. 그리고 이 스택은 메모리에 저장이 되는 것이죠. 

intel 아키텍쳐에서의 스택은 새로운 데이터가 추가될 때마다 더 낮은 메모리 주소에 쌓이게 됩니다. 따라서 데이터가 증가할수록 저장되는 주소는 점점 작아지게 됩니다.

push rdi
스택에 새로운 데이터를 집어넣습니다.
sub rsp, 8
mov [rsp], rdi
위의 어셈블리어와 동일하게 동작합니다.
데이터가 들어갈 공간을 만들어주고 그 안에 복사하는 것입니다.

pop rdi
스택의 맨 위에 있는 데이터를 뺍니다.
mov rdi, [rsp]
add rsp, 8
위의 어셈블리어와 동일하게 동작합니다.

 

여기까지! 입니다!


 

자세한 내용은 DreamHack 강의에서 보시기 바랍니다. 그곳에는 예제까지 준비되어 있답니다^^

'Hacking-기초 > Reversing' 카테고리의 다른 글

Easy Crack [100pts]  (0) 2020.09.03
리버스 엔지니어링이란  (0) 2020.05.18

+ Recent posts