This is a challenge from QiangWangBei Finals last year, it’s a RealWorld challenge. Only about 3~4 teams were able to finish it in the game. You can download the challenge files here. The challenge is called
VulnTest, it contains some obvious bugs but the difficulty lies in the exploitation. It was compiled with
AddressSanitizer(ASAN), which is designed to detect the memory corruption thoroughly, so it could provide extra protection for the program. If a vulnerability is triggered, it can be detected as soon as possible and the program died out.
Because of the ASAN insturmentation, the decompilation result from IDA is hard to read. But you can still ignore all the instrumentation stuff and recongize the core logic of the program. It is nothing new but another “menu challenge”. It provides 3 tests.
Write Card and
Show Card options, it is a test of format string vulnerability.
This function provides a chance to write
7 bytes to a stack buffer. And it will check if the inputed string contains
% character, if it does, it will go on to check if it contains any character from
$dufscnpexXog, in the hope of preventing format string attack.
char *__fastcall write_card1(char *a1)
This function contains a format string vulnerability, it can use the inputed string from
Write Card as the format string to
sprintf. Also, the sprintf arguments are over-lapping each other, it could cause unexpected behavior. To mitigate the format string vulnerability, it added a check to see if two arguments equals after the formatting process. If they are not equal, the program exits.
int __fastcall show_card1(const char *a1)
Test1 also have
Write Card and
Show Card options, it is a test of Out-of-Bound Write Vulnerability.
We can write data to a
0x30 sized buffer on the stack, and we can specify the index of where to write. It has check to see if the start index greater than
0x30, but the index could be negative, that means we can overwrite the content before the target buffer.
size = get_int();
Just an output function for the target buffer.
__int64 __fastcall show_card2(__int64 a1)
Test3 is a tyical heap challenge test, it contains
open. This test is only avaliable if Test1 and Test2 have been visited. I reversed the whole thing and there is an obvious use-after-free vulnerability. Because of the protection of ASAN, it could be hard to exploit. I did not use any function of the Test3 in my exploit, so I won’t post the psedo-code here to save some space.
It seems the vulnerabilities from these test function are well protected by the checks or ASAN mechanism, but since the variable/buffer are all located on stack, it is possible to influence mutually. The first thing to do is finding out the layout of these variable/buffer.
We can read the psuedo code and construct the layout above, but if we attach gdb to view the memory, we can see the following memory layout. On the left is the actual memory layout on the stack, on the right is so call
shadow memory, which is another memory mapping to trace the usage of the memory, in order to detect overflow or other memory corruption.
For a complete explanation of the algorithm, you can refer to AddressSanitizerAlgorithm.
For this challenge, we only need to know two points:
Shadow = (Mem >> 3) + 0x7fff8000;
- The byte used in shadow memory:
f1: Stack left redzone
f2: Stack mid redzone
f3: Stack right redzone
The binary has turned on all mitigation, so the first thing we want to do is leaking some address.
matthew@matthew-Virtual-Machine > checksec ./VulnTest
We have a format string vuln and an out-of-bound(OOB) vuln, it is natual to consider combine them together. With the OOB, we can overwrite the bad character set on the stack, then we can bypass the bad character check. And we can also write the format string to
buf_test1 with the out-of-bound vuln, then we can ignore the constrain of the max length of 7 bytes.
But the question is, even we can prepare the format string and let
sprintf process it, we could not get its output because of the
if ( a1 )
Then I check the stack again in the
test2 to see what can we do with the OOB capability. I marked the bufffer as well as the red zone, remember that what we can control is
buf_test2 itself and the content above. As we mentioned, we can fully control
buf_test1 and utilize the format string vuln, but we could not leak data from that. We can see the return address also can be controlled. I tried to partial over write the return address to somewhere else and skip the
TEST2_DONE setting, it works and we can do aribitrary times of
However, our primrary task is leaking address. I also try to write 0x30 of
buf_test2 and leaking the address in the red zone. But once the program reads or writes the red zone buffer, it dies.
00:0000│ rbp 0x7ffe86bf69f0 —▸ 0x7ffe86bf6bc0 —▸ 0x560bf17f5ea0 ◂— push r15
Notice that there is a pointer to
_IO_2_1_stdout on the stack. That reminds me the the file struct technique introduced by angelboy: Play with FILE Structure - Yet Another Binary Exploitation Technique. We can overwrite
_IO_2_1_stdout_->_IO_write_base to leak some addresses. So the idea becomes clear, we can overwrite the lowest byte of
_IO_2_1_stdout_ with OOB, from
0x80, then it will point to
_IO_write_base. And we can use the format string with
%n character to overwrite the lowest byte of
\x00, and it will leak some libc address when next time
puts is called.
How to bypass the
strcmp check? I found that the format string
%10$hhn%6$s could do the job and pass the check.
With the libc address, we can use Test2 OOB one more time to write its return address to one_gadget. And lucky for us,
rsp + 0x40 locates in
buf_test2, so it can be easily set to null.
from pwn import *
This challenge contains more function logic than normal one, and the ASAN compile option takes me more time on reversing. I’m sure that there are other solutions could reach the goal. I remember that at final, because this is a real-world style challenge, teams need to show their solution on stage, some teams need to perform some guessing to get the shell.
The binary itself is heavily protected by sundry checks, there are some checks about verifying if the address outside forbidden range before writing to it. Frankly, I didn’t look into
Test3 closely and consider how to bypass these checks. Because I know that the primary task is getting some leaks, without known address, we could not bypass the ASAN checks in
During the search of a workable memory leakage, I came up with this solution. The lesson is we should clear about what capability we’ve got, in this case OOB and format string. We can make a roadmap and ignore all the seemly scary irrelevant mitigations, then solve the checkpoints one by one.