First, a couple of clarifications
The Import Address Table is a table that only consists of the imported function's addresses. Unlike the
Import Table (which consists of
IMAGE_IMPORT_DESCRIPTORs) that also details where to lookup those imports.
To reiterate: (this is slightly oversimplified) The Import Address Table is an array of the imported functions' actual addresses in memory in the current process. Not divided to modules, no extra information like the ordinal or name. Just an array of the addresses of all imported functions.
IMAGE_IMPORT_DESCRIPTORS are way more structured, clean and partitioned. They are what enables the loader to find the function it's looking for. The "RVAs of the resolved virtual address" as you called them (There are actually two,
FirstThunk, in the
IMAGE_IMPORT_DESCRIPTOR structure) is not pointing to the actual imported function in the remote DLL, but to the location in the Import Address Table of the first item in that
IMAGE_IMPORT_DESCRIPTOR. Subsequent items in the same
IMAGE_IMPORT_DESCRIPTOR are also the following items in the Import Address Table.
As the loader goes over the
IMPORT_TABLE and the different
IMAGE_IMPORT_DESCRIPTORs, it resolves each function and places that function's final address in the function's designated Import Address Table location.
FirstThunk is how the loader knows the Import Address Table location of a function described in
but, why? (AKA answering your questions)
So as you can see, the
FirstThunk only holds one address for multiple imported functions, and it only points to the address (and not holding it directly as your question implies), so we can't easily "use the thunk" instead of the Import Address Table.
As to why you can overwrite the actual content in the IAT for a PE on disk and everything will still work - that was answered implicitly but I'll answer it explicitly as well. The data on disk is the same data as in the
Characteristics structure, the pointers are the same. However that data should not ever be actually read according to Microsoft's own documentation, the IAT is mostly made to be populated during the PE loading process. I would guess the reason it is initialized with the relevant data was kind of a thought of "why the hell not" by the developers.
but still, why?
Now, if you've been following, you should be asking a similar question (and you were, that's what this question is still roughly about):
That structure is still more complicated than it could've been, and the main reason the Import Address Table was created in a condensed manner (instead of being included as addresses in the different
IMPORT_NAME or other similar structures) is actually an optimization.
I'm glad you asked! ;) I'll explain.
As you may know, DLLs are efficiently loaded into the same addresses and use a windows mechanism known as Copy On Write to only actually have a single copy of most Modules. This is also true when Modules are mapped to different Virtual Addresses. However, when the need to populate imported functions for a Module, writes to PE structures are required. No matter where you hold the address, it's gotta be changed. The Import Address Table is there to to only trigger Copy On Write for as few pages as possible (those being the actual Import Address Table pages themselves).
This is, by the way, partially the reason there's that extra level of indirection in the PE to begin with.