서론
pwnable에서의 heap은 정말 대단한 영역인 것 같다.
수 많은 보호기법으로 지속적으로 취약점을 막으려하지만, 그 또한 뚫어내는 것을 보니 작은 해킹 생태계와 같아보인다.
Safe linking
높은 버전에서 작은 사이즈의 chunk가 생성되었다가 해제될 때
마지막에 free된 chunk의 next chunk address 부분을 보면 이전 chunk 주소가 아닌 다른 값을 본 적이 있을 것이다.
2.32 >= libc에서는 safe_linking이 적용되며 이 때문이다.
Safe_linking 시 address는 특정 값과 xor하여 mangle (엉망으로 만들기) 하기 때문에 주소를 완벽히 알기 어렵다.
사실 가장 먼저 free 된 주소를 통해 마지막 1.5바이트를 제외하고 알 수 있기에 이를 통해 base address는 알 수 있다.
하지만 여러개의 heap이 할당되고 해제되는 경우 실제 위치는 알 수 없다는 것을 이용하는 것이다.
사실 포너블 문제에서야 다 해봐야 0x1000미만의 값을 할당하고 해제하기 때문에 크게 신경쓸 부분은 아니지만...
Source code
Safe linking
Safe linking code는 아래와 같이 3줄로 간단하다.
PROTECT_PTR 함수로 정의되어있으며,
#define PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
REVEAL_PTR로 해당 함수를 다시 사용하고 있다.
REVEAL_PTR은 정의에 따라 아래와 같이 다시 표현할 수 있다.
#define REVEAL_PTR(&ptr, ptr)
((__typeof (ptr)) ((((size_t) &ptr) >> 12) ^ ((size_t) ptr)))
tcache_put
우선 tcache_put 함수를 보자.
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
함수의 이름에서도 알 수 있듯 e->next 영역에 암호화된 값을 넣고 tcache entry에 free된 주소 값을 넣는 역할을 한다.
위 함수에서 e->next가 fd 위치를 이야기하는데, 기존에는 free되면 이전 chunk address가 fd 영역에 쓰였을 것이다.
하지만 libc 2.32 이상의 버전에서는 위와 같이 PROTECT_PTR이라는 함수가 추가되었는데,
요약하면 암호화 과정을 거쳐서 이전 chunk address를 fd에 쓴다는 것이다.
복잡한 것은 버리고, 인자로 &e->next와 tcache -> entryes[tc_idx]를 사용한다는 것만 우선 기억하자.
tcache_get
다음으로 tcache_get 함수를 보자.
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
마찬가지로 이름에서 알 수 있듯 tcache에 다음에 할당할 주소를 가져오는 역할을 한다.
해당 함수에서는 반대로 REVEAL_PTR 함수를 사용하는데,
이 때 tcache_put에서 fd에 넣어준 암호화된 값, 즉 e->next를 복호화한 다음 tcache entry에 넣는 것을 볼 수 있다.
그러므로
우리는 pos와 ptr 값을 알아야 이것을 복호화할 수 있음을 쉽게 알 수 있다.
더불어 xor의 특성으로 인해 이미 암호화된 값을 알 수 있다면 암호화되기 전 값도 알 수 있다.
분석
예제 1.
간단히 아래와 같이 코딩해보았다.
#include <stdlib.h>
#include <stdio.h>
void main(){
long *ptr1 = (long*)malloc(32);
long *ptr2 = (long*)malloc(32);
free(ptr1);
free(ptr2);
printf("heap1 addr = %p, fd = %p\n",ptr1,ptr1[0]);
printf("heap2 addr = %p, fd = %p\n",ptr2,ptr2[0]);
}
(컴파일 시 printf 부분에 소소한 워닝은 무시하자)
실행해보면 출력 값은 아래와 같다.
heap1 addr = 0x558d16bde2a0, fd = 0x558d16bde
heap2 addr = 0x558d16bde2d0, fd = 0x55884e6c897e
낮은 버전의 libc 였다면 heap 2의 fd 위치에는 heap 1의 주소를 가지고 있을테고,
위에 출력하지는 않았지만, tcache는 heap 2의 주소를 가지고 있을 것이다.
하지만 위에서 보다시피 heap 2의 fd 위치의 값이 0x55e297b05e6c으로 heap 1의 주소인 0x55e7c9ccc2a0와 다르다.
이는 암호화된 값으로 위의 연산에 따라 수행된 값이다.
예제 1 분석 - free
위의 tcache_get과 tcache_put 함수는 함께 작동하기에 흐름대로 다시 분석해보자.
1. 코드에서처럼 우선 2개의 heap이 할당된다.
2. 이후 heap 1이 free 되면 heap 1의 주소를 tcache 넣으려 할 것이다.
(참고로 이 때 tcache_get 함수가 작동해야하지만, tcache->count 값이 0이기에 작동하지는 않는다.)
3. tcache_put 함수가 실행되면서 PROTECT_PTR 함수가 실행된다.
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
해당 함수는 PROTECT_PTR은 아래와 같이 계산되며,
#define PROTECT_PTR(pos, ptr)
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
각 변수를 대입하면 아래와 같고
(&e-> next >> 12) ^ tcache->entries[tc_idx]
==> (0x558d16bde2a0 >> 12) ^ 0
==> 0x558d16bde
e->next = 0x558d16bde
가 되어 해제된 chunk의 fd에 값이 들어간다.
4. 동일하게 heap 2가 free 되면 마찬가지로 해당 주소가 tcache에 들어가고 PROTECT_PTR 함수가 실행되며 아래와 같이 계산된다.
==> (&e-> next >> 12) ^ tcache->entries[tc_idx]
==> (0x558d16bde2d0 >> 12) ^ 0x558d16bde2a0
==> 0x55884e6c897e
e->next = 0x55884e6c897e
가 되어 해제된 chunk의 fd에 값이 들어간다.
예제 1 분석 - malloc
반대로 다시 heap이 할당될 때를 보자.
현재 heap1, heap2 순으로 해제되었기에 heap2에 먼저 할당될 것이며, e->next 값은 0x55884e6c897e 이다.
1. heap이 할당되면 tcache_get 함수 내에서 REVEAL_PTR 함수가 실행된다.
2. 해당 함수는 아래와 같이 실행되고,
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
#define REVEAL_PTR(&ptr, ptr)
((__typeof (ptr)) ((((size_t) &ptr) >> 12) ^ ((size_t) ptr)))
이를 각 변수를 대입하여 계산해보면
==> (&e->next >> 12) ^ e->next
==> ((0x558d16bde2d0 >>12) ^ 0x55884e6c897e)
==> 0x558d16bde2a0
가 되어 이전 heap chunk의 주소를 복구하였다.
3. 이를 다시 tcache entry에 넣는다.
분석 결과
그러므로 fd 값을 변조할 수 있다면 할당 시 tcache에 들어갈 값을 조작할 수 있다.
하지만 전제 조건이 있는데,
우리는 free되는 chunk의 주소 (즉 fd의 주소), tcache의 값을 알아야 암호화할 수 있고
할당되는 chunk의 주소와 그 값 (즉 fd의 주소와 값)을 알아야 복호화 할 수 있다.
주소를 알 수 없다면...?!
통상적으로 chunk의 주소와 tcache 값은 직접적으로 알기 어렵고,
fd 값 즉 e->next 값은 chunk index가 초기화되지 않았다면 알 수 있다.
이 값 만으로 복호화 할 수는 없을까?
정답부터 이야기하자면 아래 식과 같이 복호화 할 수 있다.
middle_state = (encrypt >> 12) ^ encrypt
next chunk addr = middle_state ^ (middle state >> 24)
여기서
encrypt = heap 2 e->next (즉, heap 2의 암호화된 fd 값)
next chunk addr = heap 1 &e->next
로 heap 1의 주소를 알아낼 수 있다. (증명은... 패스하자.)
그래서 이걸 어떻게 쓰는가.
아래와 같이 사용할 수 있다.
1. heap 2의 encrypt 된 값을 leak 한다.
2. 복호화 과정을 거쳐서 heap 1의 주소를 구한다.
3. PROTECT_PTR 과정을 거쳐서 원하는 주소의 encrypt 값을 계산한다.
4. heap 2의 fd 위치에 삽입한다.
5. 힙을 할당하면 tcache에 원하는 주소 값이 삽입된다.
6. 다시 힙을 할당하면 원하는 주소 값에 힙이 할당된다.
유의할 점은 malloc 시 aligned 된 주소여야한다는 것이다.
aligned 된 주소라는 의미는 movaps issue와 유사하다. 결국 0으로 끝나야되는 듯.
예제 2.
이를 반영하여 아래와 같이 다시 코딩해보았다.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void main(){
long *ptr1 = (long*)malloc(64);
long *ptr2 = (long*)malloc(64);
free(ptr1);
free(ptr2);
printf("heap1 addr = %p, fd = %p\n",ptr1,ptr1[0]);
printf("heap2 addr = %p, fd = %p\n",ptr2,ptr2[0]);
long middle_stat = (ptr2[0] >> 12) ^ ptr2[0];
long decrypt = middle_stat ^ (middle_stat >> 24);
long *str=&str;
long fake_addr = str;
fake_addr += 8*10;
printf("decr value = %p\n",decrypt);
printf("fake addres = %p\n",fake_addr);
long new_encrypt = (decrypt >> 12) ^ fake_addr;
printf("new encrypt = %p\n",new_encrypt);
ptr2[0] = new_encrypt;
long *ptr3 = (long*)malloc(64);
long *ptr4 = (long*)malloc(64);
printf("new chunk addr = %p\n",ptr4);
read(0,ptr4,0x16);
}
코드를 요약하자면,
1. 2개의 heap을 할당 및 해제 후
2. 두번째 chunk의 fd의 값으로 암호화된 첫번째 chunk address 값을 복호화하고,
3. 첫번째 chunk address와 stack의 main ret address 근처 주소를 암호화한 다음
4. 두번째 chunk의 fd에 다시 써주고
5. 2개의 heap을 다시 할당하여 stack 영역에 heap을 할당한 뒤
6. 값을 받는다.
여기서 받아들인 값이 ret address를 정상적으로 덮었다면 seg. fault가 발생할 것이다.
실행 결과는 아래와 같다.
실행 후 aaaaaaaabbbbbbbb 값을 삽입함.
- ~ ./test
heap1 addr = 0x55bd28f632a0, fd = 0x55bd28f63
heap2 addr = 0x55bd28f632f0, fd = 0x55b87324bdc3
decr value = 0x55bd28f632a0
fake addres = 0x7fffd8a6e6e0
new encrypt = 0x7ffa83746983
new chunk addr = 0x7fffd8a6e6e0
aaaaaaaabbbbbbbb
gdb를 통해 동적 분석함.
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$r15 : 0x00007f5de0324040 → 0x00007f5de03252e0 → 0x000055bd28a11000 → jg 0x55bd28a11047
$r9 : 0x00007fffd8a6e55c → "7fffd8a6e6e0"
$rsi : 0x00007fffd8a6e6e0 → "aaaaaaaabbbbbbbb\n"
$r11 : 0x246
$r12 : 0x00007fffd8a6e7f8 → 0x00007fffd8a701b1 → 0x5500747365742f2e ("./test"?)
$rip : 0x000055bd28a12371 → <main+424> ret
$r8 : 0x0
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$rsp : 0x00007fffd8a6e6e8 → "bbbbbbbb\n"
$rdi : 0x0
$rbp : 0x6161616161616161 ("aaaaaaaa"?)
$rdx : 0x16
$rbx : 0x0
$r10 : 0x0
$r13 : 0x000055bd28a121c9 → <main+0> endbr64
$rcx : 0x00007f5de01ce992 → 0x5677fffff0003d48 ("H="?)
$r14 : 0x000055bd28a14da0 → 0x000055bd28a12180 → <__do_global_dtors_aux+0> endbr64
$cs: 0x33 $fs: 0x00 $ds: 0x00 $ss: 0x2b $es: 0x00 $gs: 0x00
────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffd8a6e6e8│+0x0000: "bbbbbbbb\n" ← $rsp
0x00007fffd8a6e6f0│+0x0008: 0x000000000000000a ("\n"?)
0x00007fffd8a6e6f8│+0x0010: 0x000055bd28a121c9 → <main+0> endbr64
0x00007fffd8a6e700│+0x0018: 0x00000001d8a6e7e0
0x00007fffd8a6e708│+0x0020: 0x00007fffd8a6e7f8 → 0x00007fffd8a701b1 → 0x5500747365742f2e ("./test"?)
0x00007fffd8a6e710│+0x0028: 0x0000000000000000
0x00007fffd8a6e718│+0x0030: 0x28f0c3e0f7f754db
0x00007fffd8a6e720│+0x0038: 0x00007fffd8a6e7f8 → 0x00007fffd8a701b1 → 0x5500747365742f2e ("./test"?)
──────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x55bd28a12369 <main+416> je 0x55bd28a12370 <main+423>
0x55bd28a1236b <main+418> call 0x55bd28a120a0 <__stack_chk_fail@plt>
0x55bd28a12370 <main+423> leave
→ 0x55bd28a12371 <main+424> ret
[!] Cannot disassemble from $PC
──────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "test", stopped 0x55bd28a12371 in main (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x55bd28a12371 → main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤
rsp 위치인 bbbbbbbb로 return 하려다 실패한 것을 볼 수 있다.
유의사항
heap 할당 방식 상 header 부분의 값이나 bk, calloc에 의한 초기화 등 부가적인 변경 사항이 있기에
만일 stack에 heap을 할당한다면 canary나 다른 데이터들을 변조하지 않도록 유의하여 주소를 선정해야한다.
'Tips & theory' 카테고리의 다른 글
gef - for kernel debuging (0) | 2023.09.10 |
---|---|
file structure __lll_lock_wait_private 우회 (0) | 2023.09.06 |
vs code - wsl 연동 간 certificate error 발생 시. (0) | 2023.08.18 |
curl ssl certificate problem (0) | 2023.08.16 |
gem ssl verification error (0) | 2023.08.16 |