NSEC21 dontstopthefuzzing - Mon, May 24, 2021 - Lucas Bajolet
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:
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
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:
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:
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.
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.
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
.
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.
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
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 is0x2cc3
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?
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.