Calling Conventions

A more in-depth look into parameters for 32-bit and 64-bit programs

One Parameter

Calling Conventions - One Parameter

Source

Let's have a quick look at the source:

#include <stdio.h>

void vuln(int check) {
    if(check == 0xdeadbeef) {
        puts("Nice!");
    } else {
        puts("Not nice!");
    }
}

int main() {
    vuln(0xdeadbeef);
    vuln(0xdeadc0de);
}

Pretty simple.

If we run the 32-bit and 64-bit versions, we get the same output:

Just what we expected.

Analysing 32-bit

Let's open the binary up in radare2 and disassemble it.

If we look closely at the calls to sym.vuln, we see a pattern:

We literally push the parameter to the stack before calling the function. Let's break on sym.vuln.

The first value there is the return pointer that we talked about before - the second, however, is the parameter. This makes sense because the return pointer gets pushed during the call, so it should be at the top of the stack. Now let's disassemble sym.vuln.

Here I'm showing the full output of the command because a lot of it is relevant. radare2 does a great job of detecting local variables - as you can see at the top, there is one called arg_8h. Later this same one is compared to 0xdeadbeef:

Clearly that's our parameter.

So now we know, when there's one parameter, it gets pushed to the stack so that the stack looks like:

Analysing 64-bit

Let's disassemble main again here.

Hohoho, it's different. As we mentioned before, the parameter gets moved to rdi (in the disassembly here it's edi, but edi is just the lower 32 bits of rdi, and the parameter is only 32 bits long, so it says EDI instead). If we break on sym.vuln again we can check rdi with the command

Just dr will display all registers

Awesome.

Registers are used for parameters, but the return address is still pushed onto the stack and in ROP is placed right after the function address

Multiple Parameters

Calling Conventions - Multiple Parameters

Source

32-bit

We've seen the full disassembly of an almost identical binary, so I'll only isolate the important parts.

It's just as simple - push them in reverse order of how they're passed in. The reverse order becomes helpful when you db sym.vuln and print out the stack.

So it becomes quite clear how more parameters are placed on the stack:

64-bit

So as well as rdi, we also push to rdx and rsi (or, in this case, their lower 32 bits).

Bigger 64-bit values

Just to show that it is in fact ultimately rdi and not edi that is used, I will alter the original one-parameter code to utilise a bigger number:

If you disassemble main, you can see it disassembles to

movabs can be used to encode the mov instruction for 64-bit instructions - treat it as if it's a mov.

Last updated

Was this helpful?