← All talks

Shellcode: What It Is and Why It's Loved by Cybercriminals

BSides Kristiansand · 202626:4621 viewsPublished 2026-02Watch on YouTube ↗
Speakers
Tags
About this talk
Shellcode is position-independent assembly code encoded in hexadecimal, used by attackers to run malicious payloads in memory without touching disk. This talk covers the fundamentals of shellcode writing for Windows, including PEB walking to locate API functions, practical techniques to overcome process architecture constraints, and a live demo of a custom shellcode loader that executes arbitrary code.
Show transcript [en]

space for 11 years. Uh through a career as both an analyst, incident responder, pentester, and red team operator. He has a holistic view and experience set within operational cyber security. Jim holds a bachelor's degree in software engineering and the OCP certification among others. His talk today is called Shell Code, what it is and why it's loved by cyber criminals.

Uh, no, I don't need it. All right. So, yeah, as you can tell by the uh template here, I work at SP1 as an incident responder. That's about all the time for introductions I have because we have a lot of [ __ ] to get through. So, uh what is shell code and what is it used for? How do you write it and how do you use it? So, uh Andrea luckily uh sort of covered a lot of this already. So you use shell code for among other things defensation but like the core concept is you want to run malicious code right or at least that's what I use him for. Uh so we're going to get into

how to write your own custom shell code. Uh very like dumbed down simple way of doing it. Uh not as advanced as Andre would do it probably but still it's it's easier to grasp grasp the concepts when you do it this way. And also how do you use it? I'm going to show you like a dead simple shell code loader and just describe the sort of basic principles. So, uh what is shell code? It looks like this. You've probably seen it if you've played with MSF Venom for instance. Uh shell code is also called position independent code meaning it should be able to run from wherever you put it like with certain exceptions of course.

uh and uh through my research for this talk I basically figured the simplest most concise way of putting it is this is just assembly in hexadimal form. So you're encoding assembly to hexodimal and then running it. That's the gist. So think about what a compiler is actually doing. You're writing some hello world C code. You compile it. it will convert it to assembly and then to machine code that's runnable by the CPU. So if you compile this program, open it up in a debugger, you'll get this um assembly code here and the like the individual uh assembly instructions like move and push and call and RB RSP all these registers you convert this to hex. So for instance

the uh move RBP RSP uh instruction will be 4889 E5 in hexodimal. So you prepend that with a back slash X and you put that in memory and you run it. That's like the very very basic way to put it. And uh don't get confused by this. This is memory addresses not like the actual shell code but just putting it out there. So what do you use it for? Well, like I already mentioned, you run malicious code to get uh foothold on a system for instance. You might want to run uh certain actions from a specific process context. Like say you want to dump cookies or something, you might want to do it from a web browser context and not

like teams xc. Uh that might flag something for blue team to look at. Uh you can also run it as defensive. So you're you don't want to put your uh malicious code on disk for static detection purposes. You want to run it in like a memory enclave that looks uh legitimate. That might be like two best uses for this I think. And uh when we write shell code for Windows, we have a few considerations we need to take into account. So uh referring to the fact that it's position independent code it should be able to run from anywhere right uh and if you look at like the first illustration here which is the PE file basic structure you

have some headers you have blah blah section tables and then you get to the sections now your assembly code will usually be located in the text section and if you look at the example from earlier You can see that when I define a string, this ends up in like a dot string hello world sort of reference to a certain section of memory. This is because when I define the string, it'll be placed in the data section most likely. I think so. The assembly inext refers to memory space in data. And what we're trying to do is take the assembly from text, run it from anywhere, which means if you do that, it will refer to memory space that

doesn't exist in that context. So we need a way we need a way to counteract sort of that part that the data will be uh saved in data for reference by test. So there's a nice little neat trick to that I'll show you later on, but that's one consideration. uh you also have Windows process architecture which brings with it many other different uh stuff you you have to figure out. So consider uh the process architecture under windows. So disregarding the kernel space stuff we have thread environment block and process environment block in the user space. Uh seeing as the code will run from anywhere, you can't just import stuff like you would when you program

normally. You have to sort of figure out where the different uh methods you're going to run are because if you're programming without methods, it's like pretty limited what you can do. But if you want to run methods, there's uh a technique called PB walking we're going to go through. Uh so in like basic terms you're going to find the process environment block in memory. You're going to go through uh the module list. This is like the last illustration here is the uh architecture of the process environment block. And one important part here is the module list. So the module list in the process environment block will contain all the modules that your given process has imported. So for

instance uh some random program I just wrote and compiled and ran will load itself first as an XC. It will then load most likely kernel 32 and then NDLL.DL which are sort of providing the basic Windows functionality. So if you find this list, you can iterate through the list and then you can figure out where in memory is the copy of that DL located so I can utilize the uh API calls that it has. That's sort of uh the gist. So you have to account for uh both the variables in assembly and you have to account for where in memory you can find the methods you want to use. So, how do you do that in practice? Uh, when

you do a ped walking, you or there's probably a million ways to do it. I'm not an expert, but the way I do it is you use a flag called GS. And GS is a pointer to the face of the thread environment block. Now there's a method for this in basically all compilers that will uh this will be like intrristic to the compiler. So it's always available to you even though you take the assembly out of its original space and put it somewhere else. So if you use that, you get a pointer to the base of the thread environment block in your user space memory. And then if you add 0x60 as an offset to that pointer, that will give

you another pointer to the process environment block which contains the module list we want. So that's uh PB walking basically confined into two minutes. And then we want to get from that module list, we want to find the appropriate DL. We want to find get process which is a nifty little method in uh kernel32.dll and get process allows us to find the memory method no the memory address of any other method. So it's like you have to go through a little bit of hard work to get to get process but once you have get process you can find anything else basically and using get process I'm going to find winxec which is exactly what it sounds

like. It's a Windows method that executes things. So, keeping this like short and sweet, we're just going to pop calexy like any true lead hacker does to to show that they can run code. So, how do you then take this child code with all your nifty tricks and then use it? Like Andre mentioned, you want to use a shell code loader. And the objective of the shell code loader is just take your shell code, put it in memory, execute it. That's it. And of course, you can do encryption of uh of the shell code to or offiscation rather to to defeat certain detections and stuff. But I've basically chosen to leave all that out. I want to focus on

the core principles to be able to explain it better. So, over to the demo which is running on a remote ESXi. So, this is going to be fun.

>> Come on. Please.

>> Oh, it's working. GIF. Yes, it activate Windows. >> Yeah. So, let's here consider my shell code or my uh almost finished shell code. So, just going through the code real quick. Uh we declare a no inline method. As far as I understand, this is just to keep the size of the assembly down because you have limited space to work with. And then we declare a module to kernel 32. So we're going to uh store the address to kernel 32 in memory in this module or this variable later on. This is the fun part with the GS thing I talked about. So you have a method under Visual Studio. It's read GSP work and then you can supply a offset. So if you

run that method to supply the offset then you can type cast that to pointer to pb variable you have a pointer to p. So okay now you have this as a baseline and then we can start working with that. So to find the kernel 32 entry in the module list. Uh the module list is a kind of a fun structure because it's a doubly linked list. So you can do if you go to the pad and then there's a pointer to loader and then from loader there's another pointer to inmemory order module list which is the list we're looking for. If you go flink flink flink it's uh it's not because you're good it's a

forward link. Uh likewise if you wanted to go backwards in the list it's blink. So uh backward link right and uh like I said before uh the XC will first load itself and then it will load kernel or actually I said it wrong it's first itself then NDLL and then kernel 32 which is why we have three links because it jumps three times through the module list. So then you have a pointer to the third module in the list which most likely will be kernel 32. So we have that in kernel 32 entry and then you can parse the headers for this. This is just like uh groundscaping and red tape to be able to use this structure later on.

So if I go down a bit we have some more code. So now uh you want to create a pointer to the methods exposed by current 32. So it's like you're going to one pointer to a list and then sifting through the list and then you have another list entry and that has another list again. So it's like insert the meme of that guy from like is it personal development or something where he is like yeah bloodshed eyes and going insane. uh in there you want to find yeah the exports table and the code is too long here but you're basically saying yeah uh after the headers and uh there's a pointer to the data directory where

there's a list if I scroll maybe we can see it maybe we can't I guess we're going to have to do it the hard way don't you love Mac and then Windows uh in a VM. Jesus Christ. I I realize this is bad presentation technique. I'm really sorry. >> You do have a scroll bar at the end. >> Yes, but then it might get mad at me. >> All right. You you you're going to have to take my word for it, right? Uh there is a list there. Uh and then you want to parse what you found from the exports table. Uh so you have an address of functions list and address address of function names and address of name

ordinals and like the the function address and names. It's it's pretty self-explanatory. The name ordinals is like some kind of uh initials for each method. I'm sure Andre could explain this way better than I can. You find him afterwards. But basically we need these lists to be able to uh iterate over them and find what we want. And now uh once we've parsed the header we can take our previous kernel 32 method and just assign the address of that to this. And we're going to use that later once we finally have the u the get process method. Now remember the stream stuff I talked about that it gets put in different sections and all that jazz.

One nifty trick to sort of beat that is create a strct of unsigned integer 64 values and you can make as many as you want but be aware that each uh u 64 can hold eight bytes. So you split the string you want into eight byte chunks and assign them to t0, t1, t2 and so forth. And later on you can just refer to the start of the strct and uh that will be represented as a string if you type cast it to a string. So it's a pretty fun trick. I'm going to show you it afterwards. And then let's go down. Now we're going to do some uh more preparations because we're going to load

methods that we're going to use, right? So we basically create our own types, our own like data types. So we just emulate the get pocket rest and win exact types like what kind of arguments they take with the correct type and what kind of return values they have. And then we create variables based on our types. Uh just set them to null pointer for now. And then we're going to assign the addresses to them once we find what we're looking for. And then it's a simple for loop to loop over the kernel exports table as number of names. So you loop through that and for each uh iteration you check against uh let's see I'm going

to scroll a bit down so this is easier to see. >> This is what I was talking about with the strings. So if you basically take a process, paste it into like um cyershhat you will get these bytes that are uh representing each letter. So these are the first eight and luckily there's no other method in that export table in kernel 32 that starts with get prop a because that's what we're going to compare based on. So you take that value and you need to account for little Indian. So that's why 41 is first and then 63 and so forth. And then you're going to do for each iteration do a check for the current name if that is

equal to get proc a. And you can see we um type cast this to you in 64 to get like a comparison that is actually able to compare. So if that matches that means you found get proc a or get frock address in the uh list of u name ordinals from German 32 exports. So then it's uh more or less just assign this to our get process that we defined uh further up here type cast it to our type and then it's some mumbo jumbo to get the actual address of the actual function that we're going to use. So assign that there. >> Now then there's some more fun stuff. Once we done that, we can now do

basically the same thing with wexc which is the method we're going to use to pop couch. Now a quick quiz to make sure you're awake. Why do you want to have since this is only eight characters long, you could contain this in like just t0 with eight bytes. Why do you want to end with zeros? The string uses little termination. Good boy. That's correct. >> So repeat that. Sorry. The C strings use null at the end to terminate the string. >> Yeah. So if it was uh no zeros here, it might be that in that memory there's still other characters, meaning the string would just continue. So it would be when I say blah blah blah blah blah

