콘텐츠로 건너뛰기

[how2heap/glibc2.39] decrypt_safe_linking

환경

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