Essentially, I just added an array to the ship structure:
int replacement_textures[MAX_MODEL_TEXTURES];
I initialized the array to -1. Then when the mission ran through the parsing routine, I loaded the duplicate textures and assigned their ID numbers to the replacement texture array with a one-to-one correspondence to the textures in the polymodel struct.
In the model render routine, it checks to see if the texture it's currently rendering has a positive (greater than -1) ID number in the associated replacement texture array. If it does, it renders that texture instead of the texture in the polymodel struct.
That's the gist of it. I also made some special fiddly code that would skip completely over the replacement texture check if the object had absolutely no replacement textures whatsoever, in order to optimize the thing for asteroids, debris, jump nodes, and so on.

I changed the model parsing routine to load the substitute textures if it encounters a $Texture Replace entry, and to reskin a duplicate model if it encounters a $Duplicate Model Texture Replace entry. (The format of the two is exactly the same, with the +old and +new lines following the heading.)
You'll need to reskin a duplicate model if you want to substitute animated or transparent textures, but this is probably the optimal solution if you want to add cockpit kill count art, custom ship nameplates, or any stuff like that. Of course, mission designers can experiment on their own to see which method plays better in a given mission.
