Pseudo is the toughest challenge on HTB in my opinion as of 2019 (well, before headachev2 released). Nothing even comes close to this reversing challenge, which centers around an aarch64 and VM crackme. Before I start, I would like to thank davidlightman for working on it with me. He taught me many new reversing tricks and, oftentimes, managed to see things which I missed.
Starting off, we identify that the binary is UPX packed, so unpack it first. Then, we realize that it is an aarch64 binary. To actually reverse this, I ended up using qemu user mode (qemu-aarch64). Luckily, qemu-aarch64 had both a gdb remote debugging interface as well as a strace option to run when in emulation. For debugging, I used gdb-multiarch with the peda extension.
From an initial static analysis, we note that the main function is at 0x4004b0. Also, since it is statically compiled but completely stripped of symbols, it may help to identify some glibc functions. How do we recognize what functions are glibc related? Well, I mainly analyzed strings and tried to map similarities to the real life source code. Moreover, once I found that 0x49eba8 was assert(), my life became a lot easier. Remember to actually rename the functions as you go along. I relied a lot on GHIDRA's decompiler, but it helps a ton to understand arm assembly. Here were some of the references I used:
https://azeria-labs.com/writing-arm-assembly-part-1/
https://modexp.wordpress.com/2018/10/30/arm64-assembly/
After a while, I became pretty confident that functions in 0x49xxxx to 0x5xxxxx are glibc functions, so they don't matter too much. Function 400578 is where the most important part is.
There are multiple loops, random floating point arithmetic, and many conditionals. There also seems to be some bytecode starting from 0x5036b8. Let's get to some dynamic analysis. I highly recommend you use gdb's --command option to use its scripting capabilities. Upon running it, we get a message about "terminal for ants." I took it really literally and made my terminal fullscreen. Now it prompts for a password! However, there is also a better way here: using qemu's strace, you will see a call to ioctl, which can be used in terminal screen size checks. As you can see in 0x4004e8, there is where the call is happening. It is checked against the value 158 to see if your screen size is bigger than that. The solution here is to just use the following command: stty rows 159 columns 159
I didn't bother to check if rows or columns really mattered here, but you will beat the check. Now placing a breakpoint at 0x400650 (seems like this is where reading in is occuring), place a break point at 0x400570 (this also runs beforehand and it is closely tied to the massive loop but I would like to debug it after entering some passwords) and enter some random strings in the password prompt. Throughout static analysis, I noticed a lot of referencing to the w0, w20, and w21 registers. At this point, I was just printing the contents of those registers. Soon I noticed a pattern. Perhaps those bytecodes I saw earlier are part of a VM and this is like a VM crackme! Some extra analysis confirmed this: w0 is the bytecode (as it constantly changes), w20 is the offset or somewhat like a program counter (as it increases and decreases in a pattern or jumps around sometimes), and w21 points to where the base of the bytecode is (it stays constant). Also, once again from static analysis, I noticed that 0x00574c8 and 0x005740d0 are constantly referenced; perhaps they are the registers to this VM.
Moreover, all the conditionals seen in the massive loop are related to the operations that must be performed when w0 has a certain byte in it. Here is my summary of them (labeled with w0 value and corresponding location in binary):
w0 = 0x23 -> 0x4005d0
if (iVar6 == 0x23) {
PTR_DAT_005740c8 = PTR_DAT_005740c8 + -1;
Decrements the index
w0 = 0x0e -> 0x4005e8
if (iVar6 == 0xe) {
*PTR_DAT_005740c8 = *PTR_DAT_005740c8 + '\x01';
Increments value in current index
w0 = 0x01 -> 0x400600
if (iVar6 == 1) {
*PTR_DAT_005740c8 = *PTR_DAT_005740c8 + -1;
Decrements value in current index.
w0 = 0x05 -> 0x400618
if (iVar6 == 5) {
bVar3 = *PTR_DAT_005740c8;
iVar6 = FUN_0049ecb8((ulonglong)bVar3); //run it on data at 5740c8
if (iVar6 == 0 && bVar3 != 10) {
return 1;
}
FUN_004b1570((ulonglong)bVar3,PTR_DAT_00574998);
ulonglong FUN_0049ecb8(int iParm1)
{
longlong in_tpidr_el0;
return (ulonglong)
((uint)*(ushort *)(*(longlong *)((undefined *)0x90 + in_tpidr_el0) + (longlong)iParm1 * 2)
& 0x4000);
}
This function didn't matter in solving this crackme.
w0 = 0x80 -> 0x400650
PTR_DAT_005740d0 = PTR_DAT_005740c8;
PTR_DAT_005740c8 = puVar2;
*puVar2 = 0xd;
PTR_DAT_005740c8 = PTR_DAT_005740c8 + 1;
do {
uVar7 = FUN_004b1418(PTR_DAT_005749a0);
*PTR_DAT_005740c8 = (char)(uVar7 & 0xff) + -0x5e;
puVar2 = PTR_DAT_005740c8 + 1;
PTR_DAT_005740c8 = puVar2;
} while ((uVar7 & 0xff) != 10);
PTR_DAT_005740c8 = PTR_DAT_005740d0;
PTR_DAT_005740d0 = puVar2;
Reads and modifies a password. It subtracts 0x5e from each character.
w0 = 0xef -> 0x400968
A good amount of floating point operations... it didn't matter in solving the crackme.
w0 = 0xd2 -> 0x4006c0
A good amount of floating point operations... reversing these are not necessary.
Perhaps it checks if the password is correct because I saw a "\r" in there.
You can also see in GHIDRA how compares w0 to w2, and w2 is result of the floating point operations... I hypothesized that this is where the password check is occurring.
w0 = 0x02 -> 0x400958
This part did not matter in solving the crackme.
w0 = 0x7f -> 0x400af8
This part did not matter in solving the crackme.
w0 = 0x17 -> 0x400584
while (iVar6 = (int)param_1, iVar6 == 0x17) {
unaff_x20 = unaff_x20 + 1;
PTR_DAT_005740c8 = PTR_DAT_005740c8 + 1;
param_1 = (ulonglong)*(byte *)(unaff_x21 + unaff_x20);
if (*(byte *)(unaff_x21 + unaff_x20) == 0) {
return 0;
Increments counter and increments byte code offset in x20 register, then loads the next byte into the w0 register.
This part determines whether to keep looping or to stop. If w0 = 0, it exits.
Most of them don't matter... it's funny though how it is very similar to brainfuck, with a few more instructions. The only things that really matter here is 0x80 (which reads in input and subtracts 0x5e from every char) and 0xd2, which is checking our input, or password. Also from dynamic analysis, you should see that if your password is wrong, the VM goes into an infinite loop on the byte 0x7f.
Now, how exactly do we figure out the result from all the crazy floating point arithmetic used to check the password? Answer is we don't need to. Take a look at this part around 0x40091c.
0040091c 5f 00 20 6b cmp param_3,param_1, UXTB
00400920 81 ee ff 54 b.ne LAB_004006f0
param_3 and param_1 are tied to w0 and w2 in GHIDRA. w2 is the difference between the char you entered and 0x5e. w0 is the result of the floating point arithmetic. We can determine them through debugging; they just need to be equal. Moreover, from debugging, you will notice that the chars are checked in reverse order. After a few minutes, I managed to get the password out. It is the following:
~vms_all_the_way
Upon entering the password, the program prints out the flag in ASCII art and exits.
No comments:
Post a Comment