Reversing.kr - 2021 進度

Mon, Aug 23, 2021 7-minute read

題目:CHSARP, SimpleVM, PEPassword, Direct3D FPS

CHSARP

Challenge Info

CSharp.exe

Solution

是 32-bit .NET 的 flag checker

所以丟 DNSpy

會先將輸入 base64 encode,然後做檢查,動態追一下看到 check function

 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
using System;

// Token: 0x02000002 RID: 2
public class RevKrT2
{
	// Token: 0x06000001 RID: 1
	private static void MetM(byte[] key, byte[] flag_b64)
	{
		if (flag_b64.Length == 12)
		{
			key[0] = 2;
			if ((flag_b64[0] ^ 16) != 74)
			{
				key[0] = 1;
			}
			if ((flag_b64[3] ^ 51) != 70)
			{
				key[0] = 1;
			}
			if ((flag_b64[1] ^ 17) != 87)
			{
				key[0] = 1;
			}
			if ((flag_b64[2] ^ 33) != 77)
			{
				key[0] = 1;
			}
			if ((flag_b64[11] ^ 17) != 44)
			{
				key[0] = 1;
			}
			if ((flag_b64[8] ^ 144) != 241)
			{
				key[0] = 1;
			}
			if ((flag_b64[4] ^ 68) != 29)
			{
				key[0] = 1;
			}
			if ((flag_b64[5] ^ 102) != 49)
			{
				key[0] = 1;
			}
			if ((flag_b64[9] ^ 181) != 226)
			{
				key[0] = 1;
			}
			if ((flag_b64[7] ^ 160) != 238)
			{
				key[0] = 1;
			}
			if ((flag_b64[10] ^ 238) != 163)
			{
				key[0] = 1;
			}
			if ((flag_b64[6] ^ 51) != 117)
			{
				key[0] = 1;
			}
		}
	}
}

寫腳本解掉:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from base64 import b64decode

flag_b64 = [0] * 12
flag_b64[0] = 16 ^ 74
flag_b64[3] = 51 ^ 70
flag_b64[1] = 17 ^ 87
flag_b64[2] = 33 ^ 77
flag_b64[11] = 17 ^ 44
flag_b64[8] = 144 ^ 241
flag_b64[4] = 68 ^ 29
flag_b64[5] = 102 ^ 49
flag_b64[9] = 181 ^ 226
flag_b64[7] = 160 ^ 238
flag_b64[10] = 238 ^ 163
flag_b64[6] = 51 ^ 117

flag_b64 = b''.join([bytes([el]) for el in flag_b64])
print(b64decode(flag_b64).decode())

Flag

dYnaaMic


SimpleVM

Challenge Info

SimpleVM

Solution

Try

一開始 IDA 報錯,但實際上 entry point 沒有問題

把 segment size 改大,IDA 就不會判斷錯了

實際執行失敗,strace 發現會去 /lib/ld-linux.so.2, /usr/lib/libc.so.6 載入特定 library,所以建個 link 給它用

1
2
sudo ln -s /snap/core18/2074/lib/i386-linux-gnu/ld-linux.so.2 /lib/ld-linux.so.2
sudo ln -s /snap/core18/2074/lib/i386-linux-gnu/libc.so.6 /usr/lib/libc.so.6

缺 library 就裝一下

1
sudo apt-get install gcc-multilib

這時候就能成功執行,得到結果 Access Denied,索性用 sudo 執行後要求輸入東西,然後得到 Wrong

Analysis

一邊動態追,一邊用 IDA 把 code 弄好看一點,基本上是會在 0xc03000 開 rwx 空間,寫 shellcode,最後跳進去

0xc03000 dump 出來一樣丟 IDA,順手設個 base address(See Edit-->Segments-->Rebase program

shellcode 範圍大概是在 0xc01000~0xc04000 左右

sub_C03057readlink('/proc/self/exe', 0xffffc77c, 0x1000)

再追下去會進到 0x08048000~0x0804b000(RX),這邊就有好東西了

  • sub_8048556():根據權限、輸入的檢查結果輸出 'Access Denied\n', 'Error\n', 'Wrong\n', 'Correct!\n' 等訊息
  • check_8048C6D():對輸入進行檢查,基本上是利用 byte key_0804B0A0[200](前 8 bytes 為輸入)跟 int temp_0804B190[3] 做驗證(實際運行方式可以參考最後的腳本)

執行流程小亂,靜態逆 check_8048C6D() 感覺不會很舒服,所以把每次 switch case 時的 temp_0804B190 print 出來觀察:

 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
import gdb

open('out', 'w').write('')
p = open('out', 'a').write
cmd = gdb.execute
cmd('gef config context.enable 0')
cmd('set print inferior-events off')
cmd('file SimpleVM')


def gogo(data):
    open('input', 'w').write(data)
    cmd('b *0x00c01e46')
    cmd('r < input')
    cmd('b *0x00c030c9')
    cmd('c')
    cmd('b *0x08048c7a')

    i = gdb.inferiors()[0]
    cnt = 0
    while True:
        try:
            cmd('c')
            recv = i.read_memory(0x804B190, 12).tobytes()
            p(f'RECV: [{recv[0]}, {recv[4]}, {recv[8]}]\n')
            cnt += 1
        except:
            break
    cmd('d')
    p('\n')
    return cnt


gogo(input())
cmd('quit')

稍微做測試,會發現輸出基本上都長得像下面那樣,只有數字 33 的地方會隨著第一個字元變動,所以爆破第一個字元做測試,發現在輸入是 i 的時候,輸出會變 13 行,舒服

1
2
3
4
5
6
7
8
RECV: [10, 0, 0]
RECV: [2, 0, 0]
RECV: [6, 96, 96]
RECV: [2, 96, 33]
RECV: [7, 9, 9]
RECV: [9, 9, 0]
RECV: [6, 9, 2]
RECV: [11, 33, 0]

Brute Force

如果後面也是逐個字元做比對就能爆破 flag,事實上也真的是如此:

  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
137
138
139
140
141
from string import printable

data = open('init_key', 'rb').read() # key_0804B0A0
key = []
temp = []


def init(inp):
    global key, temp
    key = [el for el in data]
    temp = [0, 0, 0]

    for i, v in enumerate(inp):
        key[i] = ord(v)


def check():
    cnt = 0
    while True:
        t = next_temp()
        cnt += 1
        if t == 2:
            case_2()
        elif t == 6:
            case_6()
        elif t == 7:
            case_7()
        elif t == 9:
            case_9()
        elif t == 0xA:
            case_A()
        elif t == 0xB:
            case_B()
            if temp[0] != 0:
                return 48763
            else:
                return cnt
        else:
            return -1


def next_temp():
    temp[0] = key[key[9]]
    key[9] = key[9] + 1
    return temp[0]


def update_key():
    key[temp[0]] = temp[2]


def update_temp():
    temp[0] = key[temp[0]]


def update_key_9():
    key[9] = key[10] + temp[0]


def case_2():
    next_temp()
    temp[2] = temp[0]
    next_temp()
    temp[1] = temp[0]
    temp[0] = temp[2]
    temp[2] = temp[1]
    update_key()


def case_6():
    temp[2] = temp[0]
    t = temp[1]
    next_temp()
    t = temp[0]
    next_temp()
    update_temp()
    temp[1] = temp[0]
    temp[0] = t
    update_temp()
    temp[0] ^= temp[1]
    temp[2] = temp[0]
    temp[0] = t
    update_key()


def case_7():
    next_temp()
    update_temp()
    temp[2] = temp[0]
    next_temp()
    update_temp()
    temp[1] = temp[0]
    temp[2] = temp[2] == temp[0]
    temp[0] = 8
    update_key()


def case_9():
    temp[2] = temp[0]
    next_temp()
    temp[2] = temp[0]
    temp[0] = 8
    update_temp()
    if temp[0] == 0:
        temp[0] = temp[2]
        update_key_9()


def case_A():
    next_temp()
    update_key_9()


def case_B():
    temp[0] = 0
    update_temp()
    temp[2] = temp[0]
    temp[0] = 1
    update_temp()
    temp[1] = temp[0]
    temp[0] = temp[2]
    temp[2] = temp[1]


flag = ''
last = 0
while True:
    for el in printable:
        init(flag + el)
        recv = check()
        if recv == 48763:
            print(f'flag: {flag + el}')
            exit()
        if last <= 0:
            pass
        elif recv > last:  # curr char is flag
            flag += el
        elif recv < last:  # prev char is flag
            flag += chr(ord(el) - 1)

        last = recv

原始碼中 check_8048C6D() 執行前會把 key ^= 0x10,check_8048C6D() 會使用 xor 0x10 做 decode,所以可以直接去掉

這題的兩個腳本其實一個就夠了(做一樣的事情),但既然都寫了就放著作紀念XD

Flag

id3*ndh


PEPassword

Challenge Info

Original.exe, Packed.exe

Solution

Original.exe 執行結果如下

Packed.exe 被加殼過,實際執行有個輸入框但沒有提交按鈕之類的東西

丟 IDA 會解出幾個 function,索性全部下斷點,然後執行

發現在輸入框打字之後會撞到 sub_4091DA(),它會把輸入(esi)做 hash,結果存到 eax 中

接下來回到 0x040919C 比對 hash 值,如果一樣就會進到 sub_409200()0x401000 的實際內容解出來

這邊有兩個值得注意的地方:

  1. 0x401000 實際內容已知,就是 Original.exe
  2. 該 function 會先計算兩次 hash,並透過這兩個 hash value 解碼(第一次存 ebx, 第二次存 eax)

解碼流程簡單來說:每次用 eax 解 4bytes,然後用 ebx 更新 eax 的值。也就是說,只要有 eax 跟 ebx 就能順利解碼

eax 很好算 0x401000 解碼前後的 4 bytes xor 一下就有了

ebx 可以用爆破的,因為 eax 更新後的值也是已知,腳本如下:

 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
#include <stdio.h>

int main() {
    int origin_0 = 0x014cec81, origin_4 = 0x57560000;
    int packed_0 = 0xb6e62e17, packed_4 = 0x0d0c7e05;
    int _eax = origin_0 ^ packed_0;
    int _eax_next = origin_4 ^ packed_4;

    // brute force to get the first hash value which is stored in ebx
    for (int _ebx = 0; _ebx < 0x100000000; _ebx++) {
        int _ret = 0;
        asm volatile(
            "mov     %[_eax], %%eax\n\t"
            "mov     %[_ebx], %%ebx\n\t"
            "mov     %%al, %%cl\n\t"
            "rol     %%cl, %%ebx\n\t"
            "xor     %%ebx, %%eax\n\t"
            "mov     %%bh, %%cl\n\t"
            "ror     %%cl, %%eax\n\t"
            "mov     %%eax, %[_ret]"
            : [_ret] "=&r"(_ret)
            : [_eax] "r"(_eax), [_ebx] "r"(_ebx)

        );

        if (_ret == _eax_next) {
            printf("[possible] eax = 0x%X, ebx = 0x%X\n", _eax, _ebx);
        }
    }
    return 0;
}

結果如下,其中第二組是正確的

1
2
// [possible] eax = 0xB7AAC296, ebx = 0xA1BEEE22
// [possible] eax = 0xB7AAC296, ebx = 0xC263A2CB

動態改掉 eax, ebx 就得到 flag 了

還不錯的題目,順便練習 gcc 的 inline-assembly

Flag

From_GHL2_!!


Direct3D FPS

Challenge Info

data/, FPS.exe, D3DX9_43.dll

Solution

如同檔名,是一個 FPS 遊戲,FPS 會隨著擊殺怪物數量而提高,最後會到 78 左右

很久以前用 CE 亂戳的結果,直接把血量調成 0,可以秒殺怪物

也能鎖血

但把看得到的怪物殺光也沒 flag

丟 IDA 不難發現有個 game clear 的檢查

xref enc_flag_407028 可以找到一個 decode function,看起來是個簡單的 xor

所以遊戲啟動之後,CE attach 上去跑 lua script

1
2
3
4
5
flag = {}
for i=0, 49 do
    flag[i+1] = bXor(readBytes(0xFD7028 + i, 1), readBytes(0xFD9184 + 528 * i))
end
print(byteTableToString(flag))

得到 flag

1
Congratulation~ Game Clear! Password is Thr3EDPr0m 

Flag

Thr3EDPr0m