tl;dr
- UAF in chess game, overwrite
__malloc_hook
toone_gadget
Challenge Points: 200
Solves: 43
Solved by: d4rk_kn1gh7, 3agl3, Cyb0rG
Angstrom CTF had a great collection of pwn challenges, ranging from easy to fairly challenging. Here’s my writeup for pawn
, a challenge which we were one of the first teams to solve, and our exploit is pretty unique.
Initial analysis
We were given an x86_64
binary, along with libc-2.31
. The mitigations enabled were as follows:
1 | Arch: amd64-64-little |
On analysing the binary, you could quickly realise that it was a chess game with 5 options - to add a board (with a maximum of 5 boards), print a board, move a piece, smite a piece, and delete a board.
1 | d4rk_kn1gh7 @ BatMobile ./pawn |
Reversing & Vulnerabilities
On reversing the binary, we found out that each chess board was stored on the heap in the form of two chunks each of size 0x51
, one containing pointers to the start of each row of the board (which was used in the print
option), and one containing the actual chess board itself (where we could move
and smite
pieces).
A sample board on the heap looked like:
1 | 00:0000│ 0x405660 ◂— 0x0 |
As you can see, the first chunk contains pointers to each row, and the second is the actual board itself (even though each row is 8 bytes, it is a bit asymmetric as there’s a null byte after each row, to make the print
function work properly, that’s why there exists a separate chunk containing pointers).
The first vulnerability (and the most obvious one) was that it contained a UAF, i.e there was no check whether a particular board was in use or free, thus the path to exploitation was pretty clear - try to overwrite tcache fd with something like malloc hook
or free hook
, and get allocation there.
However, a big problem was that we had no user input on the heap, i.e the only control we had over the heap was to move
and smite
pieces. This took us to reversing the move
and smite
functions. The smite
function a simple one, which was as follows:
1 | int smite_piece(char** board, int x, int y) { |
It checks if board[y][x]
is a letter, and if it is, it overwrites it with mov_count
, where mov_count
is a byte containing the number of moves made upto that point. But a major bug here was that there was no check on x
and y
, thus writing mov_count
anywhere, provided it was a letter. Now this was a clear write primitive, and we had control over the number of bytes moved, thus control over the value written.
Now this theoretically was enough to exploit it (as most other teams did). You could brute for a heap address to consist entirely of only letters, and thus be able to overwrite each byte of that heap address. However there was no guarantee that this would happen, so I found another vulnerability.
The move
function was a bit compilicated, but in short, it emulated chess moves. It took two pairs of indices as input, a source and a destination. It first checked if the value at the source indices was a letter, then checked if the particular piece at that location was eligible to move to the destination indices (based on chess rules). This again had no check on the input index, so we could move a piece out of bounds.
Exploitation
Now that we had enough bugs to exploit this properly, we can move on to the actual exploit. The first step was to get leaks. A heap leak could be easily achieved by abusing the UAF, and allocating then freeing two chunks. When we print
the second chunk it gives us a tcache fd pointer, which is our heap leak.
Getting a libc leak was a bit more complicated. My idea was to overwrite one of the pointers in a pointer chunk with a resolved GOT
address (as there was no PIE), thus print
ing the board would leak a libc address. We could hope that the heap addresses would contain only letters and try smite
directly to overwrite it to got, but I did this entirely with the move
option, moving chess pieces to that pointer, guaranteeing that it would contain only letters, thus it could be smite
d. I did this by moving the rook, king, and the second rook on the last row of a board (unfortunately there were no moves implemented for the queen) to the first pointer on the next pointer chunk. Also for this, we needed to make sure these moves could be made, so we needed to first clear out all the pawns, knights and bishops in the way, then move the rooks and king.
The following is how a sample pointer chunk looked after overwriting the first pointer with our two rooks and a king:
1 | 00:0000│ 0xd8e490 ◂— 0x726b72 /* 'rkr' */ |
Now we were guaranteed to have all the bytes be a letter, so smite
was guaranteed to work on these bytes. We could use smite
to overwrite the pointer to a GOT
address byte-by-byte (you could manipulate the mov_count
by just moving a bishop back and forth). Once this was done, we could print
the board whose pointer we overwrote, to leak libc.
1 | 00:0000│ 0xd8e490 —▸ 0x403f98 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x7fc273c6f850 (free) |
After this, the rest of the exploit was fairly straightforward. However there was a bit of a catch. The pointer
chunks got allocated and freed first, and it was significantly easier to overwrite the fd
of a board chunk. But if we overwrote the fd of a board
chunk, the allocation we got would be that of a pointer
chunk, which we had no control over. So we overwrote the size of a pointer chunk to 0xa1
(the normal size was 0x51) using smite
, thus we could get a freed board chunk back on an even numbered allocation. Following this, the exploit was simple - overwriting a board chunk fd with malloc hook byte-by-byte (using smite
), getting a board allocated there, then overwriting the first 8 bytes of that board with one_gadget
.
1 | 00:0000│ 0x7fd97b5e9b70 (__malloc_hook) —▸ 0x7fd97b4e4c81 (execvpe+641) |
After this, calling malloc
again gave us a shell!
Exploit script
1 | #!/usr/bin/python |
Flag
1 | d4rk_kn1gh7 @ BatMobile python exp.py |