It'll be most portable, easiest to write, and easiest to maintain will be to do it in parts.
\b(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b
This will only match valid IP addresses plus split each part out for further inspection
if (int(group[1]) != 224 and (int(group[1]) != 0 or int(group[2]) != 0 or int(group[3]) != 0 or int(group[4]) != 0) ...
Regex isn't so good at "but not" operations.
But if you insist on having it as a single regex, then just enumerate all the valid options.
\b(25[0-5]|2[0134][0-9]|22[0-35-9]|1[0-9][0-9]?|[2-9][0-9]|[2-9])
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b
... which isn't perfect because it only tests that the first octet is not 224 or 0. If you want a full test against 0.0.0.0 then you'll have to reproduce the above 15 times recognizing all combinations where at least one octet is non-zero.
And of course you'll want to write a test to ensure you're returning the correct answer for all 4 billion combinations. Shouldn't take too long to run... ;-)