The most likely explanation for the value of i not changing is that the compiler is loading i into a register when it assigns "canary = i" and that it does not use that register again before it needs to check "if (canary != i)". In the C code, i is not used again so there is no reason to copy the parameter off the stack again unless it needs to use the register for something else. The call to strcpy() won't affect it because it will push the registers onto the stack before the function call and pop them back off when it returns.
Note: when I talk about the compiler loading a variable into a register, I'm talking about the assembly language or machine instructions the compiler generates during compilation.
So, the code for "canary = i" might load canary into register ebx. Then the program calls strcpy after which canary is still in the ebx register. Then it checks the value of i against canary ("if (canary!=i)") by comparing to the ebx register without copying canary from the stack a second time. If this is the case, you can overwrite canary on the stack with impunity because the compiler optimized away the security check. If you overwrite both i and canary, you're better off if the compiler doesn't keep canary in a register so that you can overwrite both of them with a new value.
You can also beat the canary check without changing i because the canary value isn't very random. Look at the code below:
i = rand() % 10;
The canary is set to a value between 0 and 9. So, when you do the overflow, you can overwrite that byte with any value between 1 and 9 and have a 1/10 chance of it working. You can't overwrite it with 0 unless you want strcpy() to terminate on that byte. I'd just overwrite it with a 1 and try the exploit until 1 turns out be a correct guess.
The better way to implement canaries is in the compiler itself so that a canary is automatically added into the stack frame for each function and checked on return. There are ways to beat this as well (if you can guess/reproduce the canary or direct a pointer right at the saved instruction pointer thus bypassing the canary entirely), but it's stronger and may prevent exploitation entirely (depending on the circumstances).
While I don't think it's happening here, there is another reason for you not to be able to change a passed parameter with a buffer overflow. In some cases, parameters are passed in registers in which case you cannot modify i with a buffer overflow. This is common when compiling 64-bit code since extra registers are available to the compiler. I don't think you'll see this in 32-bit code unless the function is declared with fastcall:
__attribute__((fastcall)) int vulnerable(char *arg, unsigned char i);
I hope this helps.
Edit: cleaned this up to make a little more sense.
Last edited by unicityd
on Tue Dec 13, 2011 1:31 am, edited 1 time in total.
BS in IT: Security, CISSP, CEH. MBA in progress.