[KOR] nolibc - reversing

mntly·2024년 8월 26일
0

CTF

목록 보기
2/5

Project SEKAI CTF 2024의 nolibc 문제에 대한 reversing 과정을 설명하였다.
전체 source code는 아래의 Github에 있다.
리버싱한 source code

모든 게 난독화 걸려있다.

그리고 libc를 사용하지 않아 난독화를 푸는 것도 힘들다.

무슨 모든 함수 호출을 syscall로 진행해

[그림 1] syscall 값들이 저장되어 있는 부분


int *__fastcall malloc_guess(int size)
{
  char *v2; // [rsp+4h] [rbp-20h]
  signed int aligned_size; // [rsp+10h] [rbp-14h]
  signed int *v4; // [rsp+14h] [rbp-10h]
  signed int *v5; // [rsp+1Ch] [rbp-8h]

  if ( !size )
    return 0LL;
  aligned_size = (size + 15) & 0xFFFFFFF0;
  v5 = (signed int *)qword_15070;
  v4 = 0LL;
  while ( 1 )
  {
    if ( !v5 )
      return 0LL;
    if ( aligned_size <= *v5 )
      break;
    v4 = v5;
    v5 = (signed int *)*((_QWORD *)v5 + 1);
  }
  if ( *v5 >= (unsigned __int64)(aligned_size + 16LL) )
  {
    v2 = (char *)v5 + aligned_size + 16;
    *(_DWORD *)v2 = *v5 - aligned_size - 16;
    *((_QWORD *)v2 + 1) = *((_QWORD *)v5 + 1);
    *((_QWORD *)v5 + 1) = v2;
    *v5 = aligned_size;
  }
  if ( v4 )
    *((_QWORD *)v4 + 1) = *((_QWORD *)v5 + 1);
  else
    qword_15070 = *((_QWORD *)v5 + 1);
  return v5 + 4;
}

printf(unsigned __int8* a1)

__int64 __fastcall printf(unsigned __int8 *a1)
{
  __int64 result; // rax

  while ( 1 ) {
    result = *a1;
    if ( !(_BYTE)result )
      break;
    __asm { syscall; LINUX - }
    ++a1;
  }
  return result;
}

[그림 2] printf 함수의 assembly - 1

[그림 3] printf 함수의 assembly - 2


[그림 3]에는 printf 함수에서 호출하는 syscall의 정보를 담고 있다.

해당 정보는 아래의 표로 정리했다.

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
write0x011rdi (a1)1

printf(unsigned __int8* a1) 함수의 기능
: 전달 받은 a1이 가리키는 문자열을 Null terminator까지 1 byte 씩 화면에 출력한다.

printfn(unsigned __int8 *a1)

__int64 __fastcall printfn(unsigned __int8 *a1)
{
  __int64 result; // rax

  printf(a1);
  result = syscall_write;
  __asm { syscall; LINUX - }
  return result;
}

[그림 4] printfn 함수의 assembly

[그림 5] unk_3000에 저장된 값


[그림 4]에는 printfn 함수에서 호출하는 syscall의 정보를 담고 있다.

해당 정보는 아래의 표로 정리했다.

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
write0x011address of '\n'1

printfn(unsigned __int8* a1) 함수의 기능
: 전달 받은 a1이 가리키는 문자열을 Null terminator까지 1 byte 씩 화면에 출력한 이후 끝에 개행을 추가한다.

read_Helper(__int64 a1, signed int length)

__int64 __fastcall read_Helper(__int64 a1, signed int length)
{
  char v3; // [rsp+1Bh] [rbp-31h] BYREF
  __int64 v4; // [rsp+1Ch] [rbp-30h]
  __int64 v5; // [rsp+24h] [rbp-28h]
  char *v6; // [rsp+2Ch] [rbp-20h]
  __int64 v7; // [rsp+34h] [rbp-18h]
  __int64 v8; // [rsp+3Ch] [rbp-10h]
  unsigned int i; // [rsp+48h] [rbp-4h]

  for ( i = 0; (int)i < length; ++i ) {
    v8 = syscall_read;                          // 0
    v7 = 0LL;
    v6 = &v3;
    v5 = 1LL;
    __asm { syscall; LINUX - }                  // read(0, v6, 1)
    v4 = syscall_read;
    if ( v3 == '\n' ) {                         // enter
      *(_BYTE *)((int)i + a1) = 0;
      return i;
    }
    *(_BYTE *)(a1 + (int)i) = v3;
  }
  return i;
}

