Program Interaction on Linux

Created On 13. Mar 2022

Updated: 2022-03-13 22:06:33.405671000 +0000

Created By: acidghost

Knowing few programming approaches is essential for efficient interaction with programs. This can be further applied for automation and exploitation.
This post is work in progress and with time should become a compact reference sheet compiled from use cases that helps get set with binary interaction.

Table of contents

  • Executing a file
  • Arguments
  • Environment Variables
  • File Redirection
  • FIFOs
  • Symbolic Links
  • Constructor and Destructor
  • Linux Tools
  • Scripts
  • Templates

Executing a file

Bash is relatively straightforward:

./file

With python a file can be executed with "subprocess" library. In such way, we can execute by printing the stdout, then read the text from an executable.

 import subprocess
process = subprocess.run("./challenge", stdout=PIPE)
print(process.stdout.decode())

However there are more ways to do this. In most of the cases, Popen is enough:

p = subprocess.Popen(["./challenge"], stdout=PIPE)
printed_text = p.communicate().decode()
print(printed_value).decode()

glob module that matches path names to a pattern can be also used:

import glob
import pwn
p = pwn.process(glob.glob(f"./chall*"), stdout=pwn.PIPE, stdin.PIPE)
p.interactive()

also:

     with pwn.process(f"./challenge") as target:
        pwn.info(target.readrepeat(1)

Each of these ways will execute the binaries differently, so it depends on the situation which one to use. For example subprocess.run will execute the command and wait until the process is finished, while subprocess.Popen can still be interfered with while the process runs.
In C the process has to be explicitly forked, before it launches:

include <stdlib.h>
int main()
{
    int i = fork();
    if (i = 0)
{
    execve("./chall", NULL, NULL)
}
else
{
    waitpid(i, NULL, 0);
}
}

Arguments

Adding arguments is as simple as well.

Bash
./chall arg1 arg2
Python
 import subprocess
process = subprocess.run(("./chall", "arg 1", "arg2"), stdout=subprocess.PIPE)
print(process.stdout)
C
include <stdlib.h>
int main(int argc, char **argv)
{
    char *args[] = {
    "arg 1",
    "arg 2",
    0
    };

    int i = fork();
    if (i = 0)
{
    execve("./challelnge", args, 0)
}
else
{
    waitpid(i, NULL, 0);
}
}

Environment Variables

By default in Linux, the program will be executed with defined environment variables in the system + the ones that are in the program. In Binary Files and Processes in Linux you can check how they can be enumerated from a little C program.

Bash

Pass environment variables in bash by prepending env=your_env:

./challl env=your_env

Execute it with 0 environment variables:

env -i chall

or a subshell can be also created in such way:

(
exec -c ./chall
)
Python
subprocess.run(["./chall"], env={"env": "your_env"}, stdout=subprocess.PIPE)

Running some shellcode as environmental variable can be then applied through python in such way:

import pwn
shellcode = b"\x90"*1000 + pwn.asm(pwn.shellcraft.readfile("flag", 1))
env = { "SHELLCODE": shellcode }
subprocess.run(["./chall"], env=env, stdout=subprocess.PIPE)
C
include <stdlib.h>
int main(int argc, char **argv, char **environ)
{
    char *envp[] = {
    "env=your_env",
    0
    };

    char *args[] = {
    "arg 1",
    "arg 2",
    0
    };

    int i = fork();
    if (i = 0)
{
    execve("./challelnge", args, envp)
}
else
{
    waitpid(i, NULL, 0);
}
}

File Redirection

Redirect the contents of one file into another and vice versa.

Stdin

Redirect /tmp/data into chall.

Bash
/tmp/data/ > chall
Python
import subprocess
process = subprocess.run("./chall", stdout=subprocess.PIPE, stdin=open("/tmp/data"))
print(process.stdout.decode())
C
include <stdlib.h>
int main()
{
    int i = fork();
    if (i = 0)
    int fw=open("/tmp/data", O_APPEND|O_WRONLY)
{
    execve("./chall", NULL, NULL)
}
else
{
    waitpid(i, NULL, 0);
}
}

the same can be achieved with freopen("/tmp/data","w",stdin).

Stdout

Redirect chall into /tmp/data.

Python
 import subprocess
process = subprocess.run("./chall", stdout=subprocess.PIPE, stdout=open("/tmp/data","w"))
print(open("/tmp/data").read())
C
freopen("/tmp/data","w",stdout)

FIFOs

Fifos on Linux systems are used for writing and reading data from one end to another through a named pipe. The pipe can only read and write only from one end to another. A fifo has to be opened from both ends for it to be unblocked, otherwise the data won't enter/reach its destination or be available to be accessed. A fifo can be created with mkfifo.

mkfifo stdin
mkfifo stdout
./challenge < stdin > stdout

from another terminal window:

echo sometext > stdin

from another one:

cat stdout

First the process will wait for input to be passed to stdin. This way it will get unblocked and cat will succeed on stdout to output the challenge text.

Working with raw bytes

Echo can do few tricks. Pass aaa in raw hex to chall's argv[1]:

./chall "$(echo -ne '\x41\x41\x41')"

same with python:

 import subprocess
process = subprocess.run(["./chall", bytes.fromhex("414141")], stdout=subprocess.PIPE, stdout=open("/tmp/data","w"))
print(open("/tmp/data").read())

Symbolic Links

By creating symbolic links we can reference another file which can help for it to be accessed easier. This way is created a soft link:

ln -s sourcefile symbolicfile

This way argv[0] is also modified which correspondingly can be of use in shellcoding when the size of the code is very important. In fact, with a symbolic link, the name of a file doesn't even need to be mentioned in the shellcode, since it can be any redundant pattern from the memory and the instruction for it to take at most 1 byte - which in such case will be passed as name of the referenced file. For example, each time after a syscall runs, there will be always the same string in rcx register. By pushing rcx, such string can be used a a reference to the original file.
A hard link is created the same way, but just without -s option. Any changes to the link will also reflect in the original file and they cannot refer to directories.

Constructor and Destructor

__attribute__((destructor))
int destructor_function()
{
    puts("byebye!\n");
}

int main()
{
    puts("hello!\n");
}

__attribute__((constructor))
int constructor_function()
{
    puts("I'm first!\n");
}

This can be used to load and unload libraries first and last. This becomes powerful when a program requests certain libraries and as an alternative we can use constructor/deconstructor to make it behave as we need it. Also see Binary Files and Processes in Linux. The program above will print:

I'm first!
hello!
byebye!

Linux Tools

There are tons of tools and with each linux distro they keep being added. However, there are few essential ones that make working with the filesystem easier. A good and compact overview of essential tools for program interaction can be found here https://abarrak.gitbook.io/linux-sysops-handbook/#shell-tips-and-tricks

cat

Pass as argv the contents of a file to chall:

chall "$(cat file)"

grep

Look up for a value in a file with regex in /mydir:

grep -r 'myValue' /mydir

sed

replace abc with cba

sed -e 'n/abc/cba' /flag

find

Combine with cat to output contents of all files in /mydir:

find /mydir -exec cat {} \;

watch

monitor for new created files in tmp every 5 min:

watch -n 300 -d ls -lR /tmp

socat

Refer to file, command, stream, network and direct it to input or output. Use a network socket to append to a file the newly arrived data (TCP4-LISTEN can be replaced for other types of interaction):

socat TCP4-LISTEN:1234,reuseaddr,fork gopen/home/file,seek-end=0,append

whiptail

Create dialogue boxes for scripts. Create a simple message box:

whiptail --msgbox "This is a message box." 10 100

display output from a file in a box:

whiptail --textbox /flag 10 100 --scrolltext

awk

A text processing utility which has it's own programming language. Get the first pid of running processes:

ps aux | awk 'NR==2{print $2 ""}'

Scripts

Copy a file to a remote server, remove it and verify if the file exists or removed after each step (curly brackets are used as placeholder):

scp -i ~/.ssh/key -P {port_number} ~/my_file user@ip_address:~/remote_folder

[ -e ~/my_file ] && ssh -i ~/.ssh/key -p {port_number} user@ip_address "[ -e  ~/remote_folder/my_file ]"  && echo "File is on the server!" || echo "Failed to copy the file!"

ssh -i ~/.ssh/key -p {port_number} user@ip_address "rm ~/remote_folder/my_file"

ssh -i ~/.ssh/key -p {port_number} user@ip_address "[ -e  ~/remote_folder/my_file ]"  && echo "File wasn't removed!" || echo "File removed successfully!"

Grep for integer output and pass it back to stdin in bash:

#ls -al /proc/self/fd
exec 100<> stdin
exec 101<> stdout

timeout 1 cat /proc/self/fd/101 | grep -P '\d+' | grep -oP '\d+' >/proc/self/fd/100
cat proc/self/fd/101

More old-school way of passing multiple amount of characters as args in with perl:

./chall $(perl -e 'print' "A"x30')

Put a shellcode into an environment variable with some nops:

export SHELLCODE =$(perl -e 'print' "A"x30')$(cat shellcode.bin)

Pass an array of signals to the process with a delay of 1 second:

array = [1, 12, 6]

for element in range(len(array)):
	os.kill({pid of the process}, array[element])
        time.sleep(1)

Decode base64 from an encoded string in a cipher file and print the ones that start with 'pwn':

import base64

with open("cipher", 'rb') as f:
	flag = f.read()

while True:
	flag = base64.b64decode(flag)
	if flag.decode("utf-8")[0:3] == 'pwn':
	print(flag)
	exit()

Password guessing or checks:

for /f %i in (PW.TXT) do @echo %i & net use TARGET IP %i /u:username 2>null && pause

Brute force a KeePass password by knowing the permutation of the words and a separator.

Templates

These are python templates mainly for pwning challenges with pwntools.
Assembly:

import pwn
import sys
import time
import os

pwn.context.arch = "amd64"
pwn.context.encoding = "latin"
pwn.context.log_level = "INFO"

assembly = """
//assembly here
"""

with pwn.process(f"./chall") as target:
	pwn.info(target.readrepeat(1))
	target.send(pwn.asm(assembly))
	pwn.info(target.readrepeat(1))

Simple BOF with 100 bytes:

from pwn import *
// context_bindary = "./chall"
// pty = process.PTY
// p = process(stdin=pty, stdout=pty) // use these lines if you are running the process locally
// p = remote('{ip_address', {port}) // use this to connect remotely
// p = pwn.process('./chall') // try this if you are on the server
pwn.context.arch = "x86_64"

p.send(b"\x00"*100)
p.wait()
p.clean()
p.sendline('cat flag')
print(p.readall().decode())

See another template for binary exploitation here https://github.com/X3eRo0/CTFMate/blob/main/template.py

References

PwnCollege, CTFs, Stackoverflow and others

Section: Binary Exploitation (PWN)

Back