pwn-- icon indicating copy to clipboard operation
pwn-- copied to clipboard

pwn++ is a Windows & Linux library oriented for exploit dev and used to play with Modern C++ (17->26)

pwn++

View Code Build Talk
Read code Open in Visual Studio Code CI - MSVC Discord

A rewrite of my PwnLib DLL in modern C++, battery-included pwn kit for Windows (and a bit for Linux).

The idea is to provide in C on Windows the same kind of functionalities than pwntools does in Python on Linux. It's also a toy library meant for exploring Windows in a more friendly way. So if you're looking for years of poorly written C/C++ tangled with performant inefficient ways to explore Windows at low-level, go no further friend this library is for you.

Note: the original PwnLib was written around Windows 7 for feature testing. This is 100% Windows 10/11 focused, so expect things to go wrong if you use any other Windows version. Some stuff may also go wrong in x86. Better use 64. It's not a bug but a design choice 😋

Requirements

Windows

Linux

  • None (AFAIK)

Note Support of C++20/23 on Linux compilers (both GCC and clang) is way incomplete so chances are it won't compile at all until the features I'm using are fully supported. Both compilers mark C++20 support as experimental.

Get started

Quick Start

Using Github

Simply use the repo template-pwn.

Using the pre-builds

To start using pwn++ lib, simply download the latest successful build from CI builds, download and extract the zip file. In your C++ file, just include pwn.h and link with pwn++.dll.

#include "path\to\pwn++\pwn.h"
#pragma comment(lib, "\path\to\pwn++\pwn.lib")

Then compile your binary linked with the lib (make sure you're at least C++17 compliant):

C:\> cl.exe whatever.cc /std:c++20
C:\> clang.exe whatever.cc -std=c++20

Examples

Basic examples of what the lib offers:

Context & logging

#include <pwn++\pwn.h>
namespace ctx = pwn::context;
auto wmain() -> int
{
    auto version = pwn::version_info();
    ok(L"running pwn++ v%d.%02d\n", std::get<0>(version), std::get<1>(version));

    ctx::set_arch(pwn::context::arch_t::x64);

    dbg(L"The default log_level is INFO, this message will never appear!\n");
    ctx::set_log_level(pwn::log::log_level_t::LOG_DEBUG);
    dbg(L"Now it will!\n");

    ok(L"Everything is awesome!\n");
    warn(L"Alright, stop! Collaborate and listen...\n");
    err(L"Can't touch this!\n");
    return 0;
}

Outputs

PS C:\Users\User> .\test.exe
[+]  running pwn++ v0.01
[DEBUG]  log_level set to 0
[DEBUG]  Now it will!
[+] Everything is awesome!
[!] Alright, stop! Collaborate and listen...
[-] Can't touch this!

Utils

pwntools cyclic()-like

#include <pwn++\pwn.h>
int wmain()
{
    std::vector<BYTE> buf;

    ok(L"pwntools.utils.cyclic() with a period of 4, and a length of 0x20 bytes\n");
    if ( pwn::utils::cyclic(0x20, 4, buf) )
        pwn::utils::hexdump(buf);

    ok(L"nice, now with period=sizeof(PTR)\n");
    if ( pwn::utils::cyclic(0x30, buf) )
        pwn::utils::hexdump(buf);

    return 0;
}

Outputs

[+] pwntools.utils.cyclic() with a period of 4, and a length of 0x20 bytes
0000   61 61 61 61 62 61 61 61  63 61 61 61 64 61 61 61  |  aaaabaaacaaadaaa
0010   65 61 61 61 66 61 61 61  67 61 61 61 68 61 61 61  |  eaaafaaagaaahaaa
[+] nice, now with period=sizeof(PTR)
0000   61 61 61 61 61 61 61 61  62 61 61 61 61 61 61 61  |  aaaaaaaabaaaaaaa
0010   63 61 61 61 61 61 61 61  64 61 61 61 61 61 61 61  |  caaaaaaadaaaaaaa
0020   65 61 61 61 61 61 61 61  66 61 61 61 61 61 61 61  |  eaaaaaaafaaaaaaa

pwntools flat()-like

#include <pwn++\pwn.h>

using namespace pwn::utils;

int wmain()
{
    std::string a("AAAA");
    std::wstring b(L"BBBB");

    auto args = std::vector<flattenable_t>{
        a,
        "AAAA",
        b,
        L"BBBB",
        p8(0x43),
        p8(0x43),
        p16(0x4343),
        p32(0x43434343),
        p64(0x4444444444444444)
    };

    hexdump( flatten(args) );
    return 0;
}
0000   41 41 41 41 41 41 41 41  42 00 42 00 42 00 42 00  |  AAAAAAAAB.B.B.B.
0010   42 00 42 00 42 00 42 00  43 43 43 43 43 43 43 43  |  B.B.B.B.CCCCCCCC
0020   44 44 44 44 44 44 44 44                           |  DDDDDDDD

(bad) random stuff

#include <pwn++\pwn.h>
int wmain()
{
    ok(L"random::byte=%x\n", pwn::utils::random::byte());
    ok(L"random::word=%x\n", pwn::utils::random::word());
    ok(L"random::dword=%x\n", pwn::utils::random::dword());
    ok(L"random::qword=%x\n", pwn::utils::random::qword());
    pwn::utils::hexdump(pwn::utils::random::buffer(16));
    ok(L"random::string=%s\n", pwn::utils::random::string(16).c_str());
    ok(L"random::alnum=%s\n", pwn::utils::random::alnum(16).c_str());
    return 0;
}

System information

#include <pwn++\pwn.h>
int wmain()
{
    info(L"computer_name=%s\n", pwn::system::name().c_str());
    info(L"pagesize=0x%x\n", pwn::system::pagesize());
    info(L"pid=%d\n", pwn::process::pid());
    info(L"ppid=%d\n", pwn::process::ppid());
    info(L"pidof('explorer.exe')=%d\n", pwn::system::pidof(std::wstring(L"explorer.exe"));
    info(L"nb_cores=%ld\n", pwn::cpu::nb_cores());
    return 0;
}

Disassembly

Powered by capstone-engine

#include <pwn++\pwn.h>
int wmain()
{
    const uint8_t* code = "\x90\x48\x31\xc0\xcc\xc3";
    std::vector<pwn::disasm::insn_t> insns;
    pwn::disasm::x64(code, ::strlen(code), insns);
    for (auto insn : insns)
        ok(L"0x%08x:\t%s\t\t%s\n", insn.address, insn.mnemonic.c_str(), insn.operands.c_str());
    return 0;
}

Outputs

[+]  0x00040000:        nop
[+]  0x00040001:        xor             rax, rax
[+]  0x00040004:        int3
[+]  0x00040005:        ret

Assembly

Powered by keystone-engine

#include <pwn++\pwn.h>
int wmain()
{
    const uint8_t code[] = "xor rax, rax; inc rax; nop; ret";
    std::vector<BYTE> bytes;
    pwn::assm::x64(code, ::strlen(code), bytes);
    pwn::utils::hexdump(bytes);
    return 0;
}

Outputs

0000   48 31 C0 48 FF C0 90 C3                           |  H1.H....

Process

Current process info

#include <pwn++\pwn.h>
void wmain()
{
    info(L"peb() is at %p\n", pwn::process::peb());
    info(L"teb() is at %p\n", pwn::process::teb());
}

Process creation

Via pwn::process::execv(), basic wrapper over ::CreateProcess()

#include <pwn++\pwn.h>
int wmain()
{
    return pwn::process::execv(L"cmd.exe") == TRUE;
}

Or ShellExecute style:

#include <pwn++\pwn.h>
int wmain()
{
    pwn::process::system(L"ms-settings:");
    return 0;
}

Process creation from specific parent

Cheap way to spawn a NT AUTHORITY\SYSTEM process from Admin prompt

#include <pwn++\pwn.h>
int wmain()
{
    auto ppid = pwn::system::pidof(L"winlogon.exe");
    info(L"found winlogon pid=%lu\n", ppid);
    auto hProcess = pwn::process::execv(L"cmd.exe", ppid);
    if(hProcess)
    {
        auto h = pwn::utils::GenericHandle(hProcess.value());
        ::WaitForSingleObject(h.get(), INFINITE);
    }
    return 0;
}

Outputs

REM In Prompt
PS C:\> whoami
Win10Eval2019\hugsy
PS C:\> .\pwn++-tests.exe
[DEBUG]  log_level set to 0
[*]  found winlogon pid=684
[DEBUG]  Spawning 'cmd.exe' with PPID=684...
[DEBUG]  'cmd.exe' spawned with PID 2024

REM New prompt appears
C:\Windows\System32>whoami
nt authority\system

Terminate a process

#include <pwn++\pwn.h>
int wmain()
{
    auto hProcess = pwn::process::execv(L"notepad.exe hello.txt");
    if ( hProcess )
    {
        auto h = pwn::utils::GenericHandle(hProcess.value());
        ::Sleep(5*1000);
        pwn::process::kill(h.get());
    }
}

Privileges

#include <pwn++\pwn.h>
void wmain()
{
    auto pid = pwn::system::pidof(L"explorer.exe");
    ok(L"is_elevated: %s\n", BOOL_AS_STR(pwn::process::is_elevated(pid)));
    ok(L"has_privilege(SeDebugPrivilege): %s\n", BOOL_AS_STR(pwn::process::has_privilege(L"SeDebugPrivilege", pid)));
    ok(L"has_privilege(SeChangeNotifyPrivilege): %s\n", BOOL_AS_STR(pwn::process::has_privilege(L"SeChangeNotifyPrivilege", pid)));
}

Integrity

#include <pwn++\pwn.h>
void wmain()
{
    auto integrity = pwn::process::get_integrity_level();
    if ( integrity )
        ok(L"integrity set to '%s'\n", integrity.value().c_str());
    else
        perror(L"pwn::process::get_integrity_level()");
}

Memory access

#include <pwn++\pwn.h>

void wmain()
{
    /// against a specific process
    auto peb_loc = (ULONG_PTR)pwn::process::peb();
    auto peb_cnt = pwn::process::mem::read(peb_loc, 0x10);
    pwn::utils::hexdump(peb_cnt);
    std::vector<BYTE> new_peb = { 0x13, 0x37, 0x13, 0x37 };
    pwn::process::mem::write(peb_loc, new_peb);
    peb_cnt = pwn::process::mem::read(peb_loc, 0x10);
    pwn::utils::hexdump(peb_cnt);

    /// or on this process
    auto p = pwn::process::mem::alloc(0x100, L"rwx");
    ok(L"allocated(rwx) at %p\n", p);
    pwn::process::mem::free(p);
    p = pwn::process::mem::alloc(0x100, L"rx");
    ok(L"allocated(rx) at %p\n", p);
    pwn::process::mem::free(p);
    p = pwn::process::mem::alloc(0x100, L"rw");
    ok(L"allocated(rw) at %p\n", p);
    pwn::process::mem::free(p);
}

Simple AppContainer

#include <pwn++\pwn.h>

void wmain()
{
  auto container_name { L"container-" + pwn::utils::random::alnum(10) };
  pwn::process::appcontainer::AppContainer app(container_name, "notepad.exe");
  app.spawn();
}

Also supports capabilities, see AppContainMe for a better example.

Jobs

#include <pwn++\pwn.h>

void wmain()
{
    /// create a notepad process and add it to an anonymous job
    HANDLE hProcess;
    auto ppid = pwn::process::ppid();
    if( pwn::process::execv(L"notepad.exe", ppid, &hProcess) )
    {
        auto hp = pwn::utils::GenericHandle(hProcess);

        auto hJob = pwn::utils::GenericHandle( pwn::job::create() );
        if( hJob )
        {
            auto pid = pwn::system::pid(hp.Get());
            pwn::job::add_process(hJob, pid);
            ::WaitForSingleObject(hp.Get(), INFINITE);
        }

        // pwn::job::close(hJob); // not necessary because of RAII
    }
}

Registry

#include <pwn++\pwn.h>

void wmain()
{
    /// dword value
    {
    std::wstring sub_key(L"Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon");
    std::wstring reg_dword(L"FirstLogon");
    DWORD value = -1;
    if ( pwn::reg::read_dword(pwn::reg::hkcu(), sub_key, reg_dword, &value) == ERROR_SUCCESS )
        ok(L"FirstLogon=%d\n", value);
    }

    /// string value
    {
        std::wstring sub_key(L"SYSTEM\\Software\\Microsoft");
        std::wstring reg_sz(L"BuildLab");
        std::wstring BuildLab;
        if ( pwn::reg::read_wstring(pwn::reg::hklm(), sub_key, reg_sz, BuildLab) == ERROR_SUCCESS )
            ok(L"BuildLab=%s\n", BuildLab.c_str());
    }

    /// binary value
    {
        std::wstring sub_key(L"SYSTEM\\RNG");
        std::wstring reg_sz(L"Seed");
        std::vector<BYTE> Seed;
        if ( pwn::reg::read_binary(pwn::reg::hklm(), sub_key, reg_sz, Seed) == ERROR_SUCCESS )
            pwn::utils::hexdump(Seed);
    }

Enumerate all processes

pwn::process::list()

#include <pwn++\pwn.h>

void wmain()
{
    for ( auto& p : pwn::process::list() )
    {
        std::wstring integrity;
        pwn::process::get_integrity_level(p.pid, integrity);
        ok(L"%d -> %s (i=%s)\n", p.pid, p.name.c_str(), integrity.c_str());
    }
}

Kernel stuff

Namespace: pwn::kernel

Enumerate driver modules

#include <pwn++\pwn.h>

void wmain()
{
    for ( auto& mod : pwn::kernel::modules() )
    {
        auto name = std::get<0>(mod);
        auto addr = std::get<1>(mod);
        ok(L"%s -> %p\n", name.c_str(), addr);
    }
}

Steal token shellcode

#include <pwn++\pwn.h>

void wmain()
{
    auto out = pwn::kernel::shellcode::steal_system_token();
    ok(L"compiled sc:\n");
    pwn::utils::hexdump(out);
    auto mem = pwn::process::mem::alloc(0x1000, L"rwx");
    ok(L"allocated %p\n", mem);
    pwn::process::mem::write(mem, out);
    ok(L"written sc at %p\n", mem);
    pwn::process::mem::free(mem);
}

Service

Namespace: pwn::windowsdows::service

Enumerate

#include <pwn++\pwn.h>

void wmain()
{
    for ( auto service : pwn::service::list() )
    {
        ok(L"Name='%s' Display='%s' Type=%d Status=%d\n",
            service.Name.c_str(),
            service.DisplayName.c_str(),
            service.Type,
            service.Status
        );
    }
}

ALPC

Namespace: pwn::windowsdows::alpc

Server

#include <pwn++\pwn.h>

void wmain()
{
    auto server = pwn::utils::GenericHandle(
        pwn::windowsdows::alpc::server::listen(L"\\RPC Control\\lotzofun")
    );

    if ( server )
    {
        ok(L"server created port (handle=%p)\n", server.Get());
        auto recv = pwn::windowsdows::alpc::send_and_receive(server.Get());
        // pwn::windowsdows::alpc::close(server); // not necessary because of RAII
    }
}

Client

#include <pwn++\pwn.h>

void wmain()
{
    auto client = pwn::utils::GenericHandle(
        pwn::windowsdows::alpc::client::connect(L"\\RPC Control\\lotzofun")
    );

    if ( client )
    {
        ok(L"client connected to epmapper (handle=%p)\n", client.Get());
        pwn::windowsdows::alpc::send_and_receive(client, { 0x41, 0x41, 0x41, 0x41 });
        // pwn::windowsdows::alpc::close(client); // not necessary because of RAII
    }
}

Crypto

Hash functions

#include <pwn++\pwn.h>
auto wmain() -> void
{
    std::vector<BYTE> data {0x41, 0x42, 0x43, 0x44} ;
    auto arr = pwn::crypto::sha256(data); // would work the same with `sha1`,`sha512`,`md5`,...
    std::vector<BYTE> vec (std::begin(arr), std::end(arr));
    pwn::utils::hexdump(m);
}
0000   E1 2E 11 5A CF 45 52 B2  56 8B 55 E9 3C BD 39 39  |  ...Z.ER.V.U.<.99
0010   4C 4E F8 1C 82 44 7F AF  C9 97 88 2A 02 D2 36 77  |  LN...D.....*..6w

File System

Create directories

Temporary

#include <pwn++\pwn.h>
auto wmain() -> int
{
    try
    {
        auto d = pwn::fs::make_tmpdir()
        ok(L"created temp dir %s\n", d);
    }
    catch(...){}
    return 0;
}

Recursively

#include <pwn++\pwn.h>

auto wmain() -> int
{
    if (!pwn::fs::mkdir(L"a\\crazy\\path\\that\\doesnt\\exist"))
        perror(L"fs::mkdir()");
    return 0;
}

Monitor directory

#include <pwn++\pwn.h>

auto wmain() -> int
{
    auto lambda_func = [](PFILE_NOTIFY_INFORMATION info)
    {
        switch(info->Action)
        {
        case FILE_ACTION_ADDED:              ok(L"FILE_ACTION_ADDED '%s'\n", info->FileName); break;
        case FILE_ACTION_REMOVED:            ok(L"FILE_ACTION_REMOVED '%s'\n", info->FileName); break;
        case FILE_ACTION_MODIFIED:           ok(L"FILE_ACTION_MODIFIED '%s'\n", info->FileName); break;
        case FILE_ACTION_RENAMED_OLD_NAME:   ok(L"FILE_ACTION_RENAMED_OLD_NAME '%s'\n", info->FileName); break;
        case FILE_ACTION_RENAMED_NEW_NAME:   ok(L"FILE_ACTION_RENAMED_NEW_NAME '%s'\n", info->FileName); break;
        default: return false;
        }
        return true;
    };

    if (!pwn::fs::watch_dir(L"c:\\windows\\system32", lambda_func))
        perror(L"watch_dir");

    return 0;
}

Create a symlink

#include <pwn++\pwn.h>

auto wmain() -> int
{
    {
        if(!pwn::utils::GenericHandle( pwn::fs::touch(L"myfile.txt") ))
            return -1;
    }

    auto l = pwn::utils::GenericHandle( pwn::fs::create_symlink(L"mylink.txt", L"myfile.txt") );
    if(!l)
        return -2;

    ok(L"created link '%s' -> '%s'\n", L"mylink.txt", L"myfile.txt");

    return 0;
}

Create a junction

#include <pwn++\pwn.h>

auto wmain() -> int
{
    // todo
    return 0;
}

Simple API import

using IMPORT_EXTERNAL_FUNCTION macro, then copy/paste the definition (from MSDN, ReactOS, Pinvoke, NirSoft, etc.)

#include <pwn++\pwn.h>

IMPORT_EXTERNAL_FUNCTION( \
    L"ntdll.dll", \
    ZwCreateEnclave, \
    NTSTATUS, \
    HANDLE  hProcess, \
    LPVOID  lpAddress, \
    ULONGLONG ZeroBits, \
    SIZE_T  dwSize, \
    SIZE_T  dwInitialCommitment, \
    DWORD   flEnclaveType, \
    LPCVOID lpEnclaveInformation, \
    DWORD   dwInfoLength, \
    LPDWORD lpEnclaveError \
);

void wmain()
{
    auto addr = 0x010000;
    ENCLAVE_CREATE_INFO_VBS enc = {0};
    auto res = ZwCreateEnclave(
        ::GetCurrentProcess(),
        &addr,
        -1,
        0x1000,
        0x2000,
        ENCLAVE_TYPE_VBS,
        &enc,
        sizeof(enc),
        nullptr
    );
    if(res == STATUS_SUCCESS)
      ok(L"enclave allocated\n");
}

Lua VM backdoor

Namespace: pwn::backdoor

The lib embeds a Lua VM (if compiled with the flag PWN_ENABLE_LUA_BACKDOOR) which allows to script your way into a remote process where the pwn++.dll is injected. On Windows it will use a Named Pipe (see tools/win32/Backdoor for a standalone example)

> .\Backdoor.exe
[DEBUG] {c:\temp\backdoor.cpp:645:wmain()} Starting as PID=15004
[...]
[DEBUG] {Z:\pwn++\src\pwn++\win32\backdoor.cpp:548:start()} Listening for connection on '\\.\pipe\WindowsBackupService_202004L_1932'
[DEBUG] {Z:\pwn++\src\pwn++\win32\backdoor.cpp:253:WaitNextConnectionAsync()} Waiting for connection

Now you can use any client to connect and interact with the Named Pipe

> .\NamedPipe.exe '\\.\pipe\WindowsBackupService_202004L_1932'
>>> return pwn.version()
>> Sent 20 bytes
<< Received 6 bytes
---
0.1.3
---
>>> return pwn.process.pid()
>> Sent 24 bytes
<< Received 6 bytes
---
15004
---
>>>

CTF stuff

Namespace: pwn::ctf

Description: Some pwntools goodies

#include <pwn++\pwn.h>

using namespace pwn::log;
namespace ctx = pwn::context;
namespace ctf = pwn::ctf;
namespace utils = pwn::utils;

void wmain()
{
    ctx::set_log_level(log_level_t::LOG_DEBUG);
    {
        auto io = ctf::Remote(L"target_vm", 1337);
        io.recvuntil(">>> ");
        io.sendline("print('hi python')");
        io.recvuntil(">>> ");

        io.interactive();
        ok(L"done\n");
    }

    utils::pause();
}

Then the Linux tool socat can be used to bind easily a Python REPL to the TCP/1337 of target_vm

$ socat TCP-L:1337,fork,reuseaddr EXEC:/usr/bin/python3.8,pty,stderr
[DEBUG]  log_level set to LOG_LEVEL_DEBUG (0)
[DEBUG]  connected to 192.168.57.64:1337
[DEBUG]  recv 46 bytes
0000   50 79 74 68 6F 6E 20 33  2E 38 2E 35 20 28 64 65  |  Python 3.8.5 (de
0010   66 61 75 6C 74 2C 20 4A  61 6E 20 32 37 20 32 30  |  fault, Jan 27 20
0020   32 31 2C 20 31 35 3A 34  31 3A 31 35 29 20        |  21, 15:41:15)
[DEBUG]  recv 100 bytes
0000   0D 0A 5B 47 43 43 20 39  2E 33 2E 30 5D 20 6F 6E  |  ..[GCC 9.3.0] on
0010   20 6C 69 6E 75 78 0D 0A  54 79 70 65 20 22 68 65  |   linux..Type "he
0020   6C 70 22 2C 20 22 63 6F  70 79 72 69 67 68 74 22  |  lp", "copyright"
0030   2C 20 22 63 72 65 64 69  74 73 22 20 6F 72 20 22  |  , "credits" or "
0040   6C 69 63 65 6E 73 65 22  20 66 6F 72 20 6D 6F 72  |  license" for mor
0050   65 20 69 6E 66 6F 72 6D  61 74 69 6F 6E 2E 0D 0A  |  e information...
0060   3E 3E 3E 20                                       |  >>>
[DEBUG]  sent 13 bytes
0000   70 72 69 6E 74 28 27 31  2B 31 27 29 0A           |  print('1+1').
[DEBUG]  recv 23 bytes
0000   70 72 69 6E 74 28 27 31  2B 31 27 29 0D 0A 31 2B  |  print('1+1')..1+
0010   31 0D 0A 3E 3E 3E 20                              |  1..>>>
[INFO]  Entering interactive mode...
>>> import sys
[DEBUG]  sent 11 bytes
0000   69 6D 70 6F 72 74 20 73  79 73 0A                 |  import sys.
>>> [DEBUG]  recv 16 bytes
0000   69 6D 70 6F 72 74 20 73  79 73 0D 0A 3E 3E 3E 20  |  import sys..>>>
[...]