본문 바로가기
hacking/pwnable

[Dream hack] x86 Assembly🤖: Essential Part(1)

by ilp 2024. 5. 1.
반응형

서론

  • 시스템 해커의 기본 지식
    • 컴퓨터 언어, 운영체제, 네트워크, 암호학
      그 중에서도 컴퓨터 언어는 가장 기본적으로 습득해야 하는 지식이다.
    • 시스템 해커는 컴퓨터의 언어로 작성된 소프트웨어에서 취약점을 발견해야 하기 때문이다.
  • 기계어(Machine Code)
    • 컴퓨터 속에는 하나의 거대한 세계가 있따. 복잡한 논리적 인과관계가 존재하고, 여러개채가 상호작용하는데
    • 그 세계에서는 기계어가 통용 된다. (해커는 그 세계의 허점을 공격하여 시스템을 장악한다.
    • 0과 1로만 구성돼 있어서, 이해하기 어렵다.
  • David Wheeler
    • EDSAC을 개발하면서어셈블리 언어(Assembly Language), 어셈블러(Assembler) 를 고안했따.
    • 어셈블리어로 코드를 작성하면 컴퓨터가 이해할 수 있는 기계어로 코드를 치환해주었다.
      어셈블리어가 기계어보다 이해하기 훨씬 쉬웠기떄문에 개발자들이 편리하게 개발할 수 있었다.
  • 역어셈블러(Disassembler)
    • 소프트웨어를 역분석하는 사람들이 개발했다.
    • 기계어를 어셈블리 언어로 번역하는것 이다.
      기계어로 구성된 소프트웨어를 어셈블리 코드로 번역해준다.

어셈블리 언어🤖

  • 어셈블리 언어 🤖
    • 컴퓨터의 기계어와 치환되는 언어이다. 기계어가 여러 종류라면 어셈블리어도 여러 종류여야 한다.
    • 명령어 집합구조를 설명했을때 CPU에 사용되는 명령어 집합 구조는 여러 종류가 있다.
      (IA-32, x86-64, ARM, MIPS)

x64 어셈블리 언어

  • 기본 구조🧱
    • x64 어셈블리 언어는 단순한 문법 구조를 가진다.
    • 동사에 해당하는 명령어(Operation Code, Opcode) 와 목적어에 해당하는 피연산자(Operand)로 구성된다.


명령어🔫

  • x64에는 많은 명령어가 존재한다. 그중에서도 중요한 21개의 명령어를 자세히 학습한다.

  • 피연산자 🎯
    • 3가지 종류가 올 수 있다.
      • 상수(Immediate Value)
      • 레지스터(Register)
      • 메모리(Menory)
    • 메모리 피연산자는 []으로 둘러싸이게 표현한다, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다.
    • BYTE,WORD, DWORD, QWORD 가 올 수 있고 각각, 1바이트, 2바이트, 4바이트, 8바이트의 크기이다.


🐦자료형 WORD의 크기가 2바이트인 이유

  • 초기에는 WORD의 크기가 16비트인 IA-16 아키텍처를 개발했었다, CPU에서 WORD는 16비트연기 떄문에 어셈블리어에서도 16비트로 정의하는 것이 자연스러웠다.
  • 이후에 IA-32, x86-64 아키텍처는 CPU의 WORD가 32,64비트로 확장됐따. 그래서 이둘의 아키텍처에선 WORD의 자료형이 32,64비트의 크기가 자연스럽다.
  • 하지만 인텔은 WORD 자료형의 크기를 변경하면 기존 프로그램들이 새로운 아키텍쳐와 호환되지 않을 수 있어서 WORD의 크기를 16비트로 유지하고, DWORD(Double Word, 32bit)와  QWORD(Quad Word, 64bit)를 추가했다.

x86-64 어셈블리 명령어

  • 데이터 이동 🚚
    • 어떤 값을 레지스터나 메모리에 옮기도록 지시한다.
mov dst, src  :src 에 들어있는 값을 dst에 대입(src의 값이 없어지지 않는다.)
mov rdi, rsi rsi의 값을 rdi에 대입
mov QWORD PTR[rdi], rsi rsi의 값을 rdi가 가리키는 주소에 대입
mov QWORD PTR[rdi+8*rcx], rsi rsi의 값을 rdi+8*rcx가 가리키는 주소에 대입
lea dst, src  :src의 유효주소(Effective Address, EA)dst에 저장한다.
lea rsi, [rbx+8*rcx] rbx+8*rcx를 rsi에 대입

  • 산술 연산
    • 덧셈, 뺄셈, 곱셈, 나눗셈연산을 지시한다. (곱셈과 나눗셈은 여기서 설명하지 않는다.)
add dst, src  :dstsrc의 값을 더한.
add eax, 3 eax += 3
add ax, WORD PTR[rdi] eax += 3
sub dst, str  :dst에서 src의 값을 뺍니다.
sub eax, 3 eax -= 3
sub ax, WORD PTR[rdi] ax -= *(WORD*)rdi
inc op: op  : op의 값을 1 증가
inc eax eax +=1
dec op :op의 값을 1 감소 시킴
dec eax eax -= 1

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

and dst, src: dst와 src의 비트가 모두 1이면 1, 아니면 0

[Register]
eax = 0xffff0000
ebx = 0xcafebabe

[Code]
and eax, ebx

[Result]
eax = 0xcafe0000

 

or dst, src: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0

[Register]
eax = 0xffff0000
ebx = 0xcafebabe

[Code]
or eax, ebx

[Result]
eax = 0xffffbabe

  • 논리연산 🤔 -xor & not

xor dst, src: dst와 stc의 비트가 서로 다르면 1, 같으면 0

[Register]
eax = 0xffffffff
ebx = 0xcafebabe

[Code]
xor eax, ebx

[Result]
eax = 0x35014541

 

not op: op의 비트 전부 반전

[Register]
eax = 0xffffffff

[Code]
not eax

[Result]
eax = 0x00000000

비교 ⚖️

  • 비교 명령어는 두 피연사자의 값을 비교하고 플래그를 설정한다.
  • cmp op1, op2: op1과 op2를 비교
    • cmp는 두 피연산자를 빼서 대소를 비교한다. 연산의 결과는 op1에 대입하지 않는다.
[Code]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
// 예를 들어, 서로 같은 두 수를 뺴면 결과가 0이 뙤어 ZF플래그가 설정되는데,
// 이후에 cpu는 이 플래그를 보고 두 값이 같았는지 판단할 수 있다.
  • test op1, op2: op1과 op2를 비교
    • test는 두 피연산자에 AND 비트연산을 취한다. 연산의 결과는 op1에 대입하지 않는다.
[Code]
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
// 예를 들어, 서로 같은 두 수를 뺴면 결과가 0이 뙤어 ZF플래그가 설정되는데,
// 이후에 cpu는 이 플래그를 보고 두 값이 같았는지 판단할 수 있다.

분기🔀

  • 분기 명령어는 rip를 이동시켜 실행 흐름을 바꾼다.
    • 분기문은 여기 소개된 것 외에 굉장히 많은 수가 존재한다. 그러나 몇개만 살펴보면 이름에서 직관적으로 의미를 파악할 수 있기 떄문에, 이를 전부 다루기 보단, 앞으로 코드를 분석하면서 배울 수 있또록 한다.
  • jmp addr: addr로 rip를 이동한다.
[Code]
1: xor rax, rax
2: jmp 1 ; jump to 1
  • je addr: 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
[Code]
1: mov rax, 0xcafebabe
2: mov rbx, 0xcafebabe
3: cmp rax, rbx ; rax == rbx
4: je 1 ; jump to 1
  • jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프(jump if greater)
[Code]
1: mov rax, 0x31337
2: mov rbx, 0x13337
3: cmp rax, rbx ; rax > rbx
4: jg 1  ; jump to 1

결론

  1. 데이터 이동 연산자
    • mov dst, src: src의 값을 dst에 대입
    • lea dst, src: src의 유효 주소를  dst에 대입
  2. 산술 연산
    • add dst, src: src의 값을 dst에 더함
    • sub dst, src: src의 값을 dst에서 뺌
    • inc op: op의 값을 1 더함
    • dec op: op의 값을 1뺌
  3. 논리연산
    • and dst, src: dst와 src가 모두 1이면 1, 아니면 0
    • or dst, src: dst와 src중 한 쪽이라도 1이면 1, 아니면 0
    • xor dst, src: dst와 src가 다르면 1, 같으면 0
    • not op: op의 비트를 모두 반전
  4. 비교
    • cmp op1, op2: op1에서 op2를 뺴고 플래그를 설정
    • test op1, op2: op1과 op2에 AND 연산을 하고, 플래그를 설정
  5. 분기
    • jmp addr: addr로 rip이동
    • je addr: 직전 비교에서 두 피연산자의 값이 같을 경우 addr로 rip 이동
    • jg addr:  직전 비교에서 두 피연산자 중 전자의 값이 더 클 경우 addr 로 rip이동

반응형