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 print
ing 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 push
ing 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