Your intuition is on the right track: if you run a pseudorandom function in counter mode with your secret key, you get a stream cipher. Some stream ciphers are designed like this, perhaps most notably Salsa20 (and its later variant ChaCha20).
But the key to answering your question, as I see it, is to note that a collision-resistant hash function like SHA-2 is more complicated than what you need for this use:
- You don't need the collision resistance property of cryptographic hash functions.
- Standard cryptographic hash functions are unkeyed; they don't take a secret key as input. You are using a custom construction to incorporate a secret key into your use of a hash function, and that means that you have to ask if your construction is secure, or use a standard construction that has been analyzed to be secure.
- Hash functions (as normally understood) feature compression—they turn input data of arbitrary length into a fixed output size. But you're feeding fixed-size inputs to the hash function.
So what you really want is a pseudorandom function that takes these three fixed-size arguments:
- The key (secret);
- A nonce (not secret);
- A block counter value (not secret);
...and produces a fixed-size keystream block. So if you read for example RFC 7539 ("ChaCha20 and Poly1305 for IETF Protocols") you'll notice that this is precisely the interface of the ChaCha20 block function (section 2.3):
2.3. The ChaCha20 Block Function
The ChaCha block function transforms a ChaCha state by running multiple quarter rounds.
The inputs to ChaCha20 are:
- A 256-bit key, treated as a concatenation of eight 32-bit little-endian integers.
- A 96-bit nonce, treated as a concatenation of three 32-bit little-endian integers.
- A 32-bit block count parameter, treated as a 32-bit little-endian integer.
The output is 64 random-looking bytes.
Or, alternatively, feed the exact same inputs to a block cipher like AES. Block ciphers are pseudorandom permutations (a subset of pseudorandom functions) and thus already keyed, and they work on fixed-size inputs already (they don't feature compression), so they're a good tool for this use.