We're sorry but this page doesn't work properly without JavaScript enabled. Please enable it to continue.
Feedback

Adversary Village - Designing a C2 Framework

00:00

Formal Metadata

Title
Adversary Village - Designing a C2 Framework
Title of Series
Number of Parts
84
Author
License
CC Attribution 3.0 Unported:
You are free to use, adapt and copy, distribute and transmit the work or content in adapted or unchanged form for any legal purpose as long as the work is attributed to the author in the manner specified by the author or licensor.
Identifiers
Publisher
Release Date
Language

Content Metadata

Subject Area
Genre
Abstract
Over recent years, there has been a huge boom in open-source C2 frameworks hitting the information security space. So much so they made a website and a logo - that’s how you know things are serious! Such a trend naturally drives more people towards taking on the gauntlet but all too often it becomes an insurmountable challenge and another dashed dream of the aspiring red teamer, or veteran alike. Believe me when I say - I’ve been there. I’ve felt the pain, the frustration, the imposter syndrome. Heck, I still do. However, I’ve (mostly) come out the other side with some hard learned lessons. Those lessons are the subject of this talk. The goal is not to write or provide code. We shall discuss how to approach initial design ideas; decide what is important and what is not; anticipate and deal with potential problem areas; consider different use cases and perspectives; and more. If you are interested in building your own C2 framework, contributing to existing frameworks, or even software development in general, there’s something in this talk for you.
Internet service providerSoftware frameworkComponent-based software engineeringControl flowInversion (music)Default (computer science)Core dumpWeightApplication service providerLatent heatOperator (mathematics)Server (computing)Data storage deviceCodeConnectivity (graph theory)Software frameworkOperator (mathematics)Default (computer science)Different (Kate Ryan album)Server (computing)CodeCore dumpWeightGame controllerLatent heatData storage deviceOpen sourceSoftware design patternStructural load.NET FrameworkArithmetic meanQuicksortResultantMappingoutputAssembly languageException handlingPrimitive (album)Term (mathematics)WritingDirectory serviceCASE <Informatik>String (computer science)Multiplication signMassAbstractionTask (computing)Focus (optics)Social classFormal languageComputing platformProper map1 (number)Boom (sailing)Web 2.0Error messageCross-platformImplementationPoint (geometry)Decision theoryProjective planeReflection (mathematics)Element (mathematics)Type theoryTime zoneClient (computing)DataflowElectronic program guideInformationEndliche ModelltheorieMathematicsPlanningSoftware bugExtension (kinesiology)Interface (computing)SoftwareArithmetic progressionDesign by contractCategory of beingAttribute grammarChainTemplate (C++)Field (computer science)BuildingBlock (periodic table)Function (mathematics)Data managementAuthorizationEntire functionView (database)Online helpOffice suiteMatrix (mathematics)Computer programmingCommunications protocolProcess (computing)Complete metric spaceProduct (business)Slide ruleNumeral (linguistics)Module (mathematics)Exploit (computer security)Video gameDescriptive statisticsMultiplicationService (economics)Computer configurationLibrary (computing)Computer wormComputer fileAlpha (investment)Software testingFrequencyFunctional (mathematics)FamilyExpandierender GraphRevision controlMereologyCheat <Computerspiel>GodAreaMaxima and minimaTouch typingTwitterInformation securityCognitionVariety (linguistics)Electronic mailing listDirect numerical simulationDrop (liquid)System administratorInversion (music)Remote procedure callImmersion (album)Axiom of choicePublic domainFront and back endsPower (physics)Gastropod shellSocket-SchnittstelleGoodness of fitStatement (computer science)Parameter (computer programming)BitGraphical user interfaceKey (cryptography)Interactive televisionPattern languageParallel portDecision tree learningRight angleGoogolOcean currentSet (mathematics)Scheduling (computing)Computer-assisted translationComputer animation
Transcript: English(auto-generated)
Hello, everyone. I hope you're all having a wonderful village. Welcome to my talk, designing a C2 framework. So my name is Daniel Duggan, otherwise known as Rastamouse.
I'm the director of Zero Point Security. You may have seen our red team ops course. I blog over at rastamouse.me as well as on offensive defense. I'm on Twitter, GitHub, Discord, Slack, all the things.
If you want to get in touch with me after the talk, by all means, do so. So what inspired this talk? Well, it feels like to me, prior to around 2018,
there weren't really that many C2s or C2 frameworks available. The main commercial offering was probably Coldbox Drive, maybe a few others. And there weren't that many open source frameworks either. We had PowerShell Empire for a long time.
We have PoshT2 for a long time. And then Covenant came along and some others came along. And suddenly we just had this huge boom in these C2 tools coming out.
We've also had a lot more commercial ones as well. So it seems to be an area of interest for sure. And it's not infrequent that I get approached and people are asking me if I've got any tips on how to build C2 specifically in C sharp.
So I thought that this kind of talk would be helpful for those who were looking to take on the process of trying to put such a tool together.
Now, if you go over to the C2 matrix, it's a curated list of commercial and open source frameworks. It now lists over 70, which is pretty astounding really.
More than 10 languages, everything from Python, Go, Rust, C-sharp, Ruby, even PowerShell. There's no shortage of variety. And they all have different capabilities.
So some by default will beacon over HTTP. Some will go over DNS. Some will go over completely custom channels and some might ride on legitimate services like the drop boxes, Office 365,
different Google services and things like that. If you're interested in learning more about some of the frameworks or some of the C2 tools already out there, I highly suggest you check out the matrix. It has a really useful search tool, I guess, where you plug in your requirements
and it will recommend some tools for you based on those requirements. So let's take a step back and talk about C2. What is C2? Well, C2 is short for command and control.
You can imagine a scenario where you have us cowboy operators and we have a target and the operator will deliver some sort of implant or payload, sometimes also called a rat, to that target.
And the operator needs to maintain some control over that implant somehow. The implant needs to talk to the operator. The operator needs to be able to give it commands and the implant needs to give the results back to the operator.
And the model that is used most, I guess, is to have some sort of intermediary control server, often called a team server.
So the implant will communicate to the team server over some sort of protocol. Again, that might be HTTP or DNS or some other legit service. And the operator will have some sort of admin interface
to that control server. So the implant will talk to the control server and it will kind of appear to the operator and the operator will be able to give it tasks. The implant will grab those tasks from the server, execute them and send the results back.
It's worth noting that some of these servers have the admin interface kind of built into them, while others require the operators to have
a standalone client that will connect to that server for them. So conceptually not too complicated. However, the point of this talk is about designing a C2 framework, not just designing C2.
And we need to understand specifically for the purposes of this talk, that C2 is not the same as a framework. So if you go onto the matrix again, for example, you'll see a lot of C2 tools that don't really readily provide
that much flexibility to the operator. Maybe a lot of those tools were designed in mind with, I want to demonstrate being able to use C2 over Office 365, for example. And that's pretty much all it's capable of.
And to me, that's not really a framework. And this is all about frameworks. So what does a framework provide? Well, they're quite clearly listed here. So let's go through them. The first is inversion of control.
Now the overall flow of the program, or the programs that are involved in this whole process, they're not strictly controlled by the user. So the flow is that the implant is gonna talk to a server and the operator is gonna task that implant.
You've got that back and forth. Now that is a flow that's not controlled by the user. You also have on the server side and on the implant side, a lot of internal flows. So the implant will receive a job, it'll process it in some way
and then send the results back. That internal flow is not controlled by the operator. And a lot of flows internal to the team server are not necessarily gonna be controlled by the user.
A framework also provides default behaviors, but most importantly, those are behaviors that can be overridden by the operator. So again, we're talking about the protocol that the implant is talking over or the protocol that the team server is listening on.
A framework may provide a default protocol for that such as HTTP, but the operator should be allowed to override that in some way, either change the behaviors within that protocol or add their own complete custom protocols.
A framework also provides extensibility and that is to introduce new behaviors and capabilities that are not currently within the toolset. So if you think about implants,
you might write your implant with a couple of commands, but it needs to be able to be customizable for the operator. They need to be able to add their own commands if they want, their own capabilities, their own post exploitation capabilities. And they also need to be able to do that on the server side as well.
So if you have something like you want reverse port forwarding on your implant, you need to be able to introduce that capability to the implant. The implant needs to send that data to somewhere, probably the team server. The team server needs to relay that traffic
to wherever it needs to go, and then it needs to send that traffic back. So both sides of that process need to be extensible by the operator to accommodate that. A framework also provides reusable components, which I think is self-explanatory,
but they're components that the framework provides to make the operator's life easier. We'll see an example of that on the next slide. So this is an example I've taken from the Metasploit framework.
Metasploit is a very mature product at this point, and it's great to look at if we're looking for inspiration on those framework-y things. But if you're not familiar with Metasploit, anybody can write a module for it. Anybody can write a numeration module
or an exploit module for that framework for other people to use. And being a framework, it provides a lot of helpers for you in writing those modules. So this example is taken from the psexec module.
And the first thing you do in a module is to define some module information. So this includes a name, a description, an author or multiple authors, references, and so on and so forth.
And that information from the module is picked up by the rest of the framework so that as the operator, whilst you're using the UI, and you search for a module, you can search it by name or whatever. And then it comes up and you can see the name, you can see its description and a bunch of other things.
You can also register options. So this being a psexec module, the author here has said, well, you can define options for the service name, paths, and a bunch of other things
that are important to that module. But more importantly, there are some options that you don't have to explicitly define in the module. So the framework knows that this is like a remote exploit module or whatever.
It knows that it needs the rhost or your targets. And the operator or the author of the module doesn't need to specifically put rhost in as an option. The framework already knows that that's required. So that takes that burden off the operator or the author.
And you also have includes and helpers, which are down here. So these includes are other Metasploit modules that you can bring into your module. And because this module is using psexec and SMB,
there's already a module for that. So you don't have to actually implement an entire SMB library in your module or even the psexec process in your module. It's bringing in PowerShell and Xs because you need to execute something. So as the module author, you don't even have to worry about the payload
that you're gonna send, the framework does it for you. And you can see here that the service file name so this is service file name here is an option. If you haven't defined this option, it takes the defaults.
So these are the default behaviors. So if you haven't provided a name, it will pick, it'll just make a random one for you. And this rand text alpha is another module in the framework.
So you don't have to worry about, oh, okay, well, I want a random string now I have to write a function for that. It's in the framework already. And those are the biggest strengths of frameworks is that as the module author, they allow you just to focus on the task that you want and not worry about things
that you don't want to worry about. Okay, so where to start? Well, this is like, it seems pretty cliche, but the first thing you should really understand is what are your motivations?
What does success look like? And that kind of sounds like we're at some sort of management retreat. But you kind of really need to think about what you're actually trying to achieve because you need to build it and you need to know what it's gonna look like at the end.
So you might be doing this just for fun, you might be doing it just to teach yourself some stuff, you might want it to teach other people, you might be writing an internal tool if you're like a pen tester or a red teamer, you might even wanna sell it, you might wanna open source it.
And if I had to draw like some sort of parallel, I think about if you were gonna build a car, it's very easy to say, I'm gonna build a car, but there are a lot of different types of car, right? If you want something to take your family to the beach,
you probably don't want a McLaren P1. And likewise, if you wanna go around the Nurburgring pretty quick, you probably don't want, I don't know, some sort of absurd people carrier. So even though they're both cars, they are quite different and they have different features
to make that goal a reality. And there are all sorts of things that you could think, oh, that would be really cool to have in my framework, that would be, but if it doesn't contribute
towards what you're actually trying to achieve, then it's kind of pointless. And if you miss features that you need, then you're not gonna achieve your goal and you're gonna end up with something that you didn't want.
So if you've never seen Moscow, this is a pretty good way to try and narrow down what you think you want. So this stands for must have, could have, sorry, must have, should have, could have and won't have. So your must haves are like mandatory things, right?
These are things that your framework absolutely has to have to perform its function. Should haves are important and they add significant value, but they are not strictly mandatory to function.
Could haves are nice to haves, but not really important. And won't haves are least critical, inappropriate or undesirable. And like the won't haves, you can split into kind of two camps, I guess.
You can have won't haves like period and won't haves this time. And I'll sort of expand on this time in a minute. So what you really wanna do is you wanna get your goal straight and think about all the things
that you think you might want in your tool. You can also take inspiration from other frameworks as well and tooling. That's perfectly okay. I don't think that's cheating because you kind of want the best parts of everything. And why reinvent the wheel sometimes?
You can take good ideas from all sorts of places and that's perfectly fine in my book. And then what you want to do is narrow everything down to an attainable first release using that Moscow method.
And by attainable, I mean attainable within your current skillset and your time budgets. And I think a mistake that a lot of people make is to look at things that have been out there for a long time. So you look at Empire, you look at Mesquite framework and you look at Covenant, Posh C2
and all of those well-established projects. And they think that's what I wanna build. I'm going to build like my version of that framework. But the thing is that those projects, they didn't get there overnight. They didn't just pop out of nowhere at the quality that they are now. Some of them are months or years old.
So if you're trying to replicate that straight off the bat, you've probably got easily six or 12 months worth of work. And what you're gonna do is you're gonna work on it for, I don't know, a couple of months or however long you can stand. You're gonna get fed up with it.
You're gonna get demotivated and then you're just gonna put it to the side. You'll come back to it, maybe at some point and you'll look at it. You'll look at it and you'll think, well, I've only done about 20% of what I hoped to achieve. And you're probably not gonna pick it up again. And it's gonna end up in the software graveyard.
So small iterative releases are easily more achievable and far more likely to keep you motivated. And you're also gonna grow your project with your skillset.
I mean, you look at a lot of features in a lot of advanced frameworks now and I mean, they're pretty like advanced concepts. So if you're doing this to teach yourself, coding, it's probably not realistic to shoot for those kinds of features or whatever.
So having a small project that's 100% complete is a lot better and it's a lot more satisfying than a lot of projects that's 10 or 20% complete.
So when I say attainable first release, it's gonna be like probably the bare minimum of what will make a C2 framework function. No bells or whistles, but it's something to aim for, right? So you're gonna set yourself a schedule.
So you're gonna say, I'm gonna target my first release, my initial release in one month or whatever you think is realistic. So you've got your Moscow, you've got your must haves and your should haves, and you're probably gonna prioritize those the most. So you could haves, you're probably not gonna really worry about too much
because you just want something that's gonna function, that's gonna work. And you need to be cognizant of scope creep. So it's really easy to just think, I'll just add this, I'll just add this. People post things on like Twitter all the time,
or just, did you know you could do this or new techniques, blah, blah, blah, blah. And it's really easy just to try and add those in when really what you wanna do is just stick to what you had planned for this release. And if you see cool stuff coming into the public domain,
you can say, well, okay, well, I'm not gonna do it now. Maybe I'll do it in the next release. And for God's sake pace yourself. So two hours a day for a month is not the same as 12 hours on four Fridays in a month. And maybe this is just a personal thing for me,
but little and often is it's just a more enjoyable way to code than having to sit at your desk for however many hours because you feel like you need to get something out.
So let's talk about languages for a sec. There are, you know, a billion different languages out there. And you're gonna have to decide on something for your server and your implant. And so depending on your model, you might consider writing a client as well
for the operators, but I'm just gonna sort of focus on the server and the implant. For the server side, I think you're much better off building off a framework that already exists, some sort of web framework.
So you have Python ones like Flask and Django, lots of different ones in C Sharp. You have Blazor, which provides, you know, like a nice web UI. You can have something that's driven more by APIs or RPC.
And you have lots of those like Vue, React and Angular. And on the implant side, you kind of have to think about maybe, well, I mean, this goes back to your goals, really. I mean, what platform you really want to target with your framework.
I mean, you can make your framework implants agnostic. A lot of them already do that, like Mystic. You can write your own implants for a framework. And that would be, you know, really great if your framework did that. But you probably also want to include an implant just to make it easier for people to pick up and use.
And so you have OS specific languages like C Sharp to target the .NET framework. And you have Swift on a Mac OS. You have languages that cross compile like Nim and Go and Rust. And then you have actual proper cross-platform languages
like .NET, .NET Core and sort of Python. But you certainly need to sort of consider how these elements are gonna talk to each other
and which language facilitates that best. So, you know, I've already said that the control server should provide a means of communicating with your implant over any protocol, any means you want. So you have to consider,
is the framework that I'm choosing gonna be able to facilitate that? Can I do that in Python? Can I do that in, you know, whatever? And then you kind of consider, well, if my chosen language doesn't really do that, do I need to consider a different language?
And if you don't really know that language, is it worth learning or is it worth sticking to what you know and just trying to make do the best you can? I think that's a decision only like you can make based on your goals and your priorities.
If you're learning, if you're doing this as a learning process, then maybe it's worth it. Maybe you specifically want to use this project as an opportunity to learn, you know, C-sharp for example, I mean, that's a pretty popular choice.
Then yeah, by all means step out of your comfort zone. And that kind of goes back to the attainable first release thing is that if you're learning something new, you've got to like start small with it.
So in terms of design patterns, there are two that I really go for. The first is this command design pattern. And I think this is pretty good for picturing the flow of stuff between the different components.
And by components, I mean like separate components such as the server and the implant in the operator, not really internal to any one of those. So we have the operator, which is kind of like the clients and the operator wants to send a task to an implant.
So you're probably gonna send some information, maybe it's over an API, you're gonna post some information or post this data to your team server, which is kind of like the director in this design pattern nomenclature.
So in this example, I'm simply, I've got a simple task model that has a command and some arguments, and I want to task an implant that we're going to identify by a GUID.
The server then takes that task and sends it to the implant. So that's kind of the command and then the implant, which is the receiver, is gonna execute that task. Now I've highlighted a task GUID here,
because it's in this model that the server deals with, but it's not in the model that the operator sends. So the server, if I want my server to track like task progress with the implant,
it's gonna need something. So my server, excuse me, in this example, adds a GUID to all of the tasks. And then when the implant can, when the implant talks back to the server, it can report, it can use the same GUID. So that the server can track commands with the implant.
And this kind of brings me on to the subject of contracts. So you have contracts between the different elements within the overall solution. So you have contracts between the operator and the server,
the server and the implant, and maybe even the server and any storage that you want. So if you're storing data, so you can start and stop your team server without losing data, then what you really need is different models
for each of those contracts. Don't try and use the same model between every element, because you're just gonna get a little bit unstuck. So here's a code example of a task request. So this is what an operator might post to the server. This is a request.
So it's got a command, an array of arguments, and an artifact. So if this was like an execute assembly command, the artifact would be like the whole assembly. So you're gonna push like, I don't know, you're gonna push Rubeus down to your implant,
and you're gonna tell it to execute with these arguments. The server will then add on that task GUID and give me back that GUID. So as the operator, I can then use that GUID to check the status of that task.
And when I'm asking for that, I don't necessarily, I mean, you might want to, it's kind of up to you. You might not want all of this original data back. Like you don't, you might not, I don't need to see like the whole assembly coming back. I don't need to have Rubeus just going backwards and forwards on the wire between me and the server. All I really want maybe is just the result
and the status. And you're gonna find that, you know, if you're using like something like entity framework for storage, if you're using C sharp, you have to decorate your classes
with all sorts of things. So you have to have like attributes on your properties that define the like primary keys and stuff. And that information just doesn't need to be coming back to the operator. It doesn't need to be going to the implant. So with every point, you have to sort of like translate each model
to a different model and then pass it along the chain. There's also this template method pattern, and this is pretty good for planning
you know, plan more carefully planning your code, what's your actual code gonna look like. And I'm sorry that all of my code examples are in C sharp mainly because that's all I really know, but also that's what people are kind of more interested in I think. So on the left, I've got an abstract class.
It's gonna act as like a building block to build custom listeners on my server. I'm gonna have a protected field at the top, which is an ITaskManager. And that is an interface that has these methods on it.
So QTask is the method that an operator would use the task and agent to take in like a GUID. And I've just put a byte array, but this would be the implant task model,
and then get tasks and receive output will be used on like the listener side so that when an implant is talking to the listener, it can just use this task manager just to grab the tasks
and any tasks that are queued for it. And also give the server any output that the implant is sending. And then the listener, it just has this init or initialize method to bring in that task manager and then just a start and a stop.
So let's have a look at what that could look like in C sharp. So this is my abstract class. You can see I've got the ITaskManager here and this init just brings in the task manager and just assigns it to that field. And then it has two abstract methods, start and stop.
And when somebody comes along and implements their own custom listener, they inherit from this listener class. And this is the entire class here. It's obviously not very functional.
It's just an example, but you can see I've highlighted where it would use the task manager. And as the author of the custom listener, you don't have to worry about where the task manager is coming from or how it works.
Because you've implemented this abstract class, the framework is taking care of that for you. And all you have to do is just the task manager is there as a field for you to use as appropriate. So to get tasks, you just call get tasks
and to send any output into the server to sort of like process, you just call receive output. So abstraction and interfaces, they're just so useful.
And in terms of any implant that you're gonna write, base primitives are better in my view than I call it commands proliferation. And you can see some CT tools just have like a bazillion,
you type like help whatever and just get a bazillion commands that you can execute. And like me, I find it a bit overwhelming because I don't know, it's just a lot, but it's also not that flexible for the operator.
So let's use Mimikatz as an example here, you could build in a command to your framework that will automatically push Mimikatz down to the implant, load it up in some way, execute it and send the results back.
You could do that for Seatbelt and for Rubeus. And it's very, okay, it's very nice just to have an automated way just to push all of these assemblies down. But then as soon as the user or the operator says, well, I wanna push down something custom,
you've not made it very easy to do that. So the base primitives are more about what allows these commands to actually happen. So Mimikatz is tied to manual mapping in C sharp anyway.
So instead of providing like a Mimikatz command, you could just provide some sort of means of just manual mapping in your implant. And that will allow the user just to send down an arbitrary DLL or executable arbitrary commands,
just map it into the implant, execute it and send the results back. You can also expose reflective DLLs, .NET reflection, PowerShell, sockets, whatever you want. But the closer or the more easily you expose
these base primitives to your operator, the more easily they can write custom commands for your implant. And in terms of commands, this is something that I see quite often, not even, not just in like C2 tools,
but all sorts of tools that bring in some sort of user input. This, the command is a string and they're just, well, this is a switch statement, but it could easily be like, if else, if else, if else.
If the command equals this, do this. And it's just not good. It's not very flexible, obviously. It's difficult to expand and maintain. If you want to add another one in here, it's gonna be like massive.
And if you've got a more complicated method than these, it's just, just don't. It's difficult to handle exceptions. You could wrap this whole thing in like a try catch, in which case you're only catching like maybe like generic exceptions,
or you could wrap each one of these in a try catch. And that just makes it even bigger. You can see we've got code duplication. So we're requesting this, like get current directory a whole bunch of times, which isn't really a problem for something so short, but if it's something more complicated,
then that just becomes untenable. It's not particularly performant because most of the time I think, especially if it's an if, else, if, else, if, it always starts from the top and you gotta go da, da, da, da, da, da, da, da, da. We're also forcing everything to be a string.
And it's just, you know, it's just ugly. This is not a good way to code something. So what's a better example perhaps? Is again, to implement those abstracts.
So I've got an abstract class here, which is called implant command. It has a string, which is called command. I probably should have called it name or something, but this is like the name of the command. And it's got an abstract method called execute.
It'll bring in the task that's been sent down to it. And it has another init, which brings in a class in this case, in case it's called implant. But implant has several public methods. You can see over on this right-hand side here
for sending results back, sending an error back and all sorts of things. So the implant class again, is not something that the operator will care about, but you can expose public methods on it for them to do useful things.
So like the listener example, we want to create our own command. It will inherit from implant command. We give it a name. In this case, it's just LS. I've also thrown like sharpsploit in here as an example, as an example of like decoupling
a lot of like the backend execution from the actual command. And that means that as a user, if I want to implement another command that uses like sharpsploit, they can do that really easily. Again, we're just making it as flexible as we can.
And of course, abstracts, you are forced to implement. So you have to put some sort of implementation in when you override this method. And you can, as the author, you can just put whatever you want in there.
And again, you don't have to worry about the task. It automatically is brought into the command for you. You don't have to worry about where implant is coming from. It's automatically done for you. You just call the methods that you want. And then you can, this is within that implant class
is that at some point we call load commands and we can use reflection to automatically instantiate every type of implant command and initialize them so they're ready to be used.
And then that handle task method where in the previous example was that big old switch case, this is what it would look like. So all we need to do is find the class that has the name that we're looking for,
you know, in the actual task that comes down. If we didn't find it, we send an error back saying that, well, the command isn't found. Otherwise we just execute it. I got another example here using attributes,
but I'm actually running out of time. So I'll have to skip to the summary. So to start with, absolutely know your goals, know what you're trying to build and why you're trying to build it. Only by knowing your goals will you really understand what features you need to put into your framework.
But I also encourage you to focus on framework features rather than C2 features. So by C2 features, I don't know, I guess I'm referring to command proliferation again, but I would definitely focus, especially in the early days of your framework,
is focus on those framework elements. You really want to provide the operators the means to customize and expand your framework the way they want to do it. Prioritize those base primitives on your implant
and provide an easy means for the operators to interact with them. Abstracts and interfaces are incredibly useful. I can't think really of a better way to provide that extensibility.
To the operators, plan small attainable releases, don't try and do like a big bang release for your first one. And if this is something that you want to maintain over the long term,
I would also say to limit each release to only one like big feature. So if you look at, usually if you look at release notes for software, most of them, the vast majority of the changes are bug fixes. You will then get other types of minor improvements
and you'll probably only really see like one or maybe two like big new features. So that is a kind of software design lifecycle thing that I would really encourage. Don't try and change too much in one go.
Yeah, I think that's pretty much it. I hope the talk was useful. If you've got any questions, please let me know. I'm gonna try and be around for a Q&A during the village. If not, feel free to hit me up on any of those socials I showed at the beginning.
I hope you enjoy the rest of the conference. Thanks very much.