Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
area41 2014 challenge writeup

Intro

Being a cheapass student, I couldn't resist to try to win a free entrance to Area41 Security Conference (don't mix with Alcoholics Anonymous Area 41 in Nebraska) after spotting @gandro23 retweet about the conference and the challenge.

Level 01

The challenge starts after downloading and extracting a binary. A quick look into it with file gives us a hint that the binary is an ELF file compiled for x86_64 Linux machine with stripped symbols.

$ file challenge_area41
challenge_area41: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, stripped

After running it we get a nice banner and a prompt asking for $$$ to pass the toll:

$ ./challenge_area41
 =================================================================================
   /$$$$$$                                      /$$   /$$   /$$
  /$$__  $$                                    | $$  | $$ /$$$$
 | $$  \ $$  /$$$$$$   /$$$$$$   /$$$$$$       | $$  | $$|_  $$
 | $$$$$$$$ /$$__  $$ /$$__  $$ |____  $$      | $$$$$$$$  | $$
 | $$__  $$| $$  \__/| $$$$$$$$  /$$$$$$$      |_____  $$  | $$
 | $$  | $$| $$      | $$_____/ /$$__  $$            | $$  | $$
 | $$  | $$| $$      |  $$$$$$$|  $$$$$$$            | $$ /$$$$$$
 |__/  |__/|__/       \_______/ \_______/            |__/|______/



   /$$$$$$  /$$                 /$$ /$$
  /$$__  $$| $$                | $$| $$
 | $$  \__/| $$$$$$$   /$$$$$$ | $$| $$  /$$$$$$  /$$$$$$$   /$$$$$$   /$$$$$$
 | $$      | $$__  $$ |____  $$| $$| $$ /$$__  $$| $$__  $$ /$$__  $$ /$$__  $$
 | $$      | $$  \ $$  /$$$$$$$| $$| $$| $$$$$$$$| $$  \ $$| $$  \ $$| $$$$$$$$
 | $$    $$| $$  | $$ /$$__  $$| $$| $$| $$_____/| $$  | $$| $$  | $$| $$_____/
 |  $$$$$$/| $$  | $$|  $$$$$$$| $$| $$|  $$$$$$$| $$  | $$|  $$$$$$$|  $$$$$$$
  \______/ |__/  |__/ \_______/|__/|__/ \_______/|__/  |__/ \____  $$ \_______/
                                                            /$$  \ $$
                                                           |  $$$$$$/
                                                            \______/
 =================================================================================

 Welcome to the Area41 challenge! If you attended HashDays, it will be easy for you...

  -> The next part of the challenge is tolled. No change given!
  -> Only CHF accepted. How much are you going to pay?
  -> 42

 BOOOOO

Hehe, no easy money with a random amount. Let's try to disassemble it:

$ objdump -d challenge_area41

challenge_area41:     file format elf64-x86-64

.text section seems to not exist. Listing out all printable characters of the binary reveals a few interesting ones:

$ strings challenge_area41
WTF!

...

$Info: This file is packed with the WTF executable packer #wtf $
$Id: WTF 3.91 The WTF Team. All WTFs Reserved. $

...

The binary is packed, but what this WTF packer is ? Googling doesn't return any link to WTF packer or any helpful info. Looking more carefully into strings output we find UPX!u which gives the hint that the binary was packed with upx, but the "UPX" occurrence in the binary was replaced by "WTF". Therefore, decompressing with upx fails hard:

$ upx -d challenge_area41
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: challenge_area41: NotPackedException: not packed by UPX

Unpacked 0 files.

The problem might be that upx can't find UPX_MAGIC_LE32 which is replaced by "WTF!" and raises the exception. Let's bring the "UPX!" back and try to decompress:

$ sed -i 's/WTF\!/UPX\!/g' challenge_area41
$ upx -d challenge_area41
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91        Markus Oberhumer, Laszlo Molnar & John Reiser   Sep 30th 2013

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     39558 <-     14960   37.82%  linux/ElfAMD   challenge_area41

Unpacked 1 file.

Worked. Now we can disassemble the binary:

$ objdump -s challenge_area41 > challenge_area41.asm

Level 02

The function main contains a check for the amount we provide:

  406f57:	e8 64 96 ff ff       	callq  4005c0 <getchar@plt>
  406f5c:	88 45 fb             	mov    %al,-0x5(%rbp)
  406f5f:	0f be 45 fb          	movsbl -0x5(%rbp),%eax
  406f63:	83 e8 30             	sub    $0x30,%eax
  406f66:	89 45 fc             	mov    %eax,-0x4(%rbp)
  406f69:	83 7d fc 05          	cmpl   $0x5,-0x4(%rbp)
  406f6d:	75 07                	jne    406f76 <main+0x76>
  406f6f:	e8 db fe ff ff       	callq  406e4f <good_toll>
  406f74:	eb 05                	jmp    406f7b <main+0x7b>
  406f76:	e8 f2 99 ff ff       	callq  40096d <wrong_toll>

The function int getchar(void) returns a value in eax register, but only 8 bits are taken from the value and written back to eax and sign is extended if needed. Then it is subtracted by 0x30 which converts a char to integer (0x30 = '0' in ASCII) and finally the converted value is compared to 0x05. So, it expects us to give 5 CHF for the pass to enter good_toll function which mostly interests us.

Level 03

After paying 5 CHF nothing useful happens:

 Welcome to the Area41 challenge! If you attended HashDays, it will be easy for you...

  -> The next part of the challenge is tolled. No change given!
  -> Only CHF accepted. How much are you going to pay?
  -> 5

Nevermind, I changed my mind

Disassembling good_toll explains a lot:

0000000000406e4f <good_toll>:
  406e4f:	55                   	push   %rbp
  406e50:	48 89 e5             	mov    %rsp,%rbp
  406e53:	48 83 ec 10          	sub    $0x10,%rsp
  406e57:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  406e5e:	83 7d fc 00          	cmpl   $0x0,-0x4(%rbp)
  406e62:	74 16                	je     406e7a <good_toll+0x2b>
  406e64:	b8 00 00 00 00       	mov    $0x0,%eax
  406e69:	e8 c1 a1 ff ff       	callq  40102f <init_data_good>
  406e6e:	b8 00 00 00 00       	mov    $0x0,%eax
  406e73:	e8 7d 99 ff ff       	callq  4007f5 <show_data_matrix>
  406e78:	eb 1e                	jmp    406e98 <good_toll+0x49>
  406e7a:	48 8b 05 ff 13 20 00 	mov    0x2013ff(%rip),%rax        # 608280 <stdout@@GLIBC_2.2.5>
  406e81:	48 89 c1             	mov    %rax,%rcx
  406e84:	ba 1e 00 00 00       	mov    $0x1e,%edx
  406e89:	be 01 00 00 00       	mov    $0x1,%esi
  406e8e:	bf b8 79 40 00       	mov    $0x4079b8,%edi
  406e93:	e8 58 97 ff ff       	callq  4005f0 <fwrite@plt>
  406e98:	c9                   	leaveq
  406e99:	c3                   	retq

Spot the obfuscation which makes a jump in the function to surpass init_data_good:

  406e57:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
  406e5e:	83 7d fc 00          	cmpl   $0x0,-0x4(%rbp)
  406e62:	74 16                	je     406e7a <good_toll+0x2b>

Zero is always equal to zero. Bummer, but gdb comes to the rescue:

$ gdb ./challenge_area41
(gdb) b *0x406e62
Breakpoint 1 at 0x406e62
(gdb) r

...

  -> The next part of the challenge is tolled. No change given!
  -> Only CHF accepted. How much are you going to pay?
  -> 5

Breakpoint 1, 0x0000000000406e62 in good_toll ()
(gdb) p $eflags
$1 = [ PF ZF IF ]
(gdb) set ($eflags) = 0x206
(gdb) p $eflags
$2 = [ PF IF ]

What we did is we reset the ZF before executing je 406e7a <good_toll+0x2b> to avoid the conditional jump.

Level 04

Now comes the interesting part... Hope you still remember my gdb session which I've started in Level 03. Let's continue it to see what happens:

(gdb) c
Continuing.

(Remember HashDays challenges...)

----------------------------------------------------------------------------------------------------
| 0 0 0 0 0 0 0 1 1 0 1 0 0 1 0 1 1 0 1 0 1 0 1 0 1 1 1 1 1 1 0 0 1 1 1 1 0 1 1 1 0 1 0 0 0 0 0 0 0 |
| 0 1 1 1 1 1 0 1 0 0 0 1 0 1 1 1 1 1 0 1 1 0 0 0 0 1 0 0 0 1 1 1 0 0 1 1 0 1 0 0 0 1 0 1 1 1 1 1 0 |
| 0 1 0 0 0 1 0 1 1 1 1 0 1 0 1 0 0 0 0 1 1 1 1 1 1 0 0 0 0 1 0 1 0 1 1 1 1 1 1 0 0 1 0 1 0 0 0 1 0 |
| 0 1 0 0 0 1 0 1 0 0 0 1 1 1 1 0 1 1 0 1 0 1 0 0 0 0 0 1 1 0 0 1 1 1 1 0 0 0 1 0 1 1 0 1 0 0 0 1 0 |
| 0 1 0 0 0 1 0 1 1 0 0 0 0 0 1 1 0 1 0 1 1 0 0 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0 1 1 1 1 0 1 0 0 0 1 0 |
| 0 1 1 1 1 1 0 1 0 0 1 1 0 1 0 0 0 1 1 0 0 1 0 1 1 1 0 0 1 1 1 1 0 0 0 1 1 0 0 1 1 1 0 1 1 1 1 1 0 |
| 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 0 0 0 0 0 0 |
| 1 1 1 1 1 1 1 1 1 0 0 1 0 1 0 1 0 1 0 0 1 1 0 1 1 1 0 1 0 1 0 1 0 1 0 1 1 0 1 0 1 1 1 1 1 1 1 1 1 |
| 0 0 0 0 0 1 0 0 0 1 0 1 1 0 1 1 0 0 0 1 1 0 0 0 0 0 0 1 1 1 0 0 1 0 1 1 0 1 0 0 0 0 1 0 1 0 1 0 1 |
| 1 1 1 1 1 1 1 1 0 0 1 0 0 0 0 1 1 1 1 0 1 0 1 0 0 1 1 0 0 0 0 1 0 1 1 1 1 0 1 1 1 0 0 0 0 0 0 0 1 |
| 0 0 0 1 0 0 0 1 1 1 1 0 0 0 0 0 1 0 1 0 1 0 1 0 0 1 0 1 1 0 1 0 1 0 0 0 1 0 0 0 1 1 1 0 0 0 0 1 0 |
| 1 0 0 0 1 1 1 1 0 0 0 1 1 1 0 0 1 1 1 1 0 1 1 1 1 1 1 1 0 1 0 1 0 0 0 0 1 1 0 0 1 0 0 0 0 1 1 1 0 |
| 1 1 0 1 0 1 0 1 1 0 1 0 0 0 0 1 1 0 1 0 0 0 0 0 0 1 1 1 1 1 0 0 1 0 1 0 0 1 0 0 0 1 0 0 1 0 0 0 0 |
| 1 0 1 0 0 1 1 0 0 0 0 0 1 1 1 0 1 1 0 0 1 0 1 0 1 0 1 0 0 0 0 0 0 0 1 1 0 0 1 0 1 0 0 0 1 1 1 1 1 |
| 0 0 0 1 1 0 0 1 1 1 0 1 1 1 1 1 1 0 0 1 1 0 0 0 1 1 0 1 0 0 0 0 0 0 0 1 0 1 0 0 1 1 1 1 1 0 1 0 0 |
| 1 1 1 0 0 0 1 1 0 1 0 0 0 0 1 0 1 0 1 1 1 1 1 1 1 1 1 1 0 1 0 1 0 0 0 1 1 0 1 0 0 1 1 1 0 0 1 1 0 |
| 0 0 0 0 0 1 0 0 1 1 1 0 0 1 1 0 0 1 0 1 0 0 0 0 0 1 1 1 1 1 0 0 1 0 1 1 0 0 0 1 0 1 0 0 1 1 0 0 0 |
| 1 0 1 1 1 0 1 1 0 0 1 0 1 1 0 0 1 1 1 0 1 0 1 0 1 0 1 0 0 0 0 1 1 1 1 0 0 0 1 1 1 0 0 1 0 0 0 1 1 |
| 1 0 0 0 0 0 0 1 0 1 1 1 0 1 1 0 0 1 0 1 0 1 1 0 1 1 0 0 1 0 1 0 1 1 0 0 1 1 0 0 0 0 1 1 0 0 0 0 0 |
| 1 1 0 1 0 0 1 1 1 0 0 0 0 0 1 0 1 0 0 1 0 1 1 1 1 0 1 0 0 0 0 1 0 0 1 1 1 0 1 1 0 1 1 1 0 1 1 1 1 |
| 0 0 1 1 0 0 0 1 1 1 1 1 0 1 1 0 1 1 0 0 0 1 0 0 0 1 1 1 1 1 1 0 1 0 1 1 0 1 0 1 0 0 1 0 1 0 0 1 0 |
| 0 1 1 1 1 1 1 1 0 1 1 0 1 0 1 1 0 0 0 0 1 0 0 1 0 0 1 0 1 0 0 0 1 1 1 0 1 0 1 1 1 0 0 0 0 0 1 0 1 |
| 0 0 1 0 0 0 0 0 0 0 1 0 0 1 1 0 0 0 1 0 1 1 0 0 0 0 0 0 1 0 1 1 1 0 0 1 1 0 0 1 0 0 0 0 0 1 1 0 0 |
| 0 0 1 1 0 1 1 1 0 1 0 0 1 1 1 1 0 1 1 0 1 1 0 1 1 1 0 1 0 1 1 1 0 1 1 1 1 1 1 0 0 1 1 1 0 0 1 0 1 |
| 0 1 0 0 0 1 0 1 0 0 1 0 1 0 1 1 0 0 0 0 0 0 0 1 0 1 0 1 1 1 0 0 1 1 0 0 0 0 1 1 0 1 0 1 0 0 0 0 0 |
| 1 0 0 0 0 1 1 1 0 1 0 0 1 1 0 1 1 0 1 0 1 0 0 1 1 1 0 0 0 0 0 1 1 1 1 0 1 0 1 0 0 1 1 1 0 1 1 1 1 |
| 1 1 1 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 0 1 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 |
| 1 0 1 1 0 0 1 0 0 1 0 1 0 1 0 0 0 1 1 1 0 1 1 0 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 1 0 1 1 1 0 1 |
| 1 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 1 0 1 0 0 0 1 1 1 0 1 0 1 0 1 0 1 1 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 |
| 1 1 1 0 1 1 1 1 1 1 0 0 0 1 1 0 1 1 0 0 1 0 0 0 0 1 1 1 1 0 0 1 0 1 1 1 1 0 1 1 0 1 1 0 1 0 1 0 1 |
| 1 0 0 0 1 0 0 0 1 1 0 1 1 1 1 1 1 1 1 0 0 1 1 1 0 0 1 0 1 0 1 1 1 0 0 0 0 1 0 0 0 0 0 1 1 0 1 0 0 |
| 0 1 0 1 1 1 1 0 0 0 0 1 1 1 0 0 1 1 1 1 0 0 1 1 1 1 0 0 0 0 1 1 0 0 0 1 1 1 1 1 0 1 1 1 0 1 1 1 1 |
| 1 1 1 1 1 0 0 0 1 0 1 0 0 0 1 1 0 0 0 1 0 0 1 1 1 1 0 1 1 0 1 1 1 1 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 |
| 1 0 0 0 0 0 1 0 1 1 0 0 1 0 0 1 1 0 1 0 1 0 1 0 0 1 0 1 1 0 0 0 1 1 1 1 0 0 1 1 0 1 1 0 1 0 1 0 1 |
| 1 0 0 1 0 1 0 0 0 1 0 1 0 1 1 0 1 1 0 1 1 1 0 1 0 0 1 0 0 1 1 1 1 1 0 0 1 1 0 0 0 1 0 0 0 1 1 0 0 |
| 0 1 1 0 0 1 1 1 1 0 0 0 0 0 1 0 1 0 0 1 1 0 1 1 0 1 0 0 0 1 0 1 0 1 0 0 1 1 0 1 0 1 1 0 1 1 1 0 1 |
| 0 0 0 0 0 1 0 0 0 0 1 1 0 1 1 0 1 1 0 1 0 0 1 0 1 0 0 1 1 0 1 0 1 0 0 0 0 1 1 0 1 1 1 0 0 1 0 1 0 |
| 0 1 1 0 0 1 1 0 0 0 1 0 1 0 1 1 0 0 0 0 1 0 1 0 0 1 1 0 1 0 0 0 1 1 1 0 1 1 1 0 1 0 0 0 1 0 0 1 0 |
| 1 0 1 1 1 0 0 0 0 1 0 1 1 0 1 0 0 1 0 0 0 1 0 1 0 0 1 0 0 1 1 1 1 0 0 0 0 1 0 0 1 0 0 0 1 1 0 0 0 |
| 1 0 0 0 1 1 1 1 1 0 0 0 1 0 0 1 0 1 1 0 0 0 0 1 0 0 0 1 0 0 1 0 0 0 0 1 1 0 0 0 0 1 1 1 1 1 1 0 0 |
| 0 0 0 1 1 1 0 0 1 0 1 1 1 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 1 1 1 0 0 1 1 0 0 0 0 0 0 1 0 0 1 |
| 1 1 1 1 1 1 1 1 0 0 1 1 0 1 0 0 1 0 1 1 1 1 0 1 1 1 0 1 0 0 0 0 0 1 1 1 0 1 1 0 0 1 1 1 0 0 0 0 0 |
| 0 0 0 0 0 0 0 1 0 1 1 0 0 0 0 0 1 0 1 1 0 0 0 1 0 1 0 0 0 1 1 1 0 0 0 1 1 1 0 0 0 1 0 1 0 0 1 0 0 |
| 0 1 1 1 1 1 0 1 1 0 1 0 0 1 0 1 0 1 1 1 1 0 0 1 1 1 0 1 0 1 0 0 0 1 1 1 1 1 1 0 0 1 1 1 0 1 1 1 0 |
| 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 1 1 0 1 1 0 1 0 0 0 0 0 1 1 0 0 0 1 0 1 0 0 1 1 0 0 0 0 0 0 1 0 1 0 |
| 0 1 0 0 0 1 0 1 0 1 1 1 0 1 1 0 1 1 0 0 1 0 0 1 1 1 0 0 1 1 0 1 1 1 1 1 0 0 1 0 0 1 0 1 0 1 1 1 1 |
| 0 1 0 0 0 1 0 1 0 1 0 0 0 1 1 1 1 0 0 1 0 0 1 1 0 1 0 0 1 1 1 0 1 0 0 1 1 0 0 0 1 0 0 0 1 1 1 1 1 |
| 0 1 1 1 1 1 0 1 0 1 1 1 0 0 1 0 1 0 0 1 1 1 1 1 0 1 1 0 0 1 1 1 0 1 1 1 1 1 1 0 1 1 0 1 1 1 1 1 0 |
| 0 0 0 0 0 0 0 1 0 0 1 0 1 0 1 0 0 1 0 0 0 1 1 1 1 1 0 1 1 1 0 0 1 0 1 0 0 1 1 0 0 1 1 0 1 1 0 0 0 |
----------------------------------------------------------------------------------------------------


Bad programmers worry about the code. Good programmers worry about data structures and their relationships.


[Inferior 1 (process 2393) exited normally]

There we go. Seems like a puzzle of 49x49 bits size. I've tried to group bits to get some meaningful codes (like ASCII), but it didn't work. Copying into vim and doing :%s/1/ /g helped to spot three squares in the corners. Hehe, does it ring a bell? If not, I recommend you to look around a mess on your desk, might be useful, really :D

Anyway, if you want to get spoiled, follow the link. The answer is here.

Level 05

Decoding the code with zbarimg gives us the following:

Fwtjjfqxtgwatkv! Gux zfsh xgvk yeh knddqbqok! Vwsa xa nro dlx log ay qr
kzi@xg.zru, zr yjq bwau Swbd41 kuqxjohvih vnpfwaql hlgm. Lhwiydkq dfi
pxomhkyfrvy djj xoegbk bbokupw!

Ehrm, seems that non-alphabetic chars are already in place, so we need to decipher alpha ones. First guess Swbd41 -> Area41. Doing replacement w -> r | b -> e | d -> a in the text doesn't reveal anything, hence it doesn't seem to be a simple substitution cipher.

The very first word looks like Congratulations!. Checking the distance between maybe decrypted and encrypted, gives us:

$ python -c 'print(list(map(lambda x: ord(x[0]) - ord(x[1]), zip("Congratulations", "Fwtjjfqxtgwatkv"))))

[-3, -8, -6, -3, 8, -5, 3, -3, -8, -6, -3, 8, -5, 3, -3]

Do you see the key? Looks like it is [-3, -8, -6, -3, 8, -5, 3]. Applying it to the ciphertext returns:

Congratulations?ou�avepassthechallengeSnduYhow_oudidittoctf�b}omto�et_o[r[reacon�erence~isco[ntcodeFed|ackandsuggestionsare{l]a_s]elcom

Close enough, but there are some non-printable chars and Area is deciphered wrongly. 83 (S) + 8 is not equal to 65 (A) as it supposed to be and it is outside A(65)..Z(90) range...

Wait! But 65 + ((83 + 8) - 90) - 1 = 65. I hope you got the idea:

#!/usr/bin/env python

enc = "Fwtjjfqxtgwatkv! Gux zfsh xgvk yeh knddqbqok! Vwsa xa nro dlx log " \
      "ay qr kzi@xg.zru, zr yjq bwau Swbd41 kuqxjohvih vnpfwaql hlgm. "    \
      "Lhwiydkq dfi pxomhkyfrvy djj xoegbk bbokupw!"
key = [-3, -8, -6, -3, 8, -5, 3]
i   = 0

for c in enc:
    if c.isalpha():
        (low, high) = (ord('a'), ord('z')) if c.islower() else \
                                                        (ord('A'), ord('Z'))
        tmp = ord(c) + key[i]
        if tmp > high:
            dec = low + (tmp - high) - 1
        elif tmp < low:
            dec = high - ((low - tmp) - 1)
        else:
            dec = tmp
        print(chr(dec), end='')
        i = (i+1) % len(key)
    else:
        print(c, end='')

print('')

Answer

Finally the answer:

Congratulations! You have pass the challenge! Send us how you did it to
ctf@fb.com, to get your Area41 conference discount code.
Feedback and suggestions are always welcome!

GG! And of course, kudos to:

$ objdump -d challenge_area41 | grep author -A4
0000000000406ef0 <author>:
  406ef0:       55                      push   %rbp
  406ef1:       48 89 e5                mov    %rsp,%rbp
  406ef4:       bf d8 79 40 00          mov    $0x4079d8,%edi
  406ef9:       e8 92 96 ff ff          callq  400590 <puts@plt>
$ objdump -s challenge_area41 | grep 4079d8 -A2
 4079d8 4c657665 6c207772 69747465 6e206279  Level written by
 4079e8 204a6176 69657220 28406a61 76757469   Javier (@javuti
 4079f8 6e290020 202d3e20 00                 n).  -> .
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment