warmup
Description:
192Solvers 43
attachment:https://share.weiyun.com/5NiPNJA password:ryzwab
or
https://drive.google.com/file/d/14ablm3PSKd1q0RDQUt4rZa-FCy7_wCSo/view?usp=sharing
nc 47.52.90.3 9999
Info extracting
Binary and libc-2.27.so is given, since is 2.27 version we know that tcache is being used and on this version there isn’t any security checks if a chunk is placed in tcache bin.
First we start by using the file command:
1 | $ file warmup |
With this we know:
- ELF compiled for x86_x64 architecture.
- Dynamically linked.
- Stripped (A little bit harder to reverse).
Next step is to check protections:
1 | checksec warmup |
Brief analysis of these protections:
- FULL RELRO (GOT entries are read only we can’t overwrite them)
- STACK CANARY (The stack is protected with the canary if there is a stack overflow we need to find a way to leak it)
- The Stack is not executable (We can’t execute shellcode, techniques like ROP can bypass this)
- PIE (Position Independent Executable) is on (If we want to use rop we need a way to leak the base address)
Testing the binary
As always for this kind of challenges we are presented with a menu:
1 | $ ./warmup |
We can add, delete and modify.
By adding we can only specify the content:
1 | # # # ##### ####### ####### |
For delete and edit we can both specify a index in case of modify we can also modify the content we previously added on creation.
Static analysis with IDA
Analysis of delete function
Analysis of add function
Analysis of edit function
Exploit
As you have noticed we don’t have any read/print function that would allow us to print the contents of the created items because of this we need to find another way to leak a libc address, we can do it by manipulation the IO_FILE struct to make puts leaking an address.
The plan is:
- Use tcache dup to overwrite the size of a chunk into a size of unsorted bin range (0x91 for example)
- Fill the tcachebin size 0x91 by freeing it 7 times (max 7)
- One more free will put this chunk into an unsorted bin both fd and bk are updated into libc addresses from the main arena.
- Do a 4 bit brute force by updating the last 2 bytes of the fd libc address to get stdout.
- Resize from 0x91 back to 0x51 so next free gets into a tcache bin again.
- Overwrite stdout->_flags with 0xfbad1800 and _IO_read_ptr, _IO_read_end, _IO_read_base with NULL and the last byte of _IO_write_base with NULL.
- Extract libc addresses from next puts.
- Overwrite free_hook with system and modify its fd to /bin/sh\x00, Doing a free will get a shell for us.
Tcache dup
Libc-2.27 uses tcache so every allocated chunk bellow 0x410 when freed is placed in a tcachebin , their behaviour will be very similar to when they were inserted in a fastbin chunk before tcache was introduced. The main problem is we can only allocate 0x40 chunks (0x51 -> size + flags) for example if we allocate one item this is how it looks like in the heap:
In order to transform the chunk above into a chunk in range of a unsorted bin(unsorted bin because after a free it will update fd bk pointers into libc addresses) we kinda need an arbitrary write.
This can be achieved with double free and by changing the fd pointer into a place we want to write, when malloc executes it will return the modified fd pointer in our case we want it to be right at the chunk header (0x5595b74c0660) so we can modify the size from 0x51 to 0x91.
Imagine after chunk A we allocate more 2 chunks B and C, if we free B first, free will check if there is any chunk inside tcachebin of size 0x50:
As you can see above there isn’t any list of size 0x50 so it will update the fd of the chunk to null.
Chunk B before free:
Chunk B after free:
The pointer to the current freed chunk is inserted into tcachebin(0x50) at the head:
If we free chunk C now its fd is updated to chunk B pointer:
And the current freed chunk pointer is inserted at the head of tcachebin(0x50):
Now if we free Chunk A its fd is going to point to chunk C:
Chunks A content pointer is added to the tcachebin(0x50) linked list:
If we double free Chunk A its fd is going to point to its own because the previous freed as chunk A:
Chunks A content pointer is added to tcachebin(0x50):
Notice that ALSR doesn’t modify the last 3 numbers of the pointers of each chunk on the heap:
The reason why there is a tcachebin is to reuse space on the heap when a new chunk is allocated, if it’s a perfect fit for example malloc will look at the list of that size and reposition the new allocated chunk on the same places where old chunks were freed. And this is why double freeing is so powerful, since one of the pointers is repeated in the list if we allocate one and modify the last byte of the fd to 0x60 we can make the next malloc to return to the fd we want getting an arbitrary write.
So this is how the exploit looks like right now:
1 | def exploit(): |
The tcache bin list right now is:1
0x50 [4]: 0x56018d3ae670 -> 0x56018d3ae670
If we do this mallocs:
1 | # 0x50 [4]: 0x56018d3ae670 -> 0x56018d3ae670 |
After this if we edit index 3:
1 | free(0) # 0x50 [ 1]: 0x56018d3ae670 <- 0x0 |
The look of chunk A in GDB after edit:
Now that we have a 0x91 chunk we need to fill tcachebin of 0x91, we can do this by freeing it 7 times:
1 | for _ in xrange(7): |
The look at tcachebins after this:
Next free(1) the chunk is going to be inserted into an unsortedbin:
The reason for an unsortedbin is because both fd and bk will be updated into libc addresses:
The reasons why I created chunk B and C was to be able to free this chunk, because tcache is full next free will have security checks, on my old write up of penpal world I did the same thing and I explained why chunk B and C bypass this checks you can find it at https://teamrocketist.github.io/2019/08/17/Pwn-RedpwnCTF-penpal-world/
The first thing we want to do now is to convert this bin again back to 0x51 size we still have its pointer saved at index 3 so we can easily do it with:
1 | edit(3, p64(0x0)+p64(0x51)) |
We want to do this because tcachebin(0x91) is full , we want to manipulate tcachebins again, since we already double freed before at tcachebin(0x50).
The other reason is that we can only malloc chunks of size 0x51.
Lets compare the difference of stdout address and the address that got placed at the fd .
Stdout address:1
2pwndbg> p/x stdout
$3 = 0x7f8421047760
fd pointer at chunk A:
1 | pwndbg> x/20gx 0x56018d3ae660+0x10 |
We want to modify 0x7f8421046ca0 to 0x7f8421047760 we only need to change the last 2 bytes, we know that the last 3 numbers of stdout never change(670), they are always the same, so the only thing we need to brute force is the 4th this means if we try to modify the last two bytes of the fd to p16(0x7760) we would have a probability of 1/16 because the only possibilities for last bytes of stdout are:
0760
1760
2760
3760
4760
5760
6760
7760
8760
9760
a760
b760
c760
d760
e760
f760
This is the look of tcachebins right now:
So on our second malloc, the pointer returned will be the libc address but before that we need to modify the last 2 bytes from one of the 16 possibilities:
1 | edit(2, p16(0x7760)) |
Now if we succeed to bruteforce stdout we need to overwrite stdout->_flags with 0xfbad1800 and _IO_read_ptr, _IO_read_end, _IO_read_base with NULL and the last byte of _IO_write_base with NULL, if we do this next puts will leak a bunch of libc addresses a more detailed explanation on why this works can be found at https://vigneshsrao.github.io/babytcache/ this guy explains it very well.1
2
3
4
5
6
7
8add('A')
try:
#context.log_level = 'debug'
add(p64(0x0fbad1800)+ 3*p64(0) + '\x00')
except:
log.failure("not lucky enough!")
r.close()
return False
if we succeed something like this is printed to the screen where we can see a lot of libc addresses leaked there:
Now adapting a bit more our code to make sure we got libc:
1 | r.recv(0x8) |
In the end, we have everything we need modify the fd of the object you want to free with /bin/sh and overwrite free_hook with system so next time we trigger free we get a shell!
1 | free(0) |
Full exploit:
1 | from pwn import * |
Running it