어셈블리어와 x86-64
어셈블리 언어
컴퓨터의 기계어와 치환되는 언어로 기계어가 여러 종류 있다면 어셈블리어도 그만큼 다양하게 존재한다.
ISA에는 IA-32, x86-64, ARM 등등 여러 종류가 있는데 이 종류만큼의 어셈블리어가 존재하는 것이다.
드림핵에서는 x86-64 어셈블리 언어만 다룬다.
x86-64 어셈블리 언어
기본 구조
명령어(Operation Code, Opcode) + 피연산자(Operand)
명령어
명령 코드 | |
데이터 이동(Data Transfer) | mov, lea |
산술 연산(Arithmetic) | inc, dec, add, sub |
논리 연산(Logical) | and, or, xor, not |
비교(Comparison) | cmp, test |
분기(Branch) | jmp, je, jg |
스택(Stack) | push, pop |
프로시져(Procedure) | call, ret, leave |
시스템 콜(System call) | syscall |
피연산자
피연산자에는 3가지 종류가 있다.
- 상수(Immediate Value)
- 레지스터(Register)
- 메모리(Memory)
메모리 피연산자는 []으로 둘러싸인 것으로 표현되며, 앞에 크기 지정자(Size Directive) TYPE PTR이 추가될 수 있다.
타입에는 BYTE, WORD, DWORD, QWORD가 올 수 있으며, 각각 1바이트, 2바이트, 4바이트, 8바이트의 크기를 나타낸다.
👇 메모리 피연산자의 예
x86-64 어셈블리 명령어
데이터 이동
어떤 값을 레지스터나 메모리에 옮기도록 지시하는 명령
예제 문제
산술 연산
덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시하는 명령어
예제 문제
논리 연산 - and & or
and, or 등의 비트 연산을 지시하는 명령어
예제 문제
논리연산 - xor & not
xor, neg 등의 비트 연산을 지시하는 명령어
예제 문제
비교
두 피연산자의 값을 비교하고, 플래그를 설정하는 명령어
분기
rip를 이동시켜 실행 흐름을 바꾸는 명령어
스택
스택을 조작하는 명령어
프로시저
프로시저(Procedure)는 특정 기능을 수행하는 코드 조각이다.
프로시저를 부르는 행위를 호출(Call), 프로시저에서 돌아오는 것을 반환(Return)이라고 부른다.
프로시저 호출 후에는 원래의 실행 흐름으로 돌아와야 하므로, 그 다음 실행할 명령어 주소가 필요하다.
call 다음의 명령어 주소(return address, 반환 주소)를 스택에 저장하고 프로시저로 rip를 이동시키면 된다.
→ 스택프레임의 할당과 해제: https://learn.dreamhack.io/63#6
→ ret(return address): 프로그램의 원래 실행 흐름 위치
시스템 콜
운영체제는 해킹으로부터 소프트웨어에 대한 권한을 보호하기 위해 커널 모드와 유저 모드로 권한을 나눈다.
커널 모드
운영체제가 전체 시스템을 제어하기 위해 시스템 소프트웨어에 부여하는 권한이다.
파일시스템, 입력/출력, 네트워크 통신, 메모리 관리 등 모든 저수준의 작업은 사용자 모르게 커널 모드에서 진행된다.
따라서 해커가 커널 모드까지 진입하게 되면 시스템은 거의 무방비 상태가 된다.
유저 모드
운영체제가 사용자에게 부여하는 권한이다.
유튜브를 시청, 게임, 프로그래밍을 하는 것 등은 모두 유저 모드에서 이루어진다. 리눅스에서 루트 권한으로 사용자를 추가하고, 패키지를 내려 받는 행위 등도 마찬가지다.
유저 모드에서는 해킹이 발생해도 해커가 유저 모드의 권한까지 밖에 획득하지 못하기 때문에 해커로 부터 커널의 막강한 권한을 보호할 수 있다.
시스템 콜(system call, syscall)
유저 모드에서 커널 모드의 시스템 소프트웨어에게 어떤 동작을 요청하기 위해 사용된다.
소프트웨어 대부분은 커널의 도움이 필요한데 도움이 필요하다는 요청을 시스템 콜이라고 한다.
ex) 사용자가 cat flag를 실행하면, cat은 flag라는 파일을 읽어서 사용자의 화면에 출력해야 한다. 이때 flag는 파일 시스템에 존재하므로 해당 파일을 읽기 위해선 파일시스템에 접근할 수 있어야 한다. 유저 모드에서는 이를 직접 할 수 없으므로 커널에 요청하여 해결한다. 이후 커널은 요청한 동작을 수행하여 유저에게 결과를 반환한다.
x64아키텍쳐에서는 시스템콜을 위해 syscall 명령어가 있다.
시스템 콜은 함수이기 때문에 필요한 기능과 인자에 대한 정보를 레지스터로 전달하면, 커널이 이를 읽어서 요청을 처리한다.
리눅스에서는 x64아키텍쳐에서 rax로 무슨 요청인지 나타내고, 아래의 순서대로 필요한 인자를 전달한다.
x64 syscall 테이블
syscall | rax | arg0 (rdi) | arg1 (rsi) | arg2 (rdx) |
read | 0x00 | unsigned int fd | char *buf | size_t count |
write | 0x01 | unsigned int fd | const char *buf | size_t count |
open | 0x02 | const char *filename | int flags | umode_t mode |
close | 0x03 | unsigned int fd | ||
mprotect | 0x0a | unsigned long start | size_t len | unsigned long prot |
connect | 0x2a | int sockfd | struct sockaddr * addr | int addrlen |
execve | 0x3b | const char *filename | const char *const *argv |
const char *const *envp
|
Quiz
1. rsi+rcx가 참조하는 메모리에서 1바이트를 가져온다.
2. 가져온 데이터를 0x30과 xor연산한다.
3. 연산한 값을 다시 데이터를 가져왔던 곳에 돌려놓는다. → rsi+rcx
4. rcx가 0x19보다 커질 때까지 반복한다.
결론 0x400000에서 0x40019까지 각 데이터에 0x30과 xor연산한다.
1. esi = 0xf, rdi = 0x400500
2. write_n 호출
3. [rbp-0x8]에 rdi값 넣기 → 0x400500
4. [rbp-0xc]에 esi값 넣기 → 0xf
5. rdx == 0
6. edx = 0xf
7. rsi = 0x400500
8. rdi = 0x1
9. rax = 0x1
10. syscall → write(0x1, 0x400500, 0xf)
결론 0x400500에서부터 문자 15개 출력.
:)
'Study > System Hacking' 카테고리의 다른 글
[dreamhack] Tool: pwntools (0) | 2022.03.27 |
---|---|
[dreamhack] Tool: gdb (0) | 2022.03.26 |
[dreamhack] Linux Memory Layout (0) | 2022.03.25 |
[dreamhack] Computer Architecture (0) | 2022.03.25 |
[lazenca] Protection Tech / RELRO & PIC & PIE (0) | 2021.02.22 |