Google CTF 2022 presented us with `oldschool`, a typical, as the namesuggests, oldschool crackme with an ncurses terminal interface. Thegoal of the challenge was to write a keygen, which would be able togenerate keys for a list of users provided by the CTF organizers. Theofficial and detailed writeup is available [here](https://github.com/google/google-ctf/tree/master/2023/rev-oldschool/solution), which goes throughthe intended solution of manually reverse engineering the keyverification algorithm. However, since we are researchers (and mostimportantly, too lazy to manually reverse engineer the keyverification), we took a look at the binary and decided that this mustbe solvable using symbolic execution. Tl;dr, yes, it is indeedpossible, but our [angr](https://angr.io/) skills were a bit rustyand it took us a bit to get to the solution.
### Initial Overview of the Binary
To start off, we took a look at what we were dealing with and saw thatwe were provided with a 32-bit ELF binary. To avoid having to pull inold 32-bit dependencies on our host system, we used the [i386/debian](https://hub.docker.com/r/i386/debian/)Docker image and installed `libncurses6`, as the binary wouldn't startwithout it. Running it for the first time, we were then presented withthe following error message:
```textroot@699a7aa1afd8:/oldschool# ./oldschool [!] Error. ASSERT_EQ failed!```
This didn't really help us a lot. Was this an error message fromncurses, or was this  part of the oldschool binary itself? The stringcannot be found in the binary directly, but we quickly realized thatall relevant strings in the binary have been obfuscated. After everycall to functions from `libncurses`, we could see decoding of errorstrings like this: ```cbVar17 = 0;local_18 = &stack0x00000004;iVar7 = initscr();if (iVar7 == 0) {  endwin();  FUN_00012a08();  for (local_98 = 0; local_98 < 0x11; local_98 = local_98 + 1) {    FUN_0002bd72();    uVar5 = FUN_0002bd96();    puVar8 = (undefined *)FUN_0002bd72();    *puVar8 = uVar5;  }  puVar8 = (undefined *)FUN_0002bd72();  *puVar8 = 0;  FUN_0002bdc6();  FUN_000129b6();  for (local_9c = 0; local_9c < 0xe; local_9c = local_9c + 1) {    FUN_0002bcfc(auStack_2858);    uVar5 = FUN_0002bd20(local_2867);    puVar8 = (undefined *)FUN_0002bcfc(auStack_2858);    *puVar8 = uVar5;  }  puVar8 = (undefined *)FUN_0002bcfc(auStack_2858);  *puVar8 = 0;  pcVar9 = (char *)FUN_0002bd50(auStack_2858);  fprintf(_stderr,pcVar9);                  /* WARNING: Subroutine does not return */  exit(-1);}iVar7 = cbreak();if (iVar7 != 0) {  /* ... */  fprintf(_stderr,pcVar9);                  /* WARNING: Subroutine does not return */  exit(-1);}iVar7 = curs_set();/* ... */```
![A picture of the CFG of the main function](https://w0y.at/images/google-ctf-2023/oldschool-cfg.png)
Looking at the decompiled code (and also the CFG of the function)reveals function calls, whose value is checked in an if conditionfollowed by *some* code looping, which we assumed to be the code thatprints the error message. In order to get the program to run, werealized that it needed the correct `TERM` variable to be set for`libncurses`, as this was a common problem when getting these programsto run:
```bashexport TERM=xterm-256color```
While we were working on manually noting down the function callsbetween the chunks of error printing code, we were at the same timerunning the code through `ltrace`, in order totrack the executed dynamic library calls. During manual analysis ofthe decompiled code, we stumbled upon the following softwareinterrupt (namely `int 0x80`, a syscall in x86 Linux):
```cpcVar2 = (code *)swi(0x80);iVar7 = (*pcVar2)();*(int *)(iStack_2900 + 0x32c) = -iVar7;local_28 = local_74;while (local_28 = local_28 + 1, local_28 <= local_74 + 0x11) {```
We didn't check what this syscall did for now, but noted it in theback of our minds, as it seemed extremely out of place for the binaryto manually perform a syscall. So we went on the play around with`ltrace` in the hopes we could find something that would narrow downthe search window for looking for meaningful stuff within the mainfunction. And we did, as the `ltrace` contained the following:
```[0x56567f01] strlen("thisistheusername               "...)[0x56567f61] newwin(7, 50, 34, 88)                        [0x56567f96] newwin(7, 50, 35, 89)                        [0x56568181] box(0x565b85a8, 0, 0, 89)                    [0x5656835a] wbkgd(0x565b74e8, 1312, 0, 89)               [0x5655714c] strlen("thisisthepassword")                  ```
We can see that strlen is called two times here, once for the usernameand once for the password, with `ltrace` thankfully giving us theinstruction pointer for instruction following the strlen (we disabledASLR using `echo 0 | sudo tee /proc/sys/kernel/randomize_va_space` sothat `ltrace` would always give us the same addresses). Checking the strlen XRefs in Ghidra, we can click through and find the tworespective function calls with the username being checked in the mainfunction and the password being checked in another one.
```c*(int *)(puVar8 + -0x10) = local_90;*(undefined4 *)(puVar8 + -0x14) = 0x22f01;local_58 = strlen(*(char **)(puVar8 + -0x10));```
The strlen takes `puVar8 + -0x10` as argument, which is set earlierto `local_90` which is where our username is located. Looking forother occurrences of `local_90` leads us to this piece of code:
```c*(undefined4 **)(puVar8 + -0xc) = &local_28e9;*(int *)(puVar8 + -0x10) = local_90;*(undefined4 *)(puVar8 + -0x14) = 0x2353b;iVar7 = FUN_0001212a();if (iVar7 == 1) {```
Ghidra does not really show this nicely, due to the arguments beingplaced on the stack, but `FUN_0001212a` actually takes two parameters,one of them being the username. We can also see that the functionreturn value is checked for 1, quickly looking at the `ltrace` outputshows us that the else branch is taken (the return value is not 1) ifour password is false. So we can assume that this function takesusername and password, returns 1 if the password is correct and 0if the password is incorrect. Peeking into the function confirms this,as we can see the password strlen that we previously identified withinour ltrace output:
```cuStack_270 = 0x1213b;sVar2 = strlen(param_2);if (sVar2 == 29) {```
This also already gives us a hint that the password should have alength of 29 characters. The function only has 200 lines of decompiledC code (which allows us to ignore the rest of the 7000 lines ofdecompiled C code of the main function). Reading on shows us some morerelatively simple to reverse infos about the password:
```cif ((((local_20 == 5) || (local_20 == 0xb)) ||      (local_20 == 0x11)) || (local_20 == 0x17)) {  if (param_2[local_20] != '-') {    return 0;  }}```
From this code we can see that every 5th character of the password issupposed to be a `-`. For every other character, the following isexecuted:
```clocal_29 = '\0';for (i = 0; (int)i < 0x20; i = i + 1) {  FUN_000120da(local_b3,&local_71);  for (j = 0; j < 0x20; j = j + 1) {    puVar3 = (undefined *)FUN_0002b8d6(auStack_92,j);    local_25d = FUN_0002b8fa(local_b3,*puVar3,j);    puVar3 = (undefined *)FUN_0002b8d6(auStack_92,j);    *puVar3 = local_25d;  }  puVar3 = (undefined *)FUN_0002b8d6(auStack_92,0x20);  *puVar3 = 0;  iVar5 = FUN_0002b92a(auStack_92);  if (iVar5[i] == password[local_20]) {    iVar6 = local_24 / 5;    iVar7 = local_24 % 5;    local_24 = local_24 + 1;    local_118[iVar6 * 5 + iVar7] = i;    local_29 = '\x01';    break;  }}if (local_29 != '\x01') {  return 0;}```
The most interesting line here is `iVar5[i] == password[local_20]`,which effectively ensures that the other characters of the passwordare only valid if they are part of the lookup table `iVar5`. Checkingwith GDB, we saw that this lookup table stays the same (despite beingrecreated every iteration), corresponding to:
```23456789ABCDEFGHJKLMNPQRSTUVWXYZ```
This means that this string is as password that passes all checks forthe formatting:
```23456-789AB-CDEFG-HJKLM-NPQRS```
Now at this point we looked at the rest of the code and figured thatwe were too lazy to reverse it by hand and thought that `angr` shouldbe able to easily solve this automatically. This seemed reasonable asthe structure of the function lends itself well for solving, sincewe have a username and password as parameter, and 0 or 1 as feedbackif the combination is valid or not. Also, all loops appear to bebounded given the input limits and there is nothing crazy happeningthat would cause us a headache when trying symbolic execution.
### First Steps with Angr 
Our first attempt with `angr` was to create a `call_state` at thefunction of interest, with parameters provided by us, and thenexploring to the location in the function where 1 is returned. Thislooked something like that:
```pythoncheck_addr = proj.loader.min_addr + 0x212apassword = PointerWrapper(claripy.BVS('password', 0x1e * 8),    buffer=True)username = PointerWrapper(b'gdwAnDgwbRVnrJvEqzvs\x00', buffer=True)state = proj.factory.call_state(    check_addr, username, password,    prototype='int password_check(char *username, char *password)')
simgr = proj.factory.simgr(state)val_addr = proj.loader.min_addr + 0x29a8 simgr.explore(find=val_addr)```
Now, we haven't touched `angr` in forever, and we're not sure if thatis how you actually use the PointerWrapper. We tried to run this, andit worked, but it didn't terminate within a reasonable amount of time.Debugging this a little, we realized that we already had issues passingthe first password check loop iteration, even when constraining thepassword to the correct characters. We realized, that we were likelymissing out on state that would be initialized before the functionis called, but letting the whole program run in `angr` would mostlikely not yield satisfying results. Due to the ncurses interface ofthe application, we were also not sure how well we can control theprogram automatically/from the outside, so using `angr` inbuiltconcolic execution techniques might not have worked. 
### Concolic Execution 
So our main way of thinking was: given a program in GDB, can we dumpits state and use that with `angr`? If you think "wow that soundscool", we have to disappoint you a bit, because we didn't end up doingthat exactly, but we found something that was sufficient. Aftersearching for GDB `angr` integrations and not being able to findsomething recent that works, we thought of whether we could somehowmake use of [avatar2](https://github.com/avatartwo/avatar2), which weknew from previous research. While this does not have direct `angr`integration, `angr` does have a module which allows us to use`avatar2` with the GDB backend, called [Symbion](https://angr.io/blog/angr_symbion/).So using `Symbion` from `angr`, so that we can use `avatar2`, so that we can finally achieve our goal of syncing GDB state with `angr`. 
To start of, we start `oldschool` in a GDB server and then connect toit using the `Symbion` `avatar2` target:
```pythonavatar_gdb = AvatarGDBConcreteTarget(    avatar2.archs.x86.X86,    '127.0.0.1',    12345,    'gdb')```
Then, we set up `angr` as usual, creating an entry state and exploreto the address offset `0x2747`. We picked this offset, as it is locatedwithin the function right before username state and password state ismixed together, meaning this is the last possible location for us toreplace the password with symbolic variables if we want to solve forit.
```pythonp = angr.Project(    './oldschool',    concrete_target=avatar_gdb,    use_sim_procedures=True)
entry_state = p.factory.entry_state()entry_state.options.add(angr.options.SYMBION_SYNC_CLE)entry_state.options.add(angr.options.SYMBION_KEEP_STUBS_ON_SYNC)
target_addr = BASE_ADDRESS + 0x2747simgr = p.factory.simgr(entry_state)simgr.use_technique(Symbion(find=[target_addr]))exploration = simgr.run()```
So far so good. If we just run `gdbserver 0.0.0.0:12345 ./oldschool`in our container and start the angr script, the password prompt of theoldschool binary will show up. Upon entering the password, our scriptsuccessfully explores the binary to the specified location and returnsus a state, great! Now we can go ahead and extend our script tointroduce symbolic variables for solving. To this end, we take thevalue from the `ebp` register and subtract `0x114`, which correspondsto the start address of the (substituted) password on the stack, whichwe determined from Ghidra. We know the length of the password withoutthe separating `-` is 25, and that every character of the password hasbeen substituted with a 32-bit number, which corresponds to the indexof the respective password character in the array of allowedcharacters. For each of these 25 characters, we create a 32 bitsymbolic variable. Because we know that the lookup dictionary of validcharacters is 32 characters long, we can also add a constraint tohighlight that the symbolic variable cannot be initialized with valuesbigger than 32. Finally, we write the symbolic variables to the memoryand save them for later:
```python# Previous state we reached through concrete execution (Symbion)state = exploration.found[0]
# Defining symbolic varssymbolic_vars = []password_base = state.regs.ebp - 0x114for i in range(25):    svar = state.solver.BVS(f'pw{i}', 8 * 4)    state.add_constraints(claripy.ULT(svar, 0x20))    state.mem[password_base + i * 4].uint32_t = svar    symbolic_vars.append(svar)```
Now we want to explore to the location in the program, where 1 isreturned, indicating a correct password:
```pythontarget_addr = BASE_ADDRESS + 0x29a8simgr = p.factory.simgr(state)simgr.explore(find=target_addr)```
We start the GDB server and run the angr script again, but afterentering our password, angr seems to crash due to memory areas beingpresumably not mapped. Our guess as to why this was happening was thatmaybe the base address for the symbolic execution was not the same asfor the concrete execution. After some research, we found that thisbug is an open issue referenced [here](https://github.com/angr/angr/issues/3346).The issue can ultimately be circumvented by specifying the base addressof the concrete execution when creating the angr project:
```pythonBASE_ADDRESS = 0x56555000 p = angr.Project(    './oldschool',    concrete_target=avatar_gdb,    use_sim_procedures=True,    load_options={'main_opts': {'base_addr': BASE_ADDRESS}})```
After setting the base address and repeating the GDB server/angr scriptprocedure, the symbolic execution takes a few seconds and we get avalid state, yay! This means there is definitely at least one solutionfor the symbolic variables that we specified and that we can now readout this solution, by asking the solver to evaluate our symbolicvariables:
```pythonstate = simgr.found[0]concrete_vals = []for svar in symbolic_vars:    val = state.solver.eval(svar, cast_to=bytes)    concrete_vals.append(val)```
The values we get back are the 32-bit integers after substitution ofthe password characters with the indices of the character dictionary.In order to get the actual password, we need to substitute it again,split it into chunks of 5, and add `-` between the chunks:
```pythondef chunks(l, n):    for i in range(0, len(l), n):         yield l[i:i + n]
lookup = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'c = chunks(''.join([lookup[x[-1]] for x in concrete_vals]), 5)print('-'.join(c))```
Which gives us the following for the first username in the list we haveto crack (`gdwAnDgwbRVnrJvEqzvs`):
```GL3EX-E3WHZ-EJ57M-UEPYX-62PK7```
### Anti-Debug 
Of course, we go ahead and try if that actually works and... it doesnot? After some trial and error, we figured out that the password doeswork - if we run the binary using the gdbserver. This means we aredealing with some anti-debugging and made us remember the syscall wefound before. We guessed that this was the `ptrace` syscall using thewell known `ptrace(PTRACE_TRACEME, 0, 1, 0)` trick, since this willfail if the program is already being ptraced (like when we run it inGDB). While we could just patch that check out, we decided it would benice to go ahead and just use the existing angr/Symbion/avatar2 flowto intercept ptrace/change the return value. So instead of exploringto the location from where we want to do symbolic execution, we exploreto the location of the `ptrace` call and change the return value. Tothis end, we set the return register to 0, and then tell Symbion toconcretize the changes (i.e. copying them to the GDB target):
```python# Exploring until after the ptrace callentry_state = p.factory.entry_state()entry_state.options.add(angr.options.SYMBION_SYNC_CLE)entry_state.options.add(angr.options.SYMBION_KEEP_STUBS_ON_SYNC)
target_addr = BASE_ADDRESS + 0xaf79simgr = p.factory.simgr(entry_state)simgr.use_technique(Symbion(find=[target_addr]))exploration = simgr.run()
# Exploring until the location where we want to do symbolic executionstate = exploration.found[0]state.regs.eax = 0
target_addr = BASE_ADDRESS + 0x2747simgr = p.factory.simgr(state)simgr.use_technique(Symbion(find=[target_addr],    register_concretize=[('eax', state.regs.eax)]))exploration = simgr.run()```
After that, we continue our symbolic execution as before. Afterevaluating it, we now get this password for the first username:
```2UTQS-QFE2D-Z3P9K-6CFM9-TRRH5```
We try it out and it works!
![Image showing a successful login](https://w0y.at/images/google-ctf-2023/oldschool-win.png)
Now the challenge requires us to do this for 49 more usernames, thenconcatenate everything together and hash it, to give us the flag. We didn't automate this process as the terminal interface was a bit of apain to automate (we were too lazy), so we just sat down and did it 49times manually.
While this is a rather anticlimactic ending for a writeup (the flagis just a hash, so there's really no funny or cool note to end thison), it shows just how useful symbolic execution can be when appliedto reversing challenges. This rings especially true, when the challengerequires a more complex state that cannot easily be handled with angr,mirroring conditions of real-world programs where the part you want toanalyze is often very deep down in the program. Our full angr scriptsolution can be found [here](https://gitlab.w0y.at/lounge/meetings/-/blob/main/2023-07-03/oldschool/solve.py).