환경은 libc 2.29
이기 때문에 ubuntu 20.04
에서 실습하였다.
heap 문제가 partial relro 이다. 이건 귀하군요... 그리고 푼사람도 점수에 비해 매우 적다.
이 문제를 풀기에 앞서 2.29 이상에서 tcache의 변경점을 간단하게 정리할 필요가 있다.
변경점이라고 해봤자 double free가 안된다는 것이다. size검사 안하는건 똑같다. 2.27에선 아주 좋은 놈이였지만 2.29 이상에서는 해당사이즈의 tcache 전체를 돌면서 검사를 하기 때문에 fastbin 과 같은 double free도 안된다. 그러나 우회 방법이 있다.
1. free 된 청크의 bk를 변조하기
2. 사이즈를 변조하기이다.
3. fastbin 쓰기
4. tcache와 fastbin에 동시에 free 하기이다.
잠시 free 코드를 살펴보도록 하겠다.
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);
/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif
e->key==tcache일 경우에만 저 double free 검사를 하게되는데 이 tcache_entry
라는 구조체는 다음과 같이 생겼다.
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;
즉 bk 부분이라는 소리다. key는 free되면 tcache arena 부분이 들어가는데, 저 key 부분만 바꿔주면 double free check를 하지 않게 된다.
그리고 코드를 보면 free 할 청크의 size만 가져와서 해당 size에 해당하는 tcache bin을 검사하기 때문에 size를 바꿔버리면 다른 사이즈의 bin에서 검사를 하기 때문에 우회가 가능하다. ( 그런데 size 변조가 가능하다는건 heap에서 할 수 있는 거의 모든게 가능한거니까..)
(참고 : https://jjy-security.tistory.com/10 )
이쯤이면 tcache는 됐고 문제를 살펴보도록 하자.
크게 allocate, realloc, free 로 이루어져 있다.
Allocate
int allocate()
{
_BYTE *v0; // rax
unsigned __int64 index; // [rsp+0h] [rbp-20h]
unsigned __int64 size; // [rsp+8h] [rbp-18h]
void *ptr; // [rsp+18h] [rbp-8h]
printf("Index:");
index = read_long();
if ( index > 1 || heap[index] )
{
LODWORD(v0) = puts("Invalid !");
}
else
{
printf("Size:");
size = read_long();
if ( size <= 0x78 )
{
ptr = realloc(0LL, size);
if ( ptr )
{
heap[index] = ptr;
printf("Data:");
v0 = (char *)heap[index] + read_input((__int64)heap[index], size);
*v0 = 0; // off - by -null
}
else
{
LODWORD(v0) = puts("alloc error");
}
}
else
{
LODWORD(v0) = puts("Too large!");
}
}
return (int)v0;
}
(헤더포함) 0x80 이하 사이즈의 heap을 할당 받고 쓸 수 있다. 인덱스는 0,1만 가능하다.
read_input 함수는 read함수를 통해 읽은 바이트를 반환하는데 *v0=0; 부분에서 off by null이 발생한다.
Free
int rfree()
{
void **v0; // rax
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
printf("Index:");
v2 = read_long();
if ( v2 > 1 )
{
LODWORD(v0) = puts("Invalid !");
}
else
{
realloc(heap[v2], 0LL);
v0 = heap;
heap[v2] = 0LL;
}
return (int)v0;
}
free하는데 특이하게 realloc을 통해 free를 한다. realloc의 size가 0이면 free와 동일하게 동작한다(고 man 페이지에 적혀있다). 그리고 포인터를 0으로 초기화해서 double free는 불가능하다.
Reallocate
int reallocate()
{
unsigned __int64 index; // [rsp+8h] [rbp-18h]
unsigned __int64 size; // [rsp+10h] [rbp-10h]
void *v3; // [rsp+18h] [rbp-8h]
printf("Index:");
index = read_long();
if ( index > 1 || !heap[index] ) // index : 0,1
return puts("Invalid !");
printf("Size:");
size = read_long();
if ( size > 0x78 )
return puts("Too large!");
v3 = realloc(heap[index], size);
if ( !v3 )
return puts("alloc error");
heap[index] = v3;
printf("Data:");
return read_input((__int64)heap[index], size);
}
사이즈 받아서 realloc 해준다. 이미 할당된 놈만 변경 가능하다. 그리고 size도 0x78초과해서 줄 수 없다. 여기서 취약점 하나가 발생하는데 size가 0 인것을 검사하지 않는다. 말했지만 realloc(ptr, 0) 은 free와 동일하게 작동한다.
Vulnerability
Reallocate 함수에서 free를 하면 double free가 가능하다! + allocate 함수의 off-by-null
그런데 갑자기 궁금해졌다. free된 놈을 realloc하면 어떻게 되는지. 그래서 간단한 프로그램을 만들어서 테스트 해보았다.
#include <stdio.h>
#include <stdlib.h>
int main()
{
setbuf(stdin,0);
setbuf(stdout,0);
int d;
char *ptr=(char*)malloc(0x40);
printf("malloc chunk : %p\n",ptr);
char *temp=malloc(0x20);
free(temp);
free(ptr);
printf("size : ");
scanf("%d",&d);
ptr=realloc(ptr,d);
printf("realloc chunk : %p\n",ptr);
getchar();
}
놀랍게도 unsorted bin 처럼 청크가 분할되어서 0x31 청크가 할당되어있다. 그리고 bins에도 0x50짜리 청크가 안사라지고 남아있다는 것이 중요한점.
대강 정리를 하자면 realloc이 작동을 한다는 것을 알 수 있다. 기존 할당된 사이즈보다 큰 것을 주면 기존에 free가 되있기 때문에 double free error가 나게 되지만 , 기존에 할당된 사이즈보다 작은 사이즈를 주면 unsorted bin처럼 분할해서 청크를 제공한다. 그러나 기존에 free 됬던 청크는 bin에 계속 남아있다.
상당히 재미있는 과정이다.
시나리오
상당히 이상한 방법으로 푼 것 같다. 일단 위 과정들을 이용해서 got를 덮어주었다. 덮는 과정은 패스한다. 머리를 잘 굴리면 쉽게 가능하다.
위 사진과 같이 atoll을 scanf로 stack check fail은 leave ret으로 덮었다.
(atoi를 system으로 덮는게 이상적이지만 내 로컬환경이 이상한건지 system("/bin/sh") 실행이 잘 안되서.. 원가젯을 이용하기로 했다. )
위 사진은 atoll이 호출될때의 인자들인데 rdi와 rsi가 같다. 이를 이용해서 atoll을 scanf로 덮으면 bof가 가능한 것을 이용했다. canary가 걸려있기 때문에 __stack_chk_fail을 leave ret으로 덮어 의미가 없게 만들었다.
이후에는 ROP가 가능하게 되는데, 한번 leak을 하고 다시 돌아와서 scanf를 다시 받을때 오류가 나서 ret을 페이로드에 좀 넣어줘서 rsp를 올리고 나니까 잘 되었다. 그런데 system("/bin/sh") 를 페이로드 구성하면 이유는 모르는 세그폴트가 나서 one gadget을 이용했다.
원가젯 조건이 괴랄하기 때문에 libc 가젯들 이용해서 맞춰주고 실행했다.
좀 삽질을 많이 한 것 같다.
풀페이로드는 공개하지 않겠다. 아마도 내 방법보다 더 좋고 간편한 방법이 있을 것이다. 그러나 이런 방법도 가능하다~ 정도만 알아두면 좋겠다.
tw 규칙을 보니까 전체 페이로드를 공개하지 말라고 했으니, 앞으로도 분석이나, 취약점등은 올릴생각이다. (문제가 된다고 생각하시면 댓글에 적어주시면 감사하겠습니다 )
'전공쪽 > pwnable.tw' 카테고리의 다른 글
[pwnable.tw] starbound (0) | 2020.12.31 |
---|---|
[pwnable.tw] death note (0) | 2020.12.29 |
[pwnable.tw] tcache tear (0) | 2020.12.27 |
[pwnable.tw] apple store (0) | 2020.12.24 |
[pwanble.tw] breakout (0) | 2020.12.22 |