Help with PAM and ctypes

C

Chris AtLee

Sorry for the repeat post...I'm not sure if my first post (on May
30th) went through or
not.

I've been trying to write a PAM module using ctypes. In the
conversation
function (my_conv in the script below), you're passed in a
pam_response**
pointer. You're supposed to allocate an array of pam_response's and
set
the pointer's value to the new array. Then you fill in the array with
appropriate data.

I can't seem to get it working in python...The authenticate function
always
returns PAM_AUTHTOK_RECOVER_ERR (21), which I think means the response
doesn't make any sense.

I've tried saving the response array outside of my_conv to make sure
it
doesn't get garbage collected, but that doesn't seem to help.

Any pointers would be appreciated!

Cheers,
Chris

from ctypes import *

libpam = CDLL("libpam.so")

class pam_handle(Structure):
_fields_ = [
("handle", c_void_p)
]

def __init__(self):
self.handle = 0

class pam_message(Structure):
_fields_ = [
("msg_style", c_int),
("msg", c_char_p),
]

def __repr__(self):
return "<pam_message %i '%s'>" % (self.msg_style, self.msg)

class pam_response(Structure):
_fields_ = [
("resp", c_char_p),
("resp_retcode", c_int),
]

def __repr__(self):
return "<pam_response %i '%s'>" % (self.resp_retcode,
self.resp)

conv_func = CFUNCTYPE(c_int,
c_int, POINTER(POINTER(pam_message)),
POINTER(POINTER(pam_response)), c_void_p)

class pam_conv(Structure):
_fields_ = [
("conv", conv_func),
("appdata_ptr", c_void_p)
]

pam_start = libpam.pam_start
pam_start.restype = c_int
pam_start.argtypes = [c_char_p, c_char_p, POINTER(pam_conv),
POINTER(pam_handle)]

pam_authenticate = libpam.pam_authenticate
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [pam_handle, c_int]

if __name__ == "__main__":
import getpass, os, sys
@conv_func
def my_conv(nMessages, messages, pResponse, appData):
# Create an array of nMessages response objects
# Does r get GC'ed after we're all done?
r = (pam_response * nMessages)()
pResponse.contents = cast(r, POINTER(pam_response))
for i in range(nMessages):
if messages.contents.msg == "Password: ":
p = getpass.getpass()
pResponse.contents[0].resp_retcode = 0
pResponse.contents[0].resp = p
return 0

handle = pam_handle()
c = pam_conv(my_conv, 0)
retval = pam_start("login", os.getlogin(), pointer(c),
pointer(handle))

if retval != 0:
print "Couldn't start pam session"
sys.exit(-1)

retval = pam_authenticate(handle, 0)
if retval == 21:
print "Authentication information cannot be recovered"
sys.exit(-1)

print retval
 
L

Lenard Lindstrom

Chris said:
Sorry for the repeat post...I'm not sure if my first post (on May
30th) went through or
not.

I've been trying to write a PAM module using ctypes. In the
conversation
function (my_conv in the script below), you're passed in a
pam_response**
pointer. You're supposed to allocate an array of pam_response's and
set
the pointer's value to the new array. Then you fill in the array with
appropriate data.

I can't seem to get it working in python...The authenticate function
always
returns PAM_AUTHTOK_RECOVER_ERR (21), which I think means the response
doesn't make any sense.

I've tried saving the response array outside of my_conv to make sure
it
doesn't get garbage collected, but that doesn't seem to help.

Any pointers would be appreciated!

Cheers,
Chris
[snip some code]

conv_func = CFUNCTYPE(c_int,
c_int, POINTER(POINTER(pam_message)),
POINTER(POINTER(pam_response)), c_void_p)

class pam_conv(Structure):
_fields_ = [
("conv", conv_func),
("appdata_ptr", c_void_p)
]

pam_start = libpam.pam_start
pam_start.restype = c_int
pam_start.argtypes = [c_char_p, c_char_p, POINTER(pam_conv),
POINTER(pam_handle)]

pam_authenticate = libpam.pam_authenticate
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [pam_handle, c_int]

if __name__ == "__main__":
import getpass, os, sys
@conv_func
def my_conv(nMessages, messages, pResponse, appData):
# Create an array of nMessages response objects
# Does r get GC'ed after we're all done?
r = (pam_response * nMessages)()

The memory allocated to r is garbage collected immediately after my_conv
returns. You need to allocate it explicitly using C's calloc or such.
This assumes pam_start will free the memory for you.

addr = calloc(sizeof(pam_response), nMessages)


addr is the memory address as a Python integer.
pResponse.contents = cast(r, POINTER(pam_response))

pResponse.contents changes the actual value of pResponse, a value on the
stack. You want to change the value of the pointer pResponse points to:

pResponse[0] = cast(addr, POINTER(pam_response))

The cast creates a POINTER(pam_reponse) instance with value addr.
for i in range(nMessages):
if messages.contents.msg == "Password: ":
p = getpass.getpass()
pResponse.contents[0].resp_retcode = 0
pResponse.contents[0].resp = p
return 0

handle = pam_handle()
c = pam_conv(my_conv, 0)
retval = pam_start("login", os.getlogin(), pointer(c),
pointer(handle))

if retval != 0:
print "Couldn't start pam session"
sys.exit(-1)

retval = pam_authenticate(handle, 0)
if retval == 21:
print "Authentication information cannot be recovered"
sys.exit(-1)

print retval


If you are going to do any serious ctypes coding consider joining the
ctypes mailing list at sourceforge.net .
 
C

Chris AtLee

if __name__ == "__main__":
import getpass, os, sys
@conv_func
def my_conv(nMessages, messages, pResponse, appData):
# Create an array of nMessages response objects
# Does r get GC'ed after we're all done?
r = (pam_response * nMessages)()

The memory allocated to r is garbage collected immediately after my_conv
returns. You need to allocate it explicitly using C's calloc or such.
This assumes pam_start will free the memory for you.

addr = calloc(sizeof(pam_response), nMessages)

addr is the memory address as a Python integer.
pResponse.contents = cast(r, POINTER(pam_response))

pResponse.contents changes the actual value of pResponse, a value on the
stack. You want to change the value of the pointer pResponse points to:

pResponse[0] = cast(addr, POINTER(pam_response))

The cast creates a POINTER(pam_reponse) instance with value addr.

Ahhh, thank you! I never understood how ctypes' pointer.contents
related
to C pointers. So, the following are equivalent?

int v = 42; v = 42
int *p = 0; p = cast(0, POINTER(c_int))
p = &v; p.contents = v
*p = 123; p[0] = 123

Using "pResponse[0] = cast(...)" got me part of the way to fixing my
problem. PAM started either crashing or saying authentication had
failed.
The crash was in free(), and some digging around revealed that PAM
tries to
free the response sent by the application, which makes sense.

My initial attempt to fix this involved wrapping strdup to allocate a
new
copy of a string to send back to PAM. Here's how I wrapped it:

strdup = libc.strdup
strdup.argstypes = [c_char_p]
strdup.restype = c_char_p

This still crashed in free(). I took a look at some of the ctypes
regression tests and something made me try this:

strdup = libc.strdup
strdup.argstypes = [c_char_p]
strdup.restype = POINTER(c_char) # NOT c_char_p !!!!

This works like a charm. Not sure why though...Does ctypes do
something
special with c_char_p return types? It seems as if Python was freeing
the
result of strdup() when the string was GC'ed, and then PAM would fail
when
trying to free the same memory.

My working code is pasted below.

Thanks,
Chris

from ctypes import *

libpam = CDLL("libpam.so")
libc = CDLL("libc.so.6")

calloc = libc.calloc
calloc.restype = c_void_p
calloc.argtypes = [c_uint, c_uint]

strdup = libc.strdup
strdup.argstypes = [c_char_p]
strdup.restype = POINTER(c_char) # NOT c_char_p !!!!

# Various constants
PAM_PROMPT_ECHO_OFF = 1
PAM_PROMPT_ECHO_ON = 2
PAM_ERROR_MSG = 3
PAM_TEXT_INFO = 4

class pam_handle(Structure):
_fields_ = [
("handle", c_void_p)
]

def __init__(self):
self.handle = 0

class pam_message(Structure):
_fields_ = [
("msg_style", c_int),
("msg", c_char_p),
]

def __repr__(self):
return "<pam_message %i '%s'>" % (self.msg_style, self.msg)

class pam_response(Structure):
_fields_ = [
("resp", c_char_p),
("resp_retcode", c_int),
]

def __repr__(self):
return "<pam_response %i '%s'>" % (self.resp_retcode,
self.resp)

conv_func = CFUNCTYPE(c_int,
c_int, POINTER(POINTER(pam_message)),
POINTER(POINTER(pam_response)), c_void_p)

class pam_conv(Structure):
_fields_ = [
("conv", conv_func),
("appdata_ptr", c_void_p)
]

pam_start = libpam.pam_start
pam_start.restype = c_int
pam_start.argtypes = [c_char_p, c_char_p, POINTER(pam_conv),
POINTER(pam_handle)]

pam_authenticate = libpam.pam_authenticate
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [pam_handle, c_int]

if __name__ == "__main__":
import getpass, os, sys
@conv_func
def my_conv(nMessages, messages, pResponse, appData):
# Create an array of nMessages response objects
addr = calloc(nMessages, sizeof(pam_response))
pResponse[0] = cast(addr, POINTER(pam_response))
for i in range(nMessages):
if messages.contents.msg_style == PAM_PROMPT_ECHO_OFF:
p = strdup(getpass.getpass(messages.contents.msg))
pResponse.contents.resp = cast(p, c_char_p)
pResponse.contents.resp_retcode = 0
else:
print "Unknown message type"
return 0

handle = pam_handle()
c = pam_conv(my_conv, 0)
retval = pam_start("login", getpass.getuser(), pointer(c),
pointer(handle))

if retval != 0:
print "Couldn't start pam session"
sys.exit(-1)

retval = pam_authenticate(handle, 0)
if retval == 21:
print "Authentication information cannot be recovered"
sys.exit(-1)
elif retval == 7:
print "Authentication failure"
elif retval == 0:
print "Ok!"
else:
print retval
 
L

Lenard Lindstrom

Chris said:
if __name__ == "__main__":
import getpass, os, sys
@conv_func
def my_conv(nMessages, messages, pResponse, appData):
# Create an array of nMessages response objects
# Does r get GC'ed after we're all done?
r = (pam_response * nMessages)()
The memory allocated to r is garbage collected immediately after my_conv
returns. You need to allocate it explicitly using C's calloc or such.
This assumes pam_start will free the memory for you.

addr = calloc(sizeof(pam_response), nMessages)

addr is the memory address as a Python integer.
pResponse.contents = cast(r, POINTER(pam_response))
pResponse.contents changes the actual value of pResponse, a value on the
stack. You want to change the value of the pointer pResponse points to:

pResponse[0] = cast(addr, POINTER(pam_response))

The cast creates a POINTER(pam_reponse) instance with value addr.

Ahhh, thank you! I never understood how ctypes' pointer.contents
related
to C pointers. So, the following are equivalent?

int v = 42; v = 42
int *p = 0; p = cast(0, POINTER(c_int))
p = &v; p.contents = v
*p = 123; p[0] = 123

Actually

int v = 42; v = c_int(42)

for p.contents = v to work. 42 is a Python integer. c_int(42) refers to
a mutable C integer in memory initialized to 42. Also, the preferred way
to create a NULL pointer in ctypes is to call the pointer type without
an argument:

int *p = 0; p = POINTER(c_int)()

Using "pResponse[0] = cast(...)" got me part of the way to fixing my
problem. PAM started either crashing or saying authentication had
failed.
The crash was in free(), and some digging around revealed that PAM
tries to
free the response sent by the application, which makes sense.

My initial attempt to fix this involved wrapping strdup to allocate a
new
copy of a string to send back to PAM. Here's how I wrapped it:

strdup = libc.strdup
strdup.argstypes = [c_char_p]
strdup.restype = c_char_p

This still crashed in free(). I took a look at some of the ctypes
regression tests and something made me try this:

strdup = libc.strdup
strdup.argstypes = [c_char_p]
strdup.restype = POINTER(c_char) # NOT c_char_p !!!!

This works like a charm. Not sure why though...Does ctypes do
something
special with c_char_p return types? It seems as if Python was freeing
the
result of strdup() when the string was GC'ed, and then PAM would fail
when
trying to free the same memory.

c_char_p implicitly converts to/from a Python string. So strdup with a
c_char_p restype returns a copy of the returned C string as a Python
string instead of a pointer to the memory allocated by strdup. And I
don't know how a Python string is cast to a c_char_p.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,757
Messages
2,569,543
Members
45,026
Latest member
camilin05

Latest Threads

Top