[그림 6] read_Helper 함수의 assembly - 1

[그림 7] read_Helper 함수의 assembly - 2

[그림 8] read_Helper 함수의 assembly - 3


[그림 7]에는 read_Helper 함수에서 호출하는 syscall의 정보를 담고 있다.

해당 정보는 아래의 표로 정리했다.

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
read0x000address of v3 (rbp-0x31)1

read_Helper(__int64 a1, signed int length) 함수의 기능
: 전달 받은 length 길이 만큼 1 byte 씩 입력 받아 전달 받은 a1이 가리키는 곳에 이어 쓴다.
length 횟수 이내에 개행을 입력 받으면 개행을 Null terminator로 변환하고,
입력 받지 않으면 Null terminator 없이 문자열 입력을 끝낸다.

입력이 종료된 이후 입력 받은 문자열의 길이를 반환한다.

custom_scanf()

__int64 custom_scanf()
{
  char v1[36]; // [rsp+0h] [rbp-30h] BYREF
  int v2; // [rsp+24h] [rbp-Ch]
  int i; // [rsp+28h] [rbp-8h]
  unsigned int v4; // [rsp+2Ch] [rbp-4h]

  v4 = 0;
  v2 = read_Helper((__int64)v1, 32);
  for ( i = 0; i < v2; ++i ) {
    if ( v1[i] <= 0x2F || v1[i] > 0x39 )        // only '1' ~ '9'
      return 0xFFFFFFFFLL;
    v4 = 10 * v4 + v1[i] - '0';
  }
  return v4;
}

custom_scanf() 함수의 기능
: 32 byte 길이의 10진수 숫자로만 구성된 문자열을 입력 받고,
입력 받은 값을 정수형으로 변환해 반환한다.

strlen_includeNULL(__int64 a1)

__int64 __fastcall strlen_includeNULL(__int64 a1)
{
  unsigned int i; // [rsp+14h] [rbp-4h]

  for ( i = 0; *(_BYTE *)((int)i + a1); ++i )
    ;
  return i;
}

strlen_includeNULL(__int64 a1) 함수의 기능
: 전달 받은 a1이 가리키는 문자열의 길이를 반환한다.
이때, 이 함수가 반환하는 문자열의 길이는 NULL terminator도 포함해서 계산한다.

strcmp(int64 a1, int64 a2)

_BOOL8 __fastcall strcmp(__int64 a1, __int64 a2)
{
  int len_a1; // [rsp+18h] [rbp-8h]
  int i; // [rsp+1Ch] [rbp-4h]

  len_a1 = strlen_includeNULL(a1);
  if ( len_a1 != (unsigned int)strlen_includeNULL(a2) )
    return 0LL;
  for ( i = 0; i < len_a1 && *(_BYTE *)(i + a1) == *(_BYTE *)(i + a2); ++i )
    ;
  return !*(_BYTE *)(i + a1) && !*(_BYTE *)(i + a2) && len_a1 == i;
}

strcmp(__int64 a1, __int64 a2) 함수의 기능
: 전달 받은 a1이 가리키는 문자열과 a2가 가리키는 문자열이 같은 문자열인지 판단하고, 그 결과를 반환한다.

두 문자열이 동일하다면 1을 반환하고, 동일하지 않으면 0을 반환한다.

exit()

void __noreturn exit()
{
  __asm { syscall; LINUX - }
}

[그림 9] exit 함수의 assembly


[그림 9]에는 exit 함수에서 호출하는 syscall의 정보를 담고 있다.

해당 정보는 아래의 표로 정리했다.

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
exit0x3c0?????0

exit() 함수의 기능
: 프로그램을 종료한다.

printInt(int a1)

__int64 __fastcall printInt(int a1)
{
  int v1; // eax
  int v2; // eax
  int v3; // eax
  int v5; // [rsp+Ch] [rbp-34h]
  unsigned __int8 v6[35]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int8 v7; // [rsp+33h] [rbp-Dh]
  int v8; // [rsp+34h] [rbp-Ch]
  int i; // [rsp+38h] [rbp-8h]
  int v10; // [rsp+3Ch] [rbp-4h]

  v5 = a1;
  v10 = 0;
  if ( a1 ) {
    if ( a1 < 0 ) {
      v2 = v10++;
      v6[v2] = 45;
      v5 = -a1;
    }
    v8 = v10;
    while ( v5 ) {
      v3 = v10++;
      v6[v3] = (char)v5 % 10 + 48;
      v5 /= 10;
    }
    for ( i = v8; i < v10 / 2; ++i ) {
      v7 = v6[i];
      v6[i] = v6[v10 - i - 1];
      v6[v10 - i - 1] = v7;
    }
  } else {
    v1 = v10++;
    v6[v1] = 48;
  }
  v6[v10] = 0;
  return printf(v6);
}

printInt(int a1) 함수의 기능
: 전달 받은 정수, a1을 문자열로 변환하고 출력한다.

strstr(int64 a1, int64 a2)

__int64 __fastcall strstr(__int64 a1, __int64 a2)
{
  int j; // [rsp+18h] [rbp-8h]
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; *(_BYTE *)(i + a1); ++i ) {
    for ( j = 0; *(_BYTE *)(j + a2) && *(_BYTE *)(i + j + a1) == *(_BYTE *)(j + a2); ++j )
      ;
    if ( !*(_BYTE *)(j + a2) )
      return 1LL;
  }
  return 0LL;
}

strstr(__int64 a1, __int64 a2) 함수의 기능
: 전달 받은 a2가 가리키는 문자열이 a1이 가리키는 문자열에 포함되어 있는지 검사하고, 그 결과를 반환한다.
a2가 가리키는 문자열이 a1이 가리키는 문자열에 포함되어 있다면 1을, 포함되어 있지 않다면 0을 반환한다.

open_write_close()

// (filename, v2, v6)
__int64 open_write_close()
{
  __asm { syscall; LINUX - }
  if ( syscall_open < 0 )
    return 0xFFFFFFFFLL;
  __asm
  {
    syscall; LINUX -
    syscall; LINUX -
  }
  return (unsigned int)syscall_write;
}

[그림 10] open_write_close 함수의 assembly - 1

[그림 11] open_write_close 함수의 assembly - 2

[그림 12] open_write_close 함수의 assembly - 3

[그림 13] open_write_close 함수의 assembly - 4


[그림 11], [그림 12], [그림 13]에는 open_write_close 함수에서 호출하는 3 개의 syscall의 정보를 담고 있다.

해당 정보는 호출 순서대로 아래의 표로 정리했다.

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)return
open0x02rdi (filename)0x41 (O_CREATO_WRONLY)0x1b6 (0u0666 : rw-rw-rw-)

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
write0x01fd (eax)rsi (v2)edx

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
close0x03fd00

open_write_close() 함수의 기능
: filename 파일을 열어서,
v2가 가리키는 곳에서 v6 byte 만큼을 읽어서 filename 파일에 작성한 후,
filename 파일을 닫는다.

open_read_close()

// (path, buf, 0x7FFF)
__int64 open_read_close()
{
  __asm { syscall; LINUX - }
  if ( syscall_open < 0 )                       // read only로 path 파일 열기
    return 0xFFFFFFFFLL;
  __asm
  {
    syscall; LINUX -                            // 읽은 내용을 buf에 넣기
    syscall; LINUX -
  }                                             // close
  return (unsigned int)syscall_read;
}

[그림 14] open_read_close 함수의 assembly - 1

[그림 15] open_read_close 함수의 assembly - 2

[그림 16] open_read_close 함수의 assembly - 3

[그림 17] open_read_close 함수의 assembly - 4


[그림 15], [그림 16], [그림 17]에는 open_read_close 함수에서 호출하는 3 개의 syscall의 정보를 담고 있다.

해당 정보는 호출 순서대로 아래의 표로 정리했다.

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)return
open0x02rdi (path)00fd (rax)

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
read0x00fd (rax)rsi (buf)edx (0x7FFF)

syscallraxarg0 (rdi)arg1 (rsi)arg2 (rdx)
close0x03fd00

open_read_close() 함수의 기능
: path 경로의 파일을 열고,
해당 파일에서 0x7FFF (edx) byte 만큼 읽어 buf가 가리키는 곳에 작성한 후
path 경로의 파일을 닫는다.

malloc_guess(int size)

// 1 <= size <= 257 or 32 or 0x7FFF or large
int *__fastcall malloc_guess(int size)
{
  __int64 v2; // [rsp+4h] [rbp-20h]
  signed int aligned_size; // [rsp+10h] [rbp-14h]
  __int64 v4; // [rsp+14h] [rbp-10h]
  __int64 v5; // [rsp+1Ch] [rbp-8h]

  if ( !size )
    return 0LL;
  aligned_size = (size + 15) & 0xFFFFFFF0;
  v5 = first_freed_heap;
  v4 = 0LL;
  while ( 1 ) {
    if ( !v5 )
      return 0LL;
    if ( aligned_size <= *(_DWORD *)v5 )
      break;
    v4 = v5;
    v5 = *(_QWORD *)(v5 + 8);
  }
  if ( *(int *)v5 >= (unsigned __int64)(aligned_size + 16LL) ) {
    v2 = aligned_size + 16LL + v5;
    *(_DWORD *)v2 = *(_DWORD *)v5 - aligned_size - 16;
    *(_QWORD *)(v2 + 8) = *(_QWORD *)(v5 + 8);
    *(_QWORD *)(v5 + 8) = v2;
    *(_DWORD *)v5 = aligned_size;
  }
  if ( v4 )
    *(_QWORD *)(v4 + 8) = *(_QWORD *)(v5 + 8);
  else
    first_freed_heap = *(_QWORD *)(v5 + 8);
  return (int *)(v5 + 16);
}

바이너리를 디컴파일해 분석하면 아래와 같은 코드를 확인할 수 있다.

sub_12C8("Enter a string: ");
v1 = sub_1031((unsigned int)(v2 + 1));
if ( !v1 )
{
	sub_1322("Failed to allocate memory");
	sub_1322(&unk_3124);
    sub_18E6(&unk_3124);
}

위 코드에서 sub_1031 함수의 반환값이 0이면 memory alloc이 되지 않았다고 출력하는것을 추측할 수 있다. (위에 언급한 함수들을 모두 파악하였다면 sub_1322가 출력을 담당하는 함수라는 것을 알 수 있다.)

이 부분에서 우리는 sub_1031 함수가 메모리 할당에 관여하는 함수, malloc이라고 추측할 수 있다. 이 함수의 동작 과정을 분석한다면 이 함수가 malloc의 기능을 수행한다고 확신할 수 있다.

이 함수의 동작 과정은 Project SEKAI CTF 2024 - nolibc : Attack Surface in malloc_guess에 작성하였다.

추가적으로 이 함수에 전달된 인자를 상호참조하거나, 이 함수가 호출되고 이 함수를 호출하기 전에 호출되는 함수를 분석하면 free 함수의 기능을 하는 함수도 찾을 수 있다.

free_guess(unsigned __int64 ptr)

int *__fastcall free_guess(unsigned __int64 ptr)
{
  int *result; // rax
  unsigned __int64 free_heap_header; // [rsp+18h] [rbp-18h]
  unsigned __int64 prev_heap_header; // [rsp+20h] [rbp-10h]
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  if ( ptr ) {
    result = (int *)&unk_5000;
    if ( ptr >= (unsigned __int64)&unk_5000 ) {
      result = &syscall_read;
      if ( ptr < (unsigned __int64)&syscall_read ) {
        free_heap_header = ptr - 16;
        v4 = first_freed_heap;
        prev_heap_header = 0LL;
        while ( v4 && v4 < free_heap_header ) {
	        // go to the heap right before the ptr heap
          prev_heap_header = v4;
          v4 = *(_QWORD *)(v4 + 8);
        }
        if ( prev_heap_header ) {
          *(_QWORD *)(free_heap_header + 8) = *(_QWORD *)(prev_heap_header + 8);
          *(_QWORD *)(prev_heap_header + 8) = free_heap_header;
        } else {
	        // modify freed heap, connect previous freed heap
          *(_QWORD *)(free_heap_header + 8) = first_freed_heap;
          first_freed_heap = ptr - 16;
        }
        return (int *)merge_guess();
      }
    }
  }
  return result;
}

merge_guess()

__int64 merge_guess()
{
  __int64 result; // rax
  int v1; // [rsp+0h] [rbp-Ch]
  int *heap_header; // [rsp+4h] [rbp-8h]

  result = first_freed_heap;
  heap_header = (int *)first_freed_heap;
  while ( heap_header ) {
    result = *((_QWORD *)heap_header + 1);
    if ( !result )
      break;
    if ( (int *)((char *)heap_header + *heap_header + 16) == *((int **)heap_header + 1) ) {
      *heap_header += **((_DWORD **)heap_header + 1) + 16;
      result = (__int64)heap_header;
      *((_QWORD *)heap_header + 1) = *(_QWORD *)(*((_QWORD *)heap_header + 1) + 8LL);
    } else {
      result = *((_QWORD *)heap_header + 1);
      heap_header = (int *)result;
    }
  }
  if ( heap_header ) {
    v1 = (_DWORD)&unk_10000 - ((char *)heap_header - (char *)&unk_5000);
    result = (unsigned int)*heap_header;
    if ( v1 > (int)result ) {
      result = (__int64)heap_header;
      *heap_header = v1 - 16;
    }
  }
  return result;
}

위 두 함수의 동작 과정은 다음과 같다.

  • 현재 해제할 chunk (free_heap_header)보다 낮은 메모리 주소의 해제된 chunk (prev_heap_header)가 현재 해제할 chunk를 가리키도록 하고, 현재 해제할 chunk (free_heap_header)는 이전에 해제된 chunk (prev_heap_header)가 가리키는 해제된 chunk를 가리키도록 한다.
  • 이후 merge_guess 함수를 호출해 인접한 해제된 chunk 끼리 합친다.

[그림 18] free_guess & merge_guess 과정 모식도 - 1

[그림 19] free_guess & merge_guess 과정 모식도 - 2

프로그램 시작 시 실행되는 함수 (start)

void __noreturn start()
{
  int choice_login_reg_exit; // [rsp+8h] [rbp-8h]
  int choice_UserMenu; // [rsp+Ch] [rbp-4h]

  init();
  while ( 1 ) {
    printfn((__int64)"Welcome to String Storage!");
    printfn((__int64)"Please login or register an account to continue :)");
    printfn((__int64)&enter);                   // 개행?
    while ( UserIndex == -1 ) {
      printfn((__int64)"1. Login");
      printfn((__int64)"2. Register");
      printfn((__int64)"3. Exit");
      printf("Choose an option: ");
      choice_login_reg_exit = custom_scanf();
      printfn((__int64)&enter);                 // 개행?
      if ( choice_login_reg_exit == 1 ) {
        login();
      } else if ( choice_login_reg_exit == 2 ) {
        register();
      } else {
        if ( choice_login_reg_exit == 3 )
          exit();
        printfn((__int64)"Invalid option");
      }
      printfn((__int64)&enter);
    }
    printf("Welcome to String Storage, ");
    printf(*(unsigned __int8 **)UserInfo_struct_list[UserIndex]);
    printfn((__int64)"!");
    printfn((__int64)&enter);
    while ( 1 ) {
      printfn((__int64)"1. Add string");
      printfn((__int64)"2. Delete string");
      printfn((__int64)"3. View strings");
      printfn((__int64)"4. Save to File");
      printfn((__int64)"5. Load from File");
      printfn((__int64)"6. Logout");
      printf("Choose an option: ");
      choice_UserMenu = custom_scanf();
      printfn((__int64)&enter);
      switch ( choice_UserMenu ) {
        case 1:
          AddString();
          goto LABEL_26;
        case 2:
          DelString();
          goto LABEL_26;
        case 3:
          ViewString();
          goto LABEL_26;
        case 4:
          SaveFile();
          goto LABEL_26;
        case 5:
          LoadFile();
          goto LABEL_26;
      }
      if ( choice_UserMenu == 6 )
        break;
      printfn((__int64)"Invalid option");
LABEL_26:
      printfn((__int64)&enter);
    }
    UserIndex = -1;
  }
}

init()

void *init() {
  void *result; // rax

  first_freed_heap = (__int64)&unk_5000;
  unk_5000 = (_DWORD)&unk_10000;
  result = &unk_5000;
  *((_QWORD *)&unk_5000 + 1) = 0LL;
  return result;
}

login()

