SecuInside CTF 2016 - Cykor_00002 CGC Challenge

Reading time ~8 minutes

Was pretty surprised to see CGC challenges on the SecuInside CTF this year so I got involved with these. Dusted off my CGC vagrant VM from Defcon earlier this year and went to town. This second challenge was a bit more in depth than Cykor_0001 so I’ll write this one up instead.

Firstly I just downloaded the binary and ran it in my CGC VM. It seemed to be pretty straightforward:

vagrant@crs:~$ uname -a
Linux crs 3.13.11-ckt21-cgc #1 SMP Mon Feb 29 16:42:11 UTC 2016 i686 GNU/Linux
vagrant@crs:~$ file cykor_00002
cykor_00002: CGC 32-bit LSB executable, (CGC/Linux)
vagrant@crs:~$ ./cykor_00002
--------------
- Simple Echo System -
--------------
What is your name?
name: Example
Hi Example
: ABCDEF
ABCDEF

Ok. Generally for CGC challenges in my experience you need to exploit them so a quick bit of fuzzing to see if any of these obvious inputs result in a segfault is generally my first quick step. In this case none of these inputs crash at any length that I tried. So onto the next step of reversing the binary.

Firstly I make a copy of the binary, then convert it into an ELF binary with cgc2elf. Once we have that we can more easily load it into IDA Pro. We do that and see the following main function which handles the interesting code path:

int __cdecl main(int argc, const char **argv, const char **envp)
{
char v3; // ST04_1@1
char v4; // ST04_1@1
char v5; // ST04_1@1
char v6; // ST04_1@3
char v7; // ST04_1@9
char v9; // [sp+4h] [bp-4B4h]@0
char v10; // [sp+4h] [bp-4B4h]@1
char v11; // [sp+4h] [bp-4B4h]@2
char name_buffer; // [sp+70h] [bp-448h]@1
char echo_buffer; // [sp+B0h] [bp-408h]@3
char v14; // [sp+B1h] [bp-407h]@4
int v15; // [sp+4B0h] [bp-8h]@1</p> 

v15 = 0;
transmit("--------------n", v9);
transmit("- Simple Echo System -n", v3);
transmit("--------------n", v4);
transmit("What is your name?n", v5);
receive(&name_buffer);
if ( !strcmp(&name_buffer, "ADMIN", 5u) )
{
transmit("+ Gimme a key : ", v10);
get_input(&key_storage, 27u, 10);
if ( validate_key() )
{
transmit("Welcome Admin :)n", v11);
transmit(": ", v6);
get_input(&echo_buffer, 1000u, 10);
if ( echo_buffer == 'C' && v14 == 'K' )
v0 = 10;
transmit("%sn", &echo_buffer);
}
else
{
transmit("Get out of here :(n", v11);
}
}
else
{
transmit("Hi %sn", &name_buffer);
transmit(": ", v7);
*(&echo_buffer + get_input(&echo_buffer, 1000u, 10)) = 0;
transmit("%sn", &echo_buffer);
}
return 0;
}

So here we learn of an alternate code path when your name is “ADMIN”. It takes a 26 byte key then validates the key and if so gives you the ability to reach this suspicious code section:

if ( echo_buffer == 'C' && v14 == 'K' )
v0 = 10;

Why is that interesting? Let’s get there and find out shall we. To do that we need to know a valid key. Let’s look at the validate_key() function:

_BOOL4 sub_8048150()
{
signed int v1; // [sp+0h] [bp-4h]@1

v1 = 0;
if ( byte_805F468
+ byte_805F457
+ byte_805F460
+ byte_805F469
+ byte_805F46B
+ byte_805F45B
+ byte_805F455
+ byte_805F45A
+ byte_805F45D
+ byte_805F465
+ byte_805F45F
+ byte_805F466
+ byte_805F459 == 1068 )
v1 = 1;
if ( byte_805F45E
+ byte_805F45B
+ byte_805F45D
+ byte_805F465
+ byte_805F466
+ byte_805F46A
+ byte_805F456
+ byte_805F455
+ byte_805F461
+ byte_805F463 == 760 )
++v1;
if ( byte_805F462
+ byte_805F455
+ byte_805F45D
+ byte_805F464
+ key_storage
+ byte_805F461
+ byte_805F463
+ byte_805F45F
+ byte_805F460
+ byte_805F468
+ byte_805F459
+ byte_805F46A
+ byte_805F469 == 997 )
++v1;
...
  

It’s actually a 290 line set of if() statements. Essentially what we have here is a set of linear equations that reference the static memory locations of our key bytes. Each equation must be simultaneously satisfied for our key to be the correct one. Sometimes you can use an online linear equation solver but for this case its 26 equations. So I wrote some Python to emit a z3 script to solve it for me.

It operates on this file which is the copy+pasted IDA pro disassembly cleaned up a bit. Here’s that code:

#!/usr/bin/python

bytelist = [x.strip() for x in open('bytes.txt','r').readlines()] 

lin = open('linear_eqs.py','w')
lin.write("#!/usr/bin/pythonnfrom z3 import *n")

for i in range(26):
lin.write("var_"+str(i)+" = Int('var_"+str(i)+"')n")

lin.write("solve(")

def replacelabel(bytenum):
firstbyte = 0x805F454
thisbyte = int(bytenum.split("_")[1],16)
intnum = thisbyte - firstbyte
return("var_" + str(intnum))

block = [] for line in bytelist:
if "=" in line:
block.append(replacelabel(line.split()[0]))
lin.write(' + '.join(block)+" == "+line.split()[2]+",")
block = [] elif "byte_" in line:
block.append(replacelabel(line))

lin.write(")")
lin.close()
  

It emits something like this:

#!/usr/bin/python
from z3 import *
var_0 = Int('var_0')
var_1 = Int('var_1')
var_2 = Int('var_2')
var_3 = Int('var_3')
var_4 = Int('var_4')
var_5 = Int('var_5')
var_6 = Int('var_6')
var_7 = Int('var_7')
var_8 = Int('var_8')
var_9 = Int('var_9')
var_10 = Int('var_10')
var_11 = Int('var_11')
var_12 = Int('var_12')
var_13 = Int('var_13')
var_14 = Int('var_14')
var_15 = Int('var_15')
var_16 = Int('var_16')
var_17 = Int('var_17')
var_18 = Int('var_18')
var_19 = Int('var_19')
var_20 = Int('var_20')
var_21 = Int('var_21')
var_22 = Int('var_22')
var_23 = Int('var_23')
var_24 = Int('var_24')
var_25 = Int('var_25')
solve(var_20 + var_3 + var_12 + var_21 + var_23 + var_7 + var_1 + var_6 + var_9 + var_17 + var_11 + var_18 + var_5 == 1068,var_10 + var_7 + var_9 + var_17 + var_18 + var_22 + var_2 + var_1 + var_13 + var_15 == 760,var_14 + var_1 + var_9 + var_16 + var_0 + var_13 + var_15 + var_11 + var_12 + var_20 + var_5 + var_22 + var_21 == 997,var_4 + var_8 + var_19 + var_21 + var_1 + var_6 + var_23 + var_13 + var_16 + var_12 == 782,var_19 + var_10 + var_18 + var_16 + var_13 + var_2 + var_7 + var_6 + var_20 + var_14 == 778,var_20 + var_19 + var_5 + var_9 + var_4 + var_14 + var_22 + var_21 + var_3 + var_24 + var_12 + var_23 + var_18 + var_13 == 1123,var_22 + var_8 + var_5 + var_13 + var_15 + var_11 + var_7 + var_10 + var_1 + var_18 + var_0 + var_14 + var_19 + var_2 + var_23 == 1180,var_5 + var_24 + var_11 + var_23 + var_19 + var_22 + var_0 + var_4 + var_3 + var_8 + var_13 + var_20 + var_18 + var_2 + var_9 + var_17 + var_7 + var_12 == 1498,var_18 + var_23 + var_14 + var_4 + var_24 + var_1 + var_0 + var_21 + var_5 + var_16 + var_7 + var_12 + var_15 + var_20 + var_3 == 1213,var_17 + var_21 + var_9 + var_10 + var_6 + var_14 + var_20 + var_1 + var_8 + var_24 == 779,var_0 + var_3 + var_19 + var_2 + var_23 + var_22 + var_15 + var_20 + var_4 == 742,var_15 + var_23 + var_19 + var_6 + var_17 + var_10 + var_8 + var_4 + var_5 + var_11 + var_1 + var_9 + var_14 + var_3 + var_20 == 1196,var_6 + var_1 + var_8 + var_13 + var_11 + var_24 + var_12 + var_21 + var_18 + var_23 + var_14 + var_15 + var_22 + var_17 == 1091,var_21 + var_17 + var_22 + var_0 + var_4 + var_1 + var_18 + var_19 + var_12 == 764,var_13 + var_16 + var_22 + var_1 + var_11 + var_24 + var_17 + var_14 + var_10 + var_4 + var_8 + var_7 + var_0 + var_18 + var_6 + var_21 + var_20 + var_9 == 1463,var_15 + var_8 + var_1 + var_11 + var_21 + var_19 == 465,var_16 + var_18 + var_11 + var_24 + var_4 + var_19 + var_12 + var_1 + var_6 + var_13 + var_0 + var_21 == 955,var_6 + var_7 + var_21 + var_18 + var_0 + var_9 + var_14 + var_17 == 654,var_1 + var_2 + var_16 + var_12 + var_23 + var_0 + var_10 + var_6 + var_20 + var_18 + var_22 + var_7 + var_15 == 1030,var_22 + var_23 + var_11 == 275,var_21 + var_3 + var_1 + var_20 + var_0 + var_8 + var_12 == 563,var_14 + var_5 + var_11 + var_18 + var_17 + var_24 == 509,var_19 + var_10 + var_12 + var_21 + var_16 + var_24 + var_14 == 556,var_1 + var_12 + var_21 + var_19 + var_18 + var_2 + var_3 + var_11 + var_15 + var_23 + var_22 + var_17 + var_24 + var_8 + var_5 + var_10 + var_20 + var_16 == 1464,var_14 + var_21 + var_7 + var_22 + var_20 + var_5 + var_16 + var_10 + var_11 == 758)

Which, if run with the z3 module installed outputs the following solution:

vagrant at vagrant-ubuntu-trusty-64 in ~
$ ./cykor2.py
[var_16 = 89,
var_23 = 85,
var_20 = 82,
var_1 = 72,
var_6 = 69,
var_12 = 77,
var_15 = 51,
var_19 = 85,
var_22 = 95,
var_21 = 78,
var_5 = 77,
var_14 = 78,
var_11 = 95,
var_9 = 72,
var_3 = 87,
var_10 = 69,
var_17 = 95,
var_4 = 95,
var_7 = 95,
var_8 = 84,
var_2 = 79,
var_13 = 48,
var_18 = 84,
var_24 = 80,
var_0 = 83] 

Which turn out to be decimal integer representations of ASCII characters which, after decoding and re-ordering spell: SHOW_ME_THE_M0N3Y_TURN_UP

So now we have a valid key, we can set our echo buffer to the chars “CK” and see what happens?

vagrant@crs:~$ ./cykor_00002
--------------
- Simple Echo System -
--------------
What is your name?
name: ADMIN
+ Gimme a key : SHOW_ME_THE_M0N3Y_TURN_UP
Welcome Admin <img src="https://ctf.rip/images/classic-smilies/icon_smile.gif" alt=":)" class="wp-smiley" style="height: 1em; max-height: 1em;" />
: CK
Segmentation fault

A crash. Nice. Is it exploitable? It doesn’t matter. The difference between SecuInside and Defcon CGC challenges is the POV bar is much lower for SecuInside. We just need to provide proof of segfault here so this is enough. We craft the following POV XML file:

<?xml version="1.0" standalone="no" ?>
<pov> <cbid>service</cbid>
<replay>
<read><delim>x0a</delim><match><data>--------------x0a</data></match></read>
<read><delim>x0a</delim><match><data>- Simple Echo System -x0a</data></match></read>
<read><delim>x0a</delim><match><data>--------------x0a</data></match></read>
<read><delim>x0a</delim><match><data>What is your name?x0a</data></match></read>
<read><delim> </delim><match><data>name: </data></match></read>
<write><data>ADMINx0a</data></write>
<read><delim>: </delim><match><data>+ Gimme a key : </data></match></read>
<write><data>SHOW_ME_THE_M0N3Y_TURN_UPx0a</data></write>
<read><delim>x0a</delim><match><data>Welcome Admin :)x0a</data></match></read>
<read><delim>: </delim><match><data>: </data></match></read>
<write><data>CKx0a</data>
</write>
</replay> </pov> 

The submit it with a simple Python submitter:

#!/usr/bin/python

from pwn import *

powfile = "pov.xml"
host, port = "cgc.cykor.kr", 34523
conn = remote(host,port)
print conn.recvline()
conn.sendline("XML")
powdata = open(powfile,'rb').read()
print conn.recvline()
conn.sendline(str(len(powdata)))
print conn.recvline()
conn.sendline(powdata)
conn.interactive()

And we get the flag!

[+] Opening connection to cgc.cykor.kr on port 34523: Done
What type of your PoV? (BIN / XML) [*] POW: pov.xml
[*] POW len: 1018
How many bytes is your XML?
Ok.... send it 
[*] Switching to interactive mode
Successfully received
# package: binutils-cgc-i386: (installed: True) version: 2.24-9735-cfe-rc5
...
# tests passed: 11
# tests failed: 0
# total tests passed: 11
# total tests failed: 0
# polls passed: 1
# polls failed: 0
ok - process cored as expected: (signal 11: SIGSEGV)
The flag is: Wh0ooooo you, are you talking me?
[*] Got EOF while reading in interactive
$

Interviewing in Tech: Security Engineer & Security Analyst

Landing a job as a security engineer or analyst at a tech company is a significant feat. It requires not only technical acumen but also s...… Continue reading

BSides Sydney 2023 Writeups

Published on November 24, 2023

DUCTF 2023 Writeups

Published on August 31, 2023