|Informative Information for the Uninformed
Next: Determining Per-byte Durations Up: Temporal Return Addresses Previous: Introduction   Contents
In order to make use of temporal addresses it is first necessary to devise a method of locating them. To begin this search it is necessary that one understand the attributes of a temporal address. All temporal addresses are defined as storing a time-associated counter that increments at a constant interval. For instance, an example would be a location in memory that stores the number of seconds since Jan. 1, 1970 that is incremented every second. As a more concrete definition, all time-associated counters found in memory are represented in terms of a scale (the unit of measure), an interval or period (how often they are updated), and have a maximum storage capacity (variable size). If any these parts are unknown or variant for a given memory location, it is impossible for an attacker to consistently leverage it for use as time-bounded return address because of the inability to predict the byte values at the location for a given period of time.
With the three major components of a temporal address identified (scale, period, and capacity), a program can be written to search through a process' address space with the goal of identifying regions of memory that are updated at a constant period. From there, a scale and capacity can be inferred based on an arbitrarily complex set of heuristics, the simplest of which can identify regions that are storing epoch time. It's important to note, though, that not all temporal addresses will have a scale that is measured as an absolute time period. Instead, a temporal address may simply store the number of seconds that have passed since the start of execution, among other scenarios. These temporal addresses are described as having a scale that is simply equivalent to their period and are for that reason referred to as counters.
To illustrate the feasibility of such a program, the author has implemented an algorithm that should be conceptually portable to all platforms, though the implementation itself is limited to Windows NT+. The approach taken by the author, at a high level, is to poll a process' address space multiple times with the intention of analyzing changes to the address space over time. In order to reduce the amount of memory that must be polled, the program is also designed to skip over regions that are backed against an image file or are otherwise inaccessible.
To accomplish this task, each polling cycle is designed to be separated by a constant (or nearly constant) time interval, such as 5 seconds3.1. The granularity of this period of time is measured in nanoseconds in order to support high resolution timers that may exist within the target process' address space. This allows the program to detect timers measured in nanoseconds, microseconds, milliseconds, and seconds. The purpose of the delay between polling cycles is to give temporal address candidates the ability to complete one or more update periods. As each polling cycle occurs, the program reads the contents of the target process' address space for a given region and caches it locally within the scanning process. This is necessary for the next phase.
After at least two polling cycles have completed, the program can compare the cached memory region differences between the most recent view of the target process' address space and the previous view. This is accomplished by walking through the contents of each cached memory region in four byte increments to see if there is any difference between the two views. If a temporal address exists, the contents of a the two views should have a difference that is no larger than the maximum period of time that occurred between the two polling cycles3.2. For instance, if the polling cycle period was 5 seconds, any portion of memory that changed by more than 5 seconds, 5000 milliseconds, or 5000000 microseconds is obviously not a temporal address candidate. To that point, any region of memory that didn't change at all is also most likely not a temporal address candidate, though it is possible that the region of memory simply has an update period that is longer than the polling cycle.
Once a memory location is identified that has a difference between the two views that is within or equal to the polling cycle period, the next step of analysis can begin. It's perfectly possible for memory locations that meet this requirement to not actually be timers, so further analysis is necessary to weed them out. At this point, though, memory locations such as these can be referred to as temporal address candidates. The next step is to attempt to determine the period of the temporal address candidate. This is accomplished by some rather silly, but functional, logic.
First, the delta between the polling cycles is calculated down to nanosecond granularity. In a best case scenario, the granularity of a polling cycle that is spaced apart by 5 seconds will be 5000000000 nanoseconds. It's not safe to assume this constant though, as thread scheduling and other non-constant parameters can affect the delta between polling cycles for a given memory region. The next step is to iteratively compare the difference between the two views to the current delta to see if the difference is greater than or equal to the current delta. If it is, it can be assumed that the difference is within the current unit of measure. If it's not, the current delta should be divided by 10 to progress to the next unit of measure. When broken down, the progressive transition in units of measurement is described in figure .
Once a unit of measure for the update period is identified, the difference is divided by the current delta to produce the update period for a given temporal address candidate. For example, if the difference was 5 and the current delta was 5, the update period for the temporal address candidate would be 1 second (5 updates over the course of 5 seconds). With the update period identified, the next step is to attempt to determine the storage capacity of the temporal address candidate.
In this case, the author chose to take a shortcut, though there are most certainly better approaches that could be taken given sufficient interest. The author chose to assume that if the update period for a temporal address candidate was measured in nanoseconds, then it was almost certainly at least the size of a 64-bit integer3.3. On the other hand, all other update periods were assumed to imply a 32-bit integer3.4.
With the temporal address candidate's storage capacity identified in terms of bytes, the next step is to identify the scale that the temporal address may be conveying (the timer's unit of measure). To accomplish this, the program calculates the number of seconds since 1970 and 1601 between the current time minus at least equal the polling cycle period and the current time itself. The temporal address candidate's current value (as stored in memory) is then converted to seconds using the determined update period and then compared against the two epoch time ranges. If the candidate's converted current value is within either epoch time range then it can most likely be assumed that the temporal address candidates's scale is measured from epoch time, either from 1970 or 1601 depending on the range it was within. While this sort of comparison is rather simple, any other arbitrarily complex set of logic could be put into place to detect other types of time scales. In the event that none of the logic matches, the temporal address candidate is deemed to simply have a scale of a counter (as defined previously in this chapter).
Finally, with the period, scale, and capacity for the temporal address candidate identified, the only thing left is to check to see if the three components are equivalent to previously collected components for the given temporal address candidate. If they differ in orders of magnitude then it is probably safe to assume that the candidate is not actually a temporal address. On the other, consistent components between polling cycles for a temporal address candidate are almost a sure sign that it is indeed a temporal address.
When everything is said and done, the program should collect every temporal address in the target process that has an update period less than or equal to the polling cycle period. It should also have determined the scale and size of the temporal address. When run on Windows against a program that is storing the current epoch time since 1970 in seconds in a variable every second, the following output is displayed:
C:\>telescope 2620 [*] Attaching to process 2620 (5 polling cycles)... [*] Polling address space........ Temporal address locations: 0x0012FE88 [Size=4, Scale=Counter, Period=1 sec] 0x0012FF7C [Size=4, Scale=Epoch (1970), Period=1 sec] 0x7FFE0000 [Size=4, Scale=Counter, Period=600 msec] 0x7FFE0014 [Size=8, Scale=Epoch (1601), Period=100 nsec]
This output tells us that the address of the variable that is
storing the epoch time since 1970 can be found at
0x0012FF7C and has an update period of one second. The
other things that were found will be discussed later in this