__int64 login()
{
  __int64 password; // [rsp+8h] [rbp-18h]
  __int64 name; // [rsp+10h] [rbp-10h]
  int i; // [rsp+1Ch] [rbp-4h]

  printf("Username: ");
  name = malloc_guess(64LL);
  if ( name ) {
    read_Helper(name, 64);
    if ( (unsigned int)strlen_includeNULL(name) ) {
      printf("Password: ");
      password = malloc_guess(64LL);
      if ( password ) {
        read_Helper(password, 64);
        if ( (unsigned int)strlen_includeNULL(password) ) {
          if ( NumUser ) {
            for ( i = 0; i < NumUser; ++i ) {
              if ( (unsigned int)strcmp(**((_QWORD **)&UserInfo_struct_list + i), name)// UserInfo_struct = &name; &password
                && (unsigned int)strcmp(*(_QWORD *)(*((_QWORD *)&UserInfo_struct_list + i) + 8LL), password) ) {
                UserIndex = i;                  // 찾은 User의 index
              }
            }
            if ( UserIndex == -1 )
              printfn((__int64)"Invalid username or password");
            else
              printfn((__int64)"Logged in successfully!");
            free_guess(name);
            return free_guess(password);
          } else {
            return printfn((__int64)"No users registered");
          }
        } else {
          printfn((__int64)"Invalid password");
          free_guess(password);
          return login();
        }
      } else {
        printfn((__int64)"Invalid password");
        free_guess(0LL);
        return login();
      }
    } else {
      printfn((__int64)"Invalid username");
      free_guess(name);
      return login();
    }
  } else {
    printfn((__int64)"Invalid username");
    free_guess(0LL);
    return login();
  }
}

register()

__int64 register()
{
  struct_UserInfo_struct *UserInfo_struct; // [rsp+8h] [rbp-18h]
  int *password; // [rsp+10h] [rbp-10h]
  int *name; // [rsp+18h] [rbp-8h]

  if ( NumUser > 0 )
    return printfn((__int64)"You can only register one account!");
  printf("Username: ");
  name = malloc_guess(32);
  if ( name ) {
    read_Helper((__int64)name, 32);
    if ( (unsigned int)strlen_includeNULL((__int64)name) ) {
      printf("Password: ");
      password = malloc_guess(32);
      if ( password ) {
        read_Helper((__int64)password, 32);
        if ( (unsigned int)strlen_includeNULL((__int64)password) ) {
          UserInfo_struct = (struct_UserInfo_struct *)malloc_guess(0x4010);
          UserInfo_struct->UserName = name;
          UserInfo_struct->PassWord = password;
          UserInfo_struct->strIndex = 0;
          UserInfo_struct_list[NumUser++] = UserInfo_struct;
          return printfn((__int64)"User registered successfully!");
        } else {
          printfn((__int64)"Invalid password");
          free_guess((unsigned __int64)password);
          return register();
        }
      } else {
        printfn((__int64)"Invalid password");
        free_guess(0LL);
        return register();
      }
    } else {
      printfn((__int64)"Invalid username");
      free_guess((unsigned __int64)name);
      return register();
    }
  } else {
    printfn((__int64)"Invalid username");
    free_guess(0LL);
    return register();
  }
}
  • UserName을 저장하기 위해 32 byte 메모리를 할당한다.

  • PassWord를 저장하기 위해 32 byte 메모리를 할당한다.

  • 새로운 User에 대한 정보를 저장하기 위해 0x4010 byte 메모리를 할당한다.

    ⇒ User의 정보를 저장한 UserInfo_struct 생성한다.

[그림 20] UserInfo_struct 구조체 모식도

+) strIndex에는 현재 UserInfo_struct에 저장된 문자열의 수 - 1이 저장된다.

AddString()

