wannaShare | Writeups UIUCTF 2021 | RE + vaudeville + pawnfish

PHAPHA_JIàN
20:46 06/08/2021

CLB An toàn Thông tin Wanna.One chia sẻ một số Challenges giải được và việc chia sẻ writeup nhằm mục đích giao lưu học thuật. Mọi đóng-góp ý-kiến bọn mình luôn-luôn tiếp nhận qua mail: wannaone.uit@gmail.com hoặc inseclab@uit.edu.vn và fanpage: fb.com/inseclab.


Mochi Nishi

vaudeville

Description

Info: The Dramatis Personae invite you to join in this latest production. nc vaudeville.chal.uiuc.tf 1337 File: vaudeville

TL;DR: Analyze the binary => we can see that it will give us a number and we must return an integer which is the answer. Analyze sub_3660(x, y) => Some sort of encrypting function. Deal with functions like fork() and pipe() => create a child process to give the parent a string and save the length of it. Goal: understanding sub_3660(x, y), write a decrypt script to find the input and submit to server to get the flag.

Deep Analyze

Here’s the main function:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __int64 v3; // rbp
  int v4; // ebx
  unsigned int v6; // [rsp+4h] [rbp-24h] BYREF
  unsigned __int64 v7; // [rsp+8h] [rbp-20h]
  v7 = __readfsqword(0x28u);
  v3 = (unsigned int)sub_35F0(a1, a2, a3);
  __printf_chk(1LL, "Challenge: %u\n", v3);
  __printf_chk(1LL, "Response: ");
  fflush(stdout);
  __isoc99_scanf("%u", &v6);
  fflush(stdin);
  v4 = sub_3660(v6, 7);
  if ( v4 == (unsigned int)sub_3660(v3, 140) )
    sub_3DD0();
  else
    puts("that's rough buddy");
  return 0LL;
}

Static analyzing it, I know that v3 is the challenge’s number, then it reads our input v6 and do some encryption related to v3, if it satisfies the condition, we got the flag. So we have to dive into sub_3660(x, y)

  v2 = 0LL;
  v50 = __readfsqword(0x28u);
  v47 = 0;
  ptr[0] = (__int128)_mm_load_si128((const __m128i *)&xmmword_C450);
  ptr[1] = ptr[0];
  v45[0] = ptr[0];
  v45[1] = ptr[0];
  v46[0] = ptr[0];
  v46[1] = ptr[0];
  do
  {
    *((_BYTE *)v45 + v2) = ((a1 & (1 << v2)) != 0) + 48;
    ++v2;
  }
  while ( v2 != 32 );
  memset(filename, 0, sizeof(filename));
  __sprintf_chk(filename, 1LL, 128LL, "/tmp/tmp_%u", a1);
  stream = fopen(filename, "wb");
  setvbuf(stream, 0LL, 2, 0LL);
  fwrite(ptr, 1uLL, 0x60uLL, stream);
  fflush(stream);
  v3 = fopen(filename, "rb");
  v4 = fopen(filename, "rb");
  setvbuf(v3, 0LL, 2, 0LL);
  setvbuf(v4, 0LL, 2, 0LL);

First part of the function, it will pre-init data base on our input, I’ll call it data, and data looks like this: "0" * 32 + (binary form of our input in reverse) + "0" * 32. Then it creates folder /tmp/tmp_(our input) and write that pre-init data to it. We can easily check it by putting breakpoint after the fwrite() and cat /tmp/tmp_%u

v39 = 0;
LABEL_5:
    fseek(v3, 32LL, 0);
    fseek(v4, 19LL, 0);
    v5 = v45;
    do
    {
      fread(&v40, 1uLL, 1uLL, v3);
      fread(&v41, 1uLL, 1uLL, v4);
      v6 = v41;
      v7 = v40;
      if ( pipe(&v42) )
      {
        fwrite("Pipe failed.\n", 1uLL, 0xDuLL, stderr);
        v11 = -1;
      }
      else
      {
        v8 = fork();
        if ( !v8 )
        {
          close(v42);
          v31 = dup(fd);
          v32 = fdopen(fd, "w");
          v33 = fdopen(v31, "w");
          fwrite(*(&off_E020 + v7), 1uLL, v7, v32);
          v34 = v6;
          v35 = *(&off_E020 + v6);
          goto LABEL_28;
        }
        if ( v8 < 0 )
        {
LABEL_33:
          fwrite("Fork failed.\n", 1uLL, 0xDuLL, stderr);
          exit(-1);
        }
        close(fd);
        v9 = fdopen(v42, "r");
        v10 = fread(v49, 1uLL, 0x200uLL, v9);
        fclose(v9);
        v11 = v10;
      }
      *(_BYTE *)v5 = v11;
      v5 = (__int128 *)((char *)v5 + 1);
    }
    while ( v46 != v5 );

This is a very interesting part in our functions:

If you look closely, you’ll see that it stores a 256 strings, sort by their lengths, from 0 to 255 :D. So basically the above code just take two chars, add them together and put it back to data. Interestingly, the rest of the code in this function will encrypt the data similarly to previous code blocks, and it will loop a2 times - second argument in the function. Now we know what this binary do: the challenge asks us to give a number so that this number after encrypt 7 times, will equals to the challenge’s number encrypted 140 times.

Final Step

I wrote a small script that emulate the encryption part

def encrypted(li):
    id1 = 32
    id2 = 19
    or_li = [i for i in li]
    for oo in range(32):
        li[id1] = (or_li[id1] + or_li[id2]) & 0xff
        id1 += 1
        id2 += 1
    id1 = 32
    id2 = 49
    or_li = [i for i in li]
    for oo in range(32):
        li[id1] = (or_li[id1] + or_li[id2]) & 0xff
        id1 += 1
        id2 += 1
    id1 = 32
    id2 = 27
    or_li = [i for i in li]
    for oo in range(32):
        li[id1] = (or_li[id1] + or_li[id2]) & 0xff
        id1 += 1
        id2 += 1
    return li

Now all we have to do is reverse this encryption. Here’s the decrypt script:

def decrypted(li):
    id = 32
    id1 = 32
    id2 = 27
    for oo in range(32):
        li[id1] = (li[id1] - li[id2]) & 0xff
        id1 += 1
        id2 += 1
    id1 = 63
    id2 = 80
    for oo in range(32):
        li[id1] = (li[id1] - li[id2]) & 0xff
        id1 -= 1
        id2 -= 1
    id1 = 32
    id2 = 19
    for oo in range(32):
        li[id1] = (li[id1] - li[id2]) & 0xff
        id1 += 1
        id2 += 1
    return li

Put everything together, we have the script:

val = int(input())
# val = 1501756717
def convert(v):
    t = ''
    while v != 0:
        t += str(v & 1)
        v >>= 1
    t += '0' * (32 - len(t))
    return t
def encrypted(li):
    id1 = 32
    id2 = 19
    or_li = [i for i in li]
    for oo in range(32):
        li[id1] = (or_li[id1] + or_li[id2]) & 0xff
        id1 += 1
        id2 += 1
    id1 = 32
    id2 = 49
    or_li = [i for i in li]
    for oo in range(32):
        li[id1] = (or_li[id1] + or_li[id2]) & 0xff
        id1 += 1
        id2 += 1
    id1 = 32
    id2 = 27
    or_li = [i for i in li]
    for oo in range(32):
        li[id1] = (or_li[id1] + or_li[id2]) & 0xff
        id1 += 1
        id2 += 1
    return li
def decrypted(li):
    id = 32
    id1 = 32
    id2 = 27
    for oo in range(32):
        li[id1] = (li[id1] - li[id2]) & 0xff
        id1 += 1
        id2 += 1
    id1 = 63
    id2 = 80
    for oo in range(32):
        li[id1] = (li[id1] - li[id2]) & 0xff
        id1 -= 1
        id2 -= 1
    id1 = 32
    id2 = 19
    for oo in range(32):
        li[id1] = (li[id1] - li[id2]) & 0xff
        id1 += 1
        id2 += 1
    return li
payload = '0' * 32 + convert(val) + '0' * 32
li = [ord(i) for i in payload]
times = 7
for i in range(140):
    li = encrypted(li)
for i in range(7):
    li = decrypted(li)
de = li
ans = 0
id = 32
for i in range(32):
    ans += ((de[i + id] & 1) << i)
print(ans)

Submit the answer and we get the flag. uiuctf{b0rne_4ward_c3as313ss1y_1nt0_teh_f-u-t-u-r-e_*dab*}

pawnfish

Description

Info: Fishes are pretty good at chess. nc pawnfish.chal.uiuc.tf 1337 File: handout.tar.gz

TL;DR: Look around the source code => We have to win 100 times and the current_player is Black to get the flag. wins variable is signed char => We can lose a lot in order to make wins equals to 100, but what about current_player? We have to win a game to turn current_player to Black. The file also use stockfish engine => Can’t win easily, we have to find some bugs and take those advantages to win. Find En Passant bugs => Find a way to win, put all the things together to get the flag.

Plan

First of all, I think this is more like a Misc than RE challenge, because you need quite chess knowledges to solve this challenge. The file gave us source code and some files related to chess stuff, so I just check chal.c. This is a very long source code, I immidiately check the condition to get the flag:

if (current_player == Black) {
    FILE* flag;
    int c;
    flag = fopen("flag.txt", "r");
    if (flag) {
        while ((c = getc(flag)) != EOF) {
            putchar(c);
        }
        putchar('\n');
        fclose(flag);
    } else {
        puts("Flag is missing :/");
    }
} else {
    puts("How is this possible?? You didn't win the 100th game!");
    puts("I won't stand a cheater!!!");
    exit(96);
}

And looking around main functions. I know that we also need to make wins equals 100 to escape the while loop and come to our check condition:

int main() {
...
while (wins != 100 || current_player == White) {
...
if (current_player == Black) {
    FILE* flag;
    int c;
    flag = fopen("flag.txt", "r");
    if (flag) {
        while ((c = getc(flag)) != EOF) {
            putchar(c);
        }
        putchar('\n');
        fclose(flag);
    }
...

For the wins condition, we just need to lose a lot, enough to make the wins equals to 100, that’s the easy part. The hard part is to make current_player equals to Black. In order to do that, we have to win a game. But this is stockfish engine, you need Hikaru Nakamura to defeat this to get us the flag. After a while, I think there has to be some bugs around…

The Bug

… and indeed, there is. After spending a lot of time to debug, my teammate find the En Passant flaw: the En Passant is meant to apply just for pawn. But for some weird reasons, this program applies En Passant to literally every chess pieces.

This is huge, we can slowly build our way to defeat this engine. After several tries, we finally win one game, and using everything we’ve known so far to write a script:

from pwn import *
p = remote('pawnfish.chal.uiuc.tf', 1337)
def send_piece(mov1, mov2):
    p.recv()
    p.sendline(mov1)
    p.recv()
    p.sendline(mov2)
for i in range(157):
    print(i)
    send_piece('e2','e4')
    send_piece('e1','e2')
    send_piece('e2','e3')
    send_piece('e3','f4')
    send_piece('f4','e5')
    p.recvuntil('lose')
    p.recvline()
send_piece('e2','e4')
send_piece('c2','c3')
send_piece('e4','e5')
send_piece('e5','d6')
send_piece('g1','f3')
send_piece('d2','d4')
send_piece('c3','d4')
send_piece('d4','e5')
send_piece('f1','e2')
send_piece('e1','g1')
send_piece('e5','d6')
send_piece('h2','h3')
send_piece('e2','f3')
send_piece('d6','c7')
send_piece('f1','e1')
send_piece('c1','e3')
send_piece('d1','e2')
send_piece('e3','c5')
send_piece('e2','e7')
send_piece('e1','e7')
send_piece('e7','a7')
send_piece('a2','a4')
send_piece('a4','b5')
send_piece('b1','c3')
send_piece('b5','b6')
send_piece('b6','b7')
send_piece('a7','a8')
send_piece('f3','b7')
send_piece('a1','b1')
send_piece('b2','b4')
send_piece('c3','a2')
send_piece('b4','b5')
send_piece('b5','b6')
send_piece('a8','a7')
send_piece('a2','c3')
send_piece('c3','d5')
send_piece('d5','e7')
send_piece('e7','c6')
send_piece('b6','b7')
send_piece('h3','g4')
send_piece('g4','f5')
send_piece('b7','b8')
send_piece('b1','b8')
send_piece('g1','h2')
send_piece('b8','f8')
send_piece('f5','f6')
send_piece('h2','h3')
send_piece('g2','g4')
send_piece('h3','g3')
send_piece('c6','e7')
send_piece('f8','d8')
send_piece('a7','c7')
send_piece('e7','d5')
send_piece('d8','b8')
send_piece('f6','f7')
send_piece('c7','a7')
p.interactive()

And the answer:

$ python3 solve.py
[+] Opening connection to pawnfish.chal.uiuc.tf on port 1337: Done
0
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
[*] Switching to interactive mode
Location to > Moving WHITE ♖ from row: 1, col: 2, TO row: 1, col: 0
You win :)
uiuctf{strange_the_only_winning_move_is_to_lose}

uiuctf{strange_the_only_winning_move_is_to_lose}

pivik

pawnfish

Dù là RE nhưng mình vẫn thấy nó giống pwn hơn.

Source của challenge mình sẽ để ở đây.

Vì source code khá dài nên mình chỉ phân tích những chỗ cần thiết.

Chương trình mô phỏng trò chơi cờ vua, nếu win đủ 100 lần thì sẽ in flag. Nhưng với trình độ cờ của bản thân mình thì chơi tới năm sau cũng không thắng nổi 🙁

Nên thôi ngồi kiếm bug vậy. Và bug đầu tiên mình thấy được là ở kiểu giá trị của biến wins:

int main() {
...
signed char wins = 0;
...

Biến wins có kiểu là signed char nên mình hoàn toàn có thể thua 128 lần để lần thua sau đó overflow thành số dương. Lúc đầu mình nghĩ nếu có thể overflow thì overflow về 100 là xong, easy game right?

Nhưng đời không như là mơ, để nó in ra flag ngoài việc số lần win là 100 thì current_player cần phải là Black.

int main() {
...
while (wins != 100 || current_player == White) {
...
if (current_player == Black) {
        FILE* flag;
        int c;
        flag = fopen("flag.txt", "r");
        if (flag) {
            while ((c = getc(flag)) != EOF) {
                putchar(c);
            }
            putchar('\n');
            fclose(flag);
        }
...

current_player chỉ đổi sang Black khi mình win và sẽ đổi trở lại thành White khi bắt đầu ván mới, đồng nghĩa với việc làm gì cũng được nhưng ván 100 phải là win. Đến đây thôi thì vẫn chưa đủ để viết exploit, nên mình cần tìm thêm bug. Và sau 1 lúc thì mình đã tìm ra được bug tiếp theo. Bug này liên quan tới trường hợp En_passant như trong hint của đề bài. Các bạn có thể tham khảo tại đây. Mình không rành về cờ vua lắm, nhưng theo wikipedia thì En_passant chỉ xảy ra với quân tốt khi có 1 quân tốt đối thủ khác nằm bên cạnh ngay khi vừa thực hiện double-step move, và En_passant cần phải được thực hiện ngay sau nước double-step move, nếu không thì quyền En_passant sẽ bị mất.

Trong bài này thì với En_passant, quân tốt của mình không chỉ ăn được quân tốt của đối thủ mà là tất cả các quân luôn, miễn là quân của đối thủ nằm kế bên quân tốt của mình. Các bạn xem trường hợp bên dưới:

ới ví dụ trên thì quân tốt của mình có thể ăn được quân mã. Tới đây mình không tìm thêm được bug nào nữa nên mình quyết định sử dụng lỗi này để đánh thắng máy luôn, và cũng hên là thắng được :v

Tới đây thì viết exploit được rồi.

Exploit code:

from pwn import *
p = remote('pawnfish.chal.uiuc.tf', 1337)
def send_piece(mov1, mov2):
    p.recv()
    p.sendline(mov1)
    p.recv()
    p.sendline(mov2)
for i in range(157):
    print(i)
    send_piece('e2','e4')
    send_piece('e1','e2')
    send_piece('e2','e3')
    send_piece('e3','f4')
    send_piece('f4','e5')
    p.recvuntil('lose')
    p.recvline()
send_piece('e2','e4')
send_piece('c2','c3')
send_piece('e4','e5')
send_piece('e5','d6')
send_piece('g1','f3')
send_piece('d2','d4')
send_piece('c3','d4')
send_piece('d4','e5')
send_piece('f1','e2')
send_piece('e1','g1')
send_piece('e5','d6')
send_piece('h2','h3')
send_piece('e2','f3')
send_piece('d6','c7')
send_piece('f1','e1')
send_piece('c1','e3')
send_piece('d1','e2')
send_piece('e3','c5')
send_piece('e2','e7')
send_piece('e1','e7')
send_piece('e7','a7')
send_piece('a2','a4')
send_piece('a4','b5')
send_piece('b1','c3')
send_piece('b5','b6')
send_piece('b6','b7')
send_piece('a7','a8')
send_piece('f3','b7')
send_piece('a1','b1')
send_piece('b2','b4')
send_piece('c3','a2')
send_piece('b4','b5')
send_piece('b5','b6')
send_piece('a8','a7')
send_piece('a2','c3')
send_piece('c3','d5')
send_piece('d5','e7')
send_piece('e7','c6')
send_piece('b6','b7')
send_piece('h3','g4')
send_piece('g4','f5')
send_piece('b7','b8')
send_piece('b1','b8')
send_piece('g1','h2')
send_piece('b8','f8')
send_piece('f5','f6')
send_piece('h2','h3')
send_piece('g2','g4')
send_piece('h3','g3')
send_piece('c6','e7')
send_piece('f8','d8')
send_piece('a7','c7')
send_piece('e7','d5')
send_piece('d8','b8')
send_piece('f6','f7')
send_piece('c7','a7')
p.interactive()
$ python3 solve.py
[+] Opening connection to pawnfish.chal.uiuc.tf on port 1337: Done
0
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
[*] Switching to interactive mode
Location to > Moving WHITE ♖ from row: 1, col: 2, TO row: 1, col: 0
You win :)
uiuctf{strange_the_only_winning_move_is_to_lose}
TIN LIÊN QUAN
Cookie Arena được tổ chức bởi Cookie Hân Hoan, một tổ chức giáo dục nhằm phổ biến kiến thức an ninh mạng đến với cộng đồng bằng sự đồng cảm, tươi vui và hài hước. "Xin chào, mình là Gấu aka th3_5had0w đến từ Wanne.One, với tư cách là á...
CLB An toàn Thông tin Wanna.One chia sẻ một số Challenges giải được và việc chia sẻ writeup nhằm mục đích giao lưu học thuật. Mọi đóng-góp ý-kiến bọn mình luôn-luôn tiếp nhận qua mail: wannaone.uit@gmail.com hoặc inseclab@uit.edu.vn và fanpage: fb.com/inseclab. lttn SILVER WATER INDUSTRIES Challengge Đầu tiên mình nc...
The Wanna.One Cyber Security Club shares writeup of some solved Challenges with the purpose of academic exchanges. We always welcome and look forward to comments from any of you via email: wannaone.uit@gmail.com FWORD CTF 2021: https://ctftime.org/event/1405 Sat, 28 Aug. 2021, 00:00 ICT — Sun, 29 Aug. 2021, 12:00 ICT Author:...