"Setting a breakpoint" suggests that you are doing dynamic analysis. Assuming that and assuming this happens on a real system, may I suggest an alternative route for Linux, based on my other assumption that you are linking OpenSSL dynamically?
When on Linux (and a number of other unixoid systems) you can guide the dynamic linker/loader to override a function. If you know Windows and DLLs it could probably be compared a little to DLL placement attacks, but it is more flexible and requires less effort.
NB: Obviously this approach doesn't teach you how about dealing with structs in IDA, but I think it is a very powerful technique to keep in mind. Because it is an alternative approach that doesn't satisfy your constraint of wanting to learn more about dealing with structs in IDA, I am making this a CW.
There are some components for this approach:
- you need a "wrapper" shared object, let's call it
mock_openssl_rsagenkey.so
, exporting RSA_generate_key()
with a binary compatible signature to that from the original OpenSSL shared object (something like libcrypto.so
)
- you need to tell
ld.so
via environment variable to preload mock_openssl_rsagenkey.so
(see below)
ld.so
will take care -- when resolving symbols -- that the preloaded ones take precedence. This is how one of the modes of operation for eFence is implemented (look for LD_PRELOAD
, the man page also lists the exported symbols).
So now your only remaining task would be to turn up the correct prototype for RSA_generate_key
from the libcrypto.so
that your binary uses (try ldd ./your_binary
to list dependencies) and write the wrapper .so
, using dlopen()
and dlsym()
to locate the original RSA_generate_key
symbols from the actual libcrypto.so
Full example
main.c
This is approximately the program you created and are analyzing.
#include <stdio.h>
#include <stdlib.h>
#include <openssl/rsa.h>
int main(int argc, char const ** argv, char const** envp)
{
int const kBits = 1024;
int const kExp = 3;
RSA *rsa = RSA_generate_key(kBits, kExp, 0, 0);
RSA_print_fp(stdout, rsa, 0);
RSA_free(rsa);
return EXIT_SUCCESS;
}
mock_openssl.c
This is the wrapper shared object, which we'll load with LD_PRELOAD
.
#include <dlfcn.h>
#include <openssl/rsa.h>
typedef RSA* (*RSA_generate_key_t)(int, unsigned long, void (*)(int, int, void *), void *);
static RSA_generate_key_t real_RSA_generate_key = 0;
static void* libcrypto = 0;
static void init() __attribute__((constructor));
static void fini() __attribute__((destructor));
void init()
{
if (!libcrypto)
{
libcrypto = dlopen("libcrypto.so.3", RTLD_NOW);
if (!libcrypto)
{
libcrypto = dlopen("libcrypto.so", RTLD_NOW);
}
fprintf(stderr, "Loaded libcrypto: %p\n", libcrypto);
if (libcrypto)
{
real_RSA_generate_key = (RSA_generate_key_t)dlsym(libcrypto, "RSA_generate_key");
}
fprintf(stderr, "\tRSA_generate_key == %p\n", (void*)real_RSA_generate_key);
}
}
void fini()
{
if (libcrypto)
{
dlclose(libcrypto);
libcrypto = 0;
real_RSA_generate_key = 0;
}
}
RSA* RSA_generate_key(int bits, unsigned long e, void (*callback)(int, int, void *), void *cb_arg)
{
if (!real_RSA_generate_key)
{
fprintf(stderr, "FATAL: Have no function pointer for original RSA_generate_key()\n");
return 0;
}
fprintf(stderr, "Calling real_RSA_generate_key(%i, %lu, %p, %p)\n", bits, e, callback, cb_arg);
RSA* ret = real_RSA_generate_key(bits, e, callback, cb_arg);
/* do something with ret here */
if (ret)
{
fprintf(stderr, "Returned RSA key: %p ... I can do what I want with it ... MUHAHAHA!\n", ret);
}
return ret;
}
Explanation:
RSA_generate_key_t
parrots the prototype of the original function as typedef
real_RSA_generate_key
is going to hold the function pointer retrieved from the real libcrypto.so
libcrypto
is going to hold the handle to the loaded libcrypto.so
init()
-- declared as constructor -- will run when our wrapper .so
gets loaded
finit()
-- declared as destructor -- will run when our wrapper .so
gets unloaded
RSA_generate_key()
is our wrapper to the original function and will be called in place of the one from libcrypto.so
by a program that preloads this .so
GNUmakefile
The GNU make
recipes to build our stuff.
CC:=gcc
CFLAGS:=$(strip -Wno-deprecated-declarations $(CFLAGS))
all: bin so
bin: your_binary
so: mock_openssl_rsagenkey.so
bin: LDLIBS=-lcrypto
your_binary: main.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
so: CFLAGS=-shared -fPIC
so: LDFLAGS=-Wl,-soname,libcrypto.so
so: LDLIBS=-ldl
mock_openssl_rsagenkey.so: mock_openssl.c
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
.PHONY: all so bin
Steps
- write each of the above with the name I gave in the subsection titles into a file
- run
make
(on Debian/Ubuntu you may want to install build-essential
, for other systems consult your documentation)
- invoke, e.g. with this
env LD_PRELOAD=./mock_openssl_rsagenkey.so ./your_binary
Output (excerpt)
$ LD_PRELOAD=./mock_openssl_rsagenkey.so ./your_binary
Loaded libcrypto: 0x7f9ad86e3700
RSA_generate_key == 0x7f9ad8478b50
Calling real_RSA_generate_key(1024, 3, (nil), (nil))
Returned RSA key: 0x55d1bfc40c10 ... I can do what I want with it ... MUHAHAHA!
RSA Private-Key: (1024 bit, 2 primes)
modulus:
00:d2:85:86:68:fb:1f:b0:92:c7:dd:09:08:7a:39:
21:7e:74:27:36:08:23:0d:1f:6c:7a:ec:47:5d:fc:
27:c9:95:c2:a4:e1:9f:99:1b:3f:d9:f8:88:65:30:
93:c6:7d:2b:31:9b:b1:cb:5c:5a:b8:7a:20:c0:4b:
63:25:c4:3a:30:c3:81:16:56:28:ac:f7:74:93:6b:
93:64:db:c9:d5:0f:64:f8:15:b4:0d:18:1d:86:c1:
60:4b:5a:2f:2f:b7:fb:90:03:13:d5:be:1e:05:05:
0e:91:54:79:5b:58:2f:02:de:0a:1f:ef:06:a4:0a:
28:e4:55:3f:31:9d:a9:26:53
publicExponent: 3 (0x3)
privateExponent:
...
Explanation:
- Lines 1..2 are from
init()
- Lines 3..4 are from
RSA_generate_key()
in our wrapper .so
- NB: this shows you have full access to all the arguments! While it may be a little more intricate to do, it would also clearly be possible to wrap the callback mechanism provided by OpenSSL into our own, e.g.:
- prior to calling
real_ RSA_generate_key()
, allocate a struct holding original callback
pointer and cb_arg
- pass our own callback function
- inside our callback function unwrap the struct and pass arguments to the original callback if and as needed
- after
real_ RSA_generate_key()
clean up the allocated struct
The above as Base64-encoded .tar.xz
file for convenience:
/Td6WFoAAATm1rRGAgAhARwAAAAQz1jM4Cf/BCJdACOThvK+tJ2QH6+woi2u3zkiEOlUoV1cX9AZ
+FC3A1WH7iUCMxAdurFVdxDDlniCFn58+KgLrhSGdLxvJiWTMjuOBhIYwKHirauzr5++28b5NTbw
5OiW8LJArEPs+IWgu/EuT61aIKGLFonGy9apeCHMMEzdN9wUILkS5CNSFe5L1gKJCeVZ2PTWGqzA
yu0nBvlpA0e7H81rA/msgoO1us3mbbQkkYuZ0lOkt5t5qn8H7c7oZe/gip3oyXmFlfJsscG2OmjK
KmKWZbFLgb3EQuM0929MRGATHYCl5McORvugai2AeHPJRTLVTKrZC2rYj9buuEY5PH6ufW1b8NT1
klXJkPU0gfOhGsEVcYQUMmp56aXGqzp6kq6xhAcIH+U+oEbbZBAC9QSSyFMH40nmRrPUOfIHnwhn
o/QclEtbierPsRwJMLjXS5uuz7IP1zxdOtshXJUDfmkXhj7/AIOCoLJ1PTWOIW4wq54yFSokdBUA
lbzUk/VluIqyIx0D81HMRU2qJuptH2c28ij92+c7nHcH7M+WncwXoUPSZL2M31SUvS7DsFQkgqbM
beWSW2XgnV9vSKGomuCUY5vT671UD7hYddEuoR+vtJdEnEXENHfIu6rmK/3Zg9UwzYrHIMvkxm9B
l1rxP+4C3x3Sy4qBzBup44Q7p62bVMG3CiSD2ZCkktQgeQFyPklGJLw3KriMexMoLc4eSA0EwfhU
rr33b6n5uW3JN3pJ8IfwRW+KXS+DI0wnroLCGHdwzfXDhFPSMTaeFL4oICzQoX6gyPjLWRPciwZR
g/2fZp9xA25j/6YP1M8i8eUcckB3Z2ispYtTsDa+D2osEcK4JAtHaDCJ/6gH3wSIJh3Dx5upaHz1
0wQDQkS4T4O3Igf7B4wigAHWSHIih8Cl9bN8L36S5ZC58o3VeMgsLpLUkcRhf0bl6dhD113rJkqO
G5GTlyp13YB7U2pynt9EY4TfVqCj7NvEAXHpt67MmNSqwl4c6Eslm/XplS0gTzKbP1tZo70AvkYO
oks0eYGhmE6HmcwwolqNJ3UQWkmPeUrBA5WHILHDvS3fhZkuoRgSJ+swyez8oKtOo5Y6sxGH1yDH
0Bt5SeDmk6wErTqUb+YTDKQw1wMzY3DdeUSOny6PJumC/D9HBwmIehhDB7YxMVaQ2jcW02//bngq
J+zcjmBbjvKhMSOcCBVAPD7EAYyB6uerSAg4DrpsUkyYAw8+49lYe6STGKpy8OOt4OXah6WnG8/E
GsbYlHm1RfJuMIgfYRU0+O+//xipV/q5EF/FzaQoDYuV2mPsx6TvYAganEUh78E4P9+GXBC8FsPE
ZjUrahpKmyLFm9zVmN0l3BYa20EpXmujuePO/51T3TZlxgcf5fjV87vF7BBmj8vcaVe+U02IcB1M
Z6OAl1cAAAAAtSETkCuEMZwAAb4IgFAAAJJ1AFKxxGf7AgAAAAAEWVo=
Telling ld.so
to preload the wrapper .so
Bash and some other shells will be contend to do it with:
LD_PRELOAD=/path/to/your/mock_openssl_rsagenkey.so ./your_binary
but the more portable way is:
env LD_PRELOAD=/path/to/your/mock_openssl_rsagenkey.so ./your_binary
which uses /usr/bin/env
-- instead of relying on a shell-builtin facility 00 to set the environment variable for a single invocation.
Obviously you could also at the prompt ($
signifies it!) to:
$ export LD_PRELOAD=/path/to/your/mock_openssl_rsagenkey.so
$ ./your_binary
or on some less comfortable (or older) shells:
$ LD_PRELOAD=/path/to/your/mock_openssl_rsagenkey.so
$ export LD_PRELOAD
$ ./your_binary
Last, but not least, you could create a little wrapper script, which we'll name your_binary.sh
based on your_binary
:
#!/usr/bin/env bash
export LD_PRELOAD=/path/to/your/mock_openssl_rsagenkey.so
exec "${0%.sh}" "$@"
#!/usr/bin/env bash
is the most portable hashbang, compared to hardcoding the path to a particular bash
binary
export LD_PRELOAD=/path/to/your/mock_openssl_rsagenkey.so
exports LD_PRELOAD
as environment variable with the shown value
exec "${0%.sh}" "$@"
:
"${0%.sh}"
strips the .sh
suffix, yielding the name without it
"$@"
expanding to the quoted arguments passed to our script (e.g. "$1" "$2"
...)
exec
isn't strictly necessary here, but I prefer it for these small wrapper scripts; it replaces the current shell with the given command
v3->d->dmax
upon reaching this breakpoint?