Hubert Hackin''
  • All posts
  • About
  • Our CTF

NSEC21 dontstopthefuzzing - Mon, May 24, 2021 - Lucas Bajolet

| Trivia Rev | Nsec21

dontstopthefuzzing

The challenge is officially a trivia, which in pure NSec fashion ends-up being a video with botched lyrics for us to watch.

Generally, we have to keep a copy of it, and painstakenly watch it 100+ times in order to look for something remotely abnormal, which can lead us to flags.

This time though, it’s a bit different, and while it’s still a MV of a relatively well-known song, no flag within the video (or at least not that we’ve found any), but when we arrive at the end, we’re greeted with this:

mv_end

Alright then, let’s take a look at that link.

$ wget dl.nsec/dontstoptheflag
$ file dontstoptheflag
dontstoptheflag: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=53c41655c74f9b38eb01eb686e45bf94c43e60d3, for GNU/Linux 3.2.0, stripped

no_anw

Reversing

We can try to execute the binary first:

$ chmod +x dontstoptheflag
$ ./dontstoptheflag
[!] Rihan-Nah :(

Note for future reference: the video was a somewhat old song of Rihanna, nice pun gentlemen.

Since we have a binary, we can get started with reversing it, time to get r2!

Hm, not a lot to look at, but we do have a few functions that we can peek into:

funcs

I’ll keep it brief:

  • fcn.00952d6e -> main
  • fcn.00952ec9 -> file reading routine
  • fnc.00001229 -> file content checking

Let’s start with the main, we’ll explain the rest of the binary afterwards.

main

First off, how do we know it’s the main? Simple enough: we have a bunch of checks that end up either printing “[!] Rihan-Nah :(” or calling a function, up to the point where we reach “[+] Rihan-Ya! :)”, which is our goal!

Now, off we go to see what we have to do to get there.

Let’s start with the first block:

main_first_block

Alright, nothing too scary here, seems like the binary grabs the value of edi (first parameter of the main function, as int32), which is the number of arguments given to the binary (a.k.a. int argc). If it has a value other than one, we go to [!] Rihan-Nah :(, so it becomes obvious that we need to give a parameter to the binary before we can go any further.

main_second_block

Next up, we get that parameter from the char **argv parameter array, at index 1 (0 being the name of the program). Then, this is fopen’d and the resulting FILE* is stored on the stack at index rbp-0x10. So we know that argv[1] must be a file, and if it does not exist, the resulting pointer will be null, and we get Rihan-Nah. Moving on.

main_third_block

We then call the function fcn.00952ec9. Opening this function in the disassembly shows us that it’s basically a big realloc call (0x40000 bytes long), followed by a fread, and a final realloc on the size of what was read from the file. So, we read all the file at once, and return both the pointer to the buffer that was acquired and filled-in with the file’s contents, and as second return value, the size of the buffer.

If for some reason reading failed, the pointer returned will be null, and we exit with Rihan-Nah.

main_fourth_block

We finally arrive at the last block, where function fcn.00001229 is called, and if it succeeds, we are greeted with Rihan-Ya! With this info, we can get to understanding the function.

fcn.00001229

First thing to notice is the sheer SIZE of that thing. It’s over 9MiB (so clearly over 9000B), there’s a lot of code in there.

First block gives us a slight hint.

valid_first_block

We can see here that we are comparing the size of the buffer to 0x7a5d7. If it fails, we return 0, which is the return value that corresponds to a failure here. So we know now that the file we are passing as parameter to the binary MUST have this size if we even want it to be analysed after being read.

Note: 0x7a5d7 is 503127 in human numbers

valid_sub_valid_blocks

The following is what the remainder of the function is like, in a nutshell.

So the sequence is something like that:

  • First, compute pointer base address (rbp-0x8) + an offset (first block is 0x2cc3 for example)
  • Then, get the byte at that address
  • And compare it to a value (still for the first block, it is 0xae)
  • Rince and repeat

This sequence is repeated a bunch of times, and there’s sometimes a difference in the branching logic, i.e. a jne becomes a je which lets us advance to the next validation block, until we reach the end.

At the end, we get the following sequence:

0x00952d5a      je 0x952d63
0x00952d5c      mov eax, 0
0x00952d61      jmp 0x952d68
0x00952d63      mov eax, 1
0x00952d68      pop rbp
0x00952d69      ret

The final je leads to the function returning non-zero, interpreted as a success from the main. The other alternative:

0x00952d5c      mov eax, 0
0x00952d61      jmp 0x952d68
[...]
0x00952d68      pop rbp
0x00952d69      ret

^ this is the sequence leading to a zero-value being returned, which is the sink for all the failed comparisons.

Wat do.

Good, so now we have a very long function with straightforward behaviour, we can now work on extracting the values for both the offset and the expected bytes in each mentioned offset here, and script something to generate the expected file.

What we did here is leverage objdump -D to get the disassembly of the complete file, extract the subset that we care for in the output disassembly:

#!/bin/sh

objdump -d dontstoptheflag >dump

sed -n '/1251:/,/952d68:/p' dump >dump2

grep -A2 -E 'add\s*\$0x([0-9a-f]+),%rax$' <dump2 | grep -v "movzbl" | grep -Ev '^--$' >addcmp.txt

We then have only the part that do the checks in dump2, which amounts to an input like this:

    1251:	48 05 c3 2c 00 00    	add    $0x2cc3,%rax
    125a:	3c ae                	cmp    $0xae,%al
    1262:	48 05 ca 4b 01 00    	add    $0x14bca,%rax
    126b:	3c 71                	cmp    $0x71,%al
    1273:	48 05 47 45 03 00    	add    $0x34547,%rax
    127c:	3c 61                	cmp    $0x61,%al
    1284:	48 05 e4 78 05 00    	add    $0x578e4,%rax
    128d:	3c fd                	cmp    $0xfd,%al
    1295:	48 05 1c 8a 00 00    	add    $0x8a1c,%rax
    129e:	3c ea                	cmp    $0xea,%al
[...]

Then, we can write a little program that outputs the blob to a file, which we can then check with the binary.

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"regexp"
	"strconv"
	"strings"
)

func main() {
	payload, err := parseInput(os.Args[1])
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to parse input: %s\n", err)
		os.Exit(1)
	}

	err = ioutil.WriteFile("outfile", payload, 0644)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to write file: %s\n", err)
		os.Exit(1)
	}
}

