어셈블리어와 x86-64
(1) 어셈블리 언어
기계어와 치환되는 언어로, CPU에 사용되는 ISA (명령어 집합구조)의 종류만큼 많은 수의 어셈블리어가 존재한다.
(2) x86-64 어셈블리
✅ 기본구조
: x64 어셈블리 언어는 우리가 사용하는 한국어보다는 훨씬 단순한 문법 구조를 지닌다.
▶ 동사에 해당하는 명령어(Opcode), 목적어에 해당하는 피연산자(Operand)로 구성된다.
✅ 명령어
- 데이터 이동 : mov, lea
- 산술 연산 : inc, dec, add, sub
- 논리 연산 : and, or, xor, not
- 비교 : cmp, test
- 분기 : jmp, je, jg
- 스택 : push, pop
- 프로시져 : call, ret, leave
- 시스템 콜 : syscall
(3) 피연산자
✅ 피연산자의 종류
- 상수
- 레지스터
- 메모리
✅ 메모리 피연산자 타입
- BYTE (1바이트)
- WORD (2바이트)
- DWORD (4바이트)
- QWORD (8바이트)
x86-64 어셈블리 명령어
(1) 데이터 이동
: 어떤 값을 레지스터나 메모리에 옮기도록 지시함.
📌 mov dst, src : src에 들어있는 값을 dst에 대입
- mov rdi, rsi : rsi의 값을 rdi에 대입
- mov QWORD PTR[rdi], rsi : rsi의 값을 rdi가 가리키는 주소에 대입
- mov QWORD PTR[rdi+8*rcx], rsi : rsi의 값을 rdi+8*rcs를 rsi에 대입
📌 lea dst, src : src의 유효 주소를 dst에 저장
- lea rsi, [rdx+8*rcx] : rbx+8*rcx를 rsi에 대입
(2) 산술연산
📌 add dst, src : dst에 src의 값을 더함
- add eax, 3 : eax += 3
- add ax, WORD PTR[rdi] : ax += *(WORD *)rdi
📌 sub dst, src: dst에서 src의 값을 뺌
- sub eax, 3 : eax -= 3
- sub ax, WORD PTR[rdi] : ax -= *(WORD *)rdi
📌 inc op: op의 값을 1 증가시킴
- inc eax : eax += 1
📌 dec op의 값을 1 감소시킴
- dec, eax : eax -= 1
(3) 논리 연산
📌 and & or
- and dst, src: dst와 src의 비트가 모두 1이면 1, 아니면 0
- or dst, src: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
📌 xor & not
- xor dst, src: dst와 src의 비트가 서로 다르면 1, 같으면
- not op: op의 비트 전부 반전
(4) 비교/분기
✅ 비교
: 두 피연산자의 값을 비교하고, 플래그를 설정
1. cmp op1, op2 : op1과 op2를 비교
▶ cmp는 두 피연산자를 빼서 대소를 비교하고, 연산의 결과는 op1에 대입하지 않는다.
1: mov rax, 0xA
2: mov rbx, 0xA
3: cmp rax, rbx ; ZF=1
2. test op1, op2: op1과 op2를 비교
▶ test는 두 피연산자에 AND 비트연산을 취하고, 연산의 결과는 op1에 대입하지 않는다.
1: xor rax, rax
2: test rax, rax ; ZF=1
✅ 분기
: 분기 명령어는 rip을 이동시켜 실행 흐름을 바꿀 수 있음
1. jmp addr: addr로 rip를 이동시킴
1: xor rax, rax
2: jmp 1 ; jump to 1
2. je addr: 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
1: mov rax, 0xcafebabe
2: mov rbx, 0xcafebabe
3: cmp rax, rbx ; rax == rbx
4: je 1 ; jump to 1
3. jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)
1: mov rax, 0x31337
2: mov rbx, 0x13337
3: cmp rax, rbx ; rax > rbx
4: jg 1 ; jump to 1
(5) 스택
1. push val : val을 스택 최상단에 쌓음
[ 예제 ]
[Register]
rsp = 0x7fffffffc400
[Stack]
0x7fffffffc400 | 0x0 <- rsp
0x7fffffffc408 | 0x0
[Code]
push 0x31337
[ 결과 ]
[Register]
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0
2. pop reg : 스택 최상단의 값을 꺼내서 reg에 대입
[ 예제 ]
[Register]
rax = 0
rsp = 0x7fffffffc3f8
[Stack]
0x7fffffffc3f8 | 0x31337 <- rsp
0x7fffffffc400 | 0x0
0x7fffffffc408 | 0x0
[Code]
pop rax
[ 결과 ]
[Register]
rax = 0x31337
rsp = 0x7fffffffc400
[Stack]
0x7fffffffc400 | 0x0 <- rsp
0x7fffffffc408 | 0x0
(6) 프로시저
컴퓨터 과학에서 프로시저는 특정 기능을 수행하는 코드 조각으로, 반복되는 연산을 프로시저 호출로 대체할 수 있어 전체 코드의 크기를 줄일 수 있으며, 코드의 가독성을 높일 수 있다.
- 호출(Call) : 프로시저를 부르는 행위
- 반환(Return) : 프로시저에서 돌아오는 것
- 반환 주소(Return Address) : call 다음의 명령어 주소
[ 프로시저 명령어 ]
1. call addr : addr에 위치한 프로시져 호출
📌예제
[Register] rip = 0x400000 rsp = 0x7fffffffc400 [Stack] 0x7fffffffc3f8 | 0x0 0x7fffffffc400 | 0x0 <- rsp [Code] 0x400000 | call 0x401000 <- rip 0x400005 | mov esi, eax ... 0x401000 | push rbp
📌 결과
[Register] rip = 0x401000 rsp = 0x7fffffffc3f8 [Stack] 0x7fffffffc3f8 | 0x400005 <- rsp 0x7fffffffc400 | 0x0 [Code] 0x400000 | call 0x401000 0x400005 | mov esi, eax ... 0x401000 | push rbp <- rip
2. leave: 스택프레임 정리
📌 예제
[Register] rsp = 0x7fffffffc400 rbp = 0x7fffffffc480 [Stack] 0x7fffffffc400 | 0x0 <- rsp ... 0x7fffffffc480 | 0x7fffffffc500 <- rbp 0x7fffffffc488 | 0x31337 [Code] leave
📌 결과
[Register] rsp = 0x7fffffffc488 rbp = 0x7fffffffc500 [Stack] 0x7fffffffc400 | 0x0 ... 0x7fffffffc480 | 0x7fffffffc500 0x7fffffffc488 | 0x31337 <- rsp ... 0x7fffffffc500 | 0x7fffffffc550 <- rbp
3. ret : return address로 반환
📌 예제
[Register] rip = 0x401008 rsp = 0x7fffffffc3f8 [Stack] 0x7fffffffc3f8 | 0x400005 <- rsp 0x7fffffffc400 | 0 [Code] 0x400000 | call 0x401000 0x400005 | mov esi, eax ... 0x401000 | mov rbp, rsp ... 0x401007 | leave 0x401008 | ret <- rip
📌 결과
[Register] rip = 0x400005 rsp = 0x7fffffffc400 [Stack] 0x7fffffffc3f8 | 0x400005 0x7fffffffc400 | 0x0 <- rsp [Code] 0x400000 | call 0x401000 0x400005 | mov esi, eax <- rip ... 0x401000 | mov rbp, rsp ... 0x401007 | leave 0x401008 | ret
💡 스택프레임이란?
함수별로 서로가 사용하는 스택의 영역을 명확히 구분하기 위해 스택프레임이 사용된다. 대부분의 Application binary interface (ABI) 에서는 함수는 호출될 때 자신의 스택프레임을 만들고, 반환할 때 이를 정리한다.
(7) 시스템 콜
1. 커널 모드
- 운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한
- 파일시스템, 입력/출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행
- 커널 모드에서는 시스템의 모든 부분을 제어할 수 있기 때문에, 해커가 커널 모드까지 진입하게 되면 시스템은 거의 무방비 상태가 됨
2. 유저 모드
- 운영체제가 사용자에게 부여하는 권한
- 유저 모드에서는 해킹이 발생해도, 해커가 유저 모드의 권한까지 밖에 획득하지 못하기 때문에 해커로 부터 커널의 막강한 권한을 보호할 수 있음
3. 시스템 콜
- 유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용
- 특정 명령어를 사용하여 파일 시스템에 접근하고자 할 때, 시스템 콜을 사용하여 도움을 요청해줄 수 있음
📌 예제
[Register] rax = 0x1 rdi = 0x1 rsi = 0x401000 rdx = 0xb [Memory] 0x401000 | "Hello Wo" 0x401008 | "rld" [Code] syscall
📌 결과
Hello World
출처 : 드림핵 (https://dreamhack.io/lecture/roadmaps/2)
'Security > System Hacking' 카테고리의 다른 글
[Tool] gdb (0) | 2024.09.09 |
---|---|
[Dreamhack] ssp_001 (0) | 2024.09.01 |
[Dreamhack] ssp_000 write-up (2) | 2024.09.01 |
[Background] Linux Memory Layout (0) | 2024.09.01 |
[Background] Computer Architecture (1) | 2024.09.01 |