本来以为去年的春节已经足够不堪了,怎料今年更是难上加难。
第一次在外地过年就遇上疫情爆发的事情,本来计划家人来过年也只能取消了。不过不能出门正好也拥有了大段空闲时间,与其像朋友圈里面的各位花式秀无聊,不如静下心来攻克之前没有完成的一些题目。
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.
Recon
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.
Test1
Test1 have Write Card
and Show Card
options, it is a test of format string vulnerability.
Write Card
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.
1 | char *__fastcall write_card1(char *a1) |
Show Card
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.
1 | int __fastcall show_card1(const char *a1) |
Test2
Test1 also have Write Card
and Show Card
options, it is a test of Out-of-Bound Write Vulnerability.
Write Card
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.
1 | size = get_int(); |
Read Card
Just an output function for the target buffer.
1 | __int64 __fastcall show_card2(__int64 a1) |
Test3
Test3 is a tyical heap challenge test, it contains add
, delete
, edit
, 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.
Main
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.
1 | struct context |
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.
1 | shadow memory |
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
Exploitation
The binary has turned on all mitigation, so the first thing we want to do is leaking some address.
1 | 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 strcmp
check.
1 | if ( a1[16] ) |
Then I check the stack again in the write_card
of 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 Test2
.
However, our primrary task is leaking address. I also try to write 0x30 of A
to buf_test2
and leaking the address in the red zone. But once the program reads or writes the red zone buffer, it dies.
1 | 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 0x60
to 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 _IO_write_base
to \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.
exp.py
1 | from pwn import * |
Wrapup
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 Test3
anyway.
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.