#include "elevation.h" #include "access_token.h" #include "file_objects.h" #include "hash.h" #include "kernel_context.h" #include "text.h" #include #include #include #include #include #include #include namespace raven { namespace { constexpr uint16_t owner_defaulted = 0x1, group_defaulted = 0x2, dacl_present = 0x4, dacl_defaulted = 0x8, sacl_present = 0x10, sacl_defaulted = 0x20; constexpr size_t acl_header = 8; // Where the fields of an absolute security descriptor sit: the four pointers follow the revision and the control word, // so they start at 4 for a 32-bit program and at 8 for a 64-bit one. struct Layout { size_t size, owner, group, sacl, dacl; }; Layout layout_of(const ApiCall &call) { const size_t width = call.pointer_width(); const size_t first = width == 4 ? 4 : 8; return {first + 4 * width, first, first + width, first + 2 * width, first + 3 * width}; } uint32_t sid_length(ApiCall &call, uint64_t sid) { return 8 + 4 * uint32_t(call.read(sid + 1, 1)[0]); } bool valid_sid(ApiCall &call, uint64_t sid) { if (!sid) return false; try { const auto header = call.read(sid, 2); return header[0] == 1 && header[1] >= 1 && header[1] <= 15; } catch (const ApiFault &) { return false; } } // A SID from its string form (S-1-...) or from one of the two-letter names of the security descriptor language. std::optional> sid_from_text(std::u16string text) { static const std::map aliases{ {u"AN", u"S-1-5-7"}, {u"AO", u"S-1-5-32-548"}, {u"AU", u"S-1-5-11"}, {u"BA", u"S-1-5-32-544"}, {u"BG", u"S-1-5-32-546"}, {u"BO", u"S-1-5-32-551"}, {u"BU", u"S-1-5-32-545"}, {u"CG", u"S-1-3-1"}, {u"CO", u"S-1-3-0"}, {u"CY", u"S-1-5-32-569"}, {u"ED", u"S-1-5-9"}, {u"HI", u"S-1-16-12288"}, {u"IU", u"S-1-5-4"}, {u"LS", u"S-1-5-19"}, {u"LW", u"S-1-16-4096"}, {u"ME", u"S-1-16-8192"}, {u"MP", u"S-1-16-8448"}, {u"NO", u"S-1-5-32-556"}, {u"NS", u"S-1-5-20"}, {u"NU", u"S-1-5-2"}, {u"PO", u"S-1-5-32-550"}, {u"PS", u"S-1-5-10"}, {u"PU", u"S-1-5-32-547"}, {u"RC", u"S-1-5-12"}, {u"RD", u"S-1-5-32-555"}, {u"RE", u"S-1-5-32-552"}, {u"RU", u"S-1-5-32-554"}, {u"SI", u"S-1-16-16384"}, {u"SO", u"S-1-5-32-549"}, {u"SS", u"S-1-18-2"}, {u"SU", u"S-1-5-6"}, {u"SY", u"S-1-5-18"}, {u"WD", u"S-1-1-0"}, {u"WR", u"S-1-5-33"}, {u"OW", u"S-1-3-4"}, {u"UD", u"S-1-5-84-0-0-0-0-0"}, {u"AC", u"S-1-15-2-1"}, {u"S1", u"S-1-16-0"}, // The local administrator and guest accounts belong to the machine domain the user is part of. {u"LA", u"S-1-5-21-1000-1000-1000-500"}, {u"LG", u"S-1-5-21-1000-1000-1000-501"}}; if (const auto alias = aliases.find(text); alias != aliases.end()) text = alias->second; unsigned long long numbers[16]; size_t count = 0; if (text.size() < 5 || text[0] != u'S' || text[1] != u'-' || text[2] != u'1' || text[3] != u'-') return std::nullopt; // The authority and up to 15 subauthorities (SID_MAX_SUB_AUTHORITIES), as capability SIDs carry ten. size_t at = 4; while (at < text.size()) { if (count == 16) return std::nullopt; unsigned long long value = 0; size_t digits = 0; while (at < text.size() && text[at] >= u'0' && text[at] <= u'9' && digits < 16) { value = value * 10 + (text[at] - u'0'); ++at; ++digits; } if (!digits || (at < text.size() && text[at] != u'-') || value > (count ? 0xffffffffULL : 0xffffffffffffULL)) return std::nullopt; numbers[count++] = value; if (at < text.size() && ++at == text.size()) return std::nullopt; } if (count < 2) return std::nullopt; std::vector sid{1, uint8_t(count - 1), 0, 0, 0, 0, 0, 0}; for (int index = 0; index < 6; ++index) sid[size_t(2 + index)] = uint8_t(numbers[0] >> (8 * (5 - index))); for (size_t index = 1; index < count; ++index) for (int shift = 0; shift < 32; shift += 8) sid.push_back(uint8_t(numbers[index] >> shift)); return sid; } // The access rights of an ACE in the security descriptor language: a number, or a run of two-letter right names. std::optional rights_from_text(std::u16string_view text) { if (text.size() > 2 && text[0] == u'0' && (text[1] == u'x' || text[1] == u'X')) { uint32_t value = 0; for (const auto character : text.substr(2)) { const int digit = character >= u'0' && character <= u'9' ? character - u'0' : character >= u'a' && character <= u'f' ? character - u'a' + 10 : character >= u'A' && character <= u'F' ? character - u'A' + 10 : -1; if (digit < 0) return std::nullopt; value = value << 4 | uint32_t(digit); } return value; } static const std::map names{ {u"GA", 0x10000000}, {u"GR", 0x80000000}, {u"GW", 0x40000000}, {u"GX", 0x20000000}, {u"RC", 0x20000}, {u"SD", 0x10000}, {u"WD", 0x40000}, {u"WO", 0x80000}, {u"RP", 0x10}, {u"WP", 0x20}, {u"CC", 0x1}, {u"DC", 0x2}, {u"LC", 0x4}, {u"SW", 0x8}, {u"LO", 0x80}, {u"DT", 0x40}, {u"CR", 0x100}, {u"FA", 0x1f01ff}, {u"FR", 0x120089}, {u"FW", 0x120116}, {u"FX", 0x1200a0}, {u"KA", 0xf003f}, {u"KR", 0x20019}, {u"KW", 0x20006}, {u"KX", 0x20019}, {u"NR", 0x2}, {u"NW", 0x1}, {u"NX", 0x4}}; uint32_t value = 0; if (text.size() % 2) return std::nullopt; for (size_t at = 0; at < text.size(); at += 2) { const auto found = names.find(text.substr(at, 2)); if (found == names.end()) return std::nullopt; value |= found->second; } return value; } // An ACL from the part of a descriptor after "D:" or "S:": optional flags (P, AI, AR), then entries in parentheses. The control bits the // flags stand for are added to `control`, shifted for a SACL. std::optional> acl_from_text(std::u16string_view text, uint16_t &control, bool system) { size_t at = 0; while (at < text.size() && text[at] != u'(') { if (text.substr(at, 2) == u"AI") { control |= system ? 0x800 : 0x400; at += 2; } else if (text.substr(at, 2) == u"AR") { control |= system ? 0x200 : 0x100; at += 2; } else if (text[at] == u'P') { control |= system ? 0x2000 : 0x1000; ++at; } else return std::nullopt; } std::vector entries; uint16_t count = 0; while (at < text.size()) { if (text[at] != u'(') return std::nullopt; const auto close = text.find(u')', at); if (close == text.npos) return std::nullopt; std::vector fields; for (auto rest = text.substr(at + 1, close - at - 1);;) { const auto semicolon = rest.find(u';'); fields.push_back(rest.substr(0, semicolon)); if (semicolon == rest.npos) break; rest = rest.substr(semicolon + 1); } at = close + 1; if (fields.size() < 6 || !fields[3].empty() || !fields[4].empty()) return std::nullopt; static const std::map types{{u"A", 0}, {u"D", 1}, {u"AU", 2}, {u"AL", 3}, {u"ML", 0x11}}; static const std::map flag_names{{u"OI", 0x1}, {u"CI", 0x2}, {u"NP", 0x4}, {u"IO", 0x8}, {u"ID", 0x10}, {u"SA", 0x40}, {u"FA", 0x80}}; const auto type = types.find(fields[0]); if (type == types.end()) return std::nullopt; uint8_t flags = 0; if (fields[1].size() % 2) return std::nullopt; for (size_t index = 0; index < fields[1].size(); index += 2) { const auto flag = flag_names.find(fields[1].substr(index, 2)); if (flag == flag_names.end()) return std::nullopt; flags |= flag->second; } const auto mask = rights_from_text(fields[2]); const auto sid = sid_from_text(std::u16string(fields[5])); if (!mask || !sid) return std::nullopt; const uint16_t size = uint16_t(8 + sid->size()); entries.push_back(type->second); entries.push_back(flags); entries.push_back(uint8_t(size)); entries.push_back(uint8_t(size >> 8)); for (int shift = 0; shift < 32; shift += 8) entries.push_back(uint8_t(*mask >> shift)); entries.insert(entries.end(), sid->begin(), sid->end()); ++count; } const uint16_t size = uint16_t(8 + entries.size()); std::vector acl{2, 0, uint8_t(size), uint8_t(size >> 8), uint8_t(count), uint8_t(count >> 8), 0, 0}; acl.insert(acl.end(), entries.begin(), entries.end()); return acl; } // A self-relative security descriptor from its string form: owner (O:), group (G:), DACL (D:) and SACL (S:), in any order. std::optional> descriptor_from_text(std::u16string_view text) { std::vector owner, group, dacl, sacl; uint16_t control = 0x8000; bool has_dacl = false, has_sacl = false; size_t at = 0; while (at < text.size()) { while (at < text.size() && text[at] == u' ') ++at; if (at + 2 > text.size() || text[at + 1] != u':') return std::nullopt; const char16_t part = text[at]; at += 2; // A part runs to the next "X:" that starts another part; colons inside entries never follow a single letter at the top level. size_t end = at, depth = 0; while (end < text.size()) { if (text[end] == u'(') ++depth; else if (text[end] == u')') --depth; else if (!depth && end + 1 < text.size() && text[end + 1] == u':' && (text[end] == u'O' || text[end] == u'G' || text[end] == u'D' || text[end] == u'S')) break; ++end; } const auto value = text.substr(at, end - at); at = end; if (part == u'O' || part == u'G') { const auto sid = sid_from_text(std::u16string(value)); if (!sid) return std::nullopt; (part == u'O' ? owner : group) = *sid; } else if (part == u'D' || part == u'S') { const auto acl = acl_from_text(value, control, part == u'S'); if (!acl) return std::nullopt; if (part == u'D') { dacl = *acl; has_dacl = true; control |= 0x4; } else { sacl = *acl; has_sacl = true; control |= 0x10; } } else { return std::nullopt; } } std::vector result(20, 0); result[0] = 1; const auto place = [&](const std::vector &part, bool present, size_t field) { if (!present) return; const uint32_t offset = uint32_t(result.size()); std::memcpy(result.data() + field, &offset, 4); result.insert(result.end(), part.begin(), part.end()); }; place(owner, !owner.empty(), 4); place(group, !group.empty(), 8); place(sacl, has_sacl, 12); place(dacl, has_dacl, 16); std::memcpy(result.data() + 2, &control, 2); return result; } } void register_security_api(ApiRegistry ®istry, KernelContext &context) { auto &objects = context.objects; #ifdef RAVEN_OPENSSL register_schannel_api(registry, context); #else // Without a TLS library no SSPI provider is installed; callers can use the documented initialization failure path. registry.add("InitSecurityInterfaceA", 0, [](ApiCall &call) { call.fail(50); }); registry.add("InitSecurityInterfaceW", 0, [](ApiCall &call) { call.fail(50); }); #endif registry.add("InitializeSecurityDescriptor", 2, [](ApiCall &call) { if (!call[0]) { call.fail(87); return; } if (call.u32(1) != 1) { call.fail(1305); return; } std::vector descriptor(layout_of(call).size); descriptor[0] = 1; call.write(call[0], descriptor); call.ret(1); }); const auto control = [](ApiCall &call, uint16_t set, uint16_t clear) { auto value = call.load(call[0] + 2); value = uint16_t((value & ~clear) | set); call.store(call[0] + 2, value); }; registry.add("SetSecurityDescriptorOwner", 3, [control](ApiCall &call) { if (!call[0] || call.read(call[0], 1)[0] != 1) { call.fail(1338); return; } call.store_pointer(call[0] + layout_of(call).owner, call[1]); control(call, call.u32(2) ? owner_defaulted : 0, owner_defaulted); call.ret(1); }); registry.add("SetSecurityDescriptorGroup", 3, [control](ApiCall &call) { if (!call[0] || call.read(call[0], 1)[0] != 1) { call.fail(1338); return; } call.store_pointer(call[0] + layout_of(call).group, call[1]); control(call, call.u32(2) ? group_defaulted : 0, group_defaulted); call.ret(1); }); registry.add("SetSecurityDescriptorDacl", 4, [control](ApiCall &call) { if (!call[0] || call.read(call[0], 1)[0] != 1) { call.fail(1338); return; } const bool present = call.u32(1) != 0; call.store_pointer(call[0] + layout_of(call).dacl, present ? call[2] : 0); control(call, uint16_t((present ? dacl_present : 0) | (call.u32(3) ? dacl_defaulted : 0)), dacl_present | dacl_defaulted); call.ret(1); }); registry.add("SetSecurityDescriptorSacl", 4, [control](ApiCall &call) { if (!call[0] || call.read(call[0], 1)[0] != 1) { call.fail(1338); return; } const bool present = call.u32(1) != 0; call.store_pointer(call[0] + layout_of(call).sacl, present ? call[2] : 0); control(call, uint16_t((present ? sacl_present : 0) | (call.u32(3) ? sacl_defaulted : 0)), sacl_present | sacl_defaulted); call.ret(1); }); registry.add("IsValidSecurityDescriptor", 1, [](ApiCall &call) { if (!call[0]) { call.fail(87); return; } call.ret(call.read(call[0], 1)[0] == 1); }); // A SID with its authority and room for the count of parts asked for; the parts themselves are the caller's to fill. registry.add("InitializeSid", 3, [](ApiCall &call) { if (!call[0] || !call[1] || call.u32(2) > 15) { call.fail(87); return; } std::vector header{1, uint8_t(call.u32(2))}; const auto authority = call.read(call[1], 6); header.insert(header.end(), authority.begin(), authority.end()); call.write(call[0], header); call.ret(1); }); registry.add("GetLengthSid", 1, [](ApiCall &call) { call.ret(sid_length(call, call[0])); }); registry.add("IsValidSid", 1, [](ApiCall &call) { call.ret(valid_sid(call, call[0])); }); registry.add("EqualSid", 2, [](ApiCall &call) { if (!valid_sid(call, call[0]) || !valid_sid(call, call[1])) { call.fail(1337); return; } const auto length = sid_length(call, call[0]); call.ret(length == sid_length(call, call[1]) && call.read(call[0], length) == call.read(call[1], length)); }); registry.add("CopySid", 3, [](ApiCall &call) { if (!valid_sid(call, call[2])) { call.fail(1337); return; } const auto length = sid_length(call, call[2]); if (call.u32(0) < length) { call.fail(122); return; } call.write(call[1], call.read(call[2], length)); call.ret(1); }); // The ntdll forms of the same: lengths and checks as they are, equality as a boolean, copies with an NTSTATUS. registry.add("RtlLengthSid", 1, [](ApiCall &call) { call.ret(sid_length(call, call[0])); }); registry.add("RtlValidSid", 1, [](ApiCall &call) { call.ret(valid_sid(call, call[0])); }); registry.add("RtlEqualSid", 2, [](ApiCall &call) { const auto length = sid_length(call, call[0]); call.ret(valid_sid(call, call[0]) && valid_sid(call, call[1]) && length == sid_length(call, call[1]) && call.read(call[0], length) == call.read(call[1], length)); }); registry.add("RtlCopySid", 3, [](ApiCall &call) { const auto length = sid_length(call, call[2]); if (call.u32(0) < length) { call.ret(0xc0000023U); return; } call.write(call[1], call.read(call[2], length)); call.ret(0); }); // An access control list is a header followed by its entries: the size of the whole, and how many entries it holds. registry.add("InitializeAcl", 3, [](ApiCall &call) { const uint32_t length = call.u32(1); if (call.u32(2) < 2 || call.u32(2) > 4) { call.fail(87); return; } if (length < acl_header || length > 0xffff) { call.fail(122); return; } std::vector header(acl_header); header[0] = uint8_t(call.u32(2)); const uint16_t size = uint16_t(length); std::memcpy(header.data() + 2, &size, 2); call.write(call[0], header); call.ret(1); }); registry.add("AddMandatoryAce", 5, [](ApiCall &call) { if (call.u32(1) != 2 && call.u32(1) != 4) { call.fail(1306); return; } if ((call.u32(2) & ~0x1fU) || (call.u32(3) & ~7U)) { call.fail(87); return; } if (!valid_sid(call, call[4])) { call.fail(1337); return; } const auto sid = call.read(call[4], sid_length(call, call[4])); if (sid[1] != 1 || sid[2] || sid[3] || sid[4] || sid[5] || sid[6] || sid[7] != 16) { call.fail(1337); return; } const auto header = call.read(call[0], acl_header); uint16_t size, count; std::memcpy(&size, header.data() + 2, 2); std::memcpy(&count, header.data() + 4, 2); if (size < acl_header || (header[0] != 2 && header[0] != 4)) { call.fail(1336); return; } auto acl = call.read(call[0], size); size_t used = acl_header; for (uint32_t index = 0; index < count; ++index) { if (used + 4 > size) { call.fail(1336); return; } uint16_t length; std::memcpy(&length, acl.data() + used + 2, 2); if (length < 4 || length % 4 || used + length > size) { call.fail(1336); return; } used += length; } const auto length = uint16_t(8 + sid.size()); if (used + length > size || count == 0xffff) { call.fail(1344); return; } acl[used] = 0x11; acl[used + 1] = uint8_t(call.u32(2)); std::memcpy(acl.data() + used + 2, &length, 2); const auto mask = call.u32(3); std::memcpy(acl.data() + used + 4, &mask, 4); std::memcpy(acl.data() + used + 8, sid.data(), sid.size()); ++count; std::memcpy(acl.data() + 4, &count, 2); acl[0] = std::max(acl[0], uint8_t(call.u32(1))); call.write(call[0], acl); call.ret(1); }); const auto add_ace = [](ApiCall &call, uint8_t type, bool with_flags) { // The Ex forms carry the inheritance flags as a third argument, ahead of the mask and the SID. const size_t mask_argument = with_flags ? 3 : 2; const uint64_t sid_address = call[mask_argument + 1]; if (!valid_sid(call, sid_address)) { call.fail(1337); return; } const auto header = call.read(call[0], acl_header); uint16_t size, count; std::memcpy(&size, header.data() + 2, 2); std::memcpy(&count, header.data() + 4, 2); uint32_t used = acl_header; for (uint16_t index = 0; index < count; ++index) { const auto entry = call.read(call[0] + used, 4); uint16_t entry_size; std::memcpy(&entry_size, entry.data() + 2, 2); used += entry_size; } const auto length = sid_length(call, sid_address); const uint32_t entry_size = 8 + length; if (used + entry_size > size) { call.fail(122); return; } std::vector entry(entry_size); entry[0] = type; entry[1] = with_flags ? uint8_t(call.u32(2)) : 0; const uint16_t entry_size16 = uint16_t(entry_size); const uint32_t mask = call.u32(mask_argument); std::memcpy(entry.data() + 2, &entry_size16, 2); std::memcpy(entry.data() + 4, &mask, 4); const auto sid = call.read(sid_address, length); std::memcpy(entry.data() + 8, sid.data(), length); call.write(call[0] + used, entry); call.store(call[0] + 4, uint16_t(count + 1)); call.ret(1); }; // Token information: the account is one fixed user in session 1, an administrator under User Account Control: medium integrity and a // limited token, or high integrity and the full token when the process runs elevated. registry.add("GetTokenInformation", 5, [&objects, &context](ApiCall &call) { // The pseudo handles of the process token, the thread token and the thread's effective token name the program's own token. const int32_t pseudo = int32_t(uint32_t(call[0])); const auto impersonation = context.threads.current().impersonation; const auto token = pseudo == -4 ? context.process_token : pseudo == -5 ? impersonation : pseudo == -6 ? (impersonation ? impersonation : context.process_token) : objects.object_as(call[0]); if (!token) { call.fail(pseudo == -5 ? 1008 : 6); return; } if ((pseudo < -6 || pseudo > -4) && !(objects.access_of(call[0]) & 8)) { call.fail(5); return; } const auto value = token->data(); const size_t width = call.pointer_width(); const size_t record = width == 4 ? 8 : 16; std::vector data; // A structure that holds a SID: the pointer sits in the structure and points at the SID placed after it. const auto with_sid = [&](const std::vector &sid, uint32_t attributes, bool pair) { const size_t head = pair ? record : width; data.assign(head + sid.size(), 0); const uint64_t at = call[2] + head; std::memcpy(data.data(), &at, width); if (pair) std::memcpy(data.data() + width, &attributes, 4); std::memcpy(data.data() + head, sid.data(), sid.size()); }; switch (call.u32(1)) { case 1: with_sid(user_sid(), 0, true); break; case 3: { data.assign(4 + 12 * value.privileges.size(), 0); const auto count = uint32_t(value.privileges.size()); std::memcpy(data.data(), &count, 4); for (size_t index = 0; index < value.privileges.size(); ++index) { std::memcpy(data.data() + 4 + 12 * index, &value.privileges[index].luid, 8); std::memcpy(data.data() + 12 + 12 * index, &value.privileges[index].attributes, 4); } break; } case 4: with_sid(user_sid(), 0, false); break; case 5: with_sid(make_sid(5, {21, 1000, 1000, 1000, 513}), 0, false); break; case 6: { data.assign(width, 0); if (!value.default_dacl.empty()) { const uint64_t at = call[2] + width; std::memcpy(data.data(), &at, width); data.insert(data.end(), value.default_dacl.begin(), value.default_dacl.end()); } break; } case 2: case 28: { const bool logon = call.u32(1) == 28; // The logon SID identifies the same session for every process in this runtime. const auto session = make_sid(5, {5, 0, 1}); std::vector> sids{session}; // The groups of an interactive desktop logon: programs look for INTERACTIVE to tell a user's session from a service. if (!logon) { sids.insert(sids.begin(), {make_sid(5, {32, 545}), make_sid(5, {32, 544}), make_sid(1, {0})}); sids.insert(sids.end(), {make_sid(5, {4}), make_sid(2, {1}), make_sid(5, {11}), make_sid(5, {15}), make_sid(2, {0})}); } const size_t first = width == 4 ? 4 : 8; size_t total = first + record * sids.size(); for (const auto &item : sids) total += item.size(); data.assign(total, 0); const uint32_t count = uint32_t(sids.size()); std::memcpy(data.data(), &count, 4); size_t placed = first + record * sids.size(); for (size_t index = 0; index < sids.size(); ++index) { // Users and Everyone: mandatory, enabled by default and enabled. The account is an administrator under User Account // Control: Administrators is enabled (and an owner) in an elevated process and held for denying access only otherwise. const uint32_t attributes = logon || index == 3 ? 0xc0000007U : index == 1 ? (process_elevated() ? 0xf : 0x10) : 7; const uint64_t at = call[2] + placed; std::memcpy(data.data() + first + index * record, &at, width); std::memcpy(data.data() + first + index * record + width, &attributes, 4); std::memcpy(data.data() + placed, sids[index].data(), sids[index].size()); placed += sids[index].size(); } break; } case 8: data = {uint8_t(value.type), 0, 0, 0}; break; case 9: if (value.type != 2) { call.fail(87); return; } data = {uint8_t(value.impersonation_level), 0, 0, 0}; break; case 12: data = {1, 0, 0, 0}; break; // TokenElevationType: full for an elevated process, limited (the filtered token of an administrator) otherwise; TokenElevation. case 18: data = {process_elevated() ? uint8_t(2) : uint8_t(3), 0, 0, 0}; break; case 20: data = {process_elevated() ? uint8_t(1) : uint8_t(0), 0, 0, 0}; break; // TokenUIAccess and TokenIsAppContainer: no process here is an accessibility tool or runs in an app container. case 26: case 29: data = {0, 0, 0, 0}; break; case 25: with_sid(make_sid(16, {value.integrity}), 0x60, true); break; case 10: data.assign(56, 0); break; default: call.fail(87); return; } if (call[4]) call.store(call[4], uint32_t(data.size())); if (!call[2] || call.u32(3) < data.size()) { call.fail(122); return; } call.write(call[2], data); call.ret(1); }); // A generic right becomes the specific rights the mapping names for it. registry.add("MapGenericMask", 2, [](ApiCall &call) { if (!call[0] || !call[1]) { call.fail(87); return; } auto mask = call.load(call[0]); const auto mapping = call.read(call[1], 16); uint32_t specific[4]; std::memcpy(specific, mapping.data(), 16); if (mask & 0x80000000U) mask |= specific[0]; if (mask & 0x40000000U) mask |= specific[1]; if (mask & 0x20000000U) mask |= specific[2]; if (mask & 0x10000000U) mask |= specific[3]; mask &= ~0xf0000000U; call.store(call[0], mask); }); // Descriptors are not kept: every object answers with the same one, so setting one changes nothing that could be read back. registry.add("SetSecurityInfo", 7, [&objects](ApiCall &call) { call.ret(0); }); registry.add("SetNamedSecurityInfoW", 7, [](ApiCall &call) { call.ret(0); }); // The access a descriptor grants to the token's account: the entries of its list in order, allow entries adding rights and deny entries // refusing them, for the account, its group, the users of the machine and everyone. registry.add("AccessCheck", 8, [&objects](ApiCall &call) { if (!objects.object_as(call[1]) || !call[0] || !call[6] || !call[7]) { call.fail(call[0] && call[6] && call[7] ? 6 : 87); return; } uint32_t wanted = call.u32(2); const auto mapping = call.read(call[3], 16); uint32_t specific[4]; std::memcpy(specific, mapping.data(), 16); if (wanted & 0x80000000U) wanted |= specific[0]; if (wanted & 0x40000000U) wanted |= specific[1]; if (wanted & 0x20000000U) wanted |= specific[2]; if (wanted & 0x10000000U) wanted |= specific[3]; wanted &= ~0xf0000000U; const uint64_t descriptor = call[0]; const uint16_t control = call.load(descriptor + 2); uint64_t acl = 0; if (control & dacl_present) { if (control & 0x8000) { const auto offset = call.load(descriptor + 16); acl = offset ? descriptor + offset : 0; } else { acl = call.load_pointer(descriptor + layout_of(call).dacl); } } // A descriptor without a list gives everyone everything. uint32_t granted = 0, denied = 0; if (!(control & dacl_present) || !acl) { granted = wanted ? wanted : 0x1f01ffU; } else { const auto header = call.read(acl, acl_header); uint16_t count; std::memcpy(&count, header.data() + 4, 2); uint64_t at = acl + acl_header; std::vector> mine{user_sid(), make_sid(5, {21, 1000, 1000, 1000, 513}), make_sid(5, {32, 545}), make_sid(1, {0}), make_sid(5, {11}), make_sid(5, {4}), make_sid(5, {1})}; // Administrators grants access only to an elevated token; the filtered token holds it for denying. if (process_elevated()) mine.push_back(make_sid(5, {32, 544})); for (uint16_t index = 0; index < count; ++index) { const auto entry = call.read(at, 8); uint16_t size; std::memcpy(&size, entry.data() + 2, 2); uint32_t mask; std::memcpy(&mask, entry.data() + 4, 4); if (size < 12 || !valid_sid(call, at + 8)) { at += size ? size : 8; continue; } const auto sid = call.read(at + 8, sid_length(call, at + 8)); const bool applies = std::any_of(mine.begin(), mine.end(), [&](const auto &candidate) { return candidate == sid; }); if (applies && entry[0] == 0) granted |= mask & ~denied; else if (applies && entry[0] == 1) denied |= mask & ~granted; at += size; } } // MAXIMUM_ALLOWED asks for everything the descriptor grants. const bool maximum = wanted & 0x02000000U; const bool allowed = maximum ? granted != 0 : wanted && (granted & wanted) == wanted; call.store(call[6], maximum ? granted : allowed ? wanted : 0); call.store(call[7], allowed); if (!allowed) call.set_last_error(5); call.ret(1); }); // Copies own their DACL and integrity level; changing one leaves the source token unchanged. registry.add("DuplicateTokenEx", 6, [&objects](ApiCall &call) { if (!objects.object_as(call[0]) || !call[5]) { call.fail(call[5] ? 6 : 87); return; } if (!(objects.access_of(call[0]) & 2)) { call.fail(5); return; } const auto access = token_access(call.u32(1)); if (!access || call.u32(3) > 3 || (call.u32(4) != 1 && call.u32(4) != 2)) { call.fail(87); return; } auto token = std::make_shared(*objects.object_as(call[0])); token->set_type(call.u32(4)); token->set_impersonation_level(call.u32(3)); const auto made = objects.add_object(token); if (made.status) { call.fail(8); return; } objects.set_access(made.value, call.u32(1) ? *access : objects.access_of(call[0])); call.store_pointer(call[5], made.value); call.ret(1); }); registry.add("DuplicateToken", 3, [&objects](ApiCall &call) { if (!objects.object_as(call[0]) || !call[2]) { call.fail(call[2] ? 6 : 87); return; } if (!(objects.access_of(call[0]) & 2)) { call.fail(5); return; } if (call.u32(1) > 3) { call.fail(87); return; } auto token = std::make_shared(*objects.object_as(call[0])); token->set_type(2); token->set_impersonation_level(call.u32(1)); const auto made = objects.add_object(token); if (made.status) { call.fail(8); return; } objects.set_access(made.value, objects.access_of(call[0])); call.store_pointer(call[2], made.value); call.ret(1); }); registry.add("CreateRestrictedToken", 9, [&objects](ApiCall &call) { const auto source = objects.object_as(call[0]); if (!source || !call[8]) { call.fail(call[8] ? 6 : 87); return; } if (!(objects.access_of(call[0]) & 2)) { call.fail(5); return; } if (call.u32(1) & ~0xfU) { call.fail(87); return; } call.write(call[8], call.read(call[8], call.pointer_width())); std::vector removed; if (!(call.u32(1) & 1)) { const auto count = call.u32(4); if (count > 64 || (count && !call[5])) { call.fail(87); return; } const auto entries = call.read(call[5], size_t(count) * 12); for (uint32_t index = 0; index < count; ++index) { uint64_t luid; std::memcpy(&luid, entries.data() + 12 * index, 8); removed.push_back(luid); } } auto token = std::make_shared(*source); token->restrict_privileges(call.u32(1) & 1, removed); const auto made = objects.add_object(token); if (made.status) { call.fail(8); return; } objects.set_access(made.value, objects.access_of(call[0])); call.store_pointer(call[8], made.value); call.ret(1); }); registry.add("SetTokenInformation", 4, [&objects](ApiCall &call) { const auto token = objects.object_as(call[0]); if (!token) { call.fail(6); return; } if (!(objects.access_of(call[0]) & 0x80)) { call.fail(5); return; } if (!call[2] || call.u32(3) < call.pointer_width()) { call.fail(87); return; } const uint64_t at = call.load_pointer(call[2]); if (call.u32(1) == 6) { std::vector acl; if (at) { const auto size = call.load(at + 2), count = call.load(at + 4); const auto revision = call.load(at); if (size < acl_header || (revision != 2 && revision != 4)) { call.fail(1336); return; } acl = call.read(at, size); size_t used = acl_header; for (uint16_t index = 0; index < count; ++index) { uint16_t entry = 0; if (used + 4 <= acl.size()) std::memcpy(&entry, acl.data() + used + 2, 2); if (entry < 4 || used + entry > acl.size()) { call.fail(1336); return; } used += entry; } } token->set_default_dacl(acl); } else if (call.u32(1) == 25) { const auto authority = make_sid(16, {0}); if (call.u32(3) < (call.pointer_width() == 4 ? 8 : 16) || !valid_sid(call, at) || call.read(at, 8) != std::vector(authority.begin(), authority.begin() + 8)) { call.fail(1337); return; } token->set_integrity(call.load(at + 8)); } else { call.fail(87); return; } call.ret(1); }); // The SIDs that exist on every system, by their WELL_KNOWN_SID_TYPE; none for a type this system does not name without a domain. const auto well_known_sid = [](uint32_t type) { std::vector sid; switch (type) { case 0: sid = make_sid(0, {0}); break; case 1: sid = make_sid(1, {0}); break; case 2: sid = make_sid(2, {0}); break; case 3: sid = make_sid(3, {0}); break; case 4: sid = make_sid(3, {1}); break; case 5: sid = make_sid(3, {2}); break; case 6: sid = make_sid(3, {3}); break; case 8: sid = make_sid(5, {1}); break; case 9: sid = make_sid(5, {2}); break; case 10: sid = make_sid(5, {3}); break; case 11: sid = make_sid(5, {4}); break; case 12: sid = make_sid(5, {6}); break; case 13: sid = make_sid(5, {7}); break; case 14: sid = make_sid(5, {8}); break; case 15: sid = make_sid(5, {9}); break; case 16: sid = make_sid(5, {10}); break; case 17: sid = make_sid(5, {11}); break; case 18: sid = make_sid(5, {12}); break; case 19: sid = make_sid(5, {13}); break; case 20: sid = make_sid(5, {14}); break; case 22: sid = make_sid(5, {18}); break; case 23: sid = make_sid(5, {19}); break; case 24: sid = make_sid(5, {20}); break; case 25: sid = make_sid(5, {32}); break; case 26: sid = make_sid(5, {32, 544}); break; case 27: sid = make_sid(5, {32, 545}); break; case 28: sid = make_sid(5, {32, 546}); break; case 29: sid = make_sid(5, {32, 547}); break; case 30: sid = make_sid(5, {32, 548}); break; case 31: sid = make_sid(5, {32, 549}); break; case 32: sid = make_sid(5, {32, 550}); break; case 33: sid = make_sid(5, {32, 551}); break; case 34: sid = make_sid(5, {32, 552}); break; case 35: sid = make_sid(5, {32, 554}); break; case 36: sid = make_sid(5, {32, 555}); break; case 65: sid = make_sid(16, {0}); break; case 66: sid = make_sid(16, {4096}); break; case 67: sid = make_sid(16, {8192}); break; case 68: sid = make_sid(16, {12288}); break; case 69: sid = make_sid(16, {16384}); break; default: break; } return sid; }; registry.add("CreateWellKnownSid", 4, [well_known_sid](ApiCall &call) { const auto sid = well_known_sid(call.u32(0)); if (sid.empty()) { call.fail(87); return; } if (!call[3]) { call.fail(87); return; } const uint32_t capacity = call.load(call[3]); call.store(call[3], uint32_t(sid.size())); if (!call[2] || capacity < sid.size()) { call.fail(122); return; } call.write(call[2], sid); call.ret(1); }); registry.add("IsWellKnownSid", 2, [well_known_sid](ApiCall &call) { if (!valid_sid(call, call[0])) { call.ret(0); return; } const auto sid = well_known_sid(call.u32(1)); call.ret(!sid.empty() && sid_length(call, call[0]) == sid.size() && call.read(call[0], sid.size()) == sid); }); // A descriptor as one self-relative block that the program frees with LocalFree: the owner, group and ACL pointers point inside it. struct Entry { uint8_t flags; uint32_t mask; std::vector sid; }; const auto answer_descriptor = [&context](ApiCall &call, const std::vector &owner, const std::vector &group, const std::vector &entries, uint16_t inheritance) { const uint32_t wanted = call.u32(2); std::vector acl(acl_header); acl[0] = 2; for (const auto &entry : entries) { const uint16_t size = uint16_t(8 + entry.sid.size()); const size_t at = acl.size(); acl.resize(at + size); acl[at + 1] = entry.flags; std::memcpy(acl.data() + at + 2, &size, 2); std::memcpy(acl.data() + at + 4, &entry.mask, 4); std::memcpy(acl.data() + at + 8, entry.sid.data(), entry.sid.size()); } const uint16_t acl_size = uint16_t(acl.size()), count = uint16_t(entries.size()); std::memcpy(acl.data() + 2, &acl_size, 2); std::memcpy(acl.data() + 4, &count, 2); std::vector block(20); const auto place = [&](const std::vector &bytes) { const uint32_t at = uint32_t(block.size()); block.insert(block.end(), bytes.begin(), bytes.end()); while (block.size() % 4) block.push_back(0); return at; }; const uint32_t owner_at = place(owner), group_at = place(group), dacl_at = place(acl); const uint16_t control = 0x8000 | dacl_present | inheritance; block[0] = 1; std::memcpy(block.data() + 2, &control, 2); std::memcpy(block.data() + 4, &owner_at, 4); std::memcpy(block.data() + 8, &group_at, 4); std::memcpy(block.data() + 16, &dacl_at, 4); const auto memory = context.allocator.allocate(block.size(), true); if (!memory) { call.ret(8); return; } call.write(memory, block); if (call[3] && (wanted & 1)) call.store_pointer(call[3], memory + owner_at); if (call[4] && (wanted & 2)) call.store_pointer(call[4], memory + group_at); if (call[5] && (wanted & 4)) call.store_pointer(call[5], memory + dacl_at); if (call[6] && (wanted & 8)) call.store_pointer(call[6], 0); if (call[7]) call.store_pointer(call[7], memory); else context.allocator.release(memory); call.ret(0); }; // Objects other than files are owned by the account, and readable and usable by everyone. const auto security_info = [answer_descriptor](ApiCall &call) { answer_descriptor(call, user_sid(), make_sid(5, {21, 1000, 1000, 1000, 513}), {{0, 0x1f01ffU, make_sid(1, {0})}}, 0); }; // A file or directory carries the list Windows gives it. The drive's root, Windows and the Program Files folders belong to // TrustedInstaller; what an installer creates in them inherits their list and belongs to the administrators. Only those, SYSTEM and // TrustedInstaller may change them, the users may read and run them, and programs such as installers check that before they trust a // folder. Everywhere else the account owns what it has, with SYSTEM and the administrators. const auto file_security = [answer_descriptor](ApiCall &call, std::u16string path, bool directory) { if (path.starts_with(u"\\\\?\\")) path.erase(0, 4); for (auto &character : path) character = character == u'/' ? u'\\' : character >= u'a' && character <= u'z' ? char16_t(character - 32) : character; while (path.size() > 2 && path.back() == u'\\') path.pop_back(); const auto system = make_sid(5, {18}), administrators = make_sid(5, {32, 544}), users = make_sid(5, {32, 545}); const auto installer = make_sid(5, {80, 956008885, 3418522649U, 1831038044, 1853292631, 2271478464U}); const uint32_t full = 0x1f01ffU, read_run = 0x1200a9U, modify = 0x1301bfU; // Entries of a directory pass to what is made in it (objects and containers); an entry only for those does not apply to the folder. const uint8_t passed = directory ? 0x3 : 0, only_below = 0xb, inherited = 0x10; if (path.size() == 2 && path[1] == u':') { answer_descriptor(call, installer, system, {{passed, full, administrators}, {passed, full, system}, {passed, read_run, users}, {0, 0x4, make_sid(5, {11})}, {only_below, modify, make_sid(5, {11})}}, 0x1000); return; } std::u16string_view top; for (const std::u16string_view folder : {u"C:\\WINDOWS", u"C:\\PROGRAM FILES", u"C:\\PROGRAM FILES (X86)"}) if (path.starts_with(folder) && (path.size() == folder.size() || path[folder.size()] == u'\\')) top = folder; if (!top.empty()) { const bool own = path.size() == top.size(); const uint8_t from = own ? 0 : inherited; std::vector entries{{uint8_t(passed | from), full, installer}, {uint8_t(passed | from), full, system}, {uint8_t(passed | from), full, administrators}, {uint8_t(passed | from), read_run, users}, {uint8_t(passed | from), read_run, make_sid(15, {2, 1})}}; if (directory) entries.push_back({uint8_t(only_below | from), 0x10000000U, make_sid(3, {0})}); answer_descriptor(call, own ? installer : administrators, system, entries, own ? 0x1400 : 0x400); return; } answer_descriptor(call, user_sid(), make_sid(5, {21, 1000, 1000, 1000, 513}), {{uint8_t(passed | inherited), full, system}, {uint8_t(passed | inherited), full, administrators}, {uint8_t(passed | inherited), full, user_sid()}}, 0x400); }; // GetSecurityInfo of a file asks the handle's file. registry.add("GetSecurityInfo", 8, [&objects, security_info, file_security](ApiCall &call) { if (call.u32(1) == 1) if (const auto file = objects.object_as(call[0])) { file_security(call, file->dos_path, file->directory); return; } security_info(call); }); // A file or directory that is not there has no descriptor to give. registry.add("GetNamedSecurityInfoW", 8, [&context, security_info, file_security](ApiCall &call) { if (call.u32(1) == 1) { const auto name = call.wide(call[0], 32768); const auto resolved = context.paths.resolve(name); if (!resolved || !resolved->exists) { call.ret(resolved && !resolved->parent_exists ? 3 : 2); return; } std::error_code error; file_security(call, context.paths.full(name), std::filesystem::is_directory(resolved->host, error)); return; } security_info(call); }); // The descriptor of a kernel object, self-relative: the user owns what the process made, with the user's domain group, and a DACL that is // present but null leaves the object open to everyone, which is how the runtime checks access to its objects. const auto object_descriptor = [](uint32_t wanted) { const auto sid = [](uint32_t last) { std::vector bytes{1, 5, 0, 0, 0, 0, 0, 5}; for (const uint32_t value : {21u, 1000u, 1000u, 1000u, last}) for (int shift = 0; shift < 32; shift += 8) bytes.push_back(uint8_t(value >> shift)); return bytes; }; std::vector descriptor(20, 0); descriptor[0] = 1; uint16_t control = 0x8000; const auto attach = [&](size_t offset_at, const std::vector &part) { const uint32_t offset = uint32_t(descriptor.size()); std::memcpy(descriptor.data() + offset_at, &offset, 4); descriptor.insert(descriptor.end(), part.begin(), part.end()); }; if (wanted & 1) attach(4, sid(1000)); if (wanted & 2) attach(8, sid(513)); if (wanted & 4) control |= 0x4; std::memcpy(descriptor.data() + 2, &control, 2); return descriptor; }; registry.add("NtQuerySecurityObject", 5, [&objects, object_descriptor](ApiCall &call) { if (call[0] != UINT64_MAX && call[0] != UINT64_MAX - 1 && !objects.object(call[0])) { call.ret(0xc0000008U); return; } const auto descriptor = object_descriptor(call.u32(1)); if (call[4]) call.store(call[4], uint32_t(descriptor.size())); if (call.u32(3) < descriptor.size()) { call.ret(0xc0000023U); return; } call.write(call[2], descriptor); call.ret(0); }); // A registry key answers the same descriptor as the runtime's objects; the size needed comes back with ERROR_INSUFFICIENT_BUFFER. registry.add("RegGetKeySecurity", 4, [object_descriptor](ApiCall &call) { if (!call[0] || !call[3]) { call.ret(!call[0] ? 6 : 87); return; } const auto descriptor = object_descriptor(call.u32(1)); const uint32_t capacity = call.load(call[3]); call.store(call[3], uint32_t(descriptor.size())); if (capacity < descriptor.size() || !call[2]) { call.ret(122); return; } call.write(call[2], descriptor); call.ret(0); }); // Objects keep no descriptor of their own, so a new one is accepted for any object that exists. registry.add("NtSetSecurityObject", 3, [&objects](ApiCall &call) { const bool known = call[0] == UINT64_MAX || call[0] == UINT64_MAX - 1 || objects.object(call[0]); call.ret(!known ? 0xc0000008U : !call[2] ? 0xc000000dU : 0); }); // No job objects exist by name here, so opening one finds nothing. registry.add("NtOpenJobObject", 3, [](ApiCall &call) { call.ret(0xc0000034U); }); // Reading a descriptor: the control bits say whether its parts are offsets from its start (self-relative) or pointers. const auto part = [](ApiCall &call, uint64_t descriptor, size_t index) -> uint64_t { const uint16_t control = call.load(descriptor + 2); if (control & 0x8000) { const uint32_t offset = call.load(descriptor + 4 + index * 4); return offset ? descriptor + offset : 0; } const size_t width = call.pointer_width(), first = width == 4 ? 4 : 8; return call.load_pointer(descriptor + first + index * width); }; registry.add("GetSecurityDescriptorControl", 3, [](ApiCall &call) { if (!call[0]) { call.fail(87); return; } if (call[1]) call.store(call[1], call.load(call[0] + 2)); if (call[2]) call.store(call[2], call.read(call[0], 1)[0]); call.ret(1); }); // Only the inheritance bits may be changed this way: automatic inheritance asked for or done, protection, and the resource manager bits. registry.add("SetSecurityDescriptorControl", 3, [](ApiCall &call) { const uint16_t interest = uint16_t(call.u32(1)), bits = uint16_t(call.u32(2)), allowed = 0x7f00; if (!call[0] || (interest & ~allowed) || (bits & ~allowed)) { call.fail(87); return; } const uint16_t control = call.load(call[0] + 2); call.store(call[0] + 2, uint16_t((control & ~interest) | (bits & interest))); call.ret(1); }); const auto owner_or_group = [part](ApiCall &call, size_t index, uint16_t defaulted) { if (!call[0] || !call[1] || !call[2]) { call.fail(87); return; } call.store_pointer(call[1], part(call, call[0], index)); call.store(call[2], (call.load(call[0] + 2) & defaulted) != 0); call.ret(1); }; registry.add("GetSecurityDescriptorOwner", 3, [owner_or_group](ApiCall &call) { owner_or_group(call, 0, owner_defaulted); }); registry.add("GetSecurityDescriptorGroup", 3, [owner_or_group](ApiCall &call) { owner_or_group(call, 1, group_defaulted); }); const auto acl_part = [part](ApiCall &call, size_t index, uint16_t present, uint16_t defaulted) { if (!call[0] || !call[1] || !call[2] || !call[3]) { call.fail(87); return; } const uint16_t control = call.load(call[0] + 2); const bool has = (control & present) != 0; call.store(call[1], has); call.store_pointer(call[2], has ? part(call, call[0], index) : 0); call.store(call[3], (control & defaulted) != 0); call.ret(1); }; registry.add("GetSecurityDescriptorSacl", 4, [acl_part](ApiCall &call) { acl_part(call, 2, sacl_present, sacl_defaulted); }); registry.add("GetSecurityDescriptorDacl", 4, [acl_part](ApiCall &call) { acl_part(call, 3, dacl_present, dacl_defaulted); }); registry.add("GetSecurityDescriptorLength", 1, [part](ApiCall &call) { // A self-relative descriptor ends where its last part ends; an absolute one counts its header and every part it points to, as // much as the self-relative copy of it needs. const auto control = call.load(call[0] + 2); if (!(control & 0x8000)) { uint64_t total = layout_of(call).size; for (size_t index = 0; index < 4; ++index) if (const auto at = part(call, call[0], index)) total += index < 2 ? 8 + 4 * uint64_t(call.read(at + 1, 1)[0]) : call.load(at + 2); call.ret(total); return; } uint64_t end = 20; for (size_t index = 0; index < 4; ++index) { const auto at = part(call, call[0], index); if (!at) continue; const uint64_t offset = at - call[0]; uint64_t size = 0; if (index < 2) size = 8 + 4 * uint64_t(call.read(at + 1, 1)[0]); else size = call.load(at + 2); end = std::max(end, offset + size); } call.ret(end); }); // The descriptor packed into one block: a 20-byte header whose four offsets lead to the owner, the group, the SACL and the DACL // that follow it, in that order. registry.add("MakeSelfRelativeSD", 3, [part](ApiCall &call) { if (!call[0] || !call[2]) { call.fail(87); return; } const auto control = call.load(call[0] + 2); if (control & 0x8000) { call.fail(1361); return; } std::vector block(20); block[0] = 1; const uint16_t packed = uint16_t(control | 0x8000); std::memcpy(block.data() + 2, &packed, 2); for (size_t index = 0; index < 4; ++index) { const auto at = part(call, call[0], index); if (!at) continue; const uint32_t size = index < 2 ? 8 + 4 * uint32_t(call.read(at + 1, 1)[0]) : call.load(at + 2); const uint32_t offset = uint32_t(block.size()); std::memcpy(block.data() + 4 + 4 * index, &offset, 4); const auto bytes = call.read(at, size); block.insert(block.end(), bytes.begin(), bytes.end()); } const uint32_t capacity = call.load(call[2]); call.store(call[2], uint32_t(block.size())); if (!call[1] || capacity < block.size()) { call.fail(122); return; } call.write(call[1], block); call.ret(1); }); registry.add("IsValidAcl", 1, [](ApiCall &call) { if (!call[0]) { call.ret(0); return; } const auto header = call.read(call[0], 8); call.ret(header[0] >= 2 && header[0] <= 4); }); registry.add("GetAclInformation", 4, [](ApiCall &call) { if (!call[0] || !call[1]) { call.fail(87); return; } const auto header = call.read(call[0], 8); uint16_t size, count; std::memcpy(&size, header.data() + 2, 2); std::memcpy(&count, header.data() + 4, 2); if (call.u32(3) == 1 && call.u32(2) >= 4) { call.store(call[1], header[0]); } else if (call.u32(3) == 2 && call.u32(2) >= 12) { uint32_t used = 8; for (uint16_t index = 0; index < count; ++index) { uint16_t entry; std::memcpy(&entry, call.read(call[0] + used + 2, 2).data(), 2); used += entry; } call.store(call[1], count); call.store(call[1] + 4, used); call.store(call[1] + 8, size > used ? size - used : 0); } else { call.fail(24); return; } call.ret(1); }); registry.add("GetAce", 3, [](ApiCall &call) { if (!call[0] || !call[2]) { call.fail(87); return; } const auto header = call.read(call[0], 8); uint16_t count; std::memcpy(&count, header.data() + 4, 2); if (call.u32(1) >= count) { call.fail(87); return; } uint32_t at = 8; for (uint32_t index = 0; index < call.u32(1); ++index) { uint16_t entry; std::memcpy(&entry, call.read(call[0] + at + 2, 2).data(), 2); at += entry; } call.store_pointer(call[2], call[0] + at); call.ret(1); }); // The entries after the one removed move down over it; the list holds one entry less. registry.add("DeleteAce", 2, [](ApiCall &call) { if (!call[0]) { call.fail(87); return; } const auto header = call.read(call[0], 8); uint16_t size, count; std::memcpy(&size, header.data() + 2, 2); std::memcpy(&count, header.data() + 4, 2); if (call.u32(1) >= count) { call.fail(87); return; } uint32_t at = 8, removed = 0, end = 8; for (uint32_t index = 0; index < count; ++index) { uint16_t entry; std::memcpy(&entry, call.read(call[0] + end + 2, 2).data(), 2); if (index == call.u32(1)) { at = end; removed = entry; } end += entry; } if (end > size) { call.fail(1336); return; } const auto rest = call.read(call[0] + at + removed, end - at - removed); call.write(call[0] + at, rest); call.store(call[0] + 4, uint16_t(count - 1)); call.ret(1); }); registry.add("GetSidSubAuthorityCount", 1, [](ApiCall &call) { call.ret(call[0] + 1); }); registry.add("GetSidIdentifierAuthority", 1, [](ApiCall &call) { call.ret(call[0] + 2); }); registry.add("GetSidSubAuthority", 2, [](ApiCall &call) { call.ret(call[0] + 8 + 4 * uint64_t(call.u32(1))); }); registry.add("GetSidLengthRequired", 1, [](ApiCall &call) { call.ret(8 + 4 * uint32_t(uint8_t(call.u32(0)))); }); // S-R-I-S-S...: the string form of a SID, in memory the program frees with LocalFree. for (const bool wide : {true, false}) registry.add(wide ? "ConvertSidToStringSidW" : "ConvertSidToStringSidA", 2, [&context, wide](ApiCall &call) { if (!valid_sid(call, call[0]) || !call[1]) { call.fail(87); return; } const auto sid = call.read(call[0], sid_length(call, call[0])); uint64_t authority = 0; for (int index = 2; index < 8; ++index) authority = authority << 8 | sid[size_t(index)]; std::string text = "S-1-" + std::to_string(authority); for (size_t index = 0; index < sid[1]; ++index) { uint32_t value; std::memcpy(&value, sid.data() + 8 + index * 4, 4); text += "-" + std::to_string(value); } const size_t unit = wide ? 2 : 1; const auto memory = context.allocator.allocate((text.size() + 1) * unit, true); if (!memory) { call.fail(8); return; } std::vector bytes((text.size() + 1) * unit); for (size_t index = 0; index < text.size(); ++index) bytes[index * unit] = uint8_t(text[index]); call.write(memory, bytes); call.store_pointer(call[1], memory); call.ret(1); }); // The string form of a security descriptor to a self-relative one in memory the program frees with LocalFree. for (const bool wide : {true, false}) registry.add(wide ? "ConvertStringSecurityDescriptorToSecurityDescriptorW" : "ConvertStringSecurityDescriptorToSecurityDescriptorA", 4, [&context, wide](ApiCall &call) { if (!call[0] || !call[2]) { call.fail(87); return; } if (call.u32(1) != 1) { call.fail(1305); return; } const auto text = wide ? call.wide(call[0], 65536) : from_utf8(call.narrow(call[0], 65536)); const auto descriptor = descriptor_from_text(text); if (!descriptor) { call.fail(1336); return; } const auto memory = context.allocator.allocate(descriptor->size(), true); if (!memory) { call.fail(8); return; } call.write(memory, *descriptor); call.store_pointer(call[2], memory); if (call[3]) call.store(call[3], uint32_t(descriptor->size())); call.ret(1); }); // A security descriptor, self-relative or absolute, in its string form: the parts the information flags ask for (owner 1, group 2, // DACL 4, SACL 8), with the two-letter names Windows writes for well-known accounts and rights. for (const bool wide : {true, false}) registry.add(wide ? "ConvertSecurityDescriptorToStringSecurityDescriptorW" : "ConvertSecurityDescriptorToStringSecurityDescriptorA", 5, [&context, wide](ApiCall &call) { if (!call[0] || !call[3]) { call.fail(87); return; } if (call.u32(1) != 1) { call.fail(1305); return; } const uint64_t descriptor = call[0]; const auto header = call.read(descriptor, 4); const uint16_t control = uint16_t(header[2] | header[3] << 8); const bool relative = control & 0x8000; const size_t width = call.pointer_width(); const auto part = [&](size_t index) -> uint64_t { if (relative) { const uint32_t offset = call.load(descriptor + 4 + index * 4); return offset ? descriptor + offset : 0; } return call.load_pointer(descriptor + (width == 8 ? 8 : 4) + index * width); }; static const std::map names{ {"S-1-5-7", "AN"}, {"S-1-5-32-548", "AO"}, {"S-1-5-11", "AU"}, {"S-1-5-32-544", "BA"}, {"S-1-5-32-546", "BG"}, {"S-1-5-32-551", "BO"}, {"S-1-5-32-545", "BU"}, {"S-1-3-1", "CG"}, {"S-1-3-0", "CO"}, {"S-1-5-9", "ED"}, {"S-1-16-12288", "HI"}, {"S-1-5-4", "IU"}, {"S-1-5-19", "LS"}, {"S-1-16-4096", "LW"}, {"S-1-16-8192", "ME"}, {"S-1-5-32-556", "NO"}, {"S-1-5-20", "NS"}, {"S-1-5-2", "NU"}, {"S-1-5-32-550", "PO"}, {"S-1-5-10", "PS"}, {"S-1-5-32-547", "PU"}, {"S-1-5-12", "RC"}, {"S-1-5-32-555", "RD"}, {"S-1-5-32-552", "RE"}, {"S-1-5-32-554", "RU"}, {"S-1-16-16384", "SI"}, {"S-1-5-32-549", "SO"}, {"S-1-5-6", "SU"}, {"S-1-5-18", "SY"}, {"S-1-1-0", "WD"}, {"S-1-5-33", "WR"}, {"S-1-3-4", "OW"}, {"S-1-15-2-1", "AC"}, {"S-1-5-21-1000-1000-1000-500", "LA"}, {"S-1-5-21-1000-1000-1000-501", "LG"}}; const auto sid_text = [&](uint64_t sid) { const auto bytes = call.read(sid, sid_length(call, sid)); uint64_t authority = 0; for (int index = 2; index < 8; ++index) authority = authority << 8 | bytes[size_t(index)]; std::string text = "S-1-" + std::to_string(authority); for (size_t index = 0; index < bytes[1]; ++index) { uint32_t value; std::memcpy(&value, bytes.data() + 8 + index * 4, 4); text += "-" + std::to_string(value); } const auto name = names.find(text); return name == names.end() ? text : name->second; }; static const std::pair rights[] = {{0x1f01ff, "FA"}, {0x120089, "FR"}, {0x120116, "FW"}, {0x1200a0, "FX"}, {0xf003f, "KA"}, {0x20019, "KR"}, {0x20006, "KW"}, {0x10000000, "GA"}, {0x80000000, "GR"}, {0x40000000, "GW"}, {0x20000000, "GX"}}; const auto acl_text = [&](uint64_t acl, uint16_t protect, uint16_t requested, uint16_t inherited) { std::string text; if (control & protect) text += "P"; if (control & requested) text += "AR"; if (control & inherited) text += "AI"; if (!acl) return text + "NO_ACCESS_CONTROL"; const uint16_t count = call.load(acl + 4); uint64_t entry = acl + 8; for (uint16_t index = 0; index < count; ++index) { const auto head = call.read(entry, 8); const uint16_t size = uint16_t(head[2] | head[3] << 8); uint32_t mask; std::memcpy(&mask, head.data() + 4, 4); static const std::map types{{0, "A"}, {1, "D"}, {2, "AU"}, {3, "AL"}, {0x11, "ML"}}; const auto type = types.find(head[0]); text += "("; text += type == types.end() ? "A" : type->second; text += ";"; static const std::pair flags[] = {{0x1, "OI"}, {0x2, "CI"}, {0x4, "NP"}, {0x8, "IO"}, {0x10, "ID"}, {0x40, "SA"}, {0x80, "FA"}}; for (const auto &[bit, flag] : flags) if (head[1] & bit) text += flag; text += ";"; const auto right = std::find_if(std::begin(rights), std::end(rights), [&](const auto &item) { return item.first == mask; }); if (right != std::end(rights)) text += right->second; else { char hex[16]; std::snprintf(hex, sizeof hex, "0x%x", mask); text += hex; } text += ";;;" + sid_text(entry + 8) + ")"; if (size < 8) break; entry += size; } return text; }; const uint32_t information = call.u32(2); std::string text; if ((information & 1) && part(0)) text += "O:" + sid_text(part(0)); if ((information & 2) && part(1)) text += "G:" + sid_text(part(1)); if ((information & 4) && (control & 0x4)) text += "D:" + acl_text(part(3), 0x1000, 0x100, 0x400); if ((information & 8) && (control & 0x10)) text += "S:" + acl_text(part(2), 0x2000, 0x200, 0x800); const size_t unit = wide ? 2 : 1; const auto memory = context.allocator.allocate((text.size() + 1) * unit, true); if (!memory) { call.fail(8); return; } std::vector bytes((text.size() + 1) * unit, 0); for (size_t index = 0; index < text.size(); ++index) bytes[index * unit] = uint8_t(text[index]); call.write(memory, bytes); call.store_pointer(call[3], memory); if (call[4]) call.store(call[4], uint32_t(text.size() + 1)); call.ret(1); }); // RtlEncryptMemory and RtlDecryptMemory (SystemFunction040 and 041) keep a buffer unreadable to other processes while it sits in // memory; a process of the prefix is not visible to the others, so the bytes stay as they are and both directions succeed. registry.add("SystemFunction040", 3, [](ApiCall &call) { call.ret(0); }); registry.add("SystemFunction041", 3, [](ApiCall &call) { call.ret(0); }); // An AppContainer's SID is S-1-15-2 and the SHA-256 of its lowercased name split in seven words. const auto container_sid = [&context](ApiCall &call, uint64_t name, uint64_t out) { auto text = call.wide(name, 64); if (text.empty()) { call.ret(0x80070057); return; } for (auto &character : text) if (character >= u'A' && character <= u'Z') character = char16_t(character + 32); const auto digest = make_digest(0x800c); digest->update({reinterpret_cast(text.data()), text.size() * sizeof(char16_t)}); const auto hash = digest->finish(); std::vector sid(8 + 8 * 4); sid[0] = 1; sid[1] = 8; sid[7] = 15; sid[8] = 2; std::memcpy(sid.data() + 12, hash.data(), 28); const auto block = context.allocator.allocate(sid.size(), false); if (!block) { call.ret(0x8007000e); return; } call.write(block, sid); call.store_pointer(out, block); call.ret(0); }; registry.add("DeriveAppContainerSidFromAppContainerName", 2, [container_sid](ApiCall &call) { container_sid(call, call[0], call[1]); }); // Containers keep no profile of their own here: creating one only names its SID, and deleting one has nothing to remove. registry.add("CreateAppContainerProfile", 6, [container_sid](ApiCall &call) { container_sid(call, call[0], call[5]); }); registry.add("DeleteAppContainerProfile", 1, [](ApiCall &call) { call.ret(0); }); registry.add("ConvertStringSidToSidW", 2, [&context](ApiCall &call) { const auto parsed = sid_from_text(call.wide(call[0], 256)); if (!parsed) { call.fail(1337); return; } const auto &sid = *parsed; const auto memory = context.allocator.allocate(sid.size(), true); if (!memory || !call[1]) { call.fail(memory ? 87 : 8); return; } call.write(memory, sid); call.store_pointer(call[1], memory); call.ret(1); }); // The trustee names programs give by name: the account itself and the well-known groups. const auto named_sid = [](std::u16string name) -> std::vector { for (auto &c : name) if (c >= u'a' && c <= u'z') c = char16_t(c - 32); if (name.starts_with(u"BUILTIN\\") || name.starts_with(u"NT AUTHORITY\\")) name = name.substr(name.find(u'\\') + 1); if (name == u"EVERYONE") return make_sid(1, {0}); if (name == u"CURRENT_USER") return user_sid(); if (name == u"USERS") return make_sid(5, {32, 545}); if (name == u"ADMINISTRATORS") return make_sid(5, {32, 544}); if (name == u"SYSTEM") return make_sid(5, {18}); return {}; }; // BuildSecurityDescriptorW: a self-relative descriptor in memory LocalFree releases. Its DACL is the null one, which grants every // access; the entries a program lists are not turned into access rules, which no other user of this machine could test anyway. registry.add("BuildSecurityDescriptorW", 9, [&context](ApiCall &call) { if (!call[7] || !call[8]) { call.ret(87); return; } std::vector descriptor(20, 0); descriptor[0] = 1; const uint16_t control = 0x8000 | 0x4; std::memcpy(descriptor.data() + 2, &control, 2); const auto memory = context.allocator.allocate(descriptor.size(), true); if (!memory) { call.ret(8); return; } call.write(memory, descriptor); call.store(call[7], uint32_t(descriptor.size())); call.store_pointer(call[8], memory); call.ret(0); }); // EXPLICIT_ACCESS_W with a trustee given by name: the permissions, the mode, the inheritance, then the TRUSTEE_W. registry.add("BuildExplicitAccessWithNameW", 5, [](ApiCall &call) { if (!call[0]) return; const size_t width = call.pointer_width(), trustee_at = width == 4 ? 12 : 16; std::vector entry(trustee_at + (width == 4 ? 20 : 32)); const uint32_t fields[3] = {call.u32(2), call.u32(3), call.u32(4)}; std::memcpy(entry.data(), fields, 12); const uint32_t form = 1; std::memcpy(entry.data() + trustee_at + (width == 4 ? 8 : 12), &form, 4); const uint64_t name = call[1]; std::memcpy(entry.data() + trustee_at + (width == 4 ? 16 : 24), &name, width); call.write(call[0], entry); }); // Building an ACL from explicit entries: each entry grants or denies a mask to a trustee named by its SID. registry.add("BuildTrusteeWithSidW", 2, [](ApiCall &call) { if (!call[0]) return; const size_t width = call.pointer_width(); std::vector trustee(width == 4 ? 20 : 32); const uint64_t sid = call[1]; std::memcpy(trustee.data() + (width == 4 ? 16 : 24), &sid, width); call.write(call[0], trustee); }); registry.add("SetEntriesInAclW", 4, [&context, named_sid](ApiCall &call) { const uint32_t count = call.u32(0); if (!call[3] || (count && !call[1])) { call.ret(87); return; } const size_t width = call.pointer_width(), trustee_size = width == 4 ? 20 : 32, entry_size = 12 + (width == 4 ? 0 : 4) + trustee_size; const size_t trustee_at = 12 + (width == 4 ? 0 : 4); std::vector acl(8); acl[0] = 2; uint16_t entries = 0; // An existing list keeps its entries first. if (call[2]) { const auto header = call.read(call[2], 8); uint16_t old_count, capacity; std::memcpy(&capacity, header.data() + 2, 2); std::memcpy(&old_count, header.data() + 4, 2); if (capacity < acl_header || (header[0] != 2 && header[0] != 4)) { call.ret(1336); return; } const auto old = call.read(call[2], capacity); uint32_t used = 8; for (uint16_t index = 0; index < old_count; ++index) { uint16_t size = 0; if (used + 4 <= old.size()) std::memcpy(&size, old.data() + used + 2, 2); if (size < 4 || used + size > old.size()) { call.ret(1336); return; } used += size; } acl.insert(acl.end(), old.begin() + acl_header, old.begin() + used); entries = old_count; } for (uint32_t index = 0; index < count; ++index) { const uint64_t at = call[1] + uint64_t(index) * entry_size; const uint32_t permissions = call.load(at), mode = call.load(at + 4); const uint64_t name = call.load_pointer(at + trustee_at + (width == 4 ? 16 : 24)); const uint32_t form = call.load(at + trustee_at + (width == 4 ? 8 : 12)); std::vector sid; if (form == 0 && valid_sid(call, name)) sid = call.read(name, sid_length(call, name)); else if (form == 1 && (sid = named_sid(call.wide(name, 64))).size()) ; else { call.ret(1332); return; } if (mode < 1 || mode > 4) { call.ret(87); return; } if (mode == 4) { for (size_t offset = acl_header; offset + 4 <= acl.size();) { uint16_t size; std::memcpy(&size, acl.data() + offset + 2, 2); if (size < 4 || offset + size > acl.size()) { call.ret(1336); return; } if (acl[offset] <= 1 && size == 8 + sid.size() && std::equal(sid.begin(), sid.end(), acl.begin() + offset + 8)) { acl.erase(acl.begin() + offset, acl.begin() + offset + size); --entries; } else { offset += size; } } continue; } const uint16_t size = uint16_t(8 + sid.size()); std::vector entry(size); entry[0] = mode == 3 ? 1 : 0; entry[1] = uint8_t(call.load(at + 8)); std::memcpy(entry.data() + 2, &size, 2); std::memcpy(entry.data() + 4, &permissions, 4); std::memcpy(entry.data() + 8, sid.data(), sid.size()); acl.insert(acl.end(), entry.begin(), entry.end()); ++entries; } if (acl.size() > UINT16_MAX) { call.ret(1344); return; } const uint16_t total = uint16_t(acl.size()); std::memcpy(acl.data() + 2, &total, 2); std::memcpy(acl.data() + 4, &entries, 2); const auto memory = context.allocator.allocate(acl.size(), true); if (!memory) { call.ret(8); return; } call.write(memory, acl); call.store_pointer(call[3], memory); call.ret(0); }); // AddAce: ready-made entries go into the list at a position, or at its end for MAXDWORD, and the entries after them move up. registry.add("AddAce", 5, [](ApiCall &call) { if (!call[0] || !call[3]) { call.fail(87); return; } const auto header = call.read(call[0], acl_header); uint16_t size, count; std::memcpy(&size, header.data() + 2, 2); std::memcpy(&count, header.data() + 4, 2); std::vector offsets; uint32_t used = acl_header; for (uint16_t index = 0; index < count; ++index) { offsets.push_back(used); uint16_t entry_size; std::memcpy(&entry_size, call.read(call[0] + used + 2, 2).data(), 2); used += entry_size; } const auto added = call.read(call[3], call.u32(4)); uint16_t added_count = 0; for (size_t at = 0; at + 4 <= added.size();) { uint16_t entry_size; std::memcpy(&entry_size, added.data() + at + 2, 2); if (entry_size < 4 || at + entry_size > added.size()) { call.fail(87); return; } at += entry_size; ++added_count; } if (used + added.size() > size) { call.fail(1344); return; } const uint32_t index = call.u32(2); const uint32_t at = index >= count ? used : offsets[index]; auto moved = call.read(call[0] + at, used - at); std::vector tail(added.begin(), added.end()); tail.insert(tail.end(), moved.begin(), moved.end()); call.write(call[0] + at, tail); call.store(call[0] + 4, uint16_t(count + added_count)); call.ret(1); }); registry.add("AddAccessAllowedAce", 4, [add_ace](ApiCall &call) { add_ace(call, 0, false); }); registry.add("AddAccessDeniedAce", 4, [add_ace](ApiCall &call) { add_ace(call, 1, false); }); registry.add("AddAccessAllowedAceEx", 5, [add_ace](ApiCall &call) { add_ace(call, 0, true); }); registry.add("AddAccessDeniedAceEx", 5, [add_ace](ApiCall &call) { add_ace(call, 1, true); }); } }