Most ICMP messages can be divided into three categories, queries, replies and errors.
Queries and their corresponding replies have an identifier and sequence number, these can be used to match up replies to the corresponding query. A I understand it in practice ICMP query clients are usually implemented using "raw sockets" to receive all incoming ICMP packets and filter them in the application. This is inefficient but the amounts of traffic involved are too small for anyone to care.
ICMP errors contain a portion of the packet that caused the error (originally the internet header and the first 64 bits of payload, however later RFCs suggested including as much of the original packet as possible to support tunneling scenarios). This portion of the packet that caused the error can be used to pass the error up the stack to the appropriate transport protcol, which in turn can pass it to the appropriate socket.