05/05/2017
Blog technique
BreizhCTF 2k17 Write-Ups
Alban Siffer, David Boisseleau, Gauthier Goujard, Loïck Bonniot, Nicolas Datin, Nicolas Zilio
Amossys was a sponsor of the [BreizhCTF 2k17](http://www.breizhctf.com/), 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
_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("JLNNa�03�03�012072�04\373|�03�01377�36c�01a�03�03�01,�17C�01a�03�03�01257202317236}|�03�01370374�03�01�06�03�03�012272�04\373|�03", "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
_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
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
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
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
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
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.
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):
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@Nx02tx1f60xaf?x1cxf1xadSxe2x9cnx97[xaaxf5xd0\xd6x86xd7x9excaUr\Mxc3Q
xaex01ex1excbzxbdx8fx89e^xde'xaaxbfxe4x19xe9xefx12rxdbxb0Xxff\>xa1xadx98
xa1+xc6bx11xxb0#xf2xc3xc6&x0cx87wxfexf0xd6)xd8xd8oxxd1xbaRxf5V4xabxa7x
"""
The title of the challenge, whose initials are « CBC », suggests that this is surely a bullshit version of the AES-CBC (Figure 1).
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:
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@Nx02tx1f60xaf? x1cxf1xadSxe2x9cnx97[xaaxf5xd0\xd6x86xd7x9excaUr\Mxc3Qxaex01ex1excbzxbdx8fx89e^xde'xaaxbf xe4x19xe9xefx12rxdbxb0Xxff\>xa1xadx98xa1+ xc6bx11xxb0#xf2xc3xc6&x0cx87wxfexf0xd6)xd8xd8oxxd1xbaRxf5V4xabxa7x92"
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
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"x02xc2xa6xaex9cxfcmx0bxa9x17xa3x89xe7x0fxc9xb9`xbbx17jx03xf9x92xadx032xb9xd3[xdex93xb0u3lx12xb4x85t5x8bFmLx080xb6{xf2x8dxa1nx9dxccxce_xc0x9fx1dxa3x91xbcx98tx1b9x15xa8xc8xe8xd7)xd7x8f`x1c+x1bxf1Gxd3x1dn;xa3x131gxcbyxc0x01xb0xf0xacx80x87xe9dSx90^xeatsxdax01x07x90xb62xbex14!x03Fx98x95xcax89zx9cx04xbezxd2xb9/xc2p,!Lxa8xd1xf4x86x1c=xecxedMxa1`/xf6x11t7x9bx87xacx0bxa8Gx12x02x9erLBL$xe1eYM^x9cxb6x98+xfbxa6xbdix0fJxc6xd8|x92x98x16xfexb0vrx94c=xa2xdcxe6Rxf9',Pxf0x9bx81Rxcf4x00RGUgOiBBbGljZQ0KQSA6IEJvYg0KXzNORF9vZl9GfGFHfQ==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.
'##::::'##:'##:::'##:'##::: ##::'#######::'##:::::'##:'########::'########:'##::::'##::'#######::
##:::: ##: ##::'##:: ###:: ##:'##.... ##: ##:'##: ##: ##.... ##: ##.....:: ##:::: ##:'##.... ##:
##:::: ##: ##:'##::: ####: ##: ##:::: ##: ##: ##: ##: ##:::: ##: ##::::::: ##:::: ##:..:::: ##::
##:::: ##: #####:::: ## ## ##: ##:::: ##: ##: ##: ##: ##:::: ##: ######::: ##:::: ##::::: ###:::
##:::: ##: ##. ##::: ##. ####: ##:::: ##: ##: ##: ##: ##:::: ##: ##...::::. ##:: ##::::: ##.::::
##:::: ##: ##:. ##:: ##:. ###: ##:::: ##: ##: ##: ##: ##:::: ##: ##::::::::. ## ##::::::..::::::
. #######:: ##::. ##: ##::. ##:. #######::. ###. ###:: ########:: ########:::. ###:::::::'##:::::
:.......:::..::::..::..::::..:::.......::::...::...:::........:::........:::::...::::::::..::::::
>>>
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
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("' { // 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.
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:
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}