Part of the solution of pwncollege
Point to https://dojo.pwn.college/challenges
As per the website requirements, only the solution ideas and exploits for the first two challenges are provided here.
heap
babyheap1.0~2.1——UAF
def exploit():
malloc(flag_size)
free()
read_flag()
puts()
babyheap3.0~3.1——uaf2.0
Note: tcache uses a LIFO structure.
babyheap4.0&4.1—tcache double free
This is the first time I've independently solved a heap challenge, even though it's the simplest type of tcache double free
.
babyheap5.0 & 5.1 — Unsorted Bin Attack
libc 2.23 → libc 2.31
By exhausting all tcache bins, small bins and unsorted bins are triggered, allowing for chunk modification.
babyheap6.0&6.1——tcache poisoning
Simple tcache poisoning
https://wargames.ret2.systems/level/how2heap_tcache_poisoning_2.31
babyheap7.0&7.1——tcache poisoning 2.0
Since the flag remains unchanged and only the latter part of the secret is needed, simply set sec_addr += 8
and rerun the program.
babyheap8.0&8.1——Brute Force
Using the previous method, the last 12 bits of the secret can be leaked, while the first 4 bits can be obtained through brute force enumeration.
The script took an entire class period to run before producing the result.
babyheap9.0——tcache double free
Due to the UAF (Use-After-Free) vulnerability, after the first free operation, setting the next pointer to 0 enables a double free.
At this point, if the next pointer is modified to 0x42b321
, the key can be observed in the provided heap information.
babyheap9.1—tcache double free | perthread corruption
Following the steps from the previous level, proceed to malloc twice. Although the challenge prevents you from reading pointers near the key, the malloc operation still executes.
Due to the linked list nature of tcache
, the value at addr+8
will be zeroed out. After performing this operation once more, you can pass the verification by entering 16 \0
characters.
In theory, it is possible to modify the address to the perthread struct and read the 8-byte key at the head of the linked list, though this approach was not tested.
babyheap10.0&10.1——Heap on Stack
The stack address and ELF address are provided, so the heap can be directly placed on the stack, and the return address can be modified accordingly.
babyheap11.0&11.1——free_hook + arbitrary read
Since the ELF address and stack address were unknown, I first overwrote free_hook
(note: when writing to free_hook
, it's best to set the size field to \x7f
) to point to system_plt
, which granted a shell but without sufficient permissions.
Then, I discovered that executing the echo
function would allocate an additional heap chunk containing addresses related to the ELF and the stack. Moreover, the echo
function did not restrict the offset size, allowing these addresses to be retrieved.
Since I already had the ability to write to free_hook
, I directly overwrote free_hook
with the address of win
to achieve the desired outcome.
babyheap12.0&12.1—modify size
Due to the mechanism of tcache
, modifying the size
of a heap chunk on the stack can bypass validity checks.
babyheap13.0——tcache double free
Similar to the approach in babyheap9.1, allocate memory near sec
, then input a series of \0
to overwrite the key
.
babyheap13.1—Modify Size
Since the offset between sec
and the heap does not exceed one byte, after modifying the size
of the heap on the stack, you can use malloc
to allocate a slightly larger heap to fill with data.
babyheap14.0&14.1—modify size + leak elf & canary
Based on level13, use echo
to leak the elf
address and canary
address, then perform stack overflow on the heap.
For 14.1, note that \x09
is not a valid input; it is recommended to use a later address instead.
babyheap15.0&15.1—Leaking ELF & Stack + Unlink
This challenge is a bit tricky. I ended up leaking the ELF, stack, libc, and heap before realizing the UAF was already gone.
For arbitrary write, aside from double free, unlink is also an option. I didn't want to bother with intricate heap feng shui, so I just went with unlink to wrap things up.
The libc version used in the following program has been upgraded from 2.31 to 2.35.
Differences between glibc 2.35 and 2.31 include:
- Safe-linking — The actual address is the encrypted value XORed with the previous item shifted right by 12 bits (introduced in 2.32).
- Malloc alignment check — Addresses must be aligned to 0x10 (introduced in 2.33).
- Removal of hooks such as
__malloc_hook
,__free_hook
, etc. (introduced in 2.34).
babyheap16.0—tcache double free + safe-linking
The logic is the same as level 9, except that an XOR operation is required when modifying the next pointer.
babyheap16.1—tcache perthread corruption + safe-linking
Due to alignment checks, it is not possible to zero out the value at addr. Here, tcache perthread corruption must be used instead, which allows allocating a chunk precisely at the key's location for reading.
Additionally, the read value must be XORed to obtain the final key.
babyheap17.0——Canary Leak
Allocate the heap at the location rbp-0x10
, obtain the canary from the printed information, and then perform a stack overflow.
babyheap17.1——off by null + unlink
By leveraging the null-termination feature of scanf, the next_chunk_size
is changed from 0x101
to 0x100
, thereby achieving unlink.
Subsequently, an entry in the ptr
pointer array is overwritten with the return address, and the return address is overwritten via scanf
to point to the win
function.
babyheap18.0——off by null + unlink
The approach is similar: after unlink, directly use puts to read the sec array.
babyheap18.1 — Fake Chunk
Fake a heap chunk and directly overwrite the sec
array.
babyheap19.0&19.1——chunk overlapping
babyheap20.0&20.1——Safe-Linking + libc.environ + ORW ROP
Leak the libc address via the unsorted bin and obtain the heap address through a heap overflow.
Allocate four chunks. The first chunk overflows into the second, changing its size to three times the original. By printing and leaking the key pointer, overflow the second chunk while keeping the third chunk unchanged. Modify the next
pointer of the fourth chunk (according to the Safe-Linking feature, this allows modifying the next
pointer of the third chunk) to libc.environ ^ (heap_base >> 12)
. Then, perform two malloc
operations and print to obtain libc.environ
.
Use libc.environ
to retrieve the stack address for unlink.
Since directly obtaining a shell cannot read the flag, ORW ROP is used here for reading.
Kernel
babykernel1.0~6.1—Basic Definition
Reference this blog post: https://www.cnblogs.com/crybaby/p/14431651.html
Kernel shellcode is quite tricky.
Note: How to check if KASLR is enabled?
After entering the kernel, use
dmesg
to check ifnokaslr
appears at the end of the second line.
babykernel7.0~7.1——struct&debug
You need to pass in a structure similar to the following:
struct shellcode{
unsigned long length;
char shellcode[0x1000];
unsigned long* shellcode_addr;
}
Note the following points:
- The shellcode must include a
ret
instruction (ret2usr). - The
shellcode_addr
can be obtained through dynamic debugging and has a fixed location.
babykernel8.0~8.1—Shellcode in Shellcode
In a nutshell—writing shellcode within shellcode.
The first entry into the kernel executes commit_creds(prepare_kernel_cred(0))
, followed by a ret2usr
to obtain a root shell. Then, upon re-entering, execute run_cmd /bin/chmod 777 /flag
to modify the flag's permissions. (Note: spawning a shell in kernel mode has no effect.)
babykernel9.0~9.1——run_cmd
Directly change the printk
pointer to run_cmd
, then use rdi
as the input to write /bin/chmod 777 /flag
.
babykernel10.0—kaslr leak
Note that kaslr is only re-randomized after a kernel reboot, so rerunning the program will not change the original kernel function addresses.
Similar to userland aslr, the lower 5 bits of kaslr are fixed. Writing 256 bytes causes the address of printk
to be leaked, thereby allowing the address of run_cmd
to be found.
babykernel10.1—Partial Overwrite
Since this driver does not print its own input, we choose to overwrite the last 3 bytes of printk
, meaning we need to brute-force the sixth-to-last bit of run_cmd
.
babykernel11.0~12.1 – Memory Scanning
The program loads the flag into memory and then deletes the flag file, so the flag can only be found in memory.
From the Discord spoiler, it is known that scanning should start from 0xffff888000000000
(i.e., the value of the macro phys_to_virt(0)
, which is also the value of __PAGE_OFFSET_BASE_L4
in the source code). Then, use dmesg
to search for the flag.
Note the following points:
- If using this website to convert assembly into bytecode, be sure to convert the bytecode back into assembly to verify that no instructions are missing.
- To reduce output, it is recommended to compare the value at an address with
pwn.coll
(or a similar 64-bit string) before printing it to the console. - Python consumes a lot of memory, and it is possible that starting a Python process may cause the memory containing the flag to be reallocated. Therefore, it is advisable to output the shellcode to a file and then use file stream input.
file
According to Linux philosophy, everything is a file, so
stdin(0)
,stdout(1)
,stderr(2)
, etc., are all files.
babyfile_level1—Arbitrary Write to File
If you do nothing and break after fwrite, the FILE structure will look like this:
pwndbg> x/30gx 0x18203b0
0x18203b0: 0x00000000fbad2c84 0x0000000001820590
0x18203c0: 0x0000000001820590 0x0000000001820590
0x18203d0: 0x0000000001820590 0x0000000001820690
0x18203e0: 0x0000000001821590 0x0000000001820590
0x18203f0: 0x0000000001821590 0x0000000000000000
0x1820400: 0x0000000000000000 0x0000000000000000
0x1820410: 0x0000000000000000 0x00007fb7069655c0
0x1820420: 0x0000000000000004 0x0000000000000000
0x1820430: 0x0000000000000000 0x0000000001820490
0x1820440: 0xffffffffffffffff 0x0000000000000000
0x1820450: 0x00000000018204a0 0x0000000000000000
0x1820460: 0x0000000000000000 0x0000000000000000
0x1820470: 0x00000000ffffffff 0x0000000000000000
0x1820480: 0x0000000000000000 0x00007fb7069614a0
0x1820490: 0x0000000000000000 0x0000000000000000
So you only need to modify the values from file+8
to file+0x48
to the desired write location.
Among these, 0xfbad2c00
is the magic number.
def exploit():
fake_file = p64(0xfbad2c00) + p64(0x4040e0)*4 + p64(0x4041e0) + p64(0x4050e0) + p64(0x4040e0) + p64(0x4050e0)
io.send(fake_file)
Finally, cat /tmp/babyfile.txt
babyfile_level2—Arbitrary Read from File
This time it's an arbitrary read:
pwndbg> x/30gx 0x6483b0
0x6483b0: 0x00000000fbad0230 0x0000000000000000
0x6483c0: 0x0000000000000000 0x0000000000648590
0x6483d0: 0x0000000000000000 0x0000000000000000
0x6483e0: 0x0000000000000000 0x0000000000648590
0x6483f0: 0x0000000000649590 0x0000000000000000
0x648400: 0x0000000000000000 0x0000000000000000
0x648410: 0x0000000000000000 0x00007fc110d2e5c0
0x648420: 0x0000000000000003 0x0000000000000000
0x648430: 0x0000000000000000 0x0000000000648490
0x648440: 0xffffffffffffffff 0x0000000000000000
0x648450: 0x00000000006484a0 0x0000000000000000
0x648460: 0x0000000000000000 0x0000000000000000
0x648470: 0x00000000ffffffff 0x0000000000000000
0x648480: 0x0000000000000000 0x00007fc110d2a4a0
0x648490: 0x0000000000000000 0x0000000000000000
Here, 0xfbad000
is the magic number.
Note that the size of _IO_buf_end - _IO_buf_base
must be greater than (not equal to) the size of the buffer.
def exploit():
fake_file = p64(0xfbad0000) + p64(0)*2 + p64(0x4041f8) + p64(0)*3 + p64(0x4041f8) + p64(0x4051f8)
io.send(fake_file)
babyfile_level3——fileno redirection
Simply modify _fileno
to standard output 1
.
babyfile_level4——fileno redirection
PIE is not enabled. Redirect to input and enter the win
address.
babyfile_level5—arbitrary write to file
Same as level1.
babyfile_level5——arbitrary read from file
Same as level2.