VKA-CTF`2020. Полковник Сандерс (Colonel Sanders) and Инженерная подготовка (Engineering training)
@ Rakovsky Stanislav | Sunday, Jul 5, 2020 | 8 minutes read | Update at Sunday, Jul 5, 2020

Team result: https://t.me/unicorn_mpei_team/89

Полковник Сандерс

Инженерная подготовка

reverse, КМБ (the second diffictulty level), 1000 points (2 solves total) > Полковник Сандерс

The official writeup

Ох уж этот коронавирус. Нас закрыли в казарме и выпускают по одному.
Для нас даже полковник Сандерс придумал акцию "Закажи себе и товарищу".
Теперь можно заказывать несколько купонов сразу... Закажешь?

File

.
├── EasyGo.exe
└── static
    ├── bootstrap
    │   ├── css
    │   │   ├── bootstrap-grid.css
    │   │   ├── bootstrap-grid.css.map
    │   │   ├── bootstrap-grid.min.css
    │   │   ├── bootstrap-grid.min.css.map
    │   │   ├── bootstrap-reboot.css
    │   │   ├── bootstrap-reboot.css.map
    │   │   ├── bootstrap-reboot.min.css
    │   │   ├── bootstrap-reboot.min.css.map
    │   │   ├── bootstrap.css
    │   │   ├── bootstrap.css.map
    │   │   ├── bootstrap.min.css
    │   │   └── bootstrap.min.css.map
    │   └── js
    │       ├── bootstrap.bundle.js
    │       ├── bootstrap.bundle.js.map
    │       ├── bootstrap.bundle.min.js
    │       ├── bootstrap.bundle.min.js.map
    │       ├── bootstrap.js
    │       ├── bootstrap.js.map
    │       ├── bootstrap.min.js
    │       └── bootstrap.min.js.map
    ├── images
    │   └── sanderslogo.png
    ├── main.html
    └── stylesheets
        ├── main.css
        └── styles.min.css

So, we have the http server EasyGo.exe. It’s a PE64 executable file, written on Golang.

Functions at the end of file

Steps:

  1. Get the key;
  2. Validate it (main_license): conditional check on blocks [a-f0-9]{4} separated by -
  3. Regexp [\d\w]{4} to parse the groups.
  4. 8 check functions for each group.

Cruel Russian story: because of coronavirus cadets were locked in barracks, so they use KFC to buy the food and ask you to order them a food using multiple coupons
  v8 = *(_QWORD *)NtCurrentTeb()->NtTib.ArbitraryUserPointer;
  if ( (unsigned __int64)&retaddr <= *(_QWORD *)(v8 + 16) )
    runtime_morestack_noctxt(a1, a2);
  strconv_Atoi(a1, a2, a3, __PAIR__(a5, v8));
  v9 = v17
     - 0x1C8E
     * (((signed __int64)((unsigned __int128)(v17 * (signed __int128)0x47B8D69B12240048LL) >> 64) >> 11) - (v17 >> 63));
  v10 = v17
      - 0x1C8E
      * (((signed __int64)((unsigned __int128)(v17 * (signed __int128)0x47B8D69B12240048LL) >> 64) >> 11) - (v17 >> 63))
      - 255
      * (((signed __int64)(v9 + ((unsigned __int128)(v9 * (signed __int128)(signed __int64)0x8080808080808081LL) >> 64)) >> 7)
       - (v9 >> 63));
  if ( !a8 )
    runtime_panicIndex(a1);
  *FLAG = v10;
  v11 = ((signed __int64)(((unsigned __int128)((v17 + 98) * (signed __int128)(signed __int64)0x8080808080808081LL) >> 64)
                        + v17
                        + 98) >> 7)
      - ((v17 + 98) >> 63);
  v12 = v17 + 98 - 255 * v11;
  if ( a8 <= 7 )
    runtime_panicIndex(v11);
  FLAG[7] = v12;
  v13 = (v17 + 19) >> 63;
  if ( a8 <= 0xE )
    runtime_panicIndex(v13);
  FLAG[14] = v17
           + 19
           - 120
           * (((signed __int64)(((unsigned __int128)((v17 + 19) * (signed __int128)(signed __int64)0x8888888888888889LL) >> 64)
                              + v17
                              + 19) >> 6)
            - v13);
  v14 = (v17 + 104) * (signed __int128)0x4DA637CF781D1E55LL;
  v15 = v17 + 104 - 211 * ((*((_QWORD *)&v14 + 1) >> 6) - ((v17 + 104) >> 63));
  if ( a8 <= 0x16 )
    runtime_panicIndex(v15);
  FLAG[22] = v15;
  return v14;

The logic to reverse is pretty complicated because of golang (yet official writeup says it’s easy), but we can find the output variable (I name it FLAG) and calculate the impact of each check block:

Block 1: 0, 7, 14, 22'nd bytes
Block 2: 1, 8, 15, 23'rd bytes
Block 3: 2, 9, 16, 24'th bytes
Block 4: 3, 10, 17, 25'th bytes
Block 5: 4, 11, 18, 26'th bytes
Block 6: 5, 12, 19, 27'th bytes
Block 7: 6, 13, 20, 28'th bytes
Block 8: 21, 29, 30, 31'st bytes

Each coupon has WORD size (65536 values), their impact is independent, so we can just brute force it:

from string import printable
import requests

# VKACTF{.+}
s = requests.Session()

base = []
for i in range(0x10000):
    j = hex(i)[2:].rjust(4 ,"0")
    p = s.post(domain, data={"coupon":f"{j}-{j}-{j}-{j}-{j}-{j}-{j}-{j}"})
    base.append(p.text[:])

### attention: dirty code

ans = {0:[], 1:[], 2:[], 3:[], 4:[], 5:[], 6:[], 7:[]} # i:{key:val, key:val}

for text in base:
    text = text[18:]
    if len(text)!=32:
        continue
    if text[0] == "V":
        if all (text[i] in printable for i in [7,14,22]):
            ans[0].append(
            {
                    0: text[0],
                    7: text[7],
                    14: text[14],
                    22: text[22]
            }
            )
    if text[1] == "K":
        if all (text[i] in printable for i in [8,15,23]):
            ans[1].append(
            {
                    1: text[1],
                    8: text[8],
                    15: text[15],
                    23: text[23]
            }
            )
    if text[2] == "A":
            if all (text[i] in printable for i in [9,16,24]):
                ans[2].append(
            {
                    2: text[2],
                    9: text[9],
                    16: text[16],
                    24: text[24]
            }
            )
        
    if text[3] == "C":
            if all (text[i] in printable for i in [10,17,25]):
                ans[3].append(
            {
                    10: text[10],
                    17: text[17],
                    25: text[25],
                    3: text[3]
            }
            )
            
        
    if text[4] == "T":
            if all (text[i] in printable for i in [11,18,26]):
                ans[4].append(
            {
                    11: text[11],
                    18: text[18],
                    26: text[26],
                    4: text[4]
            }
            )
            
    if text[5] == "F":
            if all (text[i] in printable for i in [12,19,27]):
                ans[5].append(
            {
                    12: text[12],
                    19: text[19],
                    27: text[27],
                    5: text[5]
            }
            )
            
            
    if text[6] == "{":
            if all (text[i] in printable for i in [13,20,28]):
                ans[6].append(
            {
                    13: text[13],
                    20: text[20],
                    28: text[28],
                    6: text[6]
            }
            )
            
    if text[31] == "}":
            if all (text[i] in printable for i in [21,29,30]):
                ans[7].append(
            {
                    31: text[31],
                    21: text[21],
                    29: text[29],
                    30: text[30]
            }
            )

After the cleaning of duplicates, we can output all values:

for a0 in k[0]:
    for a1 in k[1]:
        for a2 in k[2]:
            for a3 in k[3]:
                for a4 in k[4]:
                    for a5 in k[5]:
                        for a6 in k[6]:
                            for a7 in k[7]:
                                g = {}
                                g.update(a0)
                                g.update(a1)
                                g.update(a2)
                                g.update(a3)
                                g.update(a4)
                                g.update(a5)
                                g.update(a6)
                                g.update(a7)
                                
                                hh = "".join(g[i] for i in range(32))
                                if all (_ in hh for _ in ["s0lv3d}", "SaNd3rs"]): # optimization due the iterations of checks
                                    print(hh)

We are getting the candidates:

VKACTF{c0l2n3l__SaNd3rs__s0lv3d}
VKACTF{c0l2n3p__SaNd3rs__s0lv3d}
VKACTF{c0l0n3l__SaNd3rs__s0lv3d}
VKACTF{c0l0n3p__SaNd3rs__s0lv3d}

The third flag is ours.

reverse, 1-3 курс (the third difficulty level), 1997 points (3 solves total) > Инженерная подготовка

The official writeup

Наш преподаватель по инженерной подготовке совсем спятил. Он сказал, что я должен разминировать 10.000 минных полей ради пятерки на экзамене.

File

ELF64 file, C++

![](/ctf_writeups/vka_ctf_2020 /media/img21.png)

 v14 = 0;
  for ( i = 0; i <= 9999; ++i )
  {
    printf("New game_%d: \n", i);
    v12 = 0;
    v5 = SIDE * SIDE - MINES;
    initialise(v1, v2);
    placemines(MinesAll[99 * i], v1);
    v11 = 0;
    while ( !v12 )
    {
      clear();
      printf("Current game_%d: \n", i);
      printboard(v2);
      make_move(&v4, &v3);
      if ( !v11 && ismine(v4, v3, v1) )
        replacemine(v4, v3, v1);
      v14 += v4 + v3;
      ++v11;
      result = playminesuntil(v2, v1, MinesAll[99 * i], v4, v3, &v5);
      v12 = result;
      if ( result )
        return result;
      if ( v12 != 1 && !v5 )
      {
        printf("\nVictory, but still to win_%d !\n", 10000 - i);
        v12 = 1;
      }
    }
  }
  printf("VKACTF{");
  v14 = 58 * (1337 * v14 % 0xFFFFFF);
  v8 = (v14 + 7) % 49 + 28;
  for ( j = 0; j < v8; ++j )
  {
    for ( k = 0; k < v8 * v8 / 2 / v8; ++k )
    {
      v7 = inter[j * (v8 * v8 / 2 / v8) + k];
      v6 = (v14 >> k);
      v6 ^= v7;
      std::operator<<<std::char_traits<char>>(&std::cout, v6);
    }
  }
  return puts("}");

Do you see VKACTF{ and flag generation? Looks awesome! Let’s analyze.

v14 is unsigned DWORD, a sum of our answers of every 10000 rounds, and it will be reduced by the formula:

v14 = 58 * (1337 * v14 % 0xFFFFFF)

Multiplication takes precedence higher than taking the remainder, so the min/max values are 0..58*0xFFFFFE, with init range 0..0xFFFFFE, so it’s easily bruteforcable.

… I thought so.

import time
import binascii
from string import printable

inter = binascii.unhexlify("5d006a7d5b46453c1b6b2e520c00743b5b462a785d6b6a7f585229385d052a635339e4e04ec93b4e473239437d443000")

t = time.time()
for i in range(0xFFFFFFff+1):
    _break = 0
    v14 = 58 * i
    
    v8 = (v14 + 7) % 49 + 28
    #print("v14", hex(v14), "v8", hex(v8))

    #print()
    
    for j in range(v8):
        if _break:
            break
        for k in range(v8//2):
            #print("inter", hex(j*(v8//2)+k), "j",hex(j), "k", hex(k), "v8", hex(v8))
            if j*(v8//2)+k >= len(inter):
                _break = 999
                break
            v7 = inter[j*(v8//2)+k]
            v6 = (v14 // 2**k) % 0x100
            #print("prexor", hex(v6))
            v6 ^= v7


            if chr(v6) not in printable:
                if v6!=0:
                    _break = 1
                    break
            #print(chr(v6) if v6!=0 else "", end="")
            
    if _break == 999:
        print("key", i)
    
    #print(i)

print(time.time()-t)

It does not work. In debug mode for 5-6 random v14 it gives right bytes, even so!

Asked the author about this. The answer is “Попробуй пройти игру)” - “Try to complete the game)”.

Okay, okay.

Dump the mines, 99 pairs of x:y, word values, for each of 10000 maps.

# idapython
open("mines", "wb").write(GetManyBytes(0x055AF5FE4B020 , 99*4*2*10000))

The unpacker:

MINES = 99
SIZE = 24
ROUNDS = 10000
def return_field(_round):
    i = _round
    chunk = fields[i*MINES*4*2: (i+1)*MINES*4*2]
    a = struct.unpack(f"<{MINES*2}I", chunk)
    #print(a, len(a))
    mines = []
    for j in range(MINES):
        mines.append((a[2*j], a[2*j+1]))
    #print(mines)
    ans = []
    for _x in range(SIZE):
        for _y in range(SIZE):
            if (_x,_y) not in mines:
                ans.append((_x,_y))
    return ans

We can not make a sum up of coordinates and get the right v14… NO! IT DOES NOT WORK! Despair!

So let’s just mine it out (as it turned out later, the author had the similar solution):

import pwn

p = pwn.tubes.process.process("./minesweeper")
_ = p.recv()
p.sendline("")
import time
for i in range(10000):
    print(i)
    t = return_field(i)
    for ii in t:
        p.sendline(f"{ii[0]} {ii[1]}")
        time.sleep(0.0001)
        _ = p.recv()
        #print(_.decode())
        if b"Victory, but still" in _:
            break
        if b"VKACTF{" in _:
            print(_)

Flag: VKACTF{54pp3r_1s_4_d4n63r0u5_pr0f35510n}


author, editor: Rakovsky Stanislav, Unicorn CTF

Cats!

Under construction

Wow! Flipable!

Hello from another side of the Moon!

Looking for a flag? Okay, take this:
LFXXKIDBOJSSA43XMVSXIIC6LYQCAIA=

About me

Under construction. You can try to contact me and fill this field… haha… ha…