__int64 AddString()
{
  int *strbuf; // [rsp+0h] [rbp-10h]
  int strlength; // [rsp+Ch] [rbp-4h]

  if ( *(int *)(UserInfo_struct_list[UserIndex] + 16LL) > 2046 )
    return printfn((__int64)"You have reached the maximum number of strings");
  printf("Enter string length: ");
  strlength = custom_scanf();
  if ( strlength > 0 && strlength <= 256 ) {
    printf("Enter a string: ");
    strbuf = malloc_guess(strlength + 1);
    if ( !strbuf ) {
      printfn((__int64)"Failed to allocate memory");
      printfn((__int64)&enter);
      exit();
    }
    read_Helper((__int64)strbuf, strlength + 1);
    *(_QWORD *)(UserInfo_struct_list[UserIndex]
              + 8 * ((int)(*(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL))++ + 2LL)
              + 8) = strbuf;
    return printfn((__int64)"String added successfully!");
  } else {
    printfn((__int64)"Invalid length");
    return printfn((__int64)&enter);
  }
}
  • 0 초과 256 사이의 정수를 받아 해당 길이 (+1) 만큼 메모리를 할당하고, 할당한 메모리에 문자열을 입력 받는다.
  • 이후 입력 받아 저장된 문자열의 주소를 UserInfo_struct 구조체에 저장한다.
  • 이때, 문자열은 최대 2047 (0x7FF)개를 저장할 수 있다.

DelString()

__int64 DelString()
{
  int del_Index; // [rsp+8h] [rbp-8h]
  int i; // [rsp+Ch] [rbp-4h]

  if ( *(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL) ) {
    printf("Enter the index of the string to delete: ");
    del_Index = custom_scanf();
    if ( del_Index >= 0 && del_Index < *(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL) ) {
      free_guess(*(_QWORD *)(UserInfo_struct_list[UserIndex] + 8 * (del_Index + 2LL) + 8));
      for ( i = del_Index; i < *(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL) - 1; ++i )
        *(_QWORD *)(UserInfo_struct_list[UserIndex] + 8 * (i + 2LL) + 8) = *(_QWORD *)(UserInfo_struct_list[UserIndex]
                                                                                     + 8 * (i + 1 + 2LL)
                                                                                     + 8);
      --*(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL);
      return printfn((__int64)"String deleted successfully!");
    } else {
      printfn((__int64)"Invalid index");
      return printfn((__int64)&enter);
    }
  } else {
    printfn((__int64)"No strings to delete");
    return printfn((__int64)&enter);
  }
}
  • index를 입력 받아 해당 index에 저장된 문자열을 삭제하고, 해당 문자열이 들어있던 메모리 공간을 free 한다.
  • 이후, UserInfo_struct 구조체를 돌며 문자열 Pointer를 앞으로 한 칸 씩 밀어 삭제된 문자열 Pointer의 공간을 없앤다.

ViewString()

__int64 ViewString()
{
  __int64 result; // rax
  int i; // [rsp+Ch] [rbp-4h]

  if ( *(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL) ) {
    for ( i = 0; ; ++i ) {
      result = *(unsigned int *)(UserInfo_struct_list[UserIndex] + 16LL);
      if ( i >= (int)result )
        break;
      printf("String ");
      printInt((unsigned int)i);
      printf(": ");
      printfn(*(_QWORD *)(UserInfo_struct_list[UserIndex] + 8 * (i + 2LL) + 8));
    }
  } else {
    printfn((__int64)"No strings to view");
    return printfn((__int64)&enter);
  }
  return result;
}

SaveFile()

int *SaveFile()
{
  int v1; // [rsp+8h] [rbp-28h]
  int *buf; // [rsp+10h] [rbp-20h]
  int *filename; // [rsp+18h] [rbp-18h]
  int j; // [rsp+24h] [rbp-Ch]
  int i; // [rsp+28h] [rbp-8h]
  unsigned int v6; // [rsp+2Ch] [rbp-4h]

  printf("Enter the filename: ");
  filename = malloc_guess(32);
  if ( filename
    && (read_Helper((__int64)filename, 32), (unsigned int)strlen_includeNULL((__int64)filename))
    && !(unsigned int)strstr((__int64)filename, (__int64)"flag") ) {
    buf = malloc_guess(0x7FFF);
    if ( !buf ) {
      printfn((__int64)"Failed to allocate memory");
      printfn((__int64)&enter);
      exit();
    }
    v6 = 0;
    for ( i = 0; i < *(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL); ++i ) {
      v1 = strlen_includeNULL(*(_QWORD *)(UserInfo_struct_list[UserIndex] + 8 * (i + 2LL) + 8));
      for ( j = 0; j < v1; ++j )
        *((_BYTE *)buf + (int)v6++) = *(_BYTE *)(*(_QWORD *)(UserInfo_struct_list[UserIndex] + 8 * (i + 2LL) + 8) + j);
      *((_BYTE *)buf + (int)v6++) = '\n';
    }
    if ( (int)open_write_close(filename, buf, v6) >= 0 )// write string in buf to file filename {
      printfn((__int64)"Strings saved to file successfully!");
      return free_guess((unsigned __int64)buf);
    } else {
      printfn((__int64)"Failed to write file");
      return (int *)printfn((__int64)&enter);
    }
  } else {
    printfn((__int64)"Invalid filename");
    return (int *)printfn((__int64)&enter);
  }
}
  • 32 byte 크기의 파일 이름을 입력 받아 해당 파일에 지금까지 저장된 문자열을 작성한다.
  • 이때, 파일 이름에 "flag"가 포함되어 있으면 SaveFile 함수를 즉시 종료한다.
  • 문자열을 파일에 작성하기 전에 0x7FFF byte 크기의 메모리(buf)를 할당해서 파일 작성에 이용한다.
    • 만약 flag 필터링을 통과한 이후 파일을 열고 작성하는데 실패하면 할당한 메모리 공간 (buf)를 해제하지 않는다.
    • 파일을 정상적으로 열고, 작성했다면 할당한 메모리 공간(buf)을 해제한다.

LoadFile()

int *LoadFile()
{
  int *v1; // [rsp+0h] [rbp-30h]
  int read_length; // [rsp+Ch] [rbp-24h]
  int *buf; // [rsp+10h] [rbp-20h]
  int *path; // [rsp+18h] [rbp-18h]
  int i; // [rsp+20h] [rbp-10h]
  int v6; // [rsp+24h] [rbp-Ch]
  int v7; // [rsp+28h] [rbp-8h]
  int v8; // [rsp+2Ch] [rbp-4h]

  printf("Enter the filename: ");
  path = malloc_guess(32);
  if ( path
    && (read_Helper((__int64)path, 32), (unsigned int)strlen_includeNULL((__int64)path))
    && !(unsigned int)strstr((__int64)path, (__int64)"flag") ) {
    buf = malloc_guess(0x7FFF);
    if ( !buf ) {
      printfn("Failed to allocate memory");
      printfn((unsigned __int8 *)&enter);
      exit();
    }                                           // (path, buf, 0x7FFF)
    read_length = open_read_close();            // 정상적으로 파일 열림 => 읽은 byte 수 / 비정상적 => -1
    if ( read_length >= 0 ) {
      v8 = 0;
      v7 = 0;
      while ( v8 < read_length ) {
        v6 = 0;
        while ( *((_BYTE *)buf + v8) != '\n' ) {
          ++v6;
          ++v8;
        }
        v1 = malloc_guess(v6 + 1);
        if ( !v1 ) {
          printfn("Failed to allocate memory");
          printfn((unsigned __int8 *)&enter);
          exit();
        }
        for ( i = 0; i < v6; ++i )
          *((_BYTE *)v1 + i) = *((_BYTE *)buf + v7++);
        *((_BYTE *)v1 + v6) = 0;
        *(_QWORD *)(UserInfo_struct_list[UserIndex]
                  + 8 * ((int)(*(_DWORD *)(UserInfo_struct_list[UserIndex] + 16LL))++ + 2LL)
                  + 8) = v1;
        ++v8;
        ++v7;
      }
      printfn("Strings loaded from file successfully!");
      return free_guess((unsigned __int64)buf);
    } else {
      printfn("Failed to read file");
      return (int *)printfn((unsigned __int8 *)&enter);
    }
  } else {
    printfn("Invalid filename");
    return (int *)printfn((unsigned __int8 *)&enter);
  }
}
  • 32 byte 크기의 파일 이름을 입력 받아 해당 파일의 내용을 UserInfo_struct에 저장한다.
  • 이때, 파일 이름에 "flag"가 포함되어 있으면 LoadFile 함수를 즉시 종료한다.
  • 파일의 내용을 저장하기 전에 0x7FFF byte 크기의 메모리(buf)를 할당해서 파일 읽기에 이용한다.
    • 만약 flag 필터링을 통과한 이후 파일을 열고 읽는데 실패하면 할당한 메모리 공간 (buf)를 해제하지 않는다.
    • 파일을 정상적으로 열고, 읽었다면 할당한 메모리 공간(buf)을 해제한다.

0개의 댓글