 Let's start recording because now we're moving on to, now for the final module you're going to be digging in and going back to putting the skills that you've been building throughout the semester of understanding and learning web applications, sorry not web applications because we just covered that, but what we've learned about assembly, so you've been learning how to write assembly code, you've been learning how to reverse engineer programs, now we're going to start putting those pieces together to actually exploit vulnerabilities in binary applications. And first we'll start with shell coding. So fundamentally what we're going to want to be doing and where we're going is trying to answer the question, how do we trick the computer into executing some of our data as code? And we actually have already seen an example of this and we've been doing this, so when we're doing a SQL injection what are we trying to do? Like what's the goal of a SQL injection? To run, well anytime I interact with the application it's running SQL code on the server. Yeah, so specifically we trick the server into taking something that should be our data, right, because it's data that we provide to the application, so it should just be data that's stored in the database or used to query the database, but we trick the database into treating that as code as part of a SQL query, we think of a SQL query as code. Exact same thing with cross site scripting, right, cross site scripting is fundamentally you're tricking the browser into interpreting your user data as JavaScript code. This also goes into command injections are exactly the same thing, you're tricking the shell in SH into treating your user data as code. Now we're going to go, those are all kind of, I would think at a kind of a high level, now we're going to actually go down to the machine level and start thinking about at the machine level what is, how can we trick the CPU into start executing our data as code? And this goes back all the way to like the first, some of the first initial designs of computer architecture, so the, yeah, there we go. So the Von Neumann architecture, this is from 1945, which is a design of a computer system. This is kind of a fundamental difference in CPUs and computer architectures is fundamentally a Von Neumann architecture sees and stores code as data. So how does the CPU, we've been talking about data when we access things from memory, right? What's like the lowest level of information that we can access from memory? A byte, yeah, just bytes, right? It's bytes in memory. So if we ask for a byte, we get a byte. If we ask for a quad word, we get eight bytes, I think that's right. I don't know the sizes, but as for one byte, you get one byte, two bytes, you get two bytes kind of flipped around four bytes, same thing, eight bytes. And fundamentally, your instruction point or where your code, your program is going in order to start executing is exactly the same thing. So there's fundamentally no difference between code and data, right? The code that you write in assembly is compiled down to bytes that are loaded into memory. And we tell the CPUs start executing from here, but fundamentally there's absolutely no difference between the code that you write and provide that's in those bits, that those bytes in the computer's memory versus the bytes that you're storing as data. And that goes all the way back to Von Neumann architectures. Whereas Harvard was another style of architecture at the time that stored data and code separately. So here, you'd have a different memory for code and a different memory for data. Almost everything that we've been using in learning is based on the Von Neumann architecture. Does anybody have a device that is not in this list? Like on them or no of them? x86, ARM MIPS, PowerPC, Spark. Risk is definitely on there for sure. A toaster. What CPU does a toaster run? So a lot of them actually will run one of these things, usually be like a cheap MIPS processor. There definitely are use cases, I think very specific things. I haven't seen these, but they definitely do exist of these crazy architectures. And so what we're going to be talking about and focusing on is what happens if the CPU mixes up code and data? What happens if the CPU starts executing or starts executing something, but it's actually your data and not code? Like one thing to think about is from everything you've learned about assembly and CPUs, does the CPU have a way of determining this? No. And this actually is a good thing. So anybody know how, so JavaScript, we looked at JavaScript, right? How does your browser execute that JavaScript? Yeah, so it has a JavaScript engine that used to be JavaScript was really slow because it would interpret each line essentially. So it'd go through and do whatever needed to get done. But as it became clear that JavaScript performance was incredibly important, they do things like what they call just in time. So JIT compilation, where they figure out, oh, we're executing this code a lot, which means it's very important, which means I'm going to take that code, compile it to x86, put it in memory, and then start executing just that code. And so now it's executing x86 instructions that are interpreting it, which is incredibly fast, and you get insane speedups. This is why JavaScript engines are so fast now, because they use all these crazy tricks. And the way they're able to do that is because of this architecture, they can dynamically create code x86 instructions at runtime and execute them. So you can think that actually gives you a lot more power. I mean, it'll enables powerful things to be able to do that in this Von Neumann architecture. So it's not something that anybody would really want to do and say, hey, let's fix this fundamental problem and tell the CPU ahead of time what's code and what's data, because we actually want these, this thing in our applications, we want to be able to do that. So this is a weird example. Okay. Anyways, the one of, sorry, I'm trying to understand what this is trying to get at, but that makes sense. Oh, I see. Okay. That's a weird example. Okay. So I guess we can execute this example. At least don't have a dojo spun up. Yeah. Okay. Cool. So I already have an example that's seen. So here's my program. Let's just run this. So we have to run it with special compilation options, which gets to some things I'll talk about. Don't tell me things are dangerous. So I'm seeing weird behavior, right? I'm running it and I'm sometimes seeing hello a and I'm sometimes seeing illegal instructions. Let's look at the code. So the code here, if we look at the main function, we have some fixed character array. So a buffer of 1024 bytes on the stack, we then call the gets function with that. So gets if you're not familiar, this does reads from standard input and puts the data wherever we send it. So it's going to put all of our input at name here. And then specifically what it's going to do gets is really bad. If you actually noticed when we were compiling here, they actually told us that should have told us tells us that the gets function is dangerous and should not be used. And if we look at the man page, that's because it reads a line from standard input into the buffer pointed by S until either a terminating new line or end of file was replaced with a null byte. No check for buffer overruns is performed. This means that it will it has no idea what size this character array is, and it will keep writing whatever the user's input is. And because you cannot know in advance how much data will be sent, this is fundamentally an insecure function. But let's look back at this. So we read in to name 1024 bytes. We then seed random based on the time this just sets up we're setting up a random based on time. And now we've say if random mod two, so what's the mod two going to do, even our odds so roughly 50% of the time, we'll do this. Otherwise, we'll do this. And so what we do here is say, hello, we call the hello function passing in name is the first parameter by two is the name one. So that's when we pass in a and we saw farewell. It said hello a and then said farewell, which was what we saw. But 50% of the time it crashed. And if we look here, it says, it passes in as name by one. And it passes in as Nate as by funk, which is the function here, the name. So what that's what that's going to do is start trying to execute code on the stack. Let's look at this in GDB. That's all right. Wait, do I not have my guy can't I can't live like this. Sorry, I need to use a I don't need to but it's way better for me to use Jeff so we can see things. Okay, so let's look at the code here, x.20 is we're going to look at instructions. I want to stop. So let's break it hello, because that's where we'll be is that function call. And then we can bring up the here, no, here code. So, so continue. What do you want to say? Blah, blah, blah. Okay, now we call hello. And we know based on the so we can step through here. What's the calling convention of x86 64? How do I know in which of these registers is which arguments? I just can't remember. I think it's RSI RDI. But is that right? Actually, or maybe RDI RSI? I was hoping someone would know. We'll look it up. Yeah, RDI RSI. So I had it backwards RDI RSI, RDX, RCX, cool. So RDI, which is name has this, which is the address of by to an RDI. Wait, I said it the wrong way. RDI, which is the first argument name is our string blah, blah, blah. RSI, the second argument, the function we're going to call is here. So if we step through this, we're setting up the function frame, doing some stuff, calling the print f. So we're calling print f and passing in this argument to blah, blah, blah. That's where we see hello space, blah, blah, blah. So we should see that up here. Hello, blah, blah, blah, blah. Then keeping going, we see a call to RDX, X five, I let's look at what this is. This is by two. So if we look back at the code, we would see that we will see that we are here at by two. So this will after we go here, this will go into by two, and it will say farewell. So let's run it again. Okay, now out there on this, I guess 50% of the time. Okay, rerun. Okay, is this going to be very annoying? Okay, well, there's an easy way to fix that. And that's why I was just telling somebody else when you control the thing, you can do whatever you want. If you don't like it, change the code. Okay, compile it, GDP it, break, hello. Okay, so I'll type in CSE 365. Okay, now we're calling this function. And RDI, what we're going to output is here by one. But now let's say we've somehow tricked it into pass, we're passing in a pointer to a function that we're going to call. But that's actually the buffer that we have on the stack that we were just reading characters in. Oh, no, what? What are the odds container gets killed right in the middle? Did you do that on purpose? Now, here we are. So now we've accidentally as a programmer passed in or we've tricked it into passing in to swapping those two values. So passing in by one as our first argument and the buffer on the stack. So as we step through, we'll see the printf. So we call the printf. And if we look at the output, it will say, it definitely should say, oh, I didn't, sorry, I stepped into it. So I'm going to do fi to jump out of it, Finn. And it says hello, blah, blah, blah, blah, what's this blah, blah, blah, blah, blah. It's not it's garbage in the sense that it's not ASCII characters, but specifically what it is is it is if we go here, right, what the program is doing is it calls printf and passes as the percent s, the second argument to printf. So what's going to be interpreted as a string is actually this by one function. So if we examine this as a string, it will keep printing out these characters until it gets to a null byte. And so because it doesn't know, right? Again, these functions are very stupid, right? Printf says print me out a string, take whatever memory address I give you and just output it until you get to a zero. So let's look at the call at the call here. We call RDX and now we have this value on the stack that came from us, but it will be interpreted as instructions and will be executed. So I can step into there and I can actually see that we're actually kind of executing some stuff until we get to here, we try to dereference RSI and it blows up because that's not valid thing. And that's why we get a segmentation fault. But if it's executing some code that comes from us, do you know how to create bytes that the computer can interpret as x8664 instructions to do what you want? Yes, because you had a whole module writing assembly code, you've written a web server and assembly code, you've read code, written code, right? That's what we've been building up for because that's now at this moment you've tricked the computer into interpreting your data as code. Cool. So essentially what this is doing is it's actually jumping back to our code on the stack because that's where that buffer existed in the function and it's executing and interpreting those bytes that it got from us as code. Okay. Do you want to make it do something? Well, we'll get there in a second. Okay. And this is an incredibly old concept. So this is actually the Morris internet worm was one of the earliest, this actually took down the whole internet because there was a bug in the replication strategy of this, in the replication strategy of this worm where it was supposed to look if it was already running and if it was, it would like kill itself, but there was problems in that. So this worm would just infect machines and then get on those machines and then look for other machines and infect those machines. And there'd be so many copies of this worm running that it would slow down the machine to where it was unusable. And so it actually used a lot of different attack vectors. It's really cool to look this up. But in, in this finger D service, this use the shellcode injection, the other crazy thing is this worm was I think cross system and cross architecture compatible. So it would like had its own shellcode that it would compile for the target architecture and then go there. It was insane. And they literally had to shut down the internet. So they had to they distributed patches for the, whatever these services that were running, and they all had to like shut down their machines because if one, like, if a couple of machines were infected, those would spend so much time trying to look for other infected machines that would slow those machines down. So they had to like shut everything down, disconnect them, update their own local copies of stuff, and then turn them back on so that the internet could work. It gets even crazier if you look into this, the person who did this RTM, he's actually, I think a professor at MIT was or still is. And at the time he was a grad student, his dad was also I think the director of the NSA at the time. So it's like really, anyway, super interesting. So this is like a fundamental things that we're learning here. Okay. Cool. So how do we write shellcode? You actually already have a very intuitive understanding of this because you've been writing assembly code that you can turn into bytes. That's one of the key things that we've learned. So this is with the, if you go all the way back to when we were doing the assembly module, you gave the program bytes that it took and executed. Essentially, you were writing little shellcode programs. You just didn't really know that yet. And so what we actually want to do, if we get to where we're trying to go, shell here has a very specific meaning. This is what like a hacker terminology, this is like you're trying to pop a shell means that you are trying to call execvbnsh. So that that way a shell is now running with the permissions of the target program. So for instance, on the Dojo, the programs, you know, well, actually, I think we have looked at this since, right? So we learned about setuid in the access control module. So this means that it's owned by root. So when it executes, it executes as root. And so if we can get it to execvbnsh, now we have a shell as root, and we can do whatever we want. So that is our goal as attackers. And but there are some constraints. So think about where we were here, right? Think about there's some user data on the stack. And then let's say we could we redirect control flow onto our code. One of the key things is that shellcode needs to be position independent. So it has to not matter where that code lives, because otherwise, you don't know exactly where it's going to be in memory and you can't rely on any fixed layouts. The other cool thing is you can share shellcode then is there's all these repositories of different shellcode that does different things that you can then reuse. Cool. Okay. So I have my hello, let's do shellcode.s. Oh, wait, what if I need my prefix there? Or let's do this. Actually, this is good. So if you remember what exit code 60 is, I would bet it's exit. I actually don't remember how to get all the output though. As. Oh, there we go. Look at that. The test.s eight. So, so this is like, all right. Anyways, okay, we already saw this. So test.s. This is some assembly code that probably I wrote when we were doing these assembly modules. I have no idea. This was so long ago that it's out of my brain at this point, but we're moving one into RDI 60 into rax and executing a sys call. And so using exactly what we did in the assembly modules, right, compiling that to an object file, then using object copy to copy that to a binary. I now have this binary file that's 16 bytes. So I had my hello that's reading from input from us. So by running that in, I don't get a seg fault anymore. Why? Because I'm running real x86 code, not like a string that I had of CSE 365. So let's debug it just to check. Break. Hello. And then I can run it. The cool thing is you can do redirection here inside gdb. So here I'm running it and I'm saying use this file asm.bin as the standard input run to here. Hello. I can look at my arguments. RDI is by one RSI is this I can double check x dash five, I so in examine five instructions here, shows me move one into RDI move three C into RAX sys call. I can start doing next instructions here until I get to the call. And now I can step and I can see I'm actually on the stack, this seven f f f f f. This is somewhere on the stack. We're moving one into RDI. We're moving three C into RAX and we're calling sys call to exit with error code one. So now we've tricked this program into executing x86 64 code that we control of whatever we want. So now if we wanted to let's grab here, cool. Okay, so now I want to do exec v e. I want to call this sys call. How do I do this? Do you remember? sys call table, your friend, the sys call table. Exec v e is 59. We're so close in here. So move RAX 59 sys call. So that'll call the right one. Now I need to get so now I'll need so RDI needs to be string bin SH RSI needs to be zero and R we said it was RDX, right? RDX needs to be zero. So the zeros are easy. What's the way of zeroing out a register without using a move? XOR and our input to the program may be limited when we did gets it read up until a new line, but our input it may only get 1216 bytes that we can trick it to execute as code. So one of the reasons I use XOR here is because it's only one byte. This is what I know from experience where they're moving zero into RDX takes I think four or five bytes because they encodes the zero as like four null bytes. So that's actually if you really want to go, you're not only doing three levels of shell coding, but if you want to get into it more further and further levels add more and more restrictions to your shell code. Like you can't use certain characters. You have to do it in 10 bytes or eight bytes or what's the final one Connor? Do you remember six bytes? Six byte shell code. So it forces you to get creative and understand and be able to do those things. Okay. So I XOR RSI RDX. Now I need to get this string. So normally we did something like this, right? Was it how to dot data section? There's something like, uh, I actually can't remember how to do this. Was it ASCII? S D Z like this? Yeah, minus H. I need to give it a name. Let's make sure that compiles that's shocking. Actually work. Okay. We're not referencing it, but that's okay. Okay. So then we do that. And then we want to move into RDI. What was it like offset of been SH? Yeah, I don't know either, but try it, but then definitely look at it to make sure it makes sense. That's because of shell code. Okay, this makes sense. So why we thought that might blow up. Ah, see, I messed up. Yeah, but I think it adds the null byte at the end, which is important. Yeah, but it's saying it doesn't exist. So or it's unknown pseudo op. I could do the null byte myself. That seems silly. What was it? We'll bite ourselves. Are we crazy people? Okay, let's make sure. Okay, cool. So here's my input. So moving my offset definitely did not work. Okay, I'm gonna grep because I feel like I did this. Yeah, move offset buff. Why is it not Intel syntax? This is driving me insane. I feel like you're just this is like, I feel like I'm just dancing. Okay. So the offset of is not working. This is what I'm double checking. Is this in here? Okay, let's do something like this. Hey, there we go. Okay. So load effective address. So calculating the current instruction pointer, plus the offset into there, move that into RDI, clear out RSI, clear out RDI, move three B into RIX, call sis call. Let's always before we just randomly try something and decide if it works or not. I always like to debug it first. So let's break on Hello. Okay, now here I can before I even do anything I can double check this by doing checking here. So RSI I know is my buffer. So X 20 I load effective address RIP plus five plus F, which is this address X or X or sis call and I can even check that address make sure it's the string been SH. So let's step through print F, skip over that because I don't care about you print F. Now call RDX step into there. So now I can see loading effective address into RDI. So RDI should now have the string. If I look at here, RDI now points to the string been SH X or RSI X or RDX, move three B into RIX, sis call, executing no pro new program. That looks like success is a classic problem. So did it work or not work? Yeah, let's look at this. I think this should help illuminate. Okay, so if we scroll up, we see our exact fee been SH. So this is exactly what we wanted. It's executing. And we could look at exactly all these things step, step, step. But the key thing is it's reading from zero, which is standard input. And it's getting back zero. Why? Because the standard input of this program is the output of cat ASM dot bin. So if there's no more input to read, we gave it our whole file and it read it all. So you can the cheap trick, actually the best way to do this is why you use Pwn tools because you can change it to interactive and just interact with it and not have to worry about this. You can just send the bytes, you can write your shell code in your Python script in Pwn tools, assemble it and then send those bytes along. Another, the cheap trick is to just add it. So dash here means the command line. So concatenate, sorry, not the command line standard input. So first output assembly dot bin and then wait for input from the command line from standard input and then write that to standard output. So what this will first do is write out to assembly dot bin everything. And then if I do things like now I'm typing on here as if I'm talking to that bin SH command through the cat. The other thing I noticed, why was it hanging here? I typed an LS that didn't do anything. That's because gets reads until a new line. So we didn't actually send it a new line. So we could figure that out and then we could do things like, I would do anyways, cool. So we just wrote little shell code already. And we can fundamentally do whatever we want. So we'll check this shell code out. That's crazy. This is like exactly almost what we did. Well, I guess is dot string implied dot ASCII Z. So it does the thing. See, we didn't even cheat and we got there together as a group. So exactly the same code here effectively, just slightly different, right? So this is how we put data in our code, which we already saw, we can use that string, we can use ASCII Z with one I, we can use dot bite to put bites directly in our code. We can, this is another kind of classic technique is having the data be part of our instruction. So we can move this is, you have to read it from right to left because of Andy in this slash B I N, I know to F is a slash. So I don't know what the other ones are, I'm just guessing, but if this is correct slash B I N slash SH. Well, I miss mess up slash SH and the null bite. So moving that into RBX, pushing RBX onto the stack. Now the bites on the stack are that is the string. And so we can move RSP into RDI. So now because RSP points to the start of the stack. So now we have our string there on the stack. But we can also do anything. So we can, we don't have to just execute a shell, we can have it, let's say, one of the classic ones is a connect back shell code. So if you're executing a remote system, you can have it make a TCP connection back to you and have it spawn a shell that is listening to your input on an IP port. So you set up a thing that's listening there and you can type in commands that goes executes it tells you the response. This example is calling open flag and then sending that file to file descriptor one. What's file descriptor one normally? Zero is standard input, standard output. Yes. So send the flag, whatever is in that file descriptor just to standard output. And so you can step through this, but this is exactly, this should look very similar. I remember debugging with some of you in recitations, your assembly code, it looked very similar to this, right? Doing sys or and building a web server. You're just doing system calls. This is exactly what shell coding is. You're just doing it for a, let's say to achieve exploitation rather than to build a web server. Cool. Okay, we don't need to go over this because we have that running it. Great. If you, Oh, dev standard in that's like, we saw that. Anyways, tools on here so you can use, I guess, should we do this same example using phone tools without being useful to show? I'm going to cheat and have my code up here. I guess, do most of you use from phone import star? I hate that, but fine. Okay. So P equals process. So execute the hello process. Then so what we can do and a way to do this is to write your assembly code here. Now, because it's special and phone tools, you don't need to have all this stuff at the top and everything. So all we're going to have is this extra code. So let's go here. So now this is a string that exists. So I can write my code in as a Python string. And to double check that this is actually working, let's, I'm going to open up another terminal, open up a handy dandy ipython to have it right here as I execute from phone import star. So let's try this. Oh, I see because ASM. Oh, but I shouldn't call it ASM because isn't that the function? Okay, shell code. Well, we will call you shell code. All right, there is an error. And you'd probably have to dig through. You can try to figure out what's going on here. Like I already see this dash 32 is wrong. We did not set up the context, context.arch. Is that right? Equals xa664. There's actually another really cool way to do it. How is somebody doing that? Do you remember from the workshop we did where they set it based on the process? Something like that, index.exe. Okay, not going to get into that. But let's set the architecture correctly. Now when we assemble it, now we get our shell code bytes. Okay, so that should be good. We can even check by, I think we can do disam. I like to do this just to double check. And of course it prints, we want to print it out. So I can again remember it's exactly what I did before with object dump, right now I'm just doing it in Python and using ipython to kind of test that the little pieces of my script are going to work. Okay, so assemble it and p.send line. And I know I want to do send line instead of send because it gets is going to read until the end of the line. Okay, and I'll do p.interactive. So now I have the terminal here. I can just interact with it. Pretty cool. Questions. So you already have all the skills you need to be able to do all this stuff. You've already been using PwnTools to interact with the process to parse complex things. You've learned assembly. I guess you don't even need me to teach you anything, right? Laugh to me. Okay. Okay, but there are some problems that can occur. These are things, these are just more of a reminder. You may have forgotten this since you have built a web server and assembly. But one thing to be very careful about the size of the memory access that you're using, right? So we all talked about that the difference between accessing one byte versus two bytes versus four bytes versus eight bytes, right? So you can do it implicitly based on the size of the register that you're using. So BL versus RBX is going to be one byte versus eight bytes. You can also specifically use these annotations, I guess, to specify to the compiler that you want to move a single byte. So this is move byte pointer or word pointer or dword pointer. This is being very explicit telling the compiler earlier compiler, you only want to access one byte. Yeah, like we talked about. So there was something I didn't show, we could have actually really messed up our shell code. And it could have been very annoying. If there was, what would have happened if there was a new line in our shell code? It would read because gets reads a line. So it would read up until that new new line, and then stop reading the rest of our shell code and only execute half of our code. Actually, let's look at that. We can easily simulate that. Let's say in here, there was a dot byte hex 10 is a new line character. Yeah. So instead of getting a shell, now I get a segmentation fall. And the reason is, well, if we looked in here, we would see that this new line, this 10 is actually causing it to only read in part of our input. We don't, we won't see the thing. Let's check. Okay, let's read from here. It didn't read it. I didn't read that in. I was wrong. It's 10 in decimal, not 10 in hex. See, this is why I get the double check your assumptions. I could have just assumed that all day long. Okay. So let's look at our, so load that. And then we can see, and this will drive you crazy if this happens to you, that your string isn't even in there. So it's hard to see that half of your input didn't even make it in. And that's because it's not that the program necessarily forbid a new line character, uh, hex a, it's the fact that by reading and using gets reads to the end of a line in a new line. So you can't have any new lines. And so this is why I'm being careful about these things. If I was really good in my Python script, I would have called an assert and I would have asserted that in my shell code, there are no new lines. That way my Python script blows up rather than waiting and having my payload network and then having to debug the remote system. So this is why, depending on how the program is reading in your input, um, for instance, string copy can't have a null byte, whereas scan f gets gets line f gets can have no bytes, but can't have new lines. Um, scan f also, uh, goes to a white space. So you can't have sometimes a hex 20, which is the space character or tabs. Um, oftentimes, and this can get very fun is your shell code can be mangled or changed and right because it's input from you, the program may operate on that input and then you corrupt something and redirect control flow to your data. So it could be compressed or uncompressed or encrypted or decrypted. So the trick is to work backwards. So start with what you want the shell code to look like. And just like you did in the reverse engineering modules, you started from the key that you wanted. Very similarly, you can start from the shell code you want, try to work backwards. Um, sometimes there may be parts of the input, like let's say it takes your input and then right in the middle, it writes some text file or some input that you can't control. Um, you can do, if you know that, and you should know that by understanding the program, you can use tricks like jump. So you can execute part of your shell code and then jump over just like in the assembly module when you had those knobs, the knobs of like 53 bytes that you jumped over. That's the exact same tricks that you can use to write shell code that can survive being overwritten. Um, other tricks, uh, you know, if we control standard input and standard output, then we can get a shell or get it to give us the flag. But sometimes we may not be able to read the flag back. Maybe the program closes standard input and standard output. Maybe, uh, it's a remote system. And so running a shell doesn't make sense and we can't do a reverse shell. And so what we can do is use things, you can use tricks just like you're learning now in web applications of doing blind SQL injections. You can do similar techniques using shell coding. You can do timing attacks. So you can check the value of a byte and sleep for a certain amount of time and you can write a script that does that. Um, um, so there's all kinds of cool things there. Other things to remember is that your code is being executed in the context of the program and the program has a bunch of registers and a bunch of data that's already in location. So as you think about, uh, shrinking things, you can actually think about, do we need a shell? Can we put the flag somewhere else in some other file that's already open? Uh, do you need to communicate the flag in band? Uh, so you need to think about all of these things. And with that, you'll be great. Good stuff.