var addrRegex = regexp.MustCompile("^\\s+[0-9a-f]+:\\s*([0-9a-f]{2}\\s)+\\s*add\\s*\\$(0x[^,]+),.*$")
var valueRegex = regexp.MustCompile("^\\s+[0-9a-f]+:\\s*([0-9a-f]{2}\\s)+\\s*cmp\\s*\\$(0x[^,]+),.+$")

var valueTestRegex = regexp.MustCompile("test\\s*%al,%al$")

func parseInput(path string) ([]byte, error) {
	outPayload := make([]byte, 503127)

	inFile, err := ioutil.ReadFile(path)
	if err != nil {
		return nil, err
	}

	strs := strings.Split(string(inFile), "\n")
	if strs[len(strs)-1] == "" {
		strs = strs[0 : len(strs)-1]
	}

	if len(strs)%2 != 0 {
		return nil, fmt.Errorf("Not an even number of lines: %d", len(strs))
	}

	for i := 0; i < len(strs)/2; i++ {
		baseOffset := 2 * i

		addrLine := strs[baseOffset]
		fmt.Printf("address line: %s\n", addrLine)

		matches := addrRegex.FindAllStringSubmatch(addrLine, -1)
		if len(matches) == 0 {
			return nil, fmt.Errorf("addr regex mismatch: %s", err)
		}
		submatches := matches[0]
		if len(submatches) != 3 {
			return nil, fmt.Errorf("addr regex mismatch: %s", err)
		}

		hexval := submatches[2]
		offsetVal, err := strconv.ParseInt(hexval[2:], 16, 64)
		if err != nil {
			return nil, fmt.Errorf("addr parseInt failure (%s): %s",
				hexval, err)
		}

		fmt.Printf("offsetVal := %d\n", offsetVal)

		valueLine := strs[baseOffset+1]
		fmt.Printf("value line: %s\n", valueLine)

		valueVal := 0
		if !valueTestRegex.MatchString(valueLine) {
			matches := valueRegex.FindAllStringSubmatch(valueLine, -1)
			if len(matches) != 1 {
				return nil, fmt.Errorf("value regex mismatch: %s", err)
			}

			submatches := matches[0]
			if len(submatches) != 3 {
				return nil, fmt.Errorf("value regex mismatch: %s", err)
			}

			hexval := submatches[2]
			valueVali64, err := strconv.ParseInt(hexval[2:], 16, 64)
			if err != nil {
				return nil, fmt.Errorf("value parseInt failure (%s): %s",
					hexval, err)
			}

			valueVal = int(valueVali64)
		}

		fmt.Printf("valueVal: %d\n", valueVal)

		if valueVal > 255 {
			panic("value >255")
		}

		outPayload[offsetVal] = byte(valueVal)

		// panic("stop")
	}

	return outPayload, nil
}

Good this works, we can check the file:

$ ./dontstoptheflag payload_gen/outfile
[+] Rihan-Ya! :)

Gotcha!

.epilog

Once we have the outfile, we can see what to do with it:

$ file payload_gen/outfile
payload_gen/outfile: ISO Media, MP4 v2 [ISO 14496-14]

Hey, that’s a video?

outfile

OH HAI.

Basically it’s a sequence of mini clips, with the flag spelled-out letter by letter.

FLAG7c0gta5037r6o6woz4a79css996

And that’s how we scored 3 points for this challenge!

Acknowledgements

Jean Privat: adviser and basher extraordinaire, who suggested we use the objdump output and do some shenanigans on it to extract the payload, and attempted to generate the out video with a shell script:

#!/bin/sh

objdump -d dontstoptheflag > dump

sed -n '/1251:/,/952d68:/p'  dump > dump2

echo '#include<unistd.h>
int main() {
    char x[503127];
' > dump3.c
cat dump2 | sed -Ene 's/.*add.*\$(0x[0-9a-f]*).*/x[\1]=/p;s/.*cmp.*\$(0x[0-9a-f]*).*/\1;/p;s/.*test.*/0x0;/p' >> dump3.c

echo 'write(1, x, sizeof(x)); }' >> dump3.c

gcc dump3.c -o dump4

./dump4 > dump5

It unfortunately did not work.

Black Minou: avid listener, turned-up at the right time to watch the video and submit the flag.

Back to Home


Hackez la Rue! | © Hubert Hackin'' | 2024-05-27 | theme hugo.386