The Ioli Crackmes
Introduction
In this post we will take a look at the IOLI crackmes. A crackme is a binary program which accepts input and mostly answers the input is wrong. The task is to reverse engineer the binary to learn the correct input. Another option is to modify the binary to accept all inputs.
The IOLI crackmes are a popular set of 10 crackmes which can be downloaded here, here, or here.
There are multiple solutions and tutorials flying around the web, but if I only read that stuff, I am not going to learn it. Further, If I do not write a blogpost about it, I have no pressure to work on them.
I am going to look exclusively at the linux binaries. The others should be similar.
Level 0
First, let’s test the binary.
$ ./crackme0x00
IOLI Crackme Level 0x00
Password: ABC
Invalid Password!
Just, like we expected. Let’s see if there are any interesting printable strings in the binary.
$ strings crackme0x00
/lib/ld-linux.so.2
__gmon_start__
libc.so.6
printf
strcmp
scanf
_IO_stdin_used
__libc_start_main
GLIBC_2.0
PTRh
IOLI Crackme Level 0x00
Password:
250382
Invalid Password!
Password OK :)
GCC: (GNU) 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)
...
Looks like it first prints with printf the string “IOLI Crackme Level 0x00” and “Password:”, then it reads the input with scanf and compares something with strcmp. Depending on the input it either writes “Invalid Password!” or “Password OK :)”. But what does the number “250382” do? Turns out it is the password.
$ ./crackme0x00
IOLI Crackme Level 0x00
Password: 250382
Password OK :)
Level 1
Level 1 is more challenging. We start from the same position.
$ ./crackme0x01
IOLI Crackme Level 0x01
Password: ABC
Invalid Password!
But this time strings only prints garbage.
Extracting the Password
We load the binary with the binary analysis framework radare2 which is one of the best binary analysis software out there. Forget gdb!
$ radare2 crackme0x01
-- Don't do this.
[0x08048330]> aa
[x] Analyze all flags starting with sym. and entry0 (aa)
aa is short for analyze all…. Not really. But this is how I remember
it. aa is an alias for ‘af@@ sym.*;af@entry0;afva’.
If something in radare2 is unclear, you can write the command followed
by ?
. For example aa?
. This shows a help text which is sometimes
understandable.
Next, we [p]rint the [d]isassembled [f]unction @sym.main
[0x08048330]> pdf @sym.main
;-- main:
/ (fcn) sym.main 113
| sym.main ();
| ; var int local_4h @ ebp-0x4
| ; var int local_4h_2 @ esp+0x4
| ; DATA XREF from 0x08048347 (entry0)
| 0x080483e4 55 push ebp
| 0x080483e5 89e5 mov ebp, esp
| 0x080483e7 83ec18 sub esp, 0x18
| 0x080483ea 83e4f0 and esp, 0xfffffff0
| 0x080483ed b800000000 mov eax, 0
| 0x080483f2 83c00f add eax, 0xf
| 0x080483f5 83c00f add eax, 0xf
| 0x080483f8 c1e804 shr eax, 4
| 0x080483fb c1e004 shl eax, 4
| 0x080483fe 29c4 sub esp, eax
| 0x08048400 c70424288504. mov dword [esp], str.IOLI_Crackme_Level_0x01_n ; [0x8048528:4]=0x494c4f49 LEA str.IOLI_Crackme_Level_0x01_n ; "IOLI Crackme Level 0x01." @ 0x8048528
| 0x08048407 e810ffffff call sym.imp.printf
| 0x0804840c c70424418504. mov dword [esp], str.Password: ; [0x8048541:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048541
| 0x08048413 e804ffffff call sym.imp.printf
| 0x08048418 8d45fc lea eax, [ebp - local_4h]
| 0x0804841b 89442404 mov dword [esp + local_4h_2], eax
| 0x0804841f c704244c8504. mov dword [esp], 0x804854c ; [0x804854c:4]=0x49006425 ; "%d"
| 0x08048426 e8e1feffff call sym.imp.scanf
| 0x0804842b 817dfc9a1400. cmp dword [ebp - local_4h], 0x149a ; [0x149a:4]=0x2ec0804
| ,=< 0x08048432 740e je 0x8048442
| | 0x08048434 c704244f8504. mov dword [esp], str.Invalid_Password__n ; [0x804854f:4]=0x61766e49 LEA str.Invalid_Password__n ; "Invalid Password!." @ 0x804854f
| | 0x0804843b e8dcfeffff call sym.imp.printf
| ,==< 0x08048440 eb0c jmp 0x804844e
| |`-> 0x08048442 c70424628504. mov dword [esp], str.Password_OK_:__n ; [0x8048562:4]=0x73736150 LEA str.Password_OK_:__n ; "Password OK :)." @ 0x8048562
| | 0x08048449 e8cefeffff call sym.imp.printf
| | ; JMP XREF from 0x08048440 (sym.main)
| `--> 0x0804844e b800000000 mov eax, 0
| 0x08048453 c9 leave
\ 0x08048454 c3 ret
We can see that something in compared (cmp) with 0x149a, and if the comparison was valid, we jump (je = jump if equal) over the block which prints “Invalid_Password” to the address 0x8048442. There is some code which prints “Password_OK”. Maybe 0x149a is already the password. Let us convert this from hexadecimal to binary.
[0x0000149a]> ? 0x149a
5274 0x149a 012232 5.2K 0000:049a 5274 0001010010011010 5274.0 5274.000000f 5274.000000
And now test it:
$ ./crackme0x01
IOLI Crackme Level 0x01
Password: 5274
Password OK :)
Now we are officially done.
Binary patching
Since we are am curious, we want to patch the
binary such that it accepts all passwords. For this we need to change the jump
if equal instruction je 0x8048442
into something which always jumps, i.e. a jmp 0x8048442
.
je 0x8048442
is 740e. A jump seems to start with eb and then the length. so we need to write eb0e instead of 740e.
We need to open the binary with writing enabled:
$ radare2 -w crackme0x01
-- What has been executed cannot be unexecuted
V toggles into visual mode, where we can search with /740e
My syntax highlighting on the blog is not sophisticated enough to handle this,
so I just explain it. i goes into insert mode and then we can just write eb0e over 740e.
q exists the visual mode. Let us check if everything is correct.
[0x08048314]> pdf @sym.main
;-- main:
/ (fcn) sym.main 113
| sym.main ();
| ; var int local_4h @ ebp-0x4
| ; var int local_4h_2 @ esp+0x4
| ; DATA XREF from 0x08048347 (entry0)
| 0x080483e4 55 push ebp
| 0x080483e5 89e5 mov ebp, esp
| 0x080483e7 83ec18 sub esp, 0x18
| 0x080483ea 83e4f0 and esp, 0xfffffff0
| 0x080483ed b800000000 mov eax, 0
| 0x080483f2 83c00f add eax, 0xf
| 0x080483f5 83c00f add eax, 0xf
| 0x080483f8 c1e804 shr eax, 4
| 0x080483fb c1e004 shl eax, 4
| 0x080483fe 29c4 sub esp, eax
| 0x08048400 c70424288504. mov dword [esp], str.IOLI_Crackme_Level_0x01_n ; [0x8048528:4]=0x494c4f49 LEA str.IOLI_Crackme_Level_0x01_n ; "IOLI Crackme Level 0x01." @ 0x8048528
| 0x08048407 e810ffffff call sym.imp.printf
| 0x0804840c c70424418504. mov dword [esp], str.Password: ; [0x8048541:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048541
| 0x08048413 e804ffffff call sym.imp.printf
| 0x08048418 8d45fc lea eax, [ebp - local_4h]
| 0x0804841b 89442404 mov dword [esp + local_4h_2], eax
| 0x0804841f c704244c8504. mov dword [esp], 0x804854c ; [0x804854c:4]=0x49006425 ; "%d"
| 0x08048426 e8e1feffff call sym.imp.scanf
| 0x0804842b 817dfc9a1400. cmp dword [ebp - local_4h], 0x149a ; [0x149a:4]=0x2ec0804
| ,=< 0x08048432 eb0e jmp 0x8048442
| | 0x08048434 c704244f8504. mov dword [esp], str.Invalid_Password__n ; [0x804854f:4]=0x61766e49 LEA str.Invalid_Password__n ; "Invalid Password!." @ 0x804854f
| | 0x0804843b e8dcfeffff call sym.imp.printf
| ,==< 0x08048440 eb0c jmp 0x804844e
| |`-> 0x08048442 c70424628504. mov dword [esp], str.Password_OK_:__n ; [0x8048562:4]=0x73736150 LEA str.Password_OK_:__n ; "Password OK :)." @ 0x8048562
| | 0x08048449 e8cefeffff call sym.imp.printf
| | ; JMP XREF from 0x08048440 (sym.main)
| `--> 0x0804844e b800000000 mov eax, 0
| 0x08048453 c9 leave
\ 0x08048454 c3 ret
The jumps seems to look okay.
$ ./crackme0x01
IOLI Crackme Level 0x01
Password: ABCDEF
Password OK :)
Yep. done.
Level 2
Same game as before
$ ./crackme0x02
IOLI Crackme Level 0x02
Password: AAA
Invalid Password!
Binary Patching
We open it in Radare 2 and take a look.
$ radare2 crackme0x02
-- Unk, unk, unk, unk
[0x08048330]> aa
[x] Analyze all flags starting with sym. and entry0 (aa)
[0x08048330]> pdf @sym.main
;-- main:
/ (fcn) sym.main 144
| sym.main ();
| ; var int local_ch @ ebp-0xc
| ; var int local_8h @ ebp-0x8
| ; var int local_4h @ ebp-0x4
| ; var int local_4h_2 @ esp+0x4
| ; DATA XREF from 0x08048347 (entry0)
| 0x080483e4 55 push ebp
| 0x080483e5 89e5 mov ebp, esp
| 0x080483e7 83ec18 sub esp, 0x18
| 0x080483ea 83e4f0 and esp, 0xfffffff0
| 0x080483ed b800000000 mov eax, 0
| 0x080483f2 83c00f add eax, 0xf
| 0x080483f5 83c00f add eax, 0xf
| 0x080483f8 c1e804 shr eax, 4
| 0x080483fb c1e004 shl eax, 4
| 0x080483fe 29c4 sub esp, eax
| 0x08048400 c70424488504. mov dword [esp], str.IOLI_Crackme_Level_0x02_n ; [0x8048548:4]=0x494c4f49 LEA str.IOLI_Crackme_Level_0x02_n ; "IOLI Crackme Level 0x02." @ 0x8048548
| 0x08048407 e810ffffff call sym.imp.printf
| 0x0804840c c70424618504. mov dword [esp], str.Password: ; [0x8048561:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048561
| 0x08048413 e804ffffff call sym.imp.printf
| 0x08048418 8d45fc lea eax, [ebp - local_4h]
| 0x0804841b 89442404 mov dword [esp + local_4h_2], eax
| 0x0804841f c704246c8504. mov dword [esp], 0x804856c ; [0x804856c:4]=0x50006425 ; "%d"
| 0x08048426 e8e1feffff call sym.imp.scanf
| 0x0804842b c745f85a0000. mov dword [ebp - local_8h], 0x5a ; 'Z'
| 0x08048432 c745f4ec0100. mov dword [ebp - local_ch], 0x1ec
| 0x08048439 8b55f4 mov edx, dword [ebp - local_ch]
| 0x0804843c 8d45f8 lea eax, [ebp - local_8h]
| 0x0804843f 0110 add dword [eax], edx
| 0x08048441 8b45f8 mov eax, dword [ebp - local_8h]
| 0x08048444 0faf45f8 imul eax, dword [ebp - local_8h]
| 0x08048448 8945f4 mov dword [ebp - local_ch], eax
| 0x0804844b 8b45fc mov eax, dword [ebp - local_4h]
| 0x0804844e 3b45f4 cmp eax, dword [ebp - local_ch]
| ,=< 0x08048451 750e jne 0x8048461
| | 0x08048453 c704246f8504. mov dword [esp], str.Password_OK_:__n ; [0x804856f:4]=0x73736150 LEA str.Password_OK_:__n ; "Password OK :)." @ 0x804856f
| | 0x0804845a e8bdfeffff call sym.imp.printf
| ,==< 0x0804845f eb0c jmp 0x804846d
| |`-> 0x08048461 c704247f8504. mov dword [esp], str.Invalid_Password__n ; [0x804857f:4]=0x61766e49 LEA str.Invalid_Password__n ; "Invalid Password!." @ 0x804857f
| | 0x08048468 e8affeffff call sym.imp.printf
| | ; JMP XREF from 0x0804845f (sym.main)
| `--> 0x0804846d b800000000 mov eax, 0
| 0x08048472 c9 leave
\ 0x08048473 c3 ret
There is a comparison cmp eax, dword [ebp - local_ch]
and a conditional jump
(jne = jump if not equal). The jump takes us right over the block which prints
“Password_OK” to the “Invalid_Password”-section.
We do not want to jump, so we overwrite the JNE (750e) two NOP operations. A NOP is a
special operation which tells the processor to do nothing. So we do not have to
worry about the addresses changing. Interestingly the NOP operation has a real
purpose other than security exploits. Processors read data from memory in big
chunks and the NOP is used for alignment, such that one does not have to read
the previous block for the last few bytes.
The NOP operation in hexadecimal is 0x90 in x86 Assembly. It is useful to know
this by heart, instead of looking it up all the time. This instruction is used
quite often in binary exploitation.
Using the editor from radare 2 was covered in the last level, so I
just show you the ‘fixed’ binary. It may be interesting that one can
toggle into the disassembly from visual mode by pressing A
.
[0x08048451]> pdf @sym.main
;-- main:
/ (fcn) sym.main 144
| sym.main ();
| ; var int local_ch @ ebp-0xc
| ; var int local_8h @ ebp-0x8
| ; var int local_4h @ ebp-0x4
| ; var int local_4h_2 @ esp+0x4
| ; DATA XREF from 0x08048347 (entry0)
| 0x080483e4 55 push ebp
| 0x080483e5 89e5 mov ebp, esp
| 0x080483e7 83ec18 sub esp, 0x18
| 0x080483ea 83e4f0 and esp, 0xfffffff0
| 0x080483ed b800000000 mov eax, 0
| 0x080483f2 83c00f add eax, 0xf
| 0x080483f5 83c00f add eax, 0xf
| 0x080483f8 c1e804 shr eax, 4
| 0x080483fb c1e004 shl eax, 4
| 0x080483fe 29c4 sub esp, eax
| 0x08048400 c70424488504. mov dword [esp], str.IOLI_Crackme_Level_0x02_n ; [0x8048548:4]=0x494c4f49 LEA str.IOLI_Crackme_Level_0x02_n ; "IOLI Crackme Level 0x02." @ 0x8048548
| 0x08048407 e810ffffff call sym.imp.printf
| 0x0804840c c70424618504. mov dword [esp], str.Password: ; [0x8048561:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048561
| 0x08048413 e804ffffff call sym.imp.printf
| 0x08048418 8d45fc lea eax, [ebp - local_4h]
| 0x0804841b 89442404 mov dword [esp + local_4h_2], eax
| 0x0804841f c704246c8504. mov dword [esp], 0x804856c ; [0x804856c:4]=0x50006425 ; "%d"
| 0x08048426 e8e1feffff call sym.imp.scanf
| 0x0804842b c745f85a0000. mov dword [ebp - local_8h], 0x5a ; 'Z'
| 0x08048432 c745f4ec0100. mov dword [ebp - local_ch], 0x1ec
| 0x08048439 8b55f4 mov edx, dword [ebp - local_ch]
| 0x0804843c 8d45f8 lea eax, [ebp - local_8h]
| 0x0804843f 0110 add dword [eax], edx
| 0x08048441 8b45f8 mov eax, dword [ebp - local_8h]
| 0x08048444 0faf45f8 imul eax, dword [ebp - local_8h]
| 0x08048448 8945f4 mov dword [ebp - local_ch], eax
| 0x0804844b 8b45fc mov eax, dword [ebp - local_4h]
| 0x0804844e 3b45f4 cmp eax, dword [ebp - local_ch]
| 0x08048451 90 nop
| 0x08048452 90 nop
| 0x08048453 c704246f8504. mov dword [esp], str.Password_OK_:__n ; [0x804856f:4]=0x73736150 LEA str.Password_OK_:__n ; "Password OK :)." @ 0x804856f
| 0x0804845a e8bdfeffff call sym.imp.printf
| ,=< 0x0804845f eb0c jmp 0x804846d
| | 0x08048461 c704247f8504. mov dword [esp], str.Invalid_Password__n ; [0x804857f:4]=0x61766e49 LEA str.Invalid_Password__n ; "Invalid Password!." @ 0x804857f
| | 0x08048468 e8affeffff call sym.imp.printf
| | ; JMP XREF from 0x0804845f (sym.main)
| `-> 0x0804846d b800000000 mov eax, 0
| 0x08048472 c9 leave
\ 0x08048473 c3 ret
Testing if it works.
$ ./crackme0x02
IOLI Crackme Level 0x02
Password: ABCD
Password OK :)
Level up? Yeah, under normal circumstances yes. But we are curious and want to know the password.
Extracting the password by concolic execution
We could look at the disassembly really hard or run it with some test-input and look at the registers. But we are also lazy. So, let us look at concolic execution.
How it works
Symbolic execution is a technique for analyzing binaries where the binary is executed symbolically. Take the following program as an example.
if (x<4)
print "a"
else
print "b"
A concrete execution means that we execute the program with concrete input, e.g. x=10 or x=1 or something like that.
Symbolic execution on the other hand executes all program paths with symbolic variables. I.e., the execution comes to the comparison x<4 and separates the input space into x<4 and x>=4. Depending on these equations we take the first or the second branch. After executing the whole program, we have a bunch of equations which partition the input space for each branch. We can now solve the equations with an SMT solver to get the set of all concrete inputs such that we execute a special branch.
For example in our case we want to find the input such that the branch of the program is taken where it prints that we have found the correct password.
Symbolic execution has a number of drawbacks, like path explosion, weird behavior with loops, and problems when we encounter one-way functions like hashes. So concolic execution was born. Concolic execution is a little bit like symbolic execution, but for each branch taken we remember a concrete instantiation of the variables. In fact, there is more to it, but I did not quite get it.
Angr me!
We use angr, which is a python framework for concolic execution. The following steps are performed in an ipython environment, but could also be written as a standalone program.
> a = angr.Project(''crackme0x02.org')
# Here we are loading the binary as a new project
> state = a.factory.entry_state()
# We store the entry state
> path = a.factory.path(state)
# We store the path beginning at the entry state
> pathgroup = a.factory.path_group(path)
# Later there will be multiple paths, so we use a pathgroup, which
# is just a set of paths
> pathgroup.step(until=lambda lpg: len(lpg.active) > 1)
# We walk through the binary until we reach a branch
# Then we print the two inputs
> pathgroup.active[0].state.posix.dumps(0)
'+000338724B'
> pathgroup.active[1].state.posix.dumps(0)
'-242422222\x04'
One of these inputs corresponds to the execution path where we get an invalid password, the other corresponds to the valid password. Let us just test them:
$ ./crackme0x02
IOLI Crackme Level 0x02
Password: -242444422
Invalid Password!
$ ./crackme0x02
IOLI Crackme Level 0x02
Password: 338724
Password OK :)
Bingo. Now we are done with that level.
Level 3
I learned that I can start radare2 with the -A switch. This saves us the burden of having to type aa every time. Up to now I have always included the whole output. This will change now, since most of it is irrelevant noise for us. Let us look at the main method.
$ r2 -A crackme0x03
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[ ] [*] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan))
-- Wait a moment ...
[0x08048360]> pdf @main
| 0x080484c0 c70424298604. mov dword [esp], str.Password: ; [0x8048629:4]=0x73736150 LEA str.Password: ; "Password: " @ 0x8048629
| 0x080484c7 e884feffff call sym.imp.printf
| 0x080484cc 8d45fc lea eax, [ebp - local_4h]
| 0x080484cf 89442404 mov dword [esp + local_4h_2], eax
| 0x080484d3 c70424348604. mov dword [esp], 0x8048634 ; [0x8048634:4]=0x6425 ; "%d"
| 0x080484da e851feffff call sym.imp.scanf
| 0x080484df c745f85a0000. mov dword [ebp - local_8h], 0x5a ; 'Z'
| 0x080484e6 c745f4ec0100. mov dword [ebp - local_ch], 0x1ec
| 0x080484ed 8b55f4 mov edx, dword [ebp - local_ch]
| 0x080484f0 8d45f8 lea eax, [ebp - local_8h]
| 0x080484f3 0110 add dword [eax], edx
| 0x080484f5 8b45f8 mov eax, dword [ebp - local_8h]
| 0x080484f8 0faf45f8 imul eax, dword [ebp - local_8h]
| 0x080484fc 8945f4 mov dword [ebp - local_ch], eax
| 0x080484ff 8b45f4 mov eax, dword [ebp - local_ch]
| 0x08048502 89442404 mov dword [esp + local_4h_2], eax
| 0x08048506 8b45fc mov eax, dword [ebp - local_4h]
| 0x08048509 890424 mov dword [esp], eax
| 0x0804850c e85dffffff call sym.test
| 0x08048511 b800000000 mov eax, 0
| 0x08048516 c9 leave
\ 0x08048517 c3 ret
We can see that the password request is printed, then some voodoo happens and finally a function sym.test is called. This is the first Crackme of the IOLI-Crackmes with more than one function. I wonder what sym.test does.
[0x08048360]> pdf @sym.test
/ (fcn) sym.test 42
| sym.test (int arg_8h, int arg_ch);
| ; arg int arg_8h @ ebp+0x8
| ; arg int arg_ch @ ebp+0xc
| ; CALL XREF from 0x0804850c (sym.main)
| 0x0804846e 55 push ebp
| 0x0804846f 89e5 mov ebp, esp
| 0x08048471 83ec08 sub esp, 8
| 0x08048474 8b4508 mov eax, dword [ebp + arg_8h] ; [0x8:4]=0
| 0x08048477 3b450c cmp eax, dword [ebp + arg_ch] ; [0xc:4]=0
| ,=< 0x0804847a 740e je 0x804848a
| | 0x0804847c c70424ec8504. mov dword [esp], str.Lqydolg_Sdvvzrug_ ; [0x80485ec:4]=0x6479714c LEA str.Lqydolg_Sdvvzrug_ ; "Lqydolg#Sdvvzrug$" @ 0x80485ec
| | 0x08048483 e88cffffff call sym.shift
| ,==< 0x08048488 eb0c jmp 0x8048496
| || ; JMP XREF from 0x0804847a (sym.test)
| |`-> 0x0804848a c70424fe8504. mov dword [esp], str.Sdvvzrug_RN______ ; [0x80485fe:4]=0x76766453 LEA str.Sdvvzrug_RN______ ; "Sdvvzrug#RN$$$#=," @ 0x80485fe
| | 0x08048491 e87effffff call sym.shift
| | ; JMP XREF from 0x08048488 (sym.test)
| `--> 0x08048496 c9 leave
\ 0x08048497 c3 ret
Like we could have expected sym.test does a test, namely a comparison. If eax
is equal to dword [ebp + arg_ch]
we jump to 0x804848a
. Is this a good or a
bad jump? We could argue that this is the first jump, and stuff being equal is
a very rare condition. So we assume this jump is not often taken. We assume it
is a good jump, which is only taken if the password is correct. So we have to
exchange it with a jump instruction which is always taken. Writing eb0e instead
of 740e works.
Intestingly there are some strings which we cannot read, since they look
encrypted. I strongly assume that the stuff in sym.main that I called voodoo
was some kind of decryption routine.
Angr me!
With Angr we can again find out the correct password. We are doing the same as in Level 2.
> a = angr.Project('crackme0x03')
> state = a.factory.entry_state()
> path = a.factory.path(state)
> pathgroup = a.factory.path_group(path)
> pathgroup.step(until=lambda lpg: len(lpg.active) > 1)
> pathgroup.active[0].state.posix.dumps(0)
'+000338724B'
> pathgroup.active[1].state.posix.dumps(0)
'-224244844\x12'
$ ./crackme0x03.org
IOLI Crackme Level 0x03
Password: 338724
Password OK!!! :)
The other levels
The other levels are fairly similar, except there are more jumps to nop. So I won’t go into details but leave them as an exercise. If instead of patching the binaries, we want to understand the password from the disassembly, that is more challenging and I will not cover it (yet?) since my disassembly skills do not suffice for that. Interestingly, our angr-script fails at the later levels.
References
If you want to look at solutions for the other levels which you should not do except if you are stuck, you can find them under the following links: