Home HITCON CTF 2016 Quals -- ROP
Post
Cancel

HITCON CTF 2016 Quals -- ROP

Category: Reverse
Points: 250

The challenge gave us a file call rop.iseq. By checking the file header, I found that it was a binary format of Ruby’s InstructionSequence.

By googling the InstructionSequence, I found that there are some new features were added into the ruby version 2.3, for example the load_from_binary method. We can actually use these methods to load the instruction sequence from a binary file, and disassemble the instruction to a human readable format.

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/env ruby

# read rop.iseq, dump InstructionSequence

f = open("rop.iseq", "rb")
a = f.read()
d = RubyVM::InstructionSequence.load_from_binary(a)

#d.eval #execute the instruction sequence
puts d.disasm # print out the disassemble result

If we execute the line d.eval, it will run the instruction sequence:

1
2
3
bruce30262@ubuntu:~/Desktop$ ruby ./de.rb 
AAAA
Invalid Key @_@

Looks like the program will read our input and do some checking, then output the checking result.

Anyway let’s dump the disassemble result and start reversing. Here’s the whole disassemble result.

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
== disasm: #<ISeq:<compiled>@<compiled>>================================
== catch table
| catch type: break  st: 0096 ed: 0102 sp: 0000 cont: 0102
| catch type: break  st: 0239 ed: 0245 sp: 0000 cont: 0245
|------------------------------------------------------------------------
local table (size: 3, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 3] k          [ 2] xs         
0000 trace            1                                               (   1)
0002 putself          
0003 putstring        "digest"
0005 opt_send_without_block <callinfo!mid:require, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0008 pop              
0009 trace            1                                               (   2)
0011 putself          
0012 putstring        "prime"
0014 opt_send_without_block <callinfo!mid:require, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0017 pop              
0018 trace            1                                               (   4)
0020 putspecialobject 3
0022 putnil        
................................................
............... lots of stuff....................
0056 opt_send_without_block <callinfo!mid:gets, argc:0, ARGS_SIMPLE>, <callcache>
0059 opt_send_without_block <callinfo!mid:chomp, argc:0, ARGS_SIMPLE>, <callcache>
0062 setlocal_OP__WC__0 3
0064 trace            1                                               (  39)
0066 getlocal_OP__WC__0 3
0068 putstring        "-"
0070 opt_send_without_block <callinfo!mid:split, argc:1, ARGS_SIMPLE>, <callcache>
0073 setlocal_OP__WC__0 2
0075 trace            1                                               (  40)
0077 getlocal_OP__WC__0 2
0079 opt_size         <callinfo!mid:size, argc:0, ARGS_SIMPLE>, <callcache>
0082 putobject        5
0084 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0087 branchif         94
................................................
............... lots of stuff....................

Google is our friend. I found a useful reference for introducing basic ruby instruction sequence reversing.

For example for the following iseq:

1
2
3
4
0000 trace            1                                               (   1)
0002 putself          
0003 putstring        "digest"
0005 opt_send_without_block <callinfo!mid:require, argc:1, FCALL|ARGS_SIMPLE>, <callcache>

trace 1 means “A new line of Ruby code has been encountered”. Then by reading the following lines, we know that the line of the code was probably require "digest".

And so we can try to reverse the whole iseq by following the similar pattern. First we found the code that read the user input:

1
2
3
4
# input = gets.chomp
0056 opt_send_without_block <callinfo!mid:gets, argc:0, ARGS_SIMPLE>, <callcache>
0059 opt_send_without_block <callinfo!mid:chomp, argc:0, ARGS_SIMPLE>, <callcache>
0062 setlocal_OP__WC__0 3

So local_OP__WC__0 3 will be our input. Now for the first check:

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
# input.split("-")
0064 trace            1                                               (  39)
0066 getlocal_OP__WC__0 3
0068 putstring        "-"
0070 opt_send_without_block <callinfo!mid:split, argc:1, ARGS_SIMPLE>, <callcache>

# input.split("-").size == 5
0073 setlocal_OP__WC__0 2
0075 trace            1                                               (  40)
0077 getlocal_OP__WC__0 2
0079 opt_size         <callinfo!mid:size, argc:0, ARGS_SIMPLE>, <callcache>
0082 putobject        5
0084 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0087 branchif         94

# if input.split("-").size != 5, call gg() (which print "Invalid key @_@")
0089 putself          
0090 opt_send_without_block <callinfo!mid:gg, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0093 pop             

# input.split("-").all? must be true
0094 trace            1                                               (  41)
0096 getlocal_OP__WC__0 2
0098 send             <callinfo!mid:all?, argc:0>, <callcache>, block in <compiled>
0102 branchif         109
0104 putself          
0105 opt_send_without_block <callinfo!mid:gg, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>

We can see that the valid key format must be something like “X-X-X-X-X”. Here I also found a sequence of iseq which help us infer the precise key format:

1
2
3
4
5
6
0000 trace            256                                             (  41)
0002 trace            1
0004 getlocal_OP__WC__0 2
0006 putobject        /^[0-9A-F]{4}$/ <-- here
0008 opt_regexpmatch2 <callinfo!mid:=~, argc:1, ARGS_SIMPLE>, <callcache>
0011 trace            512

So now we know that the key format is “XXXX-XXXX-XXXX-XXXX-XXXX”, while “X” is in the range of [0-9A-F]. Time to recover the valid key.

The checking of the first part of the key was pretty simple:

1
2
3
4
5
6
7
8
9
10
11
# local_OP__WC__0 2 = input.split("-"), let's call it key
0111 getlocal_OP__WC__0 2
0113 putobject_OP_INT2FIX_O_0_C_ 

# key[0].to_i(16) = 31337
0114 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0117 putobject        16
0119 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache>
0122 putobject        31337
0124 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0127 branchif         134

So key[0] is hex(31337) = 7A69 The checking of the second part of the key is even more simple:

1
2
3
4
5
6
7
8
# key[1].reverse == "FACE"
0136 getlocal_OP__WC__0 2
0138 putobject_OP_INT2FIX_O_1_C_ 
0139 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0142 opt_send_without_block <callinfo!mid:reverse, argc:0, ARGS_SIMPLE>, <callcache>
0145 putstring        "FACE"
0147 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0150 branchif         157

So key[1] = "FACE".reverse = ECAF.

To verify if key[0] and key[1] were the right value, we can actually use the following command to trace the ruby code: ruby -r tracer de.rb. If the key was correct, it would perform more checking, which means it will execute more line of code, so we can know if a part of the key was right or wrong by observing the trace of the ruby tracer ( kind of a side-channel analysis. )

Back to our recovering procedure. The checking of the key[2] looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# call f(217, key[2].to_i(16), 314159)
0160 putobject        217
0162 getlocal_OP__WC__0 2
0164 putobject        2
0166 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0169 putobject        16
0171 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache>
0174 putobject        314159
0176 opt_send_without_block <callinfo!mid:f, argc:3, FCALL|ARGS_SIMPLE>, <callcache>

# return_value.to_s(28).upcase should be "48D5"
0179 putobject        28
0181 opt_send_without_block <callinfo!mid:to_s, argc:1, ARGS_SIMPLE>, <callcache>
0184 opt_send_without_block <callinfo!mid:upcase, argc:0, ARGS_SIMPLE>, <callcache>
0187 putstring        "48D5"
0189 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0192 branchif         199

It will first call a method f, with argument (217, key[2].to_i(16), 314159), then check if its return value = 94449 ( with 28 as base, 48D5 is actually 94449 in base 10 )

method f was kind of complicated, so I will just post the pseudo code instead:

1
2
3
4
5
6
7
8
9
10
11
12
def f(two17, key2, pi)
    ret = 1
    v2 = two17
    while key2 != 0
        if key2[0] == 1 # the first bit of current key2
            ret = (ret*v2)%pi
        end
        key2 = key2>>1
        v2 = (v2*v2)%pi
    end
    return ret
end

Since we know that key[2]’s format is 0000 ~ FFFF, we can just crack key[2] by writing a simple crackme:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def f(two17, key2, pi)
    ret = 1
    v2 = two17
    while key2 != 0
        if key2[0] == 1 # the first bit of current key2
            ret = (ret*v2)%pi
        end
        key2 = key2>>1
        v2 = (v2*v2)%pi
    end
    return ret
end

for i in (0..0xffff)
    ret = f(217,i, 314159)
    if ret == 94449
        puts "got it!"
        puts i.to_s(16)
    end
end

And so we got the value of key[2]: 1BD2

Moving on to the next part (key[3]):

1
2
3
4
5
6
7
8
9
10
11
12
0201 getlocal_OP__WC__0 2
0203 putobject        3
0205 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0208 putobject        10
0210 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache>
0213 opt_send_without_block <callinfo!mid:prime_division, argc:0, ARGS_SIMPLE>, <callcache>
0216 putobject        :first
0218 send             <callinfo!mid:map, argc:0, ARGS_BLOCKARG>, <callcache>, nil
0222 opt_send_without_block <callinfo!mid:sort, argc:0, ARGS_SIMPLE>, <callcache>
0225 duparray         [53, 97]
0227 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0230 branchif         237

At first I was confused at line 0216 ~ 0218. There’s a :first for map, but the argc of map was actually 0. After doing some search on the internet, I found this post and found out that the check was actually doing:

1
2
b = key[3].to_i(10).prime_division.map &:first
b.sort == [53,97]

So the value of key[3] is 53*97 == 5141 ( base 10 )

At this point we know the valid key is 7A69-ECAF-1BD2-5141-XXXX. The checking of the last part of the key was also kind of complicated and I was kind of lazy to reverse the whole thing. So far we have the first four part of the key, and there’s only one left …… so why don’t we use the old typical brute force attack to recover the last one ? ;)

1
2
3
4
5
6
7
8
9
10
11
#!/usr/bin/env ruby
for i in (0..0xffff)
    key = "7A69-ECAF-1BD2-5141-%04X" % i
    cmd = "echo \"#{key}\"|ruby de.rb "
    puts cmd
    resp = `#{cmd}`
    if not resp.include?"Invalid"
        puts resp
        break
    end
end

And after about 20 minutes….

1
2
3
4
5
........................
echo "7A69-ECAF-1BD2-5141-CA70"|ruby de.rb
echo "7A69-ECAF-1BD2-5141-CA71"|ruby de.rb
echo "7A69-ECAF-1BD2-5141-CA72"|ruby de.rb
Congratz! flag is hitcon{ROP = Ruby Obsecured Programming ^_<}

Looks like I should brute force the key from 0xffff down to 0 though :P Anyway, the valid key is 7A69-ECAF-1BD2-5141-CA72, and so we got the flag !

flag: hitcon{ROP = Ruby Obsecured Programming ^_<}

This post is licensed under CC BY-SA 4.0 by the author.

HITCON CTF 2016 Quals -- flame

HITCON CTF 2016 Quals -- Hackpad

Comments powered by Disqus.