5

I'm trying to figure out how to explore structs in IDA, and getting stuck on the basics. I wrote a very basic test app;

int main() {
    const int kBits = 1024;
    const int kExp = 3;

    RSA *rsa = RSA_generate_key(kBits, kExp, 0, 0);
    RSA_print_fp(stdout, rsa, 0);

    RSA_free(rsa);
    return 0;
}

Then I imported the OpenSSL headers, so IDA now understands what RSA* and BIGNUM* are. BIGNUM is actually pointing to bignum_st which is referenced from RSA, which is rsa_st.

Then, dropped a breakpoint in the pseudocode view; enter image description here

Now, if I hover over v3, I see it's parsing the struct. So I want to do something like grab the value of rsa->d->dmax, but there doesn't seem to be a way to do this? I can't seem to find a way to access v3 by name in IDAPython, nor can I seem to 'walk' the structure to get to the int that lives at dmax. Is there some way to do this?

5
  • 1
    Your code works as usual but it raises exception code in IDA+windbg. Can you share the compiled Windows binary?
    – Biswapriyo
    Dec 7, 2018 at 6:43
  • Can I assume what you want to do is to dump the value of v3->d->dmax upon reaching this breakpoint?
    – Pororo
    Dec 7, 2018 at 9:33
  • @Pororo yes, exactly! Dec 7, 2018 at 14:26
  • @Biswapriyo I can't imagine it would work in windows.. I'm doing this under Linux (CentOS 7 if it matters) Dec 7, 2018 at 14:27
  • for some reason I thought this was the writing stack exchange, anyway - +1 good question! will follow this. Jan 18, 2023 at 9:58

3 Answers 3

1

My knowledge of the IDAPython API isn't the most complete, but what I would do is

  1. Read the source (or look in IDA) to find the location (byte offsets) of d in rsa_st and dmax in bignum_st. This is easy since the members of the structures above d and dmax are pointers or int types.
  2. Inspect the disassembly just before the call and determine the location of v3 - from the pseudocode it appears to be stored in the stack. If you are lucky, pressing tab while your cursor is on v3 will take you to some mov operation in disassembly.
  3. When the breakpoint is hit, use a combination of idc.get_reg_value and idc.get_bytes commands (see https://www.hex-rays.com/products/ida/support/idadoc/162.shtml), using the result from step 2 as a starting point.
0

Here are the steps:

  1. Set breakpoint. Run the local debugger with F9. Open Debugger dropdown menu form menu bar and choose "Take memory snapshot". Here is the screenshot:

Take_Memory_Snapshot

  1. Double click on the variables that you want to know (here the RSA* pointer). IDA will take you to the stack view (aka. IDA View-RIP). Do not move the cursor otherwise you will get different value. Press N to name the variable in that stack view. "Rename address" window will pop-up. Here is the screenshot:

Enter_Variable_Name

  1. After naming the variable in that stack view, press Y to add the data type i.e. structure type. For this case it will be RSA, not the pointer because all stack variables placed linearly. Here is the screenshot:

Enter_Data_type

Now you can see all the struct members value.

0
0

"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:

  1. 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)
  2. 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
    • it attempts to load libcrypto.so.3
    • failing that, attempts to load libcrypto.so
    • if successful in either of the preceding steps, it uses dlsym() to fetch the address of the real RSA_generate_key and stores it in real_RSA_generate_key
    • ... also outputs the steps, something like:
      Loaded libcrypto: 0x7f9ad86e3700
              RSA_generate_key == 0x7f9ad8478b50
      
  • 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

  1. write each of the above with the name I gave in the subsection titles into a file
  2. run make (on Debian/Ubuntu you may want to install build-essential, for other systems consult your documentation)
  3. 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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.