Recently, I’ve been extremely busy, and a few key members of our team were also occupied with preparing for quizzes this weekend, so we did not participate in Plaid CTF 坐牢 for the excitement. Fortunately, we chose b01lers CTF as our weekly training, which provided a very enjoyable problem-solving experience.
In this competition, I achieved AK for all the challenges under PWN and Blockchain categories. Among these, mixtapeailbc, Zero to Hero, and seeing-red were all first blood.
This is my first time encountering a PWN challenge on the arm64 architecture. Fortunately, our team recently purchased a new Mac Studio with an M2 Max chip 😋:
Thanks to this, I can easily start a Docker and script while debugging with pwndbg, just as I would with problems on the x86_64 architecture.
Analysis
The challenge has two vulnerabilities: a format string vulnerability in the get_address function and a stack overflow in the feedback function, and there is no address randomization in the problem (no PIE). Therefore, theoretically, either one of these vulnerabilities alone should be sufficient to exploit.
However, combining the two vulnerabilities is generally a simpler approach:
First, use the format string vulnerability to leak the canary and libc addresses.
Then, exploit a stack overflow to overwrite the return address of the previous function. This is because the ret instruction on the arm64 architecture actually executes mov pc, x30. A shorter stack overflow cannot disrupt the current function’s execution flow, only allowing for the modification of the next function’s return address. This also serves as a mitigation measure for stack overflows under the arm64 architecture.
The final step is to use ROP to call system("/bin/sh\x00").
It is worth noting the function calling conventions and stack layout of the arm64 architecture:
When calling functions, the arm64 architecture requires the first four arguments to be placed in x0, x1, x2, and x3 respectively. Any additional arguments beyond the first four are placed on the stack.
The stack layout includes, in order, the stack address, return values, local variables, canary, the stack address of the next function, and the return address of the next function.
Exploitation
For exploitation, the approach can be based on the previous ideas:
Format string leak:
Completing ROP with a stack overflow, where constructing the ROP chain also troubled me for a while since the lack of a pop instruction felt quite uncomfortable. However, there are always enough gadgets in libc. The following statement can be used to set up the registers:
Finally, there’re gadgets for calling one reg with args in another reg. In my case, I got this one:
The first point was that the problem provided no output and appeared to clear the registers. However, upon closer consideration, it’s worth asking: were the segment registers also cleared? For example, the fs register is typically used to point to specific data segments, such as Thread Local Storage (TLS), and one can calculate the stack address or libc address based on where it points.
Additionally, there was no output function provided during the execution of the shellcode, but this is usually addressed using a loop and a timing side-channel to obtain the flag. In this case, it’s even simpler: the remote environment even provides a return value!
Thus, after using the fs register to locate the position of the flag in memory, the flag can be output byte by byte using the return value of the program exit:
It’s a somewhat cumbersome virtual machine with 39 instructions, including memory operations, calculations, and output. However, I’ve encountered many cumbersome VM PWNs in domestic competitions, so settling down to reverse-engineer it felt manageable.
Analysis
At the very beginning, I noticed the output function by searching the references of putchar, then I decided to reverse the VM instruction’s structure from that output function. However, after sending a p32(0xdeadbeef) I noticed my offset became negative. After debugging in gdb, I identified that this function has a negative overflow issue:
However, there is an overflow check after the output function. I further searched for other cross-references to the get_p16_arg function and then got this one, which leads to control-flow-hijacking:
After reviewing all the functionalities, I confirmed that each instruction in the VM is 4 bytes. The first two bytes often seem to be used for memory addressing, and the third byte usually serves as the primary argument.
Exploitation
At the start of the exploitation, I attempted to write a payload that could hijack the control flow to 0xcafebad0deadbeef to validate the correctness of the above analysis:
The analysis proved to be very accurate, as I successfully hijacked the function table entries and controlled several parameters to be zero. Therefore, my plan was to invoke one_gadget to achieve get_shell.
But how to obtain the libc address? This puzzled me for a long time, until I finally realized that the memory copying functionality was intended for acquiring addresses:
By exploiting an out-of-bounds condition, it’s possible to treat a piece of data on the stack as an array index. I identified __libc_start_main+243 because its last byte is fixed. To prevent errors, I pre-set the value corresponding to 0x83 to be the VM’s PC pointer.
Final exp.py
After that, I performed some calculations to get one_gadget in VM memory and realized get_shell:
The results are as follows:
seeing-red
There are two vulnerabilities: first, use a stack overflow to call use_ticket to load the flag onto the stack (note that the stack alignment needs to be adjusted using ret), and then use a format string to print the flag.
Initially, I discovered that totalSupply is of int type, and there were no checks on it, so I tried a negative overflow in stage0, but it seemed useless. I first thought of causing an integer overflow to reduce balances to 0, but that seemed to require 2255 attempts, which was unreasonable; however, I found that the method could infinitely increase the balances of the owner, and totalSupply would decrease indefinitely. Thus, I considered creating a new wallet address into which I could deposit unlimited money (initially transferring a small amount for transaction gas).
Using the above method, I could exploit the system to infinitely generate money: