Given a byte string S of length l, obtain the string D of length l-1 as D(i) = S(i) * S(i+1)
Inconsistent addressing of d
d is an array of 2-byte elements (d times l dw 0) and mov [d + esi], ax indexes into it with esi as the offset. However, esi is increased by 1 every iteration, so the resulting access pattern is that the stores keep stepping half on top of the previous store, shown by such a diagram: (storing -3, -12 and 28 in sequence)
FD FF
   F4 FF
      1C 00

This is possible to do, but probably unintentional.
Normally my recommendation would be to increase the register by 2 in the loop, but esi is also used to index s so that won't work. You can change the store to double esi though, for example:
mov [d + esi*2], ax

Or,
mov [d + esi + esi], ax

If you change d to be an array of bytes then the store should be mov [d + esi], al, with the original address but storing only 1 byte. Storing overlapping words would put the right data in d, but it's strange and stores a stray byte just after the end of d.
Calling exit
exit performs C library termination procedures, but the C library runtime environment wasn't used or even initialized (this code is in start, not main), so it's not necessary. It makes more sense to call ExitProcess (kernel32.dll), or just return (that does not work on Linux, but this is a Windows program and there it does work).
The loop instruction
loop is not great, it has some niche uses but typically it should be avoided. jecxz by contrast is OK, just unusual.
Minor things
The best way to zero esi isn't mov esi, 0, it's xor esi, esi, unless the flags must not be affected.
The two loads of the current and next element of S could be merged into a single word load, if you want. For example mov ax, [s + esi] followed by imul ah.