Info
Category: Reverse
Point: 250
Solver: bruce30262 @ BambooFox
Analyzing
題目給了我們一個叫做 rop.iseq
的檔案
查過 file header 之後,發現是 Ruby InstructionSequence 的 binary 格式。
Ruby 在 2.3 版本中針對 InstructionSequence 的部分加入了一些新功能,例如 load_from_binary 函式 可以讓使用者從一個 binary 檔案當中 load 進 InstructionSequence,並做進一步的操作 (ex. 執行指令, 印出 disassemble 的內容……等)
#!/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
上頭的 ruby script 就是一個從 binary 檔案當中 load 進 iseq 的例子。d.eval
可以讓我們執行這個 iseq:
bruce30262@ubuntu:~/Desktop$ ruby ./de.rb AAAA Invalid Key @_@
看來跟其他 reverse 的題目差不多,一樣是讀入 user input 之後做一連串的檢查,然後印出檢查的結果。這題看起來是要想辦法找出 valid key,通過檢查之後讓程式將 flag 給 print 出來。
接下來 dump 出 disassemble 的結果觀察一下 ( 內容有點大,這裡只列出部分的結果,完整的 disassemble 結果可以看這裡 )
== 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>
....................一堆東西..........
瘋狂 google 之後發現一篇有用的教學文,裡頭介紹了一些基礎的 ruby iseq reversing。例如以下的 iseq:
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
代表著 “我遇到了一行 ruby code”,接下來往下看幾行,我們大概可以知道,程式將 “digest” 視為一個參數,並且呼叫了require
,因此這行的 ruby code 應該就是在做 require "digest"
透過這樣子的 pattern ( 塞參數 –> call method ) 持續的 reverse 下去,我們其實就可以將整個程式的執行流程給還原回來。
此外,我們還可以利用 ruby -r tracer de.rb
這樣的執行方式,來讓 ruby 印出程式執行時的 trace。因為這題檢查 key 的方式是採取分段式的檢查,所以如果我們前半部的 key 有對的話,程式其實會執行到較多的指令,因此我們可以利用這樣子的方式(檢查 trace情形)來驗證我們的 key 有沒有解對 (類似 side channel attack)。
Solution
首先是 key 格式的檢查。這部分比較簡單,可以整理成以下的 pseudo code:
key = gets.chomp
key = key.split("-")
if (key.size == 5) and (key.all?)
for k in key
if not k =~ /^[0-9A-F]{4}$/
gg() # fail
end
end
else
gg() # fail
end
因此我們可以知道 key 的格式為 XXXX-XXXX-XXXX-XXXX-XXXX
,並且每個 X
的範圍都位於 [0-9A-F] 之間。接下來要還原 key 的每一個部分。
第一個部分的檢查其實很簡單:
# key[0]
0111 getlocal_OP__WC__0 2
0113 putobject_OP_INT2FIX_O_0_C_
0114 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
# key[0].to_i(16)
0117 putobject 16
0119 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache>
# key[0].to_i(16) == 31337
0122 putobject 31337
0124 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
所以就是 key[0]
的 hex value 要等於 31337,因此 key[0]
= "7A69"
第二部分 ( key[1]
) 的檢查更簡單:
# key[1]
0136 getlocal_OP__WC__0 2
0138 putobject_OP_INT2FIX_O_1_C_
0139 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
# key[1].reverse
0142 opt_send_without_block <callinfo!mid:reverse, argc:0, ARGS_SIMPLE>, <callcache>
# key[1].reverse == "FACE"
0145 putstring "FACE"
0147 opt_eq <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
key[1].reverse == "FACE"
,因此 key[1]
= "ECAF"
然後是第三部分 ( key[2]
) 的檢查:
# 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>
# f(217, key[2].to_i(16), 314159).to_s(28).upcase == "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
這邊開始變得比較複雜。首先程式會 call 函式f(217, key[2].to_i(16), 314159)
,其回傳值的 28 進位要等於 48D5 ( 換算成 10 進位的話是 94449 )。
而 f
這個函式做的事情有點複雜,這邊就只列 pseudo code:
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
這部分就是硬 reverse 努力得把 code 給看懂。
有了演算法之後就可以用爆的方式將 key[2]
給爆出來 ( 0x0000 ~ 0xffff, 最多跑 65536 次)
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
跑完之後即可得知 key[2]
= "1BD2"
接下來是第四部份 ( key[3]
) :
# key[3]
0201 getlocal_OP__WC__0 2
0203 putobject 3
0205 opt_aref <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
# key[3].to_i(10).prime_division
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>
# b = key[3].to_i(10).prime_division.map &:first
0216 putobject :first
0218 send <callinfo!mid:map, argc:0, ARGS_BLOCKARG>, <callcache>, nil
# b.sort == [53,97]
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>
這邊主要是困難在 key[3].to_i(10).prime_division.map &:first
這行,可以看到呼叫 map
時明明就有個 :first
參數,但是 map
的 argc 卻顯示為 0
。最後 google 到這篇才了解到原來還有 XXX.map &:first
這種寫法。
這邊 prime_division
是在做質因數分解,看到結果要等於 [53,97] 的時候其實就可以猜 key[3]
= 53*97
= 5141
了。原本 5141 做 prime_division 時會變成 [ [53,1] , [97,1] ]
, 意即 (53^1 * 97^1)
,不過因為 &:first
的關係,只會取到第一個元素,也就是 53 跟 97,符合 key[3]
的條件。雖然答案其實是 (53^n * 97^m)
,不過考慮到數值最大只到 65536,因此符合條件的只有 n=1, m=1
,所以可以得知 key[3]
= 5141
到了這邊其實我已經沒力了我們已經知道這題的 valid key 為 7A69-ECAF-1BD2-5141-XXXX
。第五部分 ( key[4]
) 的檢查由於很複雜的關係 ( 用了一些很怪的語法 ),加上其實只剩下最後一個部分的 key 沒有解出來,所以這邊我直接採取暴力破解的方式來找出 key 的最後一部分 ( 反正最多也就跑 65536 次 =w= ):
#!/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
放著讓它跑個 20 分鐘:
........................ 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 ^_<}
拿到 flag 了 :P ( 看來應該要從 0xffff 往下爆的,應該會更快 XD )
valid key: 7A69-ECAF-1BD2-5141-CA72
flag: hitcon{ROP = Ruby Obsecured Programming ^_<}