Transaction time manipulation in the Hyperledger Fabric blockchain

smart contracts blockchain Hyperledger Fabric. So I'll be the first. I have been researching the security of this blockchain for a year. And today I want to talk about a rather serious problem: transaction time manipulation. Consider:

  • how an attacker can manipulate transaction time;

  • what financial consequences an attack could lead to (using the example of the concept of a fictitious vulnerable smart contract simulating a digital financial asset);

  • what methods of protection do I offer?

We will also discuss why correct protection against an attack may require not only changing the smart contract, but also establishing interaction between the smart contract operation team and network administrators. The article assumes at least a basic level of familiarity of the reader with Hyperledger Fabric.

What is the problem

There are functions that can be used in a smart contract to obtain the transaction time. At the same time, the time is determined on the client side and is not checked for validity in any way. What can you learn from descriptions of the GetTxTimestamp() function And GetHistoryForKey(). The problem is known at least since 2019. Made for determinism. Determinism assumes that the results of smart contract execution are the same for different peer nodes. For this reason, in the Hyperledger Fabric blockchain, instead of the client’s time, it is impossible to take and record the time of a peer node with an accuracy of seconds – the time of nodes may differ (especially if the exact time is not ensured on all nodes). However, no warning is provided about the potential consequences of using the above functions. Which is quite strange, considering that GetHistoryForKey() is specified in detail the problem of phantom reads and ways to prevent the problem Those. One would also expect a mention of the lack of time verification received from the client. Moreover, in one of the projects from the official Hyperledger examples (Fabric samples) use GetTxTimestamp().

Example attack

I decided to show the attack on a very simplified fictional concept of a digital financial asset (since Hyperledger Fabric used incl. for issuing digital financial assets).

Let's imagine that there is a smart contract that allows the client to invest funds at 20% per annum. Consider a vulnerable smart contract time_insecure.go
Via function Stake_insecure() the time of the transaction (i.e. the time the deposit started) and the size of the deposit are recorded. Function CheckDividents_insecure() shows how much funds have been accumulated with interest at the time of calling this function (when calculating the accumulated funds, it calculates the elapsed time as the difference between GetTxTimestamp() when calling CheckDividents_insecure() itself and GetTxTimestamp() when calling Stake_insecure() ). The correctness of the calculation of dividends defined in Stake_insecure() can be checked using the debugging function CalcDividents(): it will return the calculation of dividends based on the initial conditions: the size of the deposit and the days that have passed since the deposit. Debug function subtractTimestamp() will show how much time has passed between the call to Stake_insecure() and the client's current system time (dividends are calculated based on this value).

Figure 1 shows the stages of the attack:

  • We check the current date of the client (16.06.2024);

  • make a deposit of 10,000 (via calling Stake_insecure());

  • By calling CheckDividents_insecure() we see that there are no dividends (the amount to be withdrawn is equal to the amount of the initial investment – 10,000);

  • we check the time difference by calling the subtractTimestamp() debugging function (we see that less than 2 minutes have passed);

  • We change the system time on the client a year ahead (now 17.06.2025);

  • we make sure that the time difference has changed significantly (returns 8779 hours);

  • call CheckDividents_insecure() and we see the amount to be withdrawn – 12,000

That. in a couple of minutes of action, the attacker received 20% more funds than he should have.

Figure 1. Time spoofing to carry out a financial attack on a vulnerable smart contract

Figure 1. Time spoofing to carry out a financial attack on a vulnerable smart contract

This attack variant has a peculiarity: the spoofed time must not exceed the validity of the certificate, otherwise an error will occur. Figure 2 shows that the certificate expires on 06/03/2034. When setting the time to “2035-06-10” an error occurs. When you set the time to “2034-06-02” the error disappears. There may be a way around this limitation. But that was not the purpose of the article. It is hardly worth considering shortening the validity period of a certificate as a security option.

Figure 2. Error when setting time later than certificate expiration date

Figure 2. Error when setting time later than certificate expiration date

I have come across solutions where it was not the users themselves who called GetTxTimestamp(), but the function was called from a third-party service (Figure 3). But this is not a solution to the problem. Maximum – reduces the level of danger. Because there is no certainty that the time on the service will not be lost (accidentally, due to an attack or dead battery on the motherboard).

Figure 3. The service calls GetTxTimestamp()

Figure 3. The service calls GetTxTimestamp()

Time manipulation in GetHistoryForKey()

Description of the GetHistoryForKey() function, in my opinion, is quite confusing: on the one hand, we see the same mention of a timestamp provided by the client. With another – mentioned ordering according to block height and transaction height within a block, starting with Fabric v2.0. Now let's figure out what this means. I tested the behavior on Hyperledger Fabric v2.5.5. In time_insecure.go the GetHistoryForKey() function is used in getHistory(). And it is needed to obtain data about a previously made entry via Stake_insecure(). Through the call to Stake_insecure(), I wrote down 3 different values ​​sequentially: 10,000, 20,000, 30,000. At the same time, the dates on the client were set sequentially before each call to Stake_insecure(): 2025-06-16; 2024-06-16; 2026-06-16.
As you can see in Figure 4, GetHistoryForKey() is also susceptible to time manipulation on the client side. In this case, the sequence of values ​​is located in the correct chronological order: the most recent entry comes first – i.e. sorted according to the block height, as indicated in the function description (the transaction height was not used, since each transaction ended up in a separate block).

Figure 4 Time manipulation when calling GetHistoryForKey()

Figure 4 Time manipulation when calling GetHistoryForKey()

I did not put any business logic into using GetHistoryForKey() in time_insecure.go. It is used here only as another function susceptible to the attack being discussed. The function displays the history of changes in the “amount” variable. In this case, the most recent change is the current value of “amount”.

Existing solutions

I could find only one ready-made option (3 years ago) – TimeFabric (article, sources). According to the description, the option is a patch of the Hyperledger Fabric source code. Claimed to be suitable for versions 1.4 and 2.0. Those. Before use, the deployed blockchain must be patched. And in the future there may be a need to patch the blockchain when updating it. Apparently, the project is no longer supported. Which raises the question regarding the possibility of using it on blockchain versions released over the last 3 years.

Protection options I offer

My solution options do not require blockchain patching because… are based on a smart contract. Options: comparing time with a time server and with the local time of the computer (where the smart contract is). Both described options work on versions 2.5.5 and 3.0.0-beta. Among the disadvantages, we can note the client’s requirement for a correctly set time (within a certain confidence interval). Otherwise, the client's transaction will be rejected.

At first glance, it may seem that both options violate the principle of distribution (+ a single point of failure appears): each peer node must produce a result independently of the others, and not depend on a single time source. But in general this is not the case. Local time on different peer nodes is generally set independently and can be synchronized with different time servers. Setting up various independent time sources on the peer nodes themselves is an organizational issue.

Regarding the time server – smart contracts can be configured to use different servers (they will have different packageID, but the same chaincode definition). That’s exactly what I did: I installed a smart contract on each peer node, in which everything was identical except the server addresses.

Comparing transaction times with a time server (NTP)

Let's take a look at the code from time_secure_ntp.go. I'm using the package “github.com/beevik/ntp” to get the exact time from the server NTP. Next, in functions Stake_secure_ntp() And CheckDividents_secure_ntp() I check that the time from the client differs from the time of the NTP server by no more than 300 seconds (the value was chosen only based on business logic: dividends are awarded for the full past 24 hours; perhaps in specific implementations of the blockchain architecture and its business logicians need to pay more attention to determining possible time deviations). Figure 5 shows that the same attacker sequence did not lead to success: the “Wrong time” error appeared. Therefore, the attacker returned the time back (after which the error disappeared).

Figure 5: Using an NTP server query prevents transaction time manipulation

Figure 5: Using an NTP server query prevents transaction time manipulation

The advantage of the solution is that there is no need to monitor the time accuracy on the nodes of the blockchain network. The main problem with this approach is that NTP traffic is susceptible to man-in-the-middle attacks. Here organizational aspects of traffic protection already emerge. For example, using a VPN between the client and the NTP server (i.e. you need your own NTP server, a public one is not suitable). An option to solve this problem is to use NTS.

Comparing transaction times with a time server (NTS)

time_secure_nts.go is almost a copy of the previous version. The interaction protocol has been changed to a more secure NTS. Used this package. The result of the functions is the same as with NTP.

Figure 6: Using an NTS server query prevents transaction time manipulation

Figure 6: Using an NTS server query prevents transaction time manipulation

NTS, compared to NTP, does not require additional traffic protection to counter a man-in-the-middle attack. Of the nuances: it was not possible to find public NTS servers in Russia (which may be important for some organizations in light of the geopolitical situation). But, a good practice is to create your own local time server.

Comparing transaction time with OS system time

If you are sure that the correct time is set on peer nodes (for example, there is a special software mechanism that monitors the correctness of time at a given frequency and turns off the node in case of deviations that cannot be corrected automatically), you can compare the transaction time with the system time of the peer node. The corresponding code is given in time_secure_localtime.go. The results of the security check are generally identical to the previous scenario (see Figure 7).

Figure 7: Using local node time prevents transaction time manipulation

Figure 7: Using local node time prevents transaction time manipulation

With this approach, it is necessary to remember not only about the above-mentioned control of the correctness of local time, but also to know where this time comes from. If the time source is an NTP server, we have the same problem with time substitution due to a man-in-the-middle attack. The problem is that smart contract developers are not always aware of the source of time (especially if the smart contract is custom-made for another organization).

conclusions

Using GetTxTimestamp() or GetHistoryForKey() requires additional time verification received from the client. In the example considered, the client was able to carry out a financial attack: he received a profit clearly not in the time that the developers expected. Protection against time manipulation is, in general, a non-trivial task. During development, in addition to the above changes to the smart contract itself, it may be necessary to interact with the operating team in order to determine the appropriate time tolerance (suitable for the specific business logic of the application and its architecture) between the reference and the user transaction. And also, to determine a secure time source on which the smart contract will rely (is there a secure NTP server whose traffic will not be spoofed? Is it possible to set up a local NTS server? Or rely on the time on the peer servers themselves, which accurately is it safe to receive it?). For these same reasons, it is not a trivial task for a team of source code security researchers to recommend a fix for a problem (missing transaction timing checks).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *