BreizhCTF 2k17 Write-Ups

Amossys was a sponsor of the BreizhCTF 2k17, a French hacking competition over a single night (April 28-29th). Many challenges were proposed in a wide range of topics (Web, Reverse, Cryptography, etc). For this occasion, three teams were created among our employees. Here are some write-ups of the solved challenges. And thanks to the organization team for this excellent event in Rennes!



Reverse 1 - If you do me I do tou

125 points, Nicolas Datin

This was a reverse challenge, we had to figure out how the binary computes the password to get the flag.

First of all, we've done a strings on the binary:

strings ./reverse125-ifyoudomeidotou
/lib64/ld-linux-x86-64.so.2
libstdc++.so.6
[ ... ]
Paradise is sooner! But before unlock me Grrrrrr #HornyLikeBreizh:
Huuuum. you do me ... it was so intense ... Congratz, your flag is: BZHCTF{
more harder! i want you!! gogogo
;*3$"
mfd`jf.urdblbp.hkdblb.-/).tdk`llb.fwbqzchgz,sl.uof.FFNF
[ ... ]
reverse125-ifyoudomeidotou.cpp
_ZStL8__ioinit

We noticed an interesting string: mfd`jf.urdblbp.hkdblb.-/).tdk`llb.fwbqzchgz,sl.uof.FFNF

After this, we launched the binary under Ltrace:

ltrace -s 64 ./reverse125-ifyoudomeidotou
__libc_start_main(0x400a06, 1, 0x7ffc5d0732b8, 0x400c50 <unfinished ...>
_ZNSt8ios_base4InitC1Ev(0x602331, 0xffff, 0x7ffc5d0732c8, 3)                                                    = 0
__cxa_atexit(0x400880, 0x602331, 0x602088, 6)                                                                   = 0
ptrace(16, 0, 0, 0)                                                                                             = -1
   _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(0x602220, 0x400cd8, -144, -1Paradise is sooner! But before unlock me Grrrrrr #HornyLikeBreizh:
)                           = 0x602220
_ZStrsIcSt11char_traitsIcEERSt13basic_istreamIT_T0_ES6_PS3_(0x602100, 0x7ffc5d073150, 0x7f7aa03537d8, -1MOMO
)       = 0x602100
strcmp("JLNN\a\003\003\001\2072\004\\\373|\003\001\377\036c\001\a\003\003\001,\017C\001\a\003\003\001\257\202\317\236}|\003\001\370\374\003\001\006\003\003\001\2272\004\\\373|\003", "mfd`jf.urdblbp.hkdblb.-/).tdk`llb.fwbqzchgz,sl.uof.FFNF") = -35
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(0x602220, 0x400d70, 109, 0)                             = 0x602220
_ZNSolsEPFRSoS_E(0x602220, 0x4008f0, 0x7f7aa03537d8, 0 <unfinished ...>
_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(0x602220, 0x4008f0, 0x7f7aa03537d8, 0more harder! i want you!! gogogo
)               = 0x602220
<... _ZNSolsEPFRSoS_E resumed> )                                                                                = 0x602220
_ZNSt8ios_base4InitD1Ev(0x602331, 0, 160, 0x7f7aa005ecf0)                                                       = 0x7f7aa036b100
+++ exited (status 0) +++

We spotted a very interesting call to strcmp() function with our previously found string as parameter.

With these information, we were sure that this string was the ciphered password. The next step was to figure out how this string was computed. Using GDB, we found a function named macronade that takes 2 char * as parameters. This function loops over the first parameter and applies a xor with the key 7331.

gdb ./reverse125-ifyoudomeidotou
    Dump of assembler code for function _Z9macronadePcS_:
       0x0000000000400b07 <+0>:     push   rbp
       0x0000000000400b08 <+1>:     mov    rbp,rsp
       0x0000000000400b0b <+4>:     mov    QWORD PTR [rbp-0x18],rdi
       0x0000000000400b0f <+8>:     mov    QWORD PTR [rbp-0x20],rsi
       0x0000000000400b13 <+12>:    mov    DWORD PTR [rbp-0x4],0x0
       0x0000000000400b1a <+19>:    cmp    DWORD PTR [rbp-0x4],0x37
       0x0000000000400b1e <+23>:    jg     0x400be2 <_Z9macronadePcS_+219>
       0x0000000000400b24 <+29>:    mov    eax,DWORD PTR [rbp-0x4]
       0x0000000000400b27 <+32>:    movsxd rdx,eax
       0x0000000000400b2a <+35>:    mov    rax,QWORD PTR [rbp-0x20]
       0x0000000000400b2e <+39>:    add    rax,rdx
       0x0000000000400b31 <+42>:    mov    edx,DWORD PTR [rbp-0x4]
       0x0000000000400b34 <+45>:    movsxd rcx,edx
       0x0000000000400b37 <+48>:    mov    rdx,QWORD PTR [rbp-0x18]
       0x0000000000400b3b <+52>:    add    rdx,rcx
       0x0000000000400b3e <+55>:    movzx  ecx,BYTE PTR [rdx]
       0x0000000000400b41 <+58>:    movzx  edx,BYTE PTR [rip+0x201558]        # 0x6020a0 <gode>
       0x0000000000400b48 <+65>:    xor    edx,ecx                          <-- xor with 7
       0x0000000000400b4a <+67>:    mov    BYTE PTR [rax],dl
       0x0000000000400b4c <+69>:    mov    eax,DWORD PTR [rbp-0x4]
       0x0000000000400b4f <+72>:    cdqe   
       0x0000000000400b51 <+74>:    lea    rdx,[rax+0x1]
       0x0000000000400b55 <+78>:    mov    rax,QWORD PTR [rbp-0x20]
       0x0000000000400b59 <+82>:    add    rax,rdx
       0x0000000000400b5c <+85>:    mov    edx,DWORD PTR [rbp-0x4]
       0x0000000000400b5f <+88>:    movsxd rdx,edx
       0x0000000000400b62 <+91>:    lea    rcx,[rdx+0x1]
       0x0000000000400b66 <+95>:    mov    rdx,QWORD PTR [rbp-0x18]
       0x0000000000400b6a <+99>:    add    rdx,rcx
       0x0000000000400b6d <+102>:   movzx  ecx,BYTE PTR [rdx]
       0x0000000000400b70 <+105>:   movzx  edx,BYTE PTR [rip+0x20152a]        # 0x6020a1 <gode+1>
       0x0000000000400b77 <+112>:   xor    edx,ecx                          <-- xor with 3
       0x0000000000400b79 <+114>:   mov    BYTE PTR [rax],dl
       0x0000000000400b7b <+116>:   mov    eax,DWORD PTR [rbp-0x4]
       0x0000000000400b7e <+119>:   cdqe   
       0x0000000000400b80 <+121>:   lea    rdx,[rax+0x2]
       0x0000000000400b84 <+125>:   mov    rax,QWORD PTR [rbp-0x20]
       0x0000000000400b88 <+129>:   add    rax,rdx
       0x0000000000400b8b <+132>:   mov    edx,DWORD PTR [rbp-0x4]
       0x0000000000400b8e <+135>:   movsxd rdx,edx
       0x0000000000400b91 <+138>:   lea    rcx,[rdx+0x2]
       0x0000000000400b95 <+142>:   mov    rdx,QWORD PTR [rbp-0x18]
       0x0000000000400b99 <+146>:   add    rdx,rcx
       0x0000000000400b9c <+149>:   movzx  ecx,BYTE PTR [rdx]
       0x0000000000400b9f <+152>:   movzx  edx,BYTE PTR [rip+0x2014fc]        # 0x6020a2 <gode+2>
       0x0000000000400ba6 <+159>:   xor    edx,ecx                          <-- xor with 3
       0x0000000000400ba8 <+161>:   mov    BYTE PTR [rax],dl
       0x0000000000400baa <+163>:   mov    eax,DWORD PTR [rbp-0x4]
       0x0000000000400bad <+166>:   cdqe   
       0x0000000000400baf <+168>:   lea    rdx,[rax+0x3]
       0x0000000000400bb3 <+172>:   mov    rax,QWORD PTR [rbp-0x20]
       0x0000000000400bb7 <+176>:   add    rax,rdx
       0x0000000000400bba <+179>:   mov    edx,DWORD PTR [rbp-0x4]
       0x0000000000400bbd <+182>:   movsxd rdx,edx
       0x0000000000400bc0 <+185>:   lea    rcx,[rdx+0x3]
       0x0000000000400bc4 <+189>:   mov    rdx,QWORD PTR [rbp-0x18]
       0x0000000000400bc8 <+193>:   add    rdx,rcx
       0x0000000000400bcb <+196>:   movzx  ecx,BYTE PTR [rdx]
       0x0000000000400bce <+199>:   movzx  edx,BYTE PTR [rip+0x2014ce]        # 0x6020a3 <gode+3>
       0x0000000000400bd5 <+206>:   xor    edx,ecx                          <-- xor with 1
       0x0000000000400bd7 <+208>:   mov    BYTE PTR [rax],dl
       0x0000000000400bd9 <+210>:   add    DWORD PTR [rbp-0x4],0x4
       0x0000000000400bdd <+214>:   jmp    0x400b1a <_Z9macronadePcS_+19>
       0x0000000000400be2 <+219>:   mov    rax,QWORD PTR [rbp-0x20]
       0x0000000000400be6 <+223>:   add    rax,0x37
       0x0000000000400bea <+227>:   mov    BYTE PTR [rax],0x0
       0x0000000000400bed <+230>:   nop
       0x0000000000400bee <+231>:   pop    rbp
       0x0000000000400bef <+232>:   ret

Based on this, we made a little Python script to calculate the flag from the string.

#!/usr/bin/env python

string = 'mfd`jf.urdblbp.hkdblb.-/).tdk`llb.fwbqzchgz,sl.uof.FFNF'
key = [0x07, 0x03, 0x03, 0x01]

def xor(s, k):
    flag = ''
    for i, c in enumerate(s):
        flag += chr(ord(c) ^ k[i % len(k)])
    return flag

if __name__ == '__main__':
    print 'The Flag is: BZHCTF{%s}' % xor(string, key)

The flag was: BZHCTF{jegame-tugames-ilgame-...-welcome-everybody-to-the-GAME}


Reverse 3 - Mate

150 points, Nicolas Zilio

This challenge is a Crackme type one, where the objective is to find the right password to get the flag. First, commands file and strings onto the completementalouest binary give us the following information : the binary is a 64bit ELF stripped one, and it contains two interesting strings which are : WRONG HOLE...[...] and You did it![...].

We can then launch the binary within radare2, and try to disassemble the main function (normally, radare2 is capable of handling stripped binaries).

 $ r2 completementalouest 
 [0x00400b60]> aaaa
 [x] Analyze all flags starting with sym. and entry0 (aa)
 [x] Analyze len bytes of instructions for references (aar)
 [x] Analyze function calls (aac)
 [x] Emulate code to find computed references (aae)
 [x] Analyze consecutive function (aat)
 [aav: using from to 0x400000 0x4039c0
 Using vmin 0x400000 and vmax 0x603580
 aav: using from to 0x400000 0x4039c0
 Using vmin 0x400000 and vmax 0x603580
 [x] Analyze value pointers (aav)
 [Deinitialized mem.0x100000_0xf0000 functions (afta)unc.* functions (aan)
 [x] Type matching analysis for all functions (afta)
 [x] Type matching analysis for all functions (afta)
 [0x00400b60]> pdf @main
 / (fcn) main 1561
 |   main ();
 |           ; var int local_40h @ rbp-0x40
 |           ; var int local_18h @ rbp-0x18
 |              ; DATA XREF from 0x00400b7d (entry0)
 [...]
 | |||||||   0x004011f0      be04000000     mov esi, 4
 | |||||||   0x004011f5      89c7           mov edi, eax
 | |||||||   0x004011f7      e88d000000     call fcn.00401289
 | |||||||   0x004011fc      84c0           test al, al
 | ========< 0x004011fe      7407           je 0x401207
 | |||||||   0x00401200      b801000000     mov eax, 1
 | ========< 0x00401205      eb05           jmp 0x40120c
 | --------> 0x00401207      b800000000     mov eax, 0
 | |||||||      ; JMP XREF from 0x00401205 (main)
 | --------> 0x0040120c      84c0           test al, al
 | ========< 0x0040120e      741b           je 0x40122b
 | |||||||   0x00401210      bff81d4000     mov edi, str._nYOOU_DID_IT____Well_done_mate__Valide_the_challz_with_that_password_surrounding_by_BZHCTF___as_usual_bichon____n ; str._nYOOU_DID_IT____Well_done_mate__Valide_the_challz_with_that_password_surrounding_by_BZHCTF___as_usual_bichon____n
 | |||||||   0x00401215      e846f8ffff     call sym.imp.puts
 | |||||||   0x0040121a      bf691e4000     mov edi, str.pause          ; "pause" @ 0x401e69
 | |||||||   0x0040121f      e86cf8ffff     call sym.imp.system
 | |||||||   0x00401224      bb00000000     mov ebx, 0
 | ========< 0x00401229      eb19           jmp 0x401244
 | ```````-> 0x0040122b      bf701e4000     mov edi, str._n_WRONG_WHOLE_..._BUT_HUUUUM_I_LIKE_IT__DO_IT_AGAIN_:p_ ; str._n_WRONG_WHOLE_..._BUT_HUUUUM_I_LIKE_IT__DO_IT_AGAIN_:p_
 |           0x00401230      e82bf8ffff     call sym.imp.puts

We can see here that the condition which processes our victory or not is present on the test al,al at address 0x40120c, which verifies if this sub register contains the nul value. From the precedent five lines, this al sub-register contains nul value (from the mov eax,0) if the function at address 0x401289 has returned nul value itself, otherwise the al value is one (from mov eax,1). We can so disassemble the function at address 0x401289:

 [0x00400b60]> pdf @0x401289
 / (fcn) fcn.00401289 43
 |   fcn.00401289 ();
 |           ; var int local_8h @ rbp-0x8
 |           ; var int local_4h @ rbp-0x4
 |              ; XREFS: CALL 0x004011f7  CALL 0x004011a8  CALL 0x00401155  CALL 0x00401102  CALL 0x004010af  CALL 0x0040105c  CALL 0x00401009  CALL 0x00400fb6  CALL 0x00400f63  CALL 0x00400f10  
 |              ; XREFS: CALL 0x00400ebd  CALL 0x00400e6a  CALL 0x00400e17  CALL 0x00400dc4  CALL 0x00400d71  CALL 0x00400d1e  CALL 0x00400ccb  
 |           0x00401289      55             push rbp
 |           0x0040128a      4889e5         mov rbp, rsp
 |           0x0040128d      89f8           mov eax, edi
 |           0x0040128f      8975f8         mov dword [rbp - local_8h], esi
 |           0x00401292      8845fc         mov byte [rbp - local_4h], al
 |           0x00401295      8b45f8         mov eax, dword [rbp - local_8h]
 |           0x00401298      4898           cdqe
 |           0x0040129a      0fb680c03060.  movzx eax, byte [rax + str.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_] ; [0x6030c0:1]=97 ; LEA str.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_ ; "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!" @ 0x6030c0
 |           0x004012a1      3a45fc         cmp al, byte [rbp - local_4h]
 |       ,=< 0x004012a4      7507           jne 0x4012ad
 |       |   0x004012a6      b801000000     mov eax, 1
 |      ,==< 0x004012ab      eb05           jmp 0x4012b2
 |      |`-> 0x004012ad      b800000000     mov eax, 0
 |      |       ; JMP XREF from 0x004012ab (fcn.00401289)
 |      `--> 0x004012b2      5d             pop rbp
 \           0x004012b3      c3             ret

We can see here that this function returns 1 if the comparison at address 0x4012a1 between al, filled at the precedent instruction, and the first octet of our function argument (byte [rbp -local_4h]), gives us the two values are different. Otherwise, the function returns nul. We have so to verify what are the values of those two registers at execution:

 gdb ~/Downloads/bzhctf/completementalouest -q
 Reading symbols from /home/bzh/Downloads/bzhctf/completementalouest...(no debugging symbols found)...done.
 (gdb) b *0x4012a1
 Breakpoint 1 at 0x4012a1
 (gdb) r
 Starting program: /home/bzh/Downloads/bzhctf/completementalouest 
 kaluche, is that you....? I'm trapped down in your resolving your stupid task for 1hour now!!!!
 SaxX never gonna give you up! But helps him to find the "whole ;)" he says??? 
 You can maybe call a friend!!

 MOMO

 Breakpoint 1, 0x00000000004012a1 in ?? ()
 (gdb) p /c $al
 $1 = 98 'b'
 (gdb) p /c *(char *)($rbp-0x4)
 $2 = 77 'M'
 (gdb) c
 Continuing.

 (WRONG WHOLE ... BUT HUUUUM I LIKE IT! DO IT AGAIN :p)
 sh: 1: pause: not found

It seems so that the first typed letter 'M' is here compared to what is the first character of the flag (potentially treated before). We can check M indeed corresponds to the first string character typed:

 (gdb) r
 Starting program: /home/bzh/Downloads/bzhctf/completementalouest 
 kaluche, is that you....? I'm trapped down in your resolving your stupid task for 1hour now!!!!
 SaxX never gonna give you up! But helps him to find the "whole ;)" he says??? 
 You can maybe call a friend!!

 bOMO

 Breakpoint 1, 0x00000000004012a1 in ?? ()
 (gdb) c
 Continuing.

 Breakpoint 1, 0x00000000004012a1 in ?? ()

The breakpoint has been reached twice, so our assumption seems correct. We can so modify the value of [rbp-4] at each function call to move forward in our binary, and check at each round the value of the al register, which seems to contain our flag:

 (gdb) set *(char*)($rbp-4)='b'

After several rounds, and a somewhat fastidious method to use gdb manually, we get to the following in gdb:

  (gdb) p /c $al
  $7 = 101 'e'
  (gdb) c
  Continuing.

  YOOU DID IT!!! Well done mate! Valide the challz with that password surrounding by BZHCTF{} as usual bichon ;)

  sh: 1: pause: not found
  [Inferior 1 (process 14545) exited normally]

Alright! Let's now hope there is no treatment in the flag within our binary, by testing the values get at each round:

  kaluche, is that you....? I'm trapped down in your resolving your stupid task for 1hour now!!!!
  SaxX never gonna give you up! But helps him to find the "whole ;)" he says??? 
  You can maybe call a friend!!

  bAhxZYoM!cbHiSqUe

  YOOU DID IT!!! Well done mate! Valide the challz with that password surrounding by BZHCTF{} as usual bichon ;)

  sh: 1: pause: not found

Woot, it works! Flag is indeed : bAhxZYoM!cbHiSqUe


Web 5 - Eddy Malou

350 points, Loïck Bonniot

The beginning of this challenge is a very simple PHP chat application: every client of the application can post a message using HTTP POST requests with two parameters (action, data). The default action is send_message, while the data is the message to be sent to the chat application.

Our first attempt was to change the POST action parameter with something else, like admin.

Fatal error: Uncaught ReflectionException: Method admin does not exist in /var/www/index.php:86
Stack trace:
 #0 /var/www/index.php(86): ReflectionClass->getMethod('admin')

Nice, everyone knows that reflection code is often vulnerable. PHP always adds some "hidden" methods in user classes: those are called "Magic Methods". We can check the PHP Magic Methods Page for a list of vulnerable methods, but the most useful one is probably __toString.

When we set the POST action parameter to __toString, the result was no longer a fatal error, but rather a very nice output:

[...]

["breizh_admin":"BZH_USER":private]=>bool(false)

[...]

string(12) "breizh_admin"
string(6) "logout"
string(10) "set_access"

[...]

We guessed that we had to change the value of the breizh_admin attribute to get the flag. Our first attempts to use the breizh_admin method were not really interesting, but the set_access was the key of this challenge.

Putting set_access as action and admin as data gave us what we wanted to see at the very end of the list of every message sent (and there was a huge number of junk messages published by other challengers, as you can imagine!).

Hellowwwwwww, breizh admin!
Here is your Flag: BZHCTF{R3fl3x10n_15_50_c00l_1f_Y0u_4r3_n0oBzz!#GAME}

Crypto 1 - GoGoGoBaby

100 points, Loïck Bonniot

The goal of this first crypto challenge is to reverse the following hash algorithm (written in Go, as you can guess with the title of this challenge!):

package main

import (
    "fmt"
    "strings"
)

func charCodeAt(s string, n int) rune {
    for i, r := range s {
        if i == n {
            return r
        }
    }
    return 0
}

func toHex(myStr string) string {
    var r string = ""
    letter := []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}

    for i := 0; i < len(myStr); i++ {
        r += letter[charCodeAt(myStr, i)>>4] + letter[charCodeAt(myStr, i)&15]
    }

    return r
}

func saxxCustomAlgorithmOrNot(myStr string) string {

    var init int = 0
    var out string = ""
    var value int

    for i := 0; i < len(myStr); i++ {
        value = int(charCodeAt(myStr, i))
        init ^= (value << 2) ^ value
        out += string(init & 0xff)
        init >>= 8
    }

    return strings.Replace(toHex(out), "00", "", -1)
}

// hash: 4a1b506c33694e055f9605555550a4a5a54ad2d7227266226322e4afd2a0f0227de4224ebb9cb1a5d22250a5221ee49939224e22501e05227d22721111721e50a4a5a50455555088

By reading the for loop in the saxxCustomAlgorithmOrNot function, we quickly discovered that the algorithm has a chosen-prefix vulnerability, due to the concatenation, and the reuse of previous bytes in the iteration. We can illustrate that behavior by computing several strings with a constant prefix.

fmt.Println(saxxCustomAlgorithmOrNot("A")) // 45
fmt.Println(saxxCustomAlgorithmOrNot("AB")) // 454b
fmt.Println(saxxCustomAlgorithmOrNot("ABC")) // 454b4e
fmt.Println(saxxCustomAlgorithmOrNot("ABCD")) // 454b4e55
fmt.Println(saxxCustomAlgorithmOrNot("ABCE")) // 454b4e50

We just had to build a simple chosen-prefix attack, by trying every character until a match, and then by updating the chosen prefix.

var charset = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .!#_-=+*/\"'$%&()*,:;?@{}")

func main() {
    var prefix []byte
    hash := "4a1b506c33694e055f9605555550a4a5a54ad2d7227266226322e4afd2a0f0227de4224ebb9cb1a5d22250a5221ee49939224e22501e05227d22721111721e50a4a5a50455555088"

    for i := 0; i < len(hash)/2; i++ {
        for _, r := range charset {
            res := saxxCustomAlgorithmOrNot(string(append(prefix, r)))
            addon := res[len(prefix)*2:] // Fetch only the new byte added to the hash

            if addon == hash[i*2:i*2+2] {
                fmt.Print(string(r))
                prefix = append(prefix, r)
                break
            }
        }
    }

    fmt.Println()
}
$ go build GoGoGoBaby.go
$ time ./GoGoGoBaby
BREIZHCTF{TDDE!!!Bon_OK_J_avoue_La_Crypto_Et_SaxX_C_EST_L_OPPOSE!!!TDDE}
./GoGoGoBaby  0,08s user 0,00s system 96% cpu 0,087 total

Please note that our simple algorithm only works because the strings.Replace method was not used in the original hash, it would have been a more difficult task because the length of the hash would not have matched the length of the cleartext.


Crypto 2 - Diffie Failman

200 points, Gauthier Goujard

The challenge gives us a client and a server in python as well as a network capture of an exchange between the two. The title tells us that we will probably have to face a dubious implementation of the key exchange protocol Diffie Helman.

On reading the supplied code, it is discovered that both the client and the server have a public / private key pair that will be used to establish a shared key to encrypt the data sequence with AES. The key pair is defined as follows:

shared = 65535
private_key = randint(10**24,10**32)
public_key = shared * private_key

One soon realizes that it is enough to divide the public key by 65535 to fall back on the private key. Rather surprising ...

The rest of the code tells us that the setting of the keys is done like this on the client's side:

s.send(str(public_key))
( ... )
pub = s.recv(128)
intermediate = long(pub) * private_key
shared_secret = hashlib.sha256(str(intermediate).encode("utf-8")).digest()

And like this server-side:

pub = client.recv(128)
client.send(str(public_key))
intermediate = long(pub) * private_key
shared_secret = hashlib.sha256(str(intermediate).encode("utf-8")).digest()

The intermediate value, calculated on both sides, can be written as follows:

intermediate = public_key(serveur) * private_key(client)
<=> intermediate = shared * private_key(serveur) * private_key(client)
<=> intermediate = public_key(client) * private_key(serveur)

This value will therefore be equal on the client and on the server, it is this which will be used to generate the encryption key (via SHA256) for the continuation of the exchange.

We know how to find the private keys from the public keys, we have seen that the latter are sent on the network and we know how to calculate the encryption key from these values. All that remains is to extract the correct values ​​from the pcap, calculate the key and decrypt the flag.

A little bit of Wireshark allows us to display the TCP stream and realize that the pcap reflects well the functioning of the given code (exchange of public keys and then encrypted data):

Figure1
Figure 1: TCP Stream

After extracting the encrypted data contained in the capture in cipher01.enc andcipher02.enc, we can solve the challenge:

import hashlib
from Crypto.Cipher import AES

shared = 65535

pub_client = 4658287721478817501151725639698791040
pub_server = 1449121672058438729139215000099850030
priv_server = pub_server/shared

cipher01 = open('./cipher01.enc', 'rb').read()
cipher02 = open('./cipher02.enc', 'rb').read()

intermediate = pub_client * priv_server

unpad = lambda s : s[0:-ord(s[-1])]

def decrypt(message, key):
    IV = message[:AES.block_size]
    aes = AES.new(key, AES.MODE_CBC, IV)
    return unpad(aes.decrypt(message[AES.block_size:]))

shared_secret = hashlib.sha256(str(intermediate).encode("utf-8")).digest()
print decrypt(cipher01, shared_secret)
print decrypt(cipher02, shared_secret)

Which gives us:

Can you give me the flag ?
Sure : BZHCTF{This_key_exchange_Sux}

Crypto 3 - Cyber Bullshit Cyber

250 points, Gauthier Goujard

This challenge gives us a python program called Cyber_Bullshit_Cyber.py. The header of the file contains a ciphertext to decrypt:

""" Uncipher me :
\x01@N\x02t\x1f60\xaf?\x1c\xf1\xadS\xe2\x9c\n\x97[\xaa\xf5\xd0\\\xd6\x86\xd7\x9e\xcaUr\\M\xc3Q
\xae\x01e\x1e\xcbz\xbd\x8f\x89e^\xde'\xaa\xbf\xe4\x19\xe9\xef\x12r\xdb\xb0X\xff\\>\xa1\xad\x98
\xa1+\xc6b\x11x\xb0#\xf2\xc3\xc6&\x0c\x87w\xfe\xf0\xd6)\xd8\xd8ox\xd1\xbaR\xf5V4\xab\xa7\x
"""

The title of the challenge, whose initials are "CBC", suggests that this is surely a bullshit version of the AES-CBC (Figure 1).

Figure1
Figure 1: AES-CBC

Let's focus on the cipher function:

def generate_ks(self, m1, m2):
    return hmac.new(m1, m2, hashlib.sha256).digest()

def cipher(self, key, message):
    padded_message = self.pad_message(message)
    print padded_message
    iv = os.urandom(32)
    print iv.encode('hex')
    ks = self.generate_ks(iv, key)
    print ks.encode('hex')
    blocks = self.split_to_blocks(padded_message)
    cipher_message = ""
    for block in blocks:
        cipher_block = self.xor(ks, block)
        print cipher_block.encode('hex')
        ks = cipher_block
        cipher_message = "%s%s" % (cipher_message, cipher_block)

    return "%s%s" % (iv, cipher_message)

This code is very short for a complete implementation of AES. Indeed, by looking more closely at the loop that processes the blocks one after the other and comparing it with the diagram in Figure 1, one realizes that the xor with the preceding block is well implemented... but not the encryption scheme.

We are therefore in the following case:

Figure2
Figure 2: BULLSHIT-CBC

The encrypted string supplied is 48 bytes. This gives us the IV and two encrypted blocks, each making 128 bits. Since the last block is the result of a XOR operation between the corresponding clear and the previous block, we simply need to redo a XOR between the two encrypted blocks to fall back on the cleartext (XOR commutative property).

The functions of the supplied code do the trick:

cipher = "\x01@N\x02t\x1f60\xaf?                                                                                       \x1c\xf1\xadS\xe2\x9c\n\x97[\xaa\xf5\xd0\\\xd6\x86\xd7\x9e\xcaUr\\M\xc3Q\xae\x01e\x1e\xcbz\xbd\x8f\x89e^\xde'\xaa\xbf\ xe4\x19\xe9\xef\x12r\xdb\xb0X\xff\\>\xa1\xad\x98\xa1+                                                                  \xc6b\x11x\xb0#\xf2\xc3\xc6&\x0c\x87w\xfe\xf0\xd6)\xd8\xd8ox\xd1\xbaR\xf5V4\xab\xa7\x92"

def split_to_blocks(message, length=32):
    return [message[i:i+length] for i in range(0, len(message), length)]

def xor(m1, m2):
    return "".join([chr(ord(a)^ord(b)) for a, b in zip(m1,m2)])

blocks = split_to_blocks(cipher)
print xor(blocks[1], blocks[2])

Which gives us bzhctf{YOLOCRYPTO2017}


Crypto 4 - Cryptopat

250 points, Alban Siffer

Challenge content
    alice_pubkey.pem
    alice_pubkey_2.pem
    message1
    message2
    message3
    message4
    message5.enc
    README

We managed to eavesdrop a conversation between Alice and Bob on an unsecured channel. Unfortunately, the sensitive message we want to retrieve has been encrypted through RSA.

So that, we involve the best cryptanalysts who will be rewarded if they find up a part or the whole data we look for.

To ease the task, we have extracted every message in the chronological order (message1, message2 ...). Moreover, we have caught two public keys from Alice (first alice_pubkey.pem, then alice_pubkey_2.pem). We hope it will be enough to help us.

The solution of this challenge is in two steps. The first (very easy) allows us to retrieve the beginning of the flag and some other information to get the end. The second involves retieving a RSA key built from another RSA key (vulnerable) so as to to decrypt the end of the flag.

Let us read the eavesdropped conversation (base64 encoded)

from base64 import b64decode

for i in range(1,5,1):
    m = b64decode(open('message'+str(i)).read()).decode()
    print(m+'\n')

We get:

From : Alice
To : Bob
Hi Bob, can you sned me the flag of the Crypto chall ? I cannot find it anymore !

From : Bob
To : Alice
Of course, send me your public key so that I can send it securely

From : Bob
To : Alice
Be careful ! I think your public key is vulnerable to Wiener attack, I strongly advise you to change it ! bzhctf{B3g1N_0f_Fl4g

From : Alice
To : Bob
So, I added 15 most significant bits to my new private key, now I'm no longer vulnerable to Wiener ;)
You can send me the flag without worries;)

We get easily the first part of the flag : bzhctf{B3g1N_0f_Fl4g. The end is likely to be in message5 (encrypted by the second public key [pk2]).

In this conversation, we notice that the first public key [pk1] is vulnerable to a Wiener attack. So, we are going to retrieve the secret key [sk1] with this attack. It will help us to go further because the second secret key [sk2] uses it. The attack being a bit technical, we will not detail the following code.

from Crypto.PublicKey import RSA

def euclid(a,b):
    """
    Euclid's algorithm

    Parameters
    ----------
    a : dividend
    b : divisor

    Returns
    -------
    Q : list of the computed quotients
    R : list of computed remainders

    """
    Q = []
    R = []
    r = 2
    while r>1:
        q = a//b
        r = a - q*b
        Q.append(q)
        R.append(r)
        a = b
        b = r
    return Q,R


def coeff2red(C):
    """
    Compute the reduced fraction from the coefficients

    Parameters
    ----------
    C : list of the coefficients of the continued fraction

    Returns
    -------
    The reduced fraction

    """
    if C.size==1:
        return C[0]
    else:
        return(C[0]+1/coeff2red(C[1:C.size]))

def coeff2frac(C):
    """
    Compute the pade approximant of the continued fraction C

    Parameters
    ----------
    C : list of the coefficients of the continued fraction

    Returns
    -------
    Num : list of the numerators
    Den : list of denominators
    """
    n = len(C)

    Num = []
    Num.append(C[0])
    Num.append(C[1]*Num[0]+1)

    Den = []
    Den.append(1)
    Den.append(C[1]*Den[0])

    for i in range(2,n):
        Num.append(Num[i-1]*C[i]+Num[i-2])
        Den.append(Den[i-1]*C[i]+Den[i-2])
    return Num,Den

def isqrt(n):
    """
    Calculate the greatest integer lower or equal to sqrt(n)

    Parameters
    ----------
    n : integer

    Returns
    -------
    x : the greatest integer lower or equal to sqrt(n)

    """
    x = n
    y = (x + 1) // 2
    while y < x:
        x = y
        y = (x + n // x) // 2
    return x


def wienerAttack(e,n):
    """
    Perform a wiener attack on RSA encryption with public parameters e and n

    Parameters
    ----------
    (e,n) : public key for RSA encryption

    Returns
    -------
    rsa : a RSA object if a solution has been found
    """


    q,r = euclid(e,n)   # calculate all the quotients and remainders in the
                        # euclid algorithm (the quotients correspond to the
                        # coefficients of the continued fraction expansion of e/n)
    num,den = coeff2frac(q) # build the Padé approximant of e/n from the continued fraction
    k = len(den)
    i = 0
    while num[i]==0:
        i = i+1    

    while i<k: # Test all the denominators of the Padé approximant
    # d is a correct answer if the equation : x² - (n- ‎ϕ(n)+1)x + n = 0 has two integer solutions
    # so we have to test if Δ is a perfect square or not (in fact, these solutions are p and q)

        x = e*den[i]-1
        phi = x//num[i] # calculus of ϕ
        delta = (n - phi + 1)**2 - 4*n # calculus of Δ = b²-4ac
        s = isqrt(delta)
        r = delta-s**2
        if r==0:
            p = ((n-phi+1)-s)//2
            q = ((n-phi+1)+s)//2
            d = den[i]

            if p*q == n:
                print("Solution found")
                return RSA.construct((n,e,d,p,q))

        i = i+1

    print("No solution found")
    return 0


pk1 = RSA.importKey(open('alice_pubkey.pem','rb').read())
k1 = wienerAttack(pk1.e,pk1.n)
sk1 = k1.d

The Wiener attack works well ! So we know the whole key [k1] (the public and the secret keys). As [sk2] is built from [sk1] ("I added 15 most significant bits to my new private key") we can try a bruteforce attack on the 15 new bits (only 2¹⁵= 32768 possibilities). To check if a key is the right one, we just have to check for some message

$$M$$

that

$$(M^e2)^sk2 = M [n2]$$

with

$$(e2,n2)$$

the public parameters of [pk2].

To build this new key we concatenate [prefix (15bits)] || [sk1] (binary representation). The code is given below.

pk2 = RSA.importKey(open('alice_pubkey_2.pem','rb').read())

e2 = pk2.e
n2 = pk2.n

M = 2 # simple message
N = pow(2,15) # iterations

sk1_bin = bin(sk1)[2:] # binary representation of [sk1]

for p in range(N):
    prefix = bin(p)[2:] # bits to add
    sk2 = int(prefix + sk1_bin, 2) # concatenation and integer conversion
    if pow(M,e2*sk2,n2) == M: # test
        print(sk2)
        break

k2 = RSA.construct((n2,e2,sk2))

After about 15 minutes, we get the secret key [sk2], so we can decrypt message5.

m5 = rsa2.decrypt(open('message5.enc','rb').read())
print(m5)

We retrieve the following message:

b"\x02\xc2\xa6\xae\x9c\xfcm\x0b\xa9\x17\xa3\x89\xe7\x0f\xc9\xb9`\xbb\x17j\x03\xf9\x92\xad\x032\xb9\xd3[\xde\x93\xb0u3l\x12\xb4\x85t5\x8bFmL\x080\xb6{\xf2\x8d\xa1\n\x9d\xcc\xce_\xc0\x9f\x1d\xa3\x91\xbc\x98\t\x1b9\x15\xa8\xc8\xe8\xd7)\xd7\x8f`\x1c+\x1b\xf1G\xd3\x1dn;\xa3\x131g\xcby\xc0\x01\xb0\xf0\xac\x80\x87\xe9dS\x90^\xeats\xda\x01\x07\x90\xb62\xbe\x14!\x03F\x98\x95\xca\x89z\x9c\x04\xbez\xd2\xb9/\xc2p,!L\xa8\xd1\xf4\x86\x1c=\xec\xedM\xa1`/\xf6\x11\t7\x9b\x87\xac\x0b\xa8G\x12\x02\x9erLBL$\xe1eYM^\x9c\xb6\x98+\xfb\xa6\xbdi\x0fJ\xc6\xd8|\x92\x98\x16\xfe\xb0vr\x94c=\xa2\xdc\xe6R\xf9',P\xf0\x9b\x81R\xcf4\x00RGUgOiBBbGljZQ0KQSA6IEJvYg0KXzNORF9vZl9GfGFHfQ==\n"

We notice a base64 encoded string at the end of the message:

print(b64decode('RGUgOiBBbGljZQ0KQSA6IEJvYg0KXzNORF9vZl9GfGFHfQ==').decode())

And, we get the end of the flag :

From : Alice
To : Bob
_3ND_of_F|aG}

Finally, the flag is bzhctf{B3g1N_0f_Fl4g_3ND_of_F|aG}


Programming 2 - U Know Dev?

200 points, Loïck Bonniot

For this challenge, the starting information was a (ip, port) couple. We naturally began the investigation with a telnet command to see what was there.

'##::::'##:'##:::'##:'##::: ##::'#######::'##:::::'##:'########::'########:'##::::'##::'#######::
 ##:::: ##: ##::'##:: ###:: ##:'##.... ##: ##:'##: ##: ##.... ##: ##.....:: ##:::: ##:'##.... ##:
 ##:::: ##: ##:'##::: ####: ##: ##:::: ##: ##: ##: ##: ##:::: ##: ##::::::: ##:::: ##:..:::: ##::
 ##:::: ##: #####:::: ## ## ##: ##:::: ##: ##: ##: ##: ##:::: ##: ######::: ##:::: ##::::: ###:::
 ##:::: ##: ##. ##::: ##. ####: ##:::: ##: ##: ##: ##: ##:::: ##: ##...::::. ##:: ##::::: ##.::::
 ##:::: ##: ##:. ##:: ##:. ###: ##:::: ##: ##: ##: ##: ##:::: ##: ##::::::::. ## ##::::::..::::::
. #######:: ##::. ##: ##::. ##:. #######::. ###. ###:: ########:: ########:::. ###:::::::'##:::::
:.......:::..::::..::..::::..:::.......::::...::...:::........:::........:::::...::::::::..::::::

<?php
$array = array(65228, 65232, 65264, 65259, 65263, 65217, 65227, 65250, 65196, 65263, 65260, 65258, 65230, 65220, 65232, 65230, 65259, 65248, 65262, 65233, 65234, 65247, 65201, 65212);
$const = 65144;
foreach( $array as $value ){
    echo chr($value - $const);
}
?>

>>>

Computing this obfuscated PHP code gave us TXxswISj4wtrVLXVshvYZg9D as result. We tried to manually copy/paste this result to the telnet shell, but the connection was broken from that point. Each time we restarted the telnet command, we got different random generated code snippets, in 4 languages (PHP, Python, C, and BrainFuck).

++++++++++[>+++++++++<-]>.[-]++++++++++[>++++++++++<-]>++++++++.[-]++++++++++[>+++++++++++<-]>+++++++.[-]++++++++++[>+++++++++++<-]>++.[-]++++++++++[>++++++<-]>+++++++.[-]++++++++++[>++++++++<-]>+++++++.[-]++++++++++[>++++++++<-]>++.[-]++++++++++[>++++++<-]>++++++++.[-]++++++++++[>++++++++++++<-]>++.[-]++++++++++[>++++++<-]>+++++++++.[-]++++++++++[>++++++++<-]>.[-]++++++++++[>+++++<-]>+.[-]++++++++++[>++++++<-]>++++++.[-]++++++++++[>+++++++++++<-]>++++.[-]++++++++++[>+++++++++<-]>++++++++.[-]++++++++++[>+++++++++<-]>++++++++.[-]++++++++++[>+++++++<-]>+.[-]++++++++++[>++++++++++<-]>++++++.[-]++++++++++[>+++++++++<-]>+++++++.[-]++++++++++[>+++++<-]>++++.[-]++++++++++[>++++++++<-]>+++++.[-]++++++++++[>++++++++++<-]>++++++.[-]++++++++++[>++++++++++++<-]>.[-]++++++++++[>++++++<-]>+++++++.[-]
amwgCrqQxzTahnGmsMKhYNog = ['\x99', '\xad', '\xf4', '\xf9', '\x83', '\xb7', '\x92', '\x8b', '\x99', '\xbb', '\xb6', '\x87', '\x99', '\x82', '\x9b', '\xb8', '\x88', '\x96', '\xf6', '\x88', '\xf8', '\xf6', '\xbb', '\xb5']
FuizhlooTPVuyNmzwQzIwHAU = '\xc1'
for _ in amwgCrqQxzTahnGmsMKhYNog:
    print(chr( ord(_) ^ ord(FuizhlooTPVuyNmzwQzIwHAU)))
#include <stdio.h>
int main()
{
    int constant = 46774;
    int GXHVHhiYuABwChFqflUGsqmdXbBHtXSesaYbeVnNWtkDGITftqZprHbnViNnHdNS = 46830;
    int XfADSHKtoKHzUudGnqXazbvrNuEeNedwzgmIVZkGOLbuxMcCswtnuHzfChtnaFjY = 46812;
    int zEjvVtFJDgeDgjkkxNJfSvkaJCSVKLBjsnHjJOenoCQMlyzSyWLHLDQrmLZWbfQl = 46786;
    int iVdTRXXgoLunRtdiyLEAigLoOrDgKgDHatmayTebuxOdKXFPOgRWWtFLpvDWTHOH = 46818;
    int PcfdSakkdTOilnGZvCJhWaUTtpxHfdJXGNrPxzPwdgSqdjgcwhNyaomPqzkKOwCh = 46810;
    int XcHNOWctBKxipegwBGVQcHePYPeIEkVoRMRHVoZJxKisxCKCjSCUzZPdMuZkbRmC = 46832;
    int PvNSIPAcNANwxcApWSeOoEHveCBGqgbKsSRzwJRAXFIGonBRvoWQuxzZiADeIIcp = 46821;
    int rDlrCmMIdfbSOVnwQwFnPIFKTngSDhZfCMrxYrxQtVhnltLCTlezCphpyFBrJwdw = 46835;
    int tprHnSURCMtPArPVoVASrWGItEVuOClWZwTLkbcSLEnjQzgPdPBakffNlqcaodNK = 46788;
    int ZtXwEywIimIpmPpRgrsKywOjeblMwkhjShxFAeTVQersSRFaOHRgWlBTDYWDyfCU = 46807;
    int HzREeaEJBrCjjWVMMlqYnXskGTGZWrKKtELWsVJicmKmhXTVbHmfIUEwIqqnyOqD = 46830;
    int UVbyqQKCKUmdbMvRzqSEeBBShywOKJDISRMWGcTrOkuRwwLihHWHgmvbNEMOtSwJ = 46720;
    int uaLkHiJTmDzFqyTwvbWkNXvTlFLREITQAVqjdhOGqgFFtzgInuAgUkYWHNyDsarO = 46844;
    int WpGAJKSkgiuQDAKhKSwYLsLQiLGoiCWRzbBUWFnEkqCTGfZUhqmCruZXTiKNjnmJ = 46734;
    int zBLeGbwcZFhocbfLFRQBCucpqWUJRfbZvtMPofdNlZEhMZJkWwFPnmVnFgJoSnxc = 46842;
    int USGuzOhPIeyROXeDsmNyyEEKJjilEDvYhEohpEtdGIlrmWlSEYTLsvzrFMCoEivs = 46727;
    int zJotBXuPOveblQYAuIzkyaikvQUtLcqCZQcjHtJxTECIHyUDemDUSOtHlhdxVIny = 46808;
    int eImYyGaedSARIBciVBBeHNmqcBLLmNAEjRHYeZOzkgRzcpZVaDQQaAaTYkwtquYW = 46837;
    int hZouEZvFlkktuNMKMJfIwlllTirItsErIXZicIYdjMJpeYwwGicnikbynidaUzXs = 46837;
    int XPLSIBBVjaglLBPEYAiOKsRathgNMrEZfCUbhourTBhumOXQYPFdsaOVFoawPECv = 46811;
    int JFUmGshJZZXoEwNMpdwyDFaplyGKhnBUtZbKdqsqMFdKheuXlUggdydgorCcnQbe = 46819;
    int YzJHTArSNegxNkLLSJsccTDZrDsmqzRMgwWbktORZNoHLfrVIwrsYNVKHhzSFiTM = 46725;
    int fpGuGqtDyJUqgUcJctoLTpfOPDJngRViedNtOoNFXmqaxyveNwMBuaBqmSgjjHqn = 46812;
    int fRbaGkLryakqYZkrMosYMbiqLWRaiETqXRuQAlJwbVyWgIDzbVIXTQoNmiamiNwW = 46815;

    printf("%c", char(GXHVHhiYuABwChFqflUGsqmdXbBHtXSesaYbeVnNWtkDGITftqZprHbnViNnHdNS^constant));
    printf("%c", char(XfADSHKtoKHzUudGnqXazbvrNuEeNedwzgmIVZkGOLbuxMcCswtnuHzfChtnaFjY^constant));
    printf("%c", char(zEjvVtFJDgeDgjkkxNJfSvkaJCSVKLBjsnHjJOenoCQMlyzSyWLHLDQrmLZWbfQl^constant));
    printf("%c", char(iVdTRXXgoLunRtdiyLEAigLoOrDgKgDHatmayTebuxOdKXFPOgRWWtFLpvDWTHOH^constant));
    printf("%c", char(PcfdSakkdTOilnGZvCJhWaUTtpxHfdJXGNrPxzPwdgSqdjgcwhNyaomPqzkKOwCh^constant));
    printf("%c", char(XcHNOWctBKxipegwBGVQcHePYPeIEkVoRMRHVoZJxKisxCKCjSCUzZPdMuZkbRmC^constant));
    printf("%c", char(PvNSIPAcNANwxcApWSeOoEHveCBGqgbKsSRzwJRAXFIGonBRvoWQuxzZiADeIIcp^constant));
    printf("%c", char(rDlrCmMIdfbSOVnwQwFnPIFKTngSDhZfCMrxYrxQtVhnltLCTlezCphpyFBrJwdw^constant));
    printf("%c", char(tprHnSURCMtPArPVoVASrWGItEVuOClWZwTLkbcSLEnjQzgPdPBakffNlqcaodNK^constant));
    printf("%c", char(ZtXwEywIimIpmPpRgrsKywOjeblMwkhjShxFAeTVQersSRFaOHRgWlBTDYWDyfCU^constant));
    printf("%c", char(HzREeaEJBrCjjWVMMlqYnXskGTGZWrKKtELWsVJicmKmhXTVbHmfIUEwIqqnyOqD^constant));
    printf("%c", char(UVbyqQKCKUmdbMvRzqSEeBBShywOKJDISRMWGcTrOkuRwwLihHWHgmvbNEMOtSwJ^constant));
    printf("%c", char(uaLkHiJTmDzFqyTwvbWkNXvTlFLREITQAVqjdhOGqgFFtzgInuAgUkYWHNyDsarO^constant));
    printf("%c", char(WpGAJKSkgiuQDAKhKSwYLsLQiLGoiCWRzbBUWFnEkqCTGfZUhqmCruZXTiKNjnmJ^constant));
    printf("%c", char(zBLeGbwcZFhocbfLFRQBCucpqWUJRfbZvtMPofdNlZEhMZJkWwFPnmVnFgJoSnxc^constant));
    printf("%c", char(USGuzOhPIeyROXeDsmNyyEEKJjilEDvYhEohpEtdGIlrmWlSEYTLsvzrFMCoEivs^constant));
    printf("%c", char(zJotBXuPOveblQYAuIzkyaikvQUtLcqCZQcjHtJxTECIHyUDemDUSOtHlhdxVIny^constant));
    printf("%c", char(eImYyGaedSARIBciVBBeHNmqcBLLmNAEjRHYeZOzkgRzcpZVaDQQaAaTYkwtquYW^constant));
    printf("%c", char(hZouEZvFlkktuNMKMJfIwlllTirItsErIXZicIYdjMJpeYwwGicnikbynidaUzXs^constant));
    printf("%c", char(XPLSIBBVjaglLBPEYAiOKsRathgNMrEZfCUbhourTBhumOXQYPFdsaOVFoawPECv^constant));
    printf("%c", char(JFUmGshJZZXoEwNMpdwyDFaplyGKhnBUtZbKdqsqMFdKheuXlUggdydgorCcnQbe^constant));
    printf("%c", char(YzJHTArSNegxNkLLSJsccTDZrDsmqzRMgwWbktORZNoHLfrVIwrsYNVKHhzSFiTM^constant));
    printf("%c", char(fpGuGqtDyJUqgUcJctoLTpfOPDJngRViedNtOoNFXmqaxyveNwMBuaBqmSgjjHqn^constant));
    printf("%c", char(fRbaGkLryakqYZkrMosYMbiqLWRaiETqXRuQAlJwbVyWgIDzbVIXTQoNmiamiNwW^constant));

    return 0;
}

From that point, it became clear we had to quickly write the result of each obfuscated program to the TCP connection, get another program and so on, until the flag appearance.

We automated that process using a Go program:

  • For each incoming program:
    • Isolate the source
    • Guess source language
    • Write the source to an input file
    • Compile and execute source
    • Submit result

However, we encountered some minor issues while writing the program:

  • The TCP connection was very flaky, sending a random number of bytes in each packet. We guessed that the Go TCPConn structure failed to handle the buffering properly, and decided to read the whole input byte-by-byte
  • We used g++ to compile the C code, and it required a special case with the two-step compilation / execution
  • The python code printed one character per line, requiring us to remove new lines from execution output
  • For safety, since we were directly executing remote unknown code on a local machine, we used a Linux Container

Here is our final (quick'n'dirty) Go program:

package main

import (
    "bytes"
    "fmt"
    "io/ioutil"
    "net"
    "os/exec"
)

const file = "in.c" // c extension for g++ recognition
var conn net.Conn

func main() {
    servAddr := "10.119.227.42:52000"
    tcpAddr, _ := net.ResolveTCPAddr("tcp", servAddr)
    conn, _ = net.DialTCP("tcp", nil, tcpAddr)

    for {
        _, err := conn.Write(run())
        if err != nil {
            panic(err)
        }
        fmt.Println("OK")
    }
}

func run() []byte {
    res := get()
    com := command(res)

    ioutil.WriteFile(file, res, 0777)

    // Special case for g++
    if com == "g++" {
        exec.Command("g++", file).CombinedOutput()
        com = "./a.out"
    }

    output, _ := exec.Command(com, file).CombinedOutput()
    output = bytes.Replace(output, []byte("\n"), []byte{}, 100) // python print case
    return append(output, 0x0a) // append \n
}

func command(block []byte) string {
    if bytes.Contains(block, []byte("<?php")) {
        return "php"
    }

    if bytes.Contains(block, []byte("+++++")) {
        return "bf"
    }

    if bytes.Contains(block, []byte("main()")) {
        return "g++"
    }

    return "python"
}

var offset = 796

func get() []byte {
    raw := getRaw()
    res := bytes.TrimSpace(raw[offset : len(raw)-3])
    offset = 6 // Further offsets are less important than the first one
    return res
}

func getRaw() []byte {
    var out []byte
    buf := make([]byte, 1)

    var nb int
    for {
        _, err := conn.Read(buf)
        if err != nil {
            fmt.Println(string(out), string(buf))
            panic(err)
        }
        c := buf[0]
        out = append(out, c)

        if c != '>' { // Read until ">>>"
            nb = 0
        } else {
            nb++
            if nb == 3 {
                return out
            }
        }
    }
}

After around 200 iterations in ~10 seconds, we got a final program that compiled to the challenge flag: bzhctf{H0pefully_n0_J4v4_h3r3}. As a side note (and a personal success), we were the second team to validate this challenge!


Misc 1 - Bluesniff

50 points, Nicolas Datin

We were just provided the title.
We would say that we had the nose for this challenge... so we just scanned for bluetooth devices using a smartphone.

Figure1


Misc 2 - BreizhCTF Party!

100 points, Loïck Bonniot

We started this challenge in a colorful web page displaying a binary string, then redirecting to another colorful web page (with another flashy color of course) holding another binary string. Finally, we extracted 4 different binary strings across 4 looping pages. We also extracted a strange string that could have represented a binary mask.

!!11!!1!

0010001000101110001101110010100100111100001110000010111100001100001110000000011000001111001100000000000100011001001110010000100100100101

0100000101000010010000110100010001000101010001100100011101001000010010010100101001001011010011000100110101001110010011110101000001010001

0101000101011010010100110100010101000100010100100100011001010100010001110101100101001000010101010100101001001001010010110100111101001100

0101000001001100010011110100101101001001010010100101010101001000010110010100011101010100010001100101010001000110010100100100010001000101

Firstly, we analyzed those strings using Golang's hex package.

00000000  22 2e 37 29 3c 38 2f 0c  38 06 0f 30 01 19 39 09  |".7)<8/.8..0..9.|
00000010  25                                                |%|

00000000  41 42 43 44 45 46 47 48  49 4a 4b 4c 4d 4e 4f 50  |ABCDEFGHIJKLMNOP|
00000010  51                                                |Q|

00000000  51 5a 53 45 44 52 46 54  47 59 48 55 4a 49 4b 4f  |QZSEDRFTGYHUJIKO|
00000010  4c                                                |L|

00000000  50 4c 4f 4b 49 4a 55 48  59 47 54 46 54 46 52 44  |PLOKIJUHYGTFTFRD|
00000010  45                                                |E|

From the identical length of those binary strings, we guessed we had to combine them to decipher the first unreadable string, that we expected to contain the flag. Using the 00110010 bit-mask was not useful, but sometimes the simplest solution is the good one: we just had to xor the four buffers to get our flag.

package main

import (
        "encoding/hex"
        "fmt"
)

func main() {
        str := "222E37293C382F0C38060F300119390925"
        str2 := "4142434445464748494A4B4C4D4E4F5051"
        str3 := "515A534544524654475948554A494B4F4C"
        str4 := "504C4F4B494A5548594754465446524445"

        data, _ := hex.DecodeString(str)
        data2, _ := hex.DecodeString(str2)
        data3, _ := hex.DecodeString(str3)
        data4, _ := hex.DecodeString(str4)

        for i := range data {
                data[i] = data[i] ^ data2[i] ^ data3[i] ^ data4[i]
        }

        fmt.Println(string(data)) // bzhctf{XoRXoRXoR}
}

Misc 4 - Targeted Password cracking

125 points, Nicolas Zilio

This challenge asks us to crack Robert's password, which is defined as the following:

$2a$12$3CvNRmE3NeqAvj0F8qkI9uflR1k54oEgJD9is6yUTR3pzWTu6WOG6

We can see here it's a Unix-like hash, using Blowfish hash function ($2a), and whose salt is equal to $12. Due to the presence of a salt, we couldn't have any precalculated results of the hash giving us the plaintext password. Thus, the solutions are either to bruteforce it, or to gain additional information to guess it at least partially.

Hopefully, we are here provided with some other hashes of Robert's other passwords:

  • 8d63fcd67f2cdd36cd9a09dff98241a3
  • 896f94d24c89708c21dc4e65358890fa
  • e129b5c5b079c4e7a041642a91ada0ffd6acf0e9
  • 621fddfc80c3b8e0c66027a4af7be63e683caf21
  • a8eae5f805fb302acd07373b0172de5303ce9215
  • 66d97aff5a0cfcd7af55a977dd1f3762448b4c78
  • bdf100936eb3a4bfa8b22466b55ab21f693782bf

By the way, the user is certainly using several passwords' subparts in common between each password. So let's start by cracking those other hashes, to get some information of what Robert does with his passwords, using the well-known Crackstation website:

Figure1
Figure 1: Crackstation result for the provided hashes

We can see here our first assertion seems true : Robert reuses "29" and "unicorn" between multiple passwords. We are so going to form a passwords list using the words already known in Robert's passwords cracked, and by adding few guesses based on the fact Robert seems to love Bretagne.

#!python
import itertools

data = ["breizh","brest","crepes","lorient","chouchen","capturing","unicorn","pegasusunicorn","pegasus","29","bretagne","rennes","35"]

with open('passwd.lst','w') as f:
    for i in xrange(4):
        for string in itertools.imap(''.join, itertools.product(data, repeat=i)):
            f.write(string+'\n')

The list is generated, we can then verify if Robert's final password is in it:

$ echo '$2a$12$3CvNRmE3NeqAvj0F8qkI9uflR1k54oEgJD9is6yUTR3pzWTu6WOG6' > tocrack.txt
$ john --wordlist=passwd.lst tocrack.txt
Loaded 1 password hash (bcrypt [Blowfish 32/64 X2])
Press 'q' or Ctrl-C to abort, almost any other key for status
breizhunicorn    (?)
1g 0:00:00:02 100% 0.3968g/s 8.730p/s 8.730c/s 8.730C/s breizhunicorn..breizhpegasusunicorn
Use the "--show" option to display all of the cracked passwords reliably
Session completed
$ sudo john --show tocrack.txt
?:breizhunicorn

1 password hash cracked, 0 left

The password has been cracked. The flag is finally BZHCTF{breizhunicorn}


Misc 5 - The NopSleds

150 points, David Boisseleau

The only file provided for this challenge was a .mp3 audio file.

Listening to the song, we can notice two instruments: a drumkit and bass guitar. While the drums continuously plays the rhythmic gimmick, a bass guitar plays a simple melody only made of two different keys. The 56-seconds track is divided into 14 rythmic sequences all ended by a hi-hat burst. There's only bass guitar played on the 13 first ones, with 8 keys played in each sequence. As only 2 differents key are played, we assumed that might be a binary code. With Audacity, after importing the mp3, we applied a low-pass filter (-48dB - 3kHz) to make recognition a bit easier, and slowed the tempo to be able to catch every key played. We carefully listened to every sequence several time and, assuming a lower-pitched key equals a zero and an higher-pitched key equals a one, we discovered the following binary sequence:

01110010 -> r
01101111 -> o
01100011 -> c
01101011 -> k
01110100 -> t
01101000 -> h
01100101 -> e
01100011 -> c
01100001 -> a
01110011 -> s
01100010 -> b
01100001 -> a
01101000 -> h

Finally, the flag is breizhctf{rockthecasbah}


Exploit 1 - Pyjail01

125 points, Nicolas Datin

This was a Pyjail challenge, we had to find the flag within a restricted Python environment.

First thing to try in a Pyjail challenge is to call the dir() function to get a list of names in the current namespace.

>>> dir()    
msg=dir()
['Sandbox', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'encoding', 'findall', 'forbidden_word', 'inp', 'msg', 'sandbox']

The Sandbox class seemed interesting to us, so, we've repeated the dir() operation on it.

>>> dir(Sandbox)
msg=dir(Sandbox)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'g3t_fl4G', 'make_secure']

Surprise! Sandbox class had a g3t_fl4G method, next thing to do was to try to call her.
A member function take an implicit self parameter, so we needed to instantiate a Sandbox object first.

>>> ins=Sandbox()
msg=ins=Sandbox()
<__main__.Sandbox object at 0x7f7138710fd0>

Once that we had the Sandbox instance, we called g3t_fl4G with the getattr() function.

>>> getattr(Sandbox,dir(Sandbox)[-2])(ins)
msg=getattr(Sandbox,dir(Sandbox)[-2])(ins)
[Congratulation : bzhctf{DONT_DROP_THE_SOAP_IN_DA_JAIL}]

The flag was: bzhctf{DONT_DROP_THE_SOAP_IN_DA_JAIL}


Trivia 5 - Cyber Output

25 points, Nicolas Datin

For this challenge we only had a text file. Firstly, we cat'ed the file

cat file.txt
JJFEMRKNKJFU4S2KIZKTIUZSJNEVUS2UJFKVUU2KJZCVMVKTGJKUURSLKZKVKMSLJJNEGVSNKZFVIR2KJNKVKUS
[ ... ]
VEVGMRSJFJEYRSLKVNFGSKWIVLE2U2LIZFVMSCFK5JUWTCLKUZUMSKNKNIUUSJSKVIVMSSXJNCTMVBSKBFDK===

The typical === at the end, we supposed that it was base32 encoded.
The file size was ~3.2M, so we made a Python script to decode it.
As the output was still base32 encoded, we made it decoding the result until an exception was raised.

#!/usr/bin/env python

import base64

with open('./file.txt', 'r') as i:
    content = i.read()
    while True:
        try:
            content = base64.b32decode(content)
        except TypeError:
            print 'The flag is: %s' % content
            exit(0)

The flag was: bzhctf{w0w_many_b32_enc0de_sUch_Usel3ss}