환경
Ubuntu GLIBC 2.39-0ubuntu8.4 / Ubuntu 24.04.1 LTS x86_64
요약
glibc 2.32 버전부터 fastbin 및 tcache 같은 단일 연결 리스트에는
safe-linking 메커니즘으로 인해 fd가 암호화된다.
따라서 해당글은 fd 값을 다시 복호화하는 방법에 대해 설명한다.
여기서 fd는 청크가 free되면 들어가는 값으로,
아직 사용되지 않은 다음 chunk의 주소를 의미한다.
fd 복호화 코드
def decrypt(cipher): key = 0 plain = 0 for i in range(1, 6): bits = 64-12*i if bits < 0: bits = 0 plain = ((cipher ^ key) >> bits) << bits key = plain >> 12 return plain
내용
이 기법은 safe-linking 메커니즘으로 인해 변조된 값에서 원래의 내용을 복구하는 방법을 보여줍니다.
이 공격은 평문(포인터)의 처음 12비트가 알려져 있고, 키(ASLR 슬라이드)가 포인터의 상위 비트와 동일하다는 사실을 이용합니다.
결과적으로, 포인터가 저장된 청크가 포인터 자체와 같은 페이지에 있는 한, 포인터의 값을 완전히 복구할 수 있습니다.
그렇지 않은 경우에도, 저장 위치와 포인터 사이의 페이지 오프셋을 이용해 포인터를 복구할 수 있습니다.
여기서 시연하는 것은 페이지 오프셋이 0인 특수한 경우입니다.
1.
먼저 a와 b 청크에 각각 0x20만큼 차례대로 할당함.
이후, 병합을 방지하기 위해 패딩 청크(padding chunk)를 생성하기 위해 0x10만큼 할당함.
코드:
// step 1: allocate chunks long *a = malloc(0x20); long *b = malloc(0x20); printf("First, we create chunk a @ %p and chunk b @ %p\n", a, b); malloc(0x10); puts("And then create a padding chunk to prevent consolidation.");
결과:
First, we create chunk a @ 0x5555555592a0 and chunk b @ 0x5555555592d0 And then create a padding chunk to prevent consolidation.
gef➤ heap chunks Chunk(addr=0x555555559010, size=0x290, flags=PREV_INUSE) [0x0000555555559010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................] Chunk(addr=0x5555555592a0, size=0x30, flags=PREV_INUSE) [0x00005555555592a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................] Chunk(addr=0x5555555592d0, size=0x30, flags=PREV_INUSE) [0x00005555555592d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................] Chunk(addr=0x555555559300, size=0x20d10, flags=PREV_INUSE) ← top chunk gef➤ heap chunk 0x5555555592a0 Chunk(addr=0x5555555592a0, size=0x30, flags=PREV_INUSE) Chunk size: 48 (0x30) Usable size: 40 (0x28) Previous chunk size: 0 (0x0) PREV_INUSE gef➤ heap chunk 0x5555555592d0 Chunk(addr=0x5555555592d0, size=0x30, flags=PREV_INUSE) Chunk size: 48 (0x30) Usable size: 40 (0x28) Previous chunk size: 0 (0x0) PREV_INUSE

2.
glibc2.32?부터 safe linking과정이 추가되었는데
아직 사용되지 않은 다음 chunk의 주소를 의미하는 fd는 fd ^ (heap_chunk >> 12)
변환해서 들어간다.
mangled = (real_fd ^ (chunk_address >> 12));
코드:
// step 2: free chunks puts("Now free chunk a and then free chunk b."); free(a); free(b); printf("Now the freelist is: [%p -> %p]\n", b, a); printf("Due to safe-linking, the value actually stored at b[0] is: %#lx\n", b[0]);
결과:
Now free chunk a and then free chunk b. Now the freelist is: [0x5555555592d0 -> 0x5555555592a0] Due to safe-linking, the value actually stored at b[0] is: 0x55500000c7f9
Breakpoint 1, 0x00005555555551ae in main () gdb-peda$ parseheap addr prev size status fd bk 0x555555559000 0x0 0x290 Used None None 0x555555559290 0x0 0x30 Freed 0x555555559 None 0x5555555592c0 0x0 0x30 Freed 0x55500000c7f9 None gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x5555555592f0 (size : 0x20d10) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0 (0x30) tcache_entry[1](2): 0x5555555592d0 --> 0x5555555592a0

3.
하지만 safe linking으로 인해 변환된 enc_fd는 다시 복호화시킬 수 있다고 한다.
복호화는 평문(fwd 포인터)의 처음 12비트가 알려져 있다는 사실을 이용하며, 이는 12비트 슬라이딩 덕분이다.
그리고 키인 ASLR 값은 평문(fwd 포인터)의 상위 비트들과 동일하다.
보다시피 b청크에 있던 원래의 값인 fd값이 나왔다.
코드:
long decrypt(long cipher) { puts("The decryption uses the fact that the first 12bit of the plaintext (the fwd pointer) is known,"); puts("because of the 12bit sliding."); puts("And the key, the ASLR value, is the same with the leading bits of the plaintext (the fwd pointer)"); long key = 0; long plain; for(int i=1; i<6; i++) { int bits = 64-12*i; if(bits < 0) bits = 0; plain = ((cipher ^ key) >> bits) << bits; key = plain >> 12; printf("round %d:\n", i); printf("key: %#016lx\n", key); printf("plain: %#016lx\n", plain); printf("cipher: %#016lx\n\n", cipher); } return plain; } // step 3: recover the values puts("Now decrypt the poisoned value"); long plaintext = decrypt(b[0]); printf("value: %p\n", a); printf("recovered value: %#lx\n", plaintext); assert(plaintext == (long)a);
결과:
Now decrypt the poisoned value The decryption uses the fact that the first 12bit of the plaintext (the fwd pointer) is known, because of the 12bit sliding. And the key, the ASLR value, is the same with the leading bits of the plaintext (the fwd pointer) round 1: key: 0000000000000000 plain: 0000000000000000 cipher: 0x0055500000c7f9 round 2: key: 0x00000550000000 plain: 0x00550000000000 cipher: 0x0055500000c7f9 round 3: key: 0x00000555550000 plain: 0x00555550000000 cipher: 0x0055500000c7f9 round 4: key: 0x00000555555550 plain: 0x00555555550000 cipher: 0x0055500000c7f9 round 5: key: 0x00000555555559 plain: 0x005555555592a0 cipher: 0x0055500000c7f9 value: 0x5555555592a0 recovered value: 0x5555555592a0