A sim­ple vul­ner­a­bil­ity

Created: Fri Apr 19 00:59:57 CEST 2019

Last mod­i­fied: Fri Apr 19 02:26:01 CEST 2019

After I fi­nally got around one of my first CTF I de­cided to recre­ate a vari­ant of the vul­ner­a­ble pro­gram in C.

The vul­ner­a­ble pro­gram will do the fol­low­ing thing:

  1. Ask for two num­bers, namely x and y
  2. Copy y at an ad­dress that de­pends on x

The fact that the ad­dress of ys copy de­pends on x is a vulnerability.

Our goal is to ex­ploit this vul­ner­a­bil­ity so that the pro­gram runs win(), a func­tion that was writ­ten in the source code but is nor­mally never called by main().

First, down­load and com­pile the vul­ner­a­ble program:

wget https://soykaf.me/test.c
cc ./test.c
# test.c:7:1: warning: control reaches end of non-void function [-Wreturn-type]
# }
# ^
# 1 warning generated.

Possibly with­out tak­ing a look at it. Scroll down if you still want to, there is a sim­pli­fied ver­sion at the bot­tom of the ar­ti­cle so that you can get the idea.

So we now have a vuner­a­ble a.out.

Now onto dis­as­sem­bling it us­ing ob­j­dump(1).

First of all, we need to take note of win()’s ad­dress.

0000000000201350 (win)

So 0x201350, which 2102096 in dec­i­mal.

Now skip­ping to the main pro­ce­dure, we’ll draw a small map of the stack.


Where SFP = Stack Frame Pointer, which we don’t care about here, and RET = Return ad­dress, which is the ad­dress that will be moved in RIP when ret ap­pears in main().

Moving fur­ther down in main(), we no­tice that scanf() is called.

call   201480 ; (scanf)
cmp    eax,0x2
je     2013bc ; (main+0x3c)
; [...]

Before re­turn­ing, scanf() stores a num­ber in EAX, in our case, the number must be 0x2 or else the pro­gram re­turns. It rep­re­sents the number of items that were suc­cess­fully processed from stdin.

Skipping what­ever re­mains un­til the end of the func­tion:

jmp    20138f ; (main+0xf)

We can de­duce from this that main() is en­closed in an in­fi­nite loop.

Ignoring what­ever hap­pens af­ter scanf() is suc­cess­ful; we’re sum­ming up our dis­cov­er­ies:

We can now look at the three re­main­ing in­struc­tions.

mov    eax,DWORD PTR [rbp-0x10]
movsxd rcx,DWORD PTR [rbp-0x8]
mov    DWORD PTR [rbp+rcx*4-0x14],eax  ; <= YOU ARE HERE

Where EAX = y and RCX = x, ac­cord­ing to our stack table.

Simplifying this into a more math-friendly no­ta­tion, we get [rbp + 4x - 0x14] = y.

If you want to ex­per­i­ment in GDB, you can type the fol­low­ing arithmetic for­mula: $rbp + $rcx*4 - 0x14 to sim­u­late.

So ys lo­ca­tion in mem­ory de­pends on x, which is user-sup­plied information. We have con­trol. We can use x to reach any ad­dress (with re­spect to RBP) in writable mem­ory and store y.

What if $rbp+4*$rcx-0x14 is $rbp+0x8? If you re­mem­ber it from our stack table, it’s where the Return ad­dress” is lo­cated. Address which is stored back in RIP on ret (which oc­curs in our code when scanf fails). It was placed there by the call main in­struc­tion. Were we to change this value that the pro­gram would then jump to what­ever ad­dress is stored there when the func­tion ends.

What we need:

What we have:

We’re solv­ing it right now.

equation solved

So let’s try this: x = 7 and y = 2102096.

$ ./a.out
7 2102096

Uh? What’s wrong, noth­ing hap­pens?

Scanf was suc­cess­ful once. It means a whole it­er­a­tion of the main loop just oc­cured. Scanf is called a sec­ond time and is still wait­ing for some in­put. Just type garbage: * for ex­am­ple and scanf() will fail: thus trig­ger­ing win().

7 2102096*
You win!
Segmentation fault

Don’t hes­i­tate to com­ment for clar­i­fi­ca­tion or just for dis­cussing CTFs! Bonus points: shar­ing re­sources is much ap­pre­ci­ated.

Thanks for read­ing <3

Code list­ing


  int x, y, z, k;
  while (1) {
    if (scanf("%d %d", &x, &z) != 2)
      return 1;
    *(int *)(&k + x) = z;

source code