혜랑's STORY

[System Hacking STAGE 2] x86 Assembly🤖: Essential Part(1) 본문

무지성 공부방/Dreamhack SystemHacking

[System Hacking STAGE 2] x86 Assembly🤖: Essential Part(1)

hyerang0125 2022. 1. 20. 12:41
시작하며

1. 해커들의 언어: 어셈블리

 시스템 해커가 가장 기본적으로 습득해야 하는 지식은 컴퓨터 언어와 관한 것이다. 왜냐하면, 컴퓨터의 언어로 작성된 소프트웨어에서 취약점을 발견해야 하기 때문이다.

 그런데 컴퓨터의 언어인 기계어는 우리의 일상 언어와 너무나 다르다. 기계어의 경우 0과 1로만 구성돼 있어, 우리가 한 눈에 이해하기 매우 어렵다. 그래서 컴퓨터 과학자 중 한명인 David WheelerEDSAC을 개발하면서 어셈블리 언어(Assembly Language)와 어셈블러(Assembler)라는 것을 고안했다.

 어셈블러는 개발자들이 어셈블리어로 코드를 작성하면 컴퓨터가 이해할 수 있는 기계어로 코드를 치환한다. 그런데 소프트웨어를 역분석하는 사람들은 여기에 역발상을 더해, 기계어를 어셈블리 언어로 번역하는 역어셈블리어(Disassembler)를 개발했다. 이로 인해 소프트웨어 분석다들은 소프트웨어를 분석하려고 기계어를 읽을 필요가 없어졌다.

2. x64 어셈블리 언어

기본 구조

 x64 어셈블리 언어는 명령어(Operation Code, Opcode)와 목적어에 해당하는 피연산자(Operand)로 구성된다.

명령어

학습할 명령어들은 위와 같이 분류할 수 있다.

피연산자

 피연산자에는 총 3가지 종류가 올 수 있다.

  • 상수(Immediate Value)
  • 레지스터(Register)
  • 메모리(Memory)

 메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다. 여기서 타인에는 BYTE(1바이트), WORD(2바이트), DWORD(4바이트), QWORD(8바이트)가 올 수 있다.

메모리 피연산자의 예

3. x86-64 어셈블리 명령어

데이터 이동

 데이터 이동 명령어는 어떤 값을 레지스터나 메모리에 옮기도록 지시한다.

예제
[Register]
rbx = 0x401A40
=================================
[Memory]
0x401a40 | 0x0000000012345678
0x401a48 | 0x0000000000C0FFEE
0x401a50 | 0x00000000DEADBEEF
0x401a58 | 0x00000000CAFEBABE
0x401a60 | 0x0000000087654321
=================================
[Code]
1: mov rax, [rbx+8]
2: lea rax, [rbx+8]
  • Code를 1까지 실행했을 때, rax에 저장된 값은 0xCOFFEE이다.
  • Code를 2까지 실행했을 때, rax에 들어있는 값은 0x401A48이다.

산술 연산

 산술 연산 명령어는 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다. 곱셈과 나눗셈은 여기서 설명하지 않는다.

예제
[Register]
rax = 0x31337
rbx = 0x555555554000
rcx = 0x2
=================================
[Memory]
0x555555554000| 0x0000000000000000
0x555555554008| 0x0000000000000001
0x555555554010| 0x0000000000000003
0x555555554018| 0x0000000000000005
0x555555554020| 0x000000000003133A
==================================
[Code]
1: add rax, [rbx+rcx*8]
2: add rcx, 2
3: sub rax, [rbx+rcx*8]
4: inc rax
  • Code 1까지 실행했을 때, rax에 저장된 값은 0x3133A이다.
    • rax의 값은 rbx+0x10(0x555555554010)에 저장된 0x3 만큼 증가한다.
  • Code 3까지 실행했을 때, rax에 저장된 값은 0이다.
    • rax의 값은 rbx+0x20에 저장된 0x3133A 만큼 감소한다.
  • Code 4까지 실행했을 때, rax에 저장된 값은 1이다.
    • rax의 값은 1증가한다.

논리 연산

 논리 연산 명령어는 and, or, xor, neg 등의 비트 연산을 지시한다. 이 연산은 비트 단위로 이루어 진다.

예제
[Register]
rax = 0xffffffff00000000
rbx = 0x00000000ffffffff
rcx = 0x123456789abcdef0
==================================
[Code]
1: and rax, rcx
2: and rbx, rcx
3: or rax, rbx
  • Code 1까지 실행했을 때, rax에 저장된 값은 0x1234567800000000 이다.
  • Code 2까지 실행했을 때, rbx에 저장된 값은 0x000000009abcdef0 이다.
  • Code 3까지 실행했을 때, rax에 저장된 값은 0x123456789abcdef0 이다.

예제
[Register]
rax = 0x35014541
rbx = 0xdeadbeef
==================================
[Code]
1: xor rax, rbx
2: xor rax, rbx
3: not eax
  • Code 1까지 실행했을 때, rax에 저장된 값은 0xebacfbae 이다.
  • Code 2까지 실행했을 때, rax에 저장된 값은 0x35014541 이다.
    • xor 연산을 동일한 값으로 두 번 실행할 경우, 원래 값으로 돌아간다.
  • Code 3까지 실행했을 때, rax에 저장된 값은 0xcafebabe 이다.

비교

 비교 명령어는 두 피연산자의 값을 비교하고, 플래그를 설정한다.

분기

분기 명령어는 rip를 이동시켜 실행 흐름을 바꾼다. 아래서 소개될 것 이외에 더 많은 분기문이 존재하나 몇 개만 살펴보면 이름을 통해 직관적으로 의미를 파악할 수 있기 때문에, 실제 코드를 분석하며 감을 익혀나가 보자.