going on for ages until it reaches a null and then it ends the string. So when I uh like here I cast the content of that strct to const chart pointer if there wasn't a zero it would just continue on until it found a zero which wouldn't make a really good string to be useful. So and then we use uh the nifty get process and just point it to kernel 32 say I want to find the string uh win xc in kernel 32. It returns a memory address for you. you just assign it to an insec and type cast it to our type. So now you have uh you bought the p you found the um the method you're going to

use and you assign win xc to the method. Now we're going to do the uh string trick again with calexc and then run winxec to run cal xe. The five here is just uh an option that wxc requires. I believe it refers to show the window and not do anything else. So now you might wonder how the hell do we get this in assembly and byes. So the you can enable some options in uh visual studio that I don't have time to explain but it's very easy. It's contained in the tutorial I have a link to. So you might have noticed I set a break point on the top of this method. Then it when you run the

debugger it stops at the break point and now it has compiled all the C code. So if you go over to the disassembly tab we can see here we have our uh since I've enabled the show code bytes you have bytes for every single assembly instruction. So if we now take all of this, every single part until we get to the return here. Copy that. And I am short as hell on time. So I am going to try and do this quickly. We take this into a text editor. I have a cheat sheet and we do some reg x on this. Do this. Paste this. Just Oh Jesus, I lost the pointer. There it is.

No. Um, so and then we want to get rid of all the white space and all the line breaks. Now we have one huge blob. We're going to pitify that a little bit. And hopefully I can do this under five minutes. >> No pressure. >> No, no pressure. Not at all. dollar one to get the first capture group and then line break. Yes. And now we want to prepend with our uh backslash x. So like so. Oh Jesus, I did it twice. Oh lord. There we go. Nice. And for the final trick, we're going to add parentheses on each line start and line end

like this. See, hacking isn't as sexy as they make it out to be. There we go. Okay, so we have shell code. Now we take all of this. We open up our loader. How much time do I have left now? >> Couple of minutes. >> Couple of minutes. Okay, we can work with that. And so this is like a very basic shell code loader. So you will see the shell code array is empty. uh basically does what Andre says. It allocates memory uh space based on the size of the shell code. It then writes the shell code from that buffer to the memory address we just allocated and then it runs it. And this line here I didn't understand for

like years and years until I actually googled it. And so you basically treat the uh address of the shell code as a function and you type cast it to a void pointer function and when you put the parenthesis at the end it will implicitly say this is a function please run it. That's the short description. So, if I just for uh demonstration sake, I'm in the loader say uh will we'll build this. No. God damn it. Okay, it's already built. Now, it should run and I will p pissed. >> Now, that was premature because I forgot to build it after I deleted the shell code after I ran this test yesterday. So, just to show you how this

uh this should work. If I do this, maybe it's easier to hit build. >> Going empty. Yes. Empty sh. That's the point. Yep. >> Yeah. >> Because I want to show you what happens when there is none. It just says meh. And then if we do no for [ __ ] sake. There we go. Last exit code. It'll give you some crazy number because you triggered an error. You tried to allocate memory that isn't anything. Doesn't like it. So if we now do this, place the shell code in here. It puts the lotion on its skin. No. Okay. So, we have to copy this again. I have to find the mouse pointer again. And we put it in here. There we go. Save

that. Build solution. And hopefully it went as well as it did earlier. Yes. Nice. And now the exit code is zero because it's it uh finished. So super quick summary. I know I'm out of time. Uh shell code is assembly in hexodimal form. It's position independent and can run almost anywhere. You can do some Windows internals to call the APIs you need. It's often used and loved as is the title of this uh demonstration to used by criminals to run malicious code and evade defense. And it's used in conjunction with a shell code loader that can be written in insert your favorite programming language. These are some sources. Y old chat JPT putting in work. Thank

you for your

>> thanks Tim. This is the uh end of the last the last segment and now it is time for lunch. So I bet it >> we're going to start back at 13:15. So, welcome in the room by then, please.