[Dreamhack] System Hacking 기초 강의 수강하면서 개인적으로 공부한 내용입니다.
b: break
c: continue (이어서)
r: run (처음부터)
si: step into (함수 내부로 진입)
ni: next instruction (함수 내부로 진입 X)
finish: 함수의 끝까지 실행
x: examine (메모리의 임의 값 관찰)
x/{size}{format} {location}
i: info
k: kill
pd: pdisas
[pwndbg]
entry: _start
함수에 bp를 걸어주는 명령어
u: pwndbg에서 제공하는 디스어셈블 명령어(예쁘게 보여줌)
tele: telescope (특정 주소의 메모리 값 + 메모리가 참조하고 있는 주소를 재귀적으로 보여줌)
vmmap: 가상 메모리의 레이아웃을 보여주는 명령어. 어떤 라이브러리가 어떤 주소에 매핑되어 있는지 알 수 있음
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비트 값을 레지스터에 쓰는 명령어checksec.sh --file ./file
(checksec.sh 다운로드 및 환경변수 설정 필요)no-pie
옵션: gcc 실행 시에 해당 옵션으로 PIE 활성화 여부를 결정할 수 있음# 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는 기계어 형태
int main(void)
{
int ret = foo(2, 3);
return 0;
}
int foo(int a, int b)
{
return a + b;
}
CDECL(C Declaration, x86)
_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)
_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)
_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개 이상 이용한 경우 여기에서 스택 정리
int 0x80
명령어를 통해 system call 호출syscall
명령어를 통해 system call 호출호출 파라미터는 [참고] (너무 잘 정리되어 있음)
int fd = open(“/tmp/flag”, O_RDONLY, NULL)
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)
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 // 쉘코드 추출