logo头像

Hacked By Swing

codegate-2020-2

CodeGate 两道简单题 (2)

reverse: RS

题目信息

题目给了一个文件 result 和一个 binary ,先看看 result 里边的内容:

1
2
$ ./rs
ef 43 4b 3f 5e b9 f0 d0 8c b5 7e 6f 7b c8 a6 7b 09 e2 61 9d 98 03 5f 56 5d 66 82 0b 9e 2b 76 92 5b c3 dc f2 3c d0 b6 81 60 34 a5 66 ca bd 7d 6a 00 fe e4 0b 44 e1 ba 81 cb ae 8b 24 0b a5 1f 6d ba 0e 61 1a 30 a7 77 51 23 41 a6 1a c0 7f 71 71 9f d5 93 e5 38 ce 52 8b 25 86 b3 12 b7 a7 1c 43 b4 08 81 47 ae d6 18 46 c5 6b 69 63 0b cc 95 ab 49 53 6f de be 2f 2e d9 9b dc dd 76 69 a4 f0 58

这个意思很明确了,那么我们也运行一下这个 binary 试试:

1
2
3
$ ./rs
thread 'main' panicked at 'Cannot open flag: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:999:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

这个意思熟悉 Rust 的同学应该可以一下意识到,不熟悉的看这个报错也差不多可以猜到,是缺少文件,所以我直接在本地建了一个名为 flag 的文件(如果不对还可以尝试 /flag, /home/ctf/flag 等路径),内容的话就写了 flag 然后看看输出结果:

1
2
$ ./rs
79 22 e2 6a 67 ec 4d 77 c1 95 cf cd ef d8 3d bd f1 28 5a 44 41 07 a8 64 91 4b 8a ed b9 fa d0 e6

那么看起来就是 flag 里的内容被加密了,这是加密的输出内容,然后加密的内容在当前目录的 flag 文件里。

这个时候我打算心存侥幸的尝试一下看看输出内容有没有一些规律,所以我又加密了 flagflagflagflag 的内容:

1
2
$ ./rs
40 c7 02 78 81 46 a7 a2 7c cf d8 83 87 9e 39 b8 de 49 15 8d 97 2b 1c f6 16 bf 53 f0 52 b6 42 ba c0 17 f4 6b f5 fb ea 79 c3 ee bc c8 4e 14 8d 87 35 d4 db 14 56 e8 2f 71 b1 a4 05 74 34 34 13 6f

可以看到出现了重复,这个时候心里基本有了点数了,接下来再尝试看看更改一个值会出现什么内容:

加密 flagflagflagflagf

1
2
$ ./rs
40 c7 02 78 81 46 a7 a2 7c cf d8 83 87 9e 39 b8 de 49 15 8d 97 2b 1c f6 16 bf 53 f0 52 b6 42 ba 3b cb f5 b6 15 8a e4 87 76 60 86 02 1d 5c 8b b5 fd a9 59 65 d4 f8 e8 05 09 2b 22 a1 89 8c f5 36

可以看到前半部分是没有发生变化的,但是最后发生变化了,说明整个加密过程是分块的,而且分块的结果没有办法直接通过不逆向进行猜测,统计规律丢失还是比较严重的(有点类似 hash ,更改了一个字母会导致这个块都发生变化,而不是只引起几个字母发生变化,这样的话就没办法通过不逆向直接获取了)。

所以接下来就要去进行逆向部分。

找到关键函数

程序是用 Rust 写的,并且已经被 strip 了,整个程序看起来很大,而且有许多是 Rust 的库函数,我们也没有太好的办法可以直接看出来是不是 Rust 的库函数,所以一个一个逆是肯定不可能的。

所以任务的第一步是找到输入位置和输出位置,从而找到真正进行加密的位置。

输入位置的获取通过在 read 函数下断点,可以得到输入的目标,也就是存储了 flag 中的明文数据的位置,需要注意的是,在逆向的时候一定要把 ASLR 关掉,如果在 linux 下,使用 gdb 如果有权限应该是会自动关掉的,否则后面下 watchpoint 就不好办了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(gdb) b read
(gdb) c

RAX 0xe
RBX 0x7fffffffddc0 ◂— 0x0
RCX 0xe
RDX 0xe
RDI 0x3
RSI 0x55555579db72 ◂— 0x0
R8 0x55555579db60 ◂— 'flagflagflagflagf\n'
R9 0x1
R10 0x30
R11 0x246
R12 0x7fffff7ff000 ◂— 0x0
R13 0x7fffff7fe000
R14 0x7fffffffe140 ◂— 0x0
R15 0x7ffff7f64b20 (sigaction) ◂— endbr64
RBP 0x0
RSP 0x7fffffffdcd8 —▸ 0x55555556a363 ◂— cmp rax, -1
RIP 0x7ffff7f63640 (read) ◂— endbr64

这个过程 c 了两次其实,然后就看到了字符串,接下来我需要找到什么地方使用了这个字符串,所以:

1
2
(gdb) rw *0x55555579db60
(gdb) c

接下来的逆向过程就比较枯燥了,通过查看每一个使用到的地方,去使用到的地方附近找,看有没有出现可疑的程序模式:

  1. 循环:因为程序是类似块加密的,既然有分块,就一定存在循环,而且程序存在改一个字母会整个块发生变化的情况,也说明应该存在循环的处理,所以如果找到循环的处理,说明这个位置很可疑
  2. 修改:如果对原位置进行了修改,就很可能是加密过程(不过最后在这个程序中没有出现在原位置上修改)
  3. 输出数据:如果看到输出数据出现了,那么即使没有找到程序位置,也可以通过下 watchpoint 查看对输出数据位置进行修改,就可以找到程序的关键位置了

这个的中间过程比较长,细节也很多,我也没有办法把每一个动作都记下来,总的来说,通过不断的调试跟踪,我找到了输出数据的位置,位于 0x55555579dc80 (关闭 ASLR),然后通过 watchpoint 找到了程序的加密函数位置,位于 0x97f0 ,其中有两个循环。

其中有一个逆向 Rust 程序的经验,Rust 的程序的参数如果是 Result<T, E> 这种,或者 Option<T> 这种,一般来说第一个参数是输出,也就是需要返回的 Result 或者 Option ,为了取出其中的数据,就会有很多额外的函数,这些函数没有太大的逆向意义,里边会有比较深的调用,但是仔细看会发现最终基本都会是一个函数直接返回其参数值或是返回其参数值位置的数据(类似于 return *a1 这样的),这些函数就可以直接认为是取出内容值就行了,不需要仔细去逆向,这样可以避免迷失在大量的函数当中。

找到关键函数之后就不难了,通过在循环里下断点观察循环的执行过程,然后就可以定位到两个关键函数,一个是位于 0x9b4b 的函数调用,其参数是取出的一个 key 和上一位的数据,另一个是位于 0x9b64 的函数调用,是一个异或操作。

所以整个算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
input: key[], chunk[](在初始状态下,chunk 中存储了 flag 的内容,后面的内容都为 \x00)

def op1(x, y):
val = 0
while i > 0:
if x & 1 > 0:
val ^= x
x *= 2
if x >= 0x100:
x ^= 0x11d
return val

def cipher(key, chunk(0x10 为一个chunk)):
for i in range(0x10):
for j in range(0x20):
chunk[i + j] = op1(key[j], chunk[i])
chunk[i + j] ^= chunk[i + j + 1]

return chunk[0x10:] (大小为0x20)

根据这个算法,首先 op1 函数类似于指数操作,所以可以通过爆破方式求”对数“,异或操作的一个 bug 在于每一个都需要额外的一个 byte ,而这个额外的 byte 始终是 0 ,所以相当于最后一个 byte 是可以恢复的,通过最后一个 byte 就可以恢复出 chunk[i] ,从而恢复出每一轮。

求解脚本

Ocaml 写的,运行需要 coreutop (utop 主要是用来当脚本运行,如果没有也可以编译运行),运行方式:utop -require core solve.ml

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
open Core

let key = [|
0x74; 0x40; 0x34;
0xae; 0x36; 0x7e; 0x10;
0xc2; 0xa2; 0x21; 0x21;
0x9d; 0xb0; 0xc5; 0xe1;
0x0c; 0x3b; 0x37; 0xfd;
0xe4; 0x94; 0x2f; 0xb3;
0xb9; 0x18; 0x8a; 0xfd;
0x14; 0x8e; 0x37; 0xac;
0x58;
|]

let op1 x y =
let init_val = ref 0 in
let x = ref x in
let y = ref y in
while !y > 0 do
if !y land 1 = 1 then begin
init_val := !init_val lxor !x;
end;
x := !x * 2;
if !x >= 0x100 then begin
x := !x lxor 0x11d;
end;
y := !y lsr 1;
done;
!init_val

let op1_rev res x =
let y = ref 0 in
try
for i = 0 to 0xff do
y := i;
if op1 x !y = res then begin
raise Exit
end
done;
raise (Failure "not found")
with
| Exit -> !y

let chunk_rev chunk =
let chunk = ref chunk in
let current_byte = ref 0 in
let chunk_len = Array.length !chunk in
let key_len = Array.length key in

for i = chunk_len - 1 downto 0 do
let final_key = 0x58 in
current_byte := op1_rev !chunk.(chunk_len - 1) final_key;
(*Printf.printf "current byte 0x%x\n" !current_byte;*)

for j = key_len - 1 downto 0 do
!chunk.(j) <- (op1 key.(j) !current_byte) lxor !chunk.(j);
done;

chunk := Array.append [|!current_byte|] !chunk;

(*
for j = 0 to chunk_len do
Printf.printf "0x%x " !chunk.(j);
done;

Printf.printf "\n";*)
done;
!chunk

let decode fmt =
String.split ~on:' ' fmt |>
List.map ~f:(fun x -> Int.of_string ("0x" ^ x)) |>
List.to_array

let hex x = Printf.sprintf "0x%x" x

let print_result arr =
Array.filter ~f:((<>) 0) arr |>
Array.iter ~f:(fun x -> Printf.printf "%c" (Char.of_int_exn x))

let print_arr arr =
Array.iter ~f:(fun x -> Printf.printf "0x%x " x) arr

let split_arr arr = Array.partitioni_tf arr ~f:(fun i x ->
if i < 32 then true else false)

let solve s =
let a = decode s in
let rec solve_part chunk rest =
if Array.length chunk > 0 then
chunk_rev chunk |> print_result;

if Array.length rest > 0 then
let chunk, rest = split_arr rest in
solve_part chunk rest;
in
solve_part [||] a

let cipher = "ef 43 4b 3f 5e b9 f0 d0 8c b5 7e 6f 7b c8 a6 7b 09 e2 61 9d 98 03 5f 56 5d 66 82 0b 9e 2b 76 92 5b c3 dc f2 3c d0 b6 81 60 34 a5 66 ca bd 7d 6a 00 fe e4 0b 44 e1 ba 81 cb ae 8b 24 0b a5 1f 6d ba 0e 61 1a 30 a7 77 51 23 41 a6 1a c0 7f 71 71 9f d5 93 e5 38 ce 52 8b 25 86 b3 12 b7 a7 1c 43 b4 08 81 47 ae d6 18 46 c5 6b 69 63 0b cc 95 ab 49 53 6f de be 2f 2e d9 9b dc dd 76 69 a4 f0 58"

let () = solve cipher