Mama Trace was an extension of Baby Trace (baby shark theme much?). For this we're given files similar to baby trace:
Dockerfile, headerquery2, pitas.py, flagleak
pitas.py and the Dockerfile are effectively the same as before. headerquery2 is basically the original headerquery elf except with our leak removed. With that in mind, time to look at flagleak.
Flagleak is another 64-bit elf binary, similar in nature to headerquery2. Here's what Ghidra says main looks like:
int main(void)
{
long lVar1;
int fd_flag;
ulong iCounter;
long in_FS_OFFSET;
ulong buf_my_input;
char buf_flag [264];
long canary;
lVar1 = *(long *)(in_FS_OFFSET + 0x28);
fd_flag = open("/flag",0);
read(fd_flag,buf_flag,0x100);
read(0,&buf_my_input,8);
puts("Checking input...");
if (9 < (byte)buf_my_input) {
puts("OOO is unwilling to reveal that much of the flag at this time.");
/* WARNING: Subroutine does not return */
exit(1);
}
iCounter = 0;
while (iCounter < (buf_my_input & 0xff)) {
printf("Flag byte %lld: %c\n",iCounter,(ulong)(uint)(int)buf_flag[iCounter]);
iCounter = iCounter + 1;
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
Given this is the new binary, and the other was patched, my assumption was this is the target for mamashark. At first glance, it appears that the logic here works. If we could make it truly symbolic, this would be trivial. However, we have to exercise our limited control to read out the flag.
I flailed on this for quite a while.. Tried setting every register symbolic and stepping through, which ended up in a mess of constraints being listed and nothing helpful happening. Eventually, I figured it had to be some implementation detail in the angr script that i'm missing. Reading through the pitass.py script more carefully I discovered the following lines:
def add_unconstrained():
stdin_name = input("Variable name: ")
stdin_len = int(input("Variable length (in bytes): "))
stdin_var = claripy.BVS(stdin_name, stdin_len*8, explicit_name=True)
s.posix.stdin.write(None, stdin_var)
def add_constrained():
stdin_name = input("Variable name: ")
stdin_str = bytes.fromhex(input("Variable contents (in hex): "))
stdin_len = len(stdin_str)
stdin_var = claripy.BVS(stdin_name, stdin_len*8, explicit_name=True)
s.posix.stdin.write(None, stdin_var)
s.add_constraints(stdin_var == stdin_str)
The main thing to note here is explicit_name=True. Being a long time angr user, this was a new flag to me. After looking at the help on it, we see:
:param bool explicit_name: If False, an identifier is appended to the name to ensure uniqueness.
So it has to do with uniqueness! angr uses something called Single Static Assignment to manage it's variables. It's a powerful concept, but it has an assumption, namely the variable should only be assigned once. With this in mind, I decided to see if i could name a variable the same as one angr already uses. To do so (after a few different attempts), I executed to `0x85e`, which is right before we set our integer to zero and iterate up to the size we provided as input. I used the register read trick from babytrace to discover what the symbolic name for r12 will be (this is our maximum read char). In this case, that variable happened to be named r12_52_64. I then started over again, this time my input was (concrete) 09 (symbolic) r12_52_64 constrained to a large value such as 0x60. When I executed to the same location again and told angr to symbolize r12, the two variables name collided. The final trick at this point was needing to call concretize on r12. This forced angr to resolve the value of r12, in doing so it resolved my defined value instead of the concrete one it was previously, and stored that large value into r12. Stepping to the end of execution allowed the program to spit out the flag.
OOO{brumley was right, hash consing is awesome!}