- 코드 세그먼트
실행 가능한 기계 코드가 위치하는 영역, 읽기 권한과 실행 권한이 부여된다.- 데이터 세그먼트
컴파일 시점에 값이 정해진 전역 변수/상수들이 위치하는 영역, 읽기 권한이 부여된다.
- data 세그먼트 : 값이 변할 수 있는 데이터가 위치하는 영역
- rodata 세그먼트 : 값이 변하지 않는 데이터가 위치하는 영역
- BSS 세그먼트
컴파일 시점에 값이 정해지지 않은 전역 변수가 위치하는 영역, 읽기 권한이 부여된다.
프로그램이 시작될 때 모두 0으로 초기화된다.- 스택 세그먼트
함수의 인자, 지역 변수 등이 위치하는 영역, 읽기와 쓰기 권한이 부여된다.
스택 프레임이라는 단위로 사용된다. 함수가 호출될 때 생성, 반환될 때 해제- 힙 세그먼트
동적으로 할당된 데이터가 위치하는 영역, 읽기와 쓰기 권한이 부여된다.
C언어의 malloc(), calloc() 등을 호출해서 할당받는 메모리가 이 세그먼트에 위치
컴퓨터가 효율적으로 작동할 수 있도록 하드웨어 및 소프트웨어의 기능을 고안하고, 이들을 구성하는 방법
- 폰 노이만 구조
- 중앙처리장치 : 프로그램의 연산을 처리, 시스템을 관리
ALU, CU, Register- 기억장치 : 컴퓨터의 동작에 필요한 여러 데이터를 저장
주기억장치(RAM), 보조기억장치(SSD, HDD)- 버스 : 컴퓨터-컴퓨터, 부품-부품 사이에 신호를 전송하는 통로
데이터 버스, 주소 버스, 제어 버스, 랜선, 프로토콜, ...- 명령어 집합구조 - x86-64 아키텍처
x64 아키텍처 : 인텔의 64비트 CPU 아키텍처 (CPU가 한번에 처리할 수 있는 데이터의 크기)
- 범용 레지스터 : 다양한 용도로 사용되는 레지스터 - 64비트(32비트, ...)
- rax(eax, ax, ah, al) : 함수의 반환값
- rbx(ebx, bx, bh, bl)
- rcx(ecx, cx, ch, cl) : 반복문의 반복 횟수, 각종 연산의 시행 횟수
- rdx(edx, dx, dh, dl)
- rsi(esi, si) : 데이터 이동 시 원본 포인터
- rdi(edi, di) : 데이터 이동 시 목적지 포인터
- rsp(esp, sp) : 사용중인 스택 위치 포인터
- rbp(ebp, bp) : 스택 바닥 포인터
- 세그먼트 레지스터 : cs, ss, ds, es, fs, gs
fs에는 Stack Canary도 포함- 명령어 포인터 레지스터 : 어느 부분의 코드를 실행할 지 가리키는 포인터 (rip)
- 플래그 레지스터 : 프로세스의 현재 상태를 저장하는 레지스터
CF(Carry Flag), ZF(Zero Flag), SF(Sign Flag), OF(Overflow Flag)
명령어, 피연산자로 구성된다.
피연산자 : 상수, 레지스터, 메모리
메모리 피연산자는 []으로 둘러싸이고, 앞에 크기 지정자가 추가될 수 있다.
QWORD(8바이트), DWORD(4바이트), WORD(2바이트), BYTE(1바이트)
QWORD PTR [0x8048000] : 0x8048000의 데이터를 8바이트만큼 참조
데이터 이동
- mov : 어떤 값을 레지스터, 메모리에 옮기는 명령어
- lea : 유효 주소를 저장
산술 연산
- add, sub, inc(++), dec(--)
논리 연산 (비트 단위)
- and, or, xor, not
비교
- cmp : 두 피연산자를 빼서 비교
- test : 두 피연산자에 AND를 취해서 비교
분기
- jmp : rip 이동
- je : jump if equal (직전에 비교한 결과가 같았다면)
- jg : jump if greater
스택
- push : 스택 최상단에 데이터 저장
- pop : 스택 최상단의 값을 꺼내서 대입
프로시저
- call : 함수 호출
- leave : 스택프레임 정리
- ret : 반환주소로 반환
시스템 콜
- syscall : 함수
인자 순서 : rsi → rdi → rdx → rcx → r8 → r9 → 스택
syscall rax rdi rsi rdx read 0x00 fd buf count write 0x01 fd buf count open 0x02 filename flag mode close 0x03 fd evecve 0x3b filename ? ?
- start : 진입점부터 프로그램 분석
- break(b) : 중단점 지정
- continue(c) : 다음 중단점까지 실행
- run(r) : 단순 실행(중단점이 없다면 프로그램 끝까지)
- disassemble, u, disas, nearpc, pdisassemble : 함수 디스어셈블
- ni : next instruction, 다음 명령어 한줄 실행
- si : step into, 다음 명령어를 한줄 실행하되, 서브루틴 있다면 내부로 진입
- finish : 서브루틴에 진입했을 때 함수 끝까지 한 번에 실행
- x : 특정 주소에서 원하는 길이만큼의 데이터를 원하는 형식으로 인코딩
포맷 지정자 : o(8), x(16), d(10), i(instruction), s(string), ...
크기 지정자 : b(byte), h(halfword), w(word), g(giant, 8 bytes)- tele : 메모리 덤프 기능
- vmmap : 가상 메모리 레이아웃(매핑 영역까지)
- process : 로컬 바이너리를 대상으로 익스플로잇 (주로 디버깅)
- remote : 원격 서버를 대상으로 익스플로잇 (실제 공격)
- send : 데이터를 프로세스에 전송
- send('A') : "A"를 전송
- sendline('A') : "A\n" 를 전송
- sendafter('hello', 'A') : 바이너리가 "hello"를 출력하면 "A" 전송
- sendlineafter('hello', 'A') : 바이너리가 "hello"를 출력하면 "A\n" 전송
- recv : 프로세스에서 데이터를 수신
- recv(1024) : 최대 1024바이트까지 수신
- recvline() : 개행 문자를 만날 때까지 수신
- recvn(5) : 5바이트 수신
- recvuntil('hello') : "hello" 까지 수신
- recvall() : 전부 수신
- p64 : packing (p32, p16, p8) 리틀 엔디언의 바이트 배열로 변경
- u64 : unpacking (u32, u16, u8) 역의 과정
- interactive : 셸을 획득했거나 특정 상황에 직접 입력을 주고 싶을때 사용
- ELF : 바이너리의 ELF 헤더 정보 가져오기
e=ELF("./test") e.plt['read'] / e.got['read'] / ...
- context.log_level : 로깅 기능
context.log_level='error' : 에러만 출력 context.log_level='debug' : 오고 가는 모든 데이터 출력 context.log_level='info' : 비교적 중요한 정보들만 출력
- context.arch : 아키텍처 지정
amd64 (x86-64), i386(x86), arm(arm)- shellcraft : 셸코드 생성
- asm : 셸 코드를 기계어로 어셈블
함수의 호출 및 반환에 대한 약속
- x86 : cedcl, stdcall, fastcall, thiscall
- cdecl
- 레지스터의 수가 적어 스택을 통해 인자를 전달 (마지막부터 첫 번째까지 거꾸로 전달)
- 호출자가 스택을 정리
- x86-64 : System V AMD64 ABI / MS ABI의 Calling Convention
- System V AMD64 ABI의 Calling Convention
- rdi, rsi, rdx, rcx, r8, r9, 스택을 이용하여 인자를 전달
- 호출자가 스택을 정리
지역 변수가 선언되었더라도, 반환의 용도로만 사용되면 스택에 할당하지 않음
- 컴퓨터 시스템에서 프로그램들이 함수나 변수를 공유해서 사용할 수 있게 함
printf
,scanf
,malloc
등등의 C 함수들이 라이브러리에 포함
리눅스에는 C의 표준 라이브러리인libc
가 탑재되어 있음
- 호출된 함수와 실제 라이브러리의 함수가 연결되는 과정
- 동적 링크 (Dynamic Link)
- 동적 라이브러리가 프로세스의 메모리가 매핑
라이브러리의 함수를 호출하면 매핑된 라이브러리에서 호출할 함수의 주소를 찾고,
그 함수를 실행- 정적 링크 (Static Link)
- 바이너리에 정적 라이브러리의 모든 함수가 포함 (바이너리의 용량이 매우 커짐)
- 라이브러리에서 동적 링크된 심볼의 주소를 찾을 때 사용하는 테이블
라이브러리 함수를 호출하면, 함수의 이름을 바탕으로 심볼을 탐색하고
해당 함수의 정의를 발견하면 그 주소로 $rip을 이동 (runtime_resolve)
- 함수가 처음 호출되면 GOT에는 PLT 내부의 주소가 적혀 있는 상태
첫 호출 과정에서 runtime_resolve가 실행되면서, 해당 함수의 주소가 GOT에 써짐
이후에 그 함수를 호출하면 GOT에 써져 있는 주소를 바탕으로 함수가 바로 실행됨- PLT : 라이브러리의 심볼 주소
- GOT : 바이너리에서 매핑된 함수 주소
- 함수가 여러 번 호출될 때, GOT의 값을 검증하지 않으므로 보안상의 취약점이 존재
→ 어떤 함수의 GOT를 공격자가 원하는 코드가 실행되게끔 변경할 수 있음 (GOT Overwrite)
익스플로잇 : 상대 시스템을 공격하는 것
셸코드 : 익스플로잇을 위해 제작된 어셈블리 코드 조각 (일반적으로 셸을 얻기 위함)
- orw 셸코드 (open-read-write)
파일을 열고, 읽고, 출력해주는 셸코드- execve 셸코드
임의의 프로그램을 실행하는 코드
/bin/sh 를 실행하면 셸을 획득할 수 있다.
스택의 버퍼에서 발생하는 오버플로우
버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조되어 문제가 발생할 수 있다.
scanf, read, gets 등에서 발생할 수 있다.
- Brute Force (무차별 대입)
- TLS 접근
셸 코드와 Return Address Overwrite를 이용하여 셸 획득
버퍼에 셸 코드를 넣고, Return Address를 버퍼로 덮어쓰기
- 실행 권한이 남아있는 코드 영역(바이너리의 코드 영역, 라이브러리의 코드 영역)으로
반환 주소를 덮는 공격 기법- ASLR이 적용되어 있더라도 PIE가 적용되어 있지 않다면 PLT의 주소는 고정되므로
라이브러리의 베이스 주소를 몰라도 라이브러리 함수를 실행할 수 있음- Return gadget을 이용 (
ret
로 끝나는 어셈블리 코드 조각을 계속 연결)- 64비트의 경우 가젯-인자-함수 순으로 배치
- 가젯은 ROPgadget을 이용하여 찾을 수 있음
- 함수의 프롤로그에서 스택 버퍼와 반환 주소 사이에 임의의 값을 삽입하고, 함수의 에필로그에서 해당 값의 변조를 확인하는 보호 기법
- 변조가 확인되면 프로세스를 강제로 종료 (
stack smashing detected
)- x86-64 아키텍처에서 Stack Canary 값은
fs:0x28
의 값
- 카나리 값은 프로세스가 시작될 때 TLS에 전역 변수로 저장되고, 각 함수마다 이 값을 참조
- ASLR
- 바이너리가 실행될 때마다 스택, 힙, 공유 라이브러리 등을 임의의 주소에 할당하는 보호 기법
- 커널에서 지원하는 보호 기법
cat /proc/sys/kernel/randomize_va_space
로 적용 여부 확인 가능
0(적용하지 않음), 1(스택, 힙, 라이브러리, vdso 등), 2(1 + brk로 할당한 영역)
brk : 데이터 세그먼트의 영역을 확장시켜 주는 syscall- main 함수를 제외한 다른 영역의 주소들은 실행할 때마다 바뀜
- 라이브러리 매핑 주소로부터 다른 심볼들까지의 오프셋은 항상 동일
- NX
- 실행에 사용되는 메모리 영역과 쓰기에 사용되는 메모리 영역을 분리하는 보호 기법
- NX가 적용되면 코드 영역 외에는 실행 권한이 부여되지 않음
→ Return to Shellcode와 같은 공격 기법을 사용하지 못함(스택에 실행권한이 없음)checksec
을 이용해 적용 여부 확인 가능(컴파일러 옵션에 의해 적용)