Commit 83c3410267d1383b36e0928d994fefdd634f1a1d
1 parent
29851c2f
Fix remote buffer overflow vulnerability on write requests
Protects against crafted write requests with a large quantity but a small byte count. If address + quantity was in the mapping space of the server and quantity greater than the response size, it was possible to crash the server. The sleep/flush sequence improves the handling of following requests.
Showing
3 changed files
with
162 additions
and
77 deletions
src/modbus.c
| ... | ... | @@ -718,6 +718,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 718 | 718 | "Illegal nb of values %d in read_bits (max %d)\n", |
| 719 | 719 | nb, MODBUS_MAX_READ_BITS); |
| 720 | 720 | } |
| 721 | + _sleep_response_timeout(ctx); | |
| 722 | + modbus_flush(ctx); | |
| 721 | 723 | rsp_length = response_exception( |
| 722 | 724 | ctx, &sft, |
| 723 | 725 | MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); |
| ... | ... | @@ -749,6 +751,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 749 | 751 | "Illegal nb of values %d in read_input_bits (max %d)\n", |
| 750 | 752 | nb, MODBUS_MAX_READ_BITS); |
| 751 | 753 | } |
| 754 | + _sleep_response_timeout(ctx); | |
| 755 | + modbus_flush(ctx); | |
| 752 | 756 | rsp_length = response_exception( |
| 753 | 757 | ctx, &sft, |
| 754 | 758 | MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); |
| ... | ... | @@ -778,6 +782,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 778 | 782 | "Illegal nb of values %d in read_holding_registers (max %d)\n", |
| 779 | 783 | nb, MODBUS_MAX_READ_REGISTERS); |
| 780 | 784 | } |
| 785 | + _sleep_response_timeout(ctx); | |
| 786 | + modbus_flush(ctx); | |
| 781 | 787 | rsp_length = response_exception( |
| 782 | 788 | ctx, &sft, |
| 783 | 789 | MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); |
| ... | ... | @@ -812,6 +818,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 812 | 818 | "Illegal number of values %d in read_input_registers (max %d)\n", |
| 813 | 819 | nb, MODBUS_MAX_READ_REGISTERS); |
| 814 | 820 | } |
| 821 | + _sleep_response_timeout(ctx); | |
| 822 | + modbus_flush(ctx); | |
| 815 | 823 | rsp_length = response_exception( |
| 816 | 824 | ctx, &sft, |
| 817 | 825 | MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); |
| ... | ... | @@ -842,6 +850,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 842 | 850 | "Illegal data address %0X in write_bit\n", |
| 843 | 851 | address); |
| 844 | 852 | } |
| 853 | + _sleep_response_timeout(ctx); | |
| 854 | + modbus_flush(ctx); | |
| 845 | 855 | rsp_length = response_exception( |
| 846 | 856 | ctx, &sft, |
| 847 | 857 | MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS, rsp); |
| ... | ... | @@ -870,6 +880,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 870 | 880 | fprintf(stderr, "Illegal data address %0X in write_register\n", |
| 871 | 881 | address); |
| 872 | 882 | } |
| 883 | + _sleep_response_timeout(ctx); | |
| 884 | + modbus_flush(ctx); | |
| 873 | 885 | rsp_length = response_exception( |
| 874 | 886 | ctx, &sft, |
| 875 | 887 | MODBUS_EXCEPTION_ILLEGAL_DATA_ADDRESS, rsp); |
| ... | ... | @@ -884,7 +896,21 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 884 | 896 | case MODBUS_FC_WRITE_MULTIPLE_COILS: { |
| 885 | 897 | int nb = (req[offset + 3] << 8) + req[offset + 4]; |
| 886 | 898 | |
| 887 | - if ((address + nb) > mb_mapping->nb_bits) { | |
| 899 | + if (nb < 1 || MODBUS_MAX_WRITE_BITS < nb) { | |
| 900 | + if (ctx->debug) { | |
| 901 | + fprintf(stderr, | |
| 902 | + "Illegal number of values %d in write_bits (max %d)\n", | |
| 903 | + nb, MODBUS_MAX_WRITE_BITS); | |
| 904 | + } | |
| 905 | + /* May be the indication has been truncated on reading because of | |
| 906 | + * invalid address (eg. nb is 0 but the request contains values to | |
| 907 | + * write) so it's necessary to flush. */ | |
| 908 | + _sleep_response_timeout(ctx); | |
| 909 | + modbus_flush(ctx); | |
| 910 | + rsp_length = response_exception( | |
| 911 | + ctx, &sft, | |
| 912 | + MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); | |
| 913 | + } else if ((address + nb) > mb_mapping->nb_bits) { | |
| 888 | 914 | if (ctx->debug) { |
| 889 | 915 | fprintf(stderr, "Illegal data address %0X in write_bits\n", |
| 890 | 916 | address + nb); |
| ... | ... | @@ -905,8 +931,21 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 905 | 931 | break; |
| 906 | 932 | case MODBUS_FC_WRITE_MULTIPLE_REGISTERS: { |
| 907 | 933 | int nb = (req[offset + 3] << 8) + req[offset + 4]; |
| 908 | - | |
| 909 | - if ((address + nb) > mb_mapping->nb_registers) { | |
| 934 | + if (nb < 1 || MODBUS_MAX_WRITE_REGISTERS < nb) { | |
| 935 | + if (ctx->debug) { | |
| 936 | + fprintf(stderr, | |
| 937 | + "Illegal number of values %d in write_registers (max %d)\n", | |
| 938 | + nb, MODBUS_MAX_WRITE_REGISTERS); | |
| 939 | + } | |
| 940 | + /* May be the indication has been truncated on reading because of | |
| 941 | + * invalid address (eg. nb is 0 but the request contains values to | |
| 942 | + * write) so it's necessary to flush. */ | |
| 943 | + _sleep_response_timeout(ctx); | |
| 944 | + modbus_flush(ctx); | |
| 945 | + rsp_length = response_exception( | |
| 946 | + ctx, &sft, | |
| 947 | + MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); | |
| 948 | + } else if ((address + nb) > mb_mapping->nb_registers) { | |
| 910 | 949 | if (ctx->debug) { |
| 911 | 950 | fprintf(stderr, "Illegal data address %0X in write_registers\n", |
| 912 | 951 | address + nb); |
| ... | ... | @@ -988,6 +1027,8 @@ int modbus_reply(modbus_t *ctx, const uint8_t *req, |
| 988 | 1027 | nb_write, nb, |
| 989 | 1028 | MODBUS_MAX_WR_WRITE_REGISTERS, MODBUS_MAX_WR_READ_REGISTERS); |
| 990 | 1029 | } |
| 1030 | + _sleep_response_timeout(ctx); | |
| 1031 | + modbus_flush(ctx); | |
| 991 | 1032 | rsp_length = response_exception( |
| 992 | 1033 | ctx, &sft, |
| 993 | 1034 | MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, rsp); | ... | ... |
tests/unit-test-client.c
| ... | ... | @@ -30,7 +30,11 @@ enum { |
| 30 | 30 | RTU |
| 31 | 31 | }; |
| 32 | 32 | |
| 33 | -int test_raw_request(modbus_t *, int); | |
| 33 | +int test_server(modbus_t *ctx, int use_backend); | |
| 34 | +int send_crafted_request(modbus_t *ctx, int function, | |
| 35 | + uint8_t *req, int req_size, | |
| 36 | + uint16_t max_value, uint16_t bytes, | |
| 37 | + int backend_length, int backend_offset); | |
| 34 | 38 | |
| 35 | 39 | #define BUG_REPORT(_cond, _format, _args ...) \ |
| 36 | 40 | printf("\nLine %d: assertion error for '%s': " _format "\n", __LINE__, # _cond, ## _args) |
| ... | ... | @@ -205,7 +209,6 @@ int main(int argc, char *argv[]) |
| 205 | 209 | ASSERT_TRUE(rc == 1, "FAILED (nb points %d)\n", rc); |
| 206 | 210 | ASSERT_TRUE(tab_rp_registers[0] == 0x1234, "FAILED (%0X != %0X)\n", |
| 207 | 211 | tab_rp_registers[0], 0x1234); |
| 208 | - | |
| 209 | 212 | /* End of single register */ |
| 210 | 213 | |
| 211 | 214 | /* Many registers */ |
| ... | ... | @@ -509,7 +512,7 @@ int main(int argc, char *argv[]) |
| 509 | 512 | |
| 510 | 513 | /* A wait and flush operation is done by the error recovery code of |
| 511 | 514 | * libmodbus but after a sleep of current response timeout |
| 512 | - * so 0 can't be too short! | |
| 515 | + * so 0 can be too short! | |
| 513 | 516 | */ |
| 514 | 517 | usleep(old_response_to_sec * 1000000 + old_response_to_usec); |
| 515 | 518 | modbus_flush(ctx); |
| ... | ... | @@ -590,8 +593,8 @@ int main(int argc, char *argv[]) |
| 590 | 593 | printf("* modbus_read_registers at special address: "); |
| 591 | 594 | ASSERT_TRUE(rc == -1 && errno == EMBXSBUSY, ""); |
| 592 | 595 | |
| 593 | - /** RAW REQUEST */ | |
| 594 | - if (test_raw_request(ctx, use_backend) == -1) { | |
| 596 | + /** SERVER **/ | |
| 597 | + if (test_server(ctx, use_backend) == -1) { | |
| 595 | 598 | goto close; |
| 596 | 599 | } |
| 597 | 600 | |
| ... | ... | @@ -617,19 +620,23 @@ close: |
| 617 | 620 | return 0; |
| 618 | 621 | } |
| 619 | 622 | |
| 620 | -int test_raw_request(modbus_t *ctx, int use_backend) | |
| 623 | +/* Send crafted requests to test server resilience | |
| 624 | + and ensure proper exceptions are returned. */ | |
| 625 | +int test_server(modbus_t *ctx, int use_backend) | |
| 621 | 626 | { |
| 622 | 627 | int rc; |
| 623 | - int i, j; | |
| 624 | - const int RAW_REQ_LENGTH = 6; | |
| 625 | - uint8_t raw_req[] = { | |
| 628 | + int i; | |
| 629 | + /* Read requests */ | |
| 630 | + const int READ_RAW_REQ_LEN = 6; | |
| 631 | + uint8_t read_raw_req[] = { | |
| 626 | 632 | /* slave */ |
| 627 | 633 | (use_backend == RTU) ? SERVER_ID : 0xFF, |
| 628 | 634 | /* function, addr 1, 5 values */ |
| 629 | - MODBUS_FC_READ_HOLDING_REGISTERS, 0x00, 0x01, 0x0, 0x05, | |
| 635 | + MODBUS_FC_READ_HOLDING_REGISTERS, 0x00, 0x01, 0x0, 0x05 | |
| 630 | 636 | }; |
| 631 | 637 | /* Write and read registers request */ |
| 632 | - uint8_t raw_rw_req[] = { | |
| 638 | + const int RW_RAW_REQ_LEN = 13; | |
| 639 | + uint8_t rw_raw_req[] = { | |
| 633 | 640 | /* slave */ |
| 634 | 641 | (use_backend == RTU) ? SERVER_ID : 0xFF, |
| 635 | 642 | /* function, addr to read, nb to read */ |
| ... | ... | @@ -646,104 +653,138 @@ int test_raw_request(modbus_t *ctx, int use_backend) |
| 646 | 653 | /* One data to write... */ |
| 647 | 654 | 0x12, 0x34 |
| 648 | 655 | }; |
| 649 | - /* See issue #143, test with MAX_WR_WRITE_REGISTERS */ | |
| 656 | + const int WRITE_RAW_REQ_LEN = 13; | |
| 657 | + uint8_t write_raw_req[] = { | |
| 658 | + /* slave */ | |
| 659 | + (use_backend == RTU) ? SERVER_ID : 0xFF, | |
| 660 | + /* function will be set in the loop */ | |
| 661 | + MODBUS_FC_WRITE_MULTIPLE_REGISTERS, | |
| 662 | + /* Address */ | |
| 663 | + UT_REGISTERS_ADDRESS >> 8, | |
| 664 | + UT_REGISTERS_ADDRESS & 0xFF, | |
| 665 | + /* 3 values, 6 bytes */ | |
| 666 | + 0x00, 0x03, 0x06, | |
| 667 | + /* Dummy data to write */ | |
| 668 | + 0x02, 0x2B, 0x00, 0x01, 0x00, 0x64 | |
| 669 | + }; | |
| 650 | 670 | int req_length; |
| 651 | 671 | uint8_t rsp[MODBUS_TCP_MAX_ADU_LENGTH]; |
| 652 | - int tab_function[] = { | |
| 672 | + int tab_read_function[] = { | |
| 653 | 673 | MODBUS_FC_READ_COILS, |
| 654 | 674 | MODBUS_FC_READ_DISCRETE_INPUTS, |
| 655 | 675 | MODBUS_FC_READ_HOLDING_REGISTERS, |
| 656 | 676 | MODBUS_FC_READ_INPUT_REGISTERS |
| 657 | 677 | }; |
| 658 | - int tab_nb_max[] = { | |
| 678 | + int tab_read_nb_max[] = { | |
| 659 | 679 | MODBUS_MAX_READ_BITS + 1, |
| 660 | 680 | MODBUS_MAX_READ_BITS + 1, |
| 661 | 681 | MODBUS_MAX_READ_REGISTERS + 1, |
| 662 | 682 | MODBUS_MAX_READ_REGISTERS + 1 |
| 663 | 683 | }; |
| 664 | - int length; | |
| 665 | - int offset; | |
| 666 | - const int EXCEPTION_RC = 2; | |
| 684 | + int backend_length; | |
| 685 | + int backend_offset; | |
| 667 | 686 | |
| 668 | 687 | if (use_backend == RTU) { |
| 669 | - length = 3; | |
| 670 | - offset = 1; | |
| 688 | + backend_length = 3; | |
| 689 | + backend_offset = 1; | |
| 671 | 690 | } else { |
| 672 | - length = 7; | |
| 673 | - offset = 7; | |
| 691 | + backend_length = 7; | |
| 692 | + backend_offset = 7; | |
| 674 | 693 | } |
| 675 | 694 | |
| 676 | 695 | printf("\nTEST RAW REQUESTS:\n"); |
| 677 | 696 | |
| 678 | - req_length = modbus_send_raw_request(ctx, raw_req, | |
| 679 | - RAW_REQ_LENGTH * sizeof(uint8_t)); | |
| 697 | + req_length = modbus_send_raw_request(ctx, read_raw_req, READ_RAW_REQ_LEN); | |
| 680 | 698 | printf("* modbus_send_raw_request: "); |
| 681 | - ASSERT_TRUE(req_length == (length + 5), "FAILED (%d)\n", req_length); | |
| 699 | + ASSERT_TRUE(req_length == (backend_length + 5), "FAILED (%d)\n", req_length); | |
| 682 | 700 | |
| 683 | 701 | printf("* modbus_receive_confirmation: "); |
| 684 | - rc = modbus_receive_confirmation(ctx, rsp); | |
| 685 | - ASSERT_TRUE(rc == (length + 12), "FAILED (%d)\n", rc); | |
| 686 | - | |
| 687 | - /* Try to crash server with raw requests to bypass checks of client. */ | |
| 688 | - | |
| 689 | - /* Address */ | |
| 690 | - raw_req[2] = 0; | |
| 691 | - raw_req[3] = 0; | |
| 702 | + rc = modbus_receive_confirmation(ctx, rsp); | |
| 703 | + ASSERT_TRUE(rc == (backend_length + 12), "FAILED (%d)\n", rc); | |
| 692 | 704 | |
| 693 | 705 | /* Try to read more values than a response could hold for all data |
| 694 | 706 | * types. |
| 695 | 707 | */ |
| 696 | 708 | for (i=0; i<4; i++) { |
| 697 | - raw_req[1] = tab_function[i]; | |
| 698 | - | |
| 699 | - for (j=0; j<2; j++) { | |
| 700 | - if (j == 0) { | |
| 701 | - /* Try to read zero values on first iteration */ | |
| 702 | - raw_req[4] = 0x00; | |
| 703 | - raw_req[5] = 0x00; | |
| 704 | - } else { | |
| 705 | - /* Try to read max values + 1 on second iteration */ | |
| 706 | - raw_req[4] = (tab_nb_max[i] >> 8) & 0xFF; | |
| 707 | - raw_req[5] = tab_nb_max[i] & 0xFF; | |
| 708 | - } | |
| 709 | - | |
| 710 | - req_length = modbus_send_raw_request(ctx, raw_req, | |
| 711 | - RAW_REQ_LENGTH * sizeof(uint8_t)); | |
| 712 | - if (j == 0) { | |
| 713 | - printf("* try to read 0 values with function %d: ", tab_function[i]); | |
| 714 | - } else { | |
| 715 | - printf("* try an exploit with function %d: ", tab_function[i]); | |
| 716 | - } | |
| 717 | - rc = modbus_receive_confirmation(ctx, rsp); | |
| 718 | - ASSERT_TRUE(rc == (length + EXCEPTION_RC) && | |
| 719 | - rsp[offset] == (0x80 + tab_function[i]) && | |
| 720 | - rsp[offset + 1] == MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, ""); | |
| 721 | - } | |
| 709 | + rc = send_crafted_request(ctx, tab_read_function[i], | |
| 710 | + read_raw_req, READ_RAW_REQ_LEN, | |
| 711 | + tab_read_nb_max[i], 0, | |
| 712 | + backend_length, backend_offset); | |
| 713 | + if (rc == -1) | |
| 714 | + goto close; | |
| 722 | 715 | } |
| 723 | 716 | |
| 724 | 717 | /* Modbus write and read multiple registers */ |
| 725 | - i = 0; | |
| 726 | - tab_function[i] = MODBUS_FC_WRITE_AND_READ_REGISTERS; | |
| 727 | - for (j=0; j<2; j++) { | |
| 718 | + rc = send_crafted_request(ctx, MODBUS_FC_WRITE_AND_READ_REGISTERS, | |
| 719 | + rw_raw_req, RW_RAW_REQ_LEN, | |
| 720 | + MODBUS_MAX_WR_READ_REGISTERS + 1, 0, | |
| 721 | + backend_length, backend_offset); | |
| 722 | + if (rc == -1) | |
| 723 | + goto close; | |
| 724 | + | |
| 725 | + /* Modbus write multiple registers with large number of values but a set a | |
| 726 | + small number of bytes in requests (not nb * 2 as usual). */ | |
| 727 | + rc = send_crafted_request(ctx, MODBUS_FC_WRITE_MULTIPLE_REGISTERS, | |
| 728 | + write_raw_req, WRITE_RAW_REQ_LEN, | |
| 729 | + MODBUS_MAX_WRITE_REGISTERS + 1, 6, | |
| 730 | + backend_length, backend_offset); | |
| 731 | + if (rc == -1) | |
| 732 | + goto close; | |
| 733 | + | |
| 734 | + rc = send_crafted_request(ctx, MODBUS_FC_WRITE_MULTIPLE_COILS, | |
| 735 | + write_raw_req, WRITE_RAW_REQ_LEN, | |
| 736 | + MODBUS_MAX_WRITE_BITS + 1, 6, | |
| 737 | + backend_length, backend_offset); | |
| 738 | + if (rc == -1) | |
| 739 | + goto close; | |
| 740 | + | |
| 741 | + return 0; | |
| 742 | +close: | |
| 743 | + return -1; | |
| 744 | +} | |
| 745 | + | |
| 746 | + | |
| 747 | +int send_crafted_request(modbus_t *ctx, int function, | |
| 748 | + uint8_t *req, int req_len, | |
| 749 | + uint16_t max_value, uint16_t bytes, | |
| 750 | + int backend_length, int backend_offset) | |
| 751 | +{ | |
| 752 | + const int EXCEPTION_RC = 2; | |
| 753 | + uint8_t rsp[MODBUS_TCP_MAX_ADU_LENGTH]; | |
| 754 | + | |
| 755 | + for (int j=0; j<2; j++) { | |
| 756 | + int rc; | |
| 757 | + | |
| 758 | + req[1] = function; | |
| 728 | 759 | if (j == 0) { |
| 729 | - /* Try to read zero values on first iteration */ | |
| 730 | - raw_rw_req[4] = 0x00; | |
| 731 | - raw_rw_req[5] = 0x00; | |
| 760 | + /* Try to read or write zero values on first iteration */ | |
| 761 | + req[4] = 0x00; | |
| 762 | + req[5] = 0x00; | |
| 763 | + if (bytes) { | |
| 764 | + /* Write query */ | |
| 765 | + req[6] = 0x00; | |
| 766 | + } | |
| 732 | 767 | } else { |
| 733 | - /* Try to read max values + 1 on second iteration */ | |
| 734 | - raw_rw_req[4] = (MODBUS_MAX_WR_READ_REGISTERS + 1) >> 8; | |
| 735 | - raw_rw_req[5] = (MODBUS_MAX_WR_READ_REGISTERS + 1) & 0xFF; | |
| 768 | + /* Try to read or write max values + 1 on second iteration */ | |
| 769 | + req[4] = (max_value >> 8) & 0xFF; | |
| 770 | + req[5] = max_value & 0xFF; | |
| 771 | + if (bytes) { | |
| 772 | + /* Write query (nb values * 2 to convert in bytes for registers) */ | |
| 773 | + req[6] = bytes; | |
| 774 | + } | |
| 736 | 775 | } |
| 737 | - req_length = modbus_send_raw_request(ctx, raw_rw_req, 13 * sizeof(uint8_t)); | |
| 776 | + | |
| 777 | + modbus_send_raw_request(ctx, req, req_len * sizeof(uint8_t)); | |
| 738 | 778 | if (j == 0) { |
| 739 | - printf("* try to read 0 values with function %d: ", tab_function[i]); | |
| 779 | + printf("* try function 0x%X: %s 0 values: ", function, bytes ? "write": "read"); | |
| 740 | 780 | } else { |
| 741 | - printf("* try an exploit with function %d: ", tab_function[i]); | |
| 781 | + printf("* try function 0x%X: %s %d values: ", function, bytes ? "write": "read", | |
| 782 | + max_value); | |
| 742 | 783 | } |
| 743 | 784 | rc = modbus_receive_confirmation(ctx, rsp); |
| 744 | - ASSERT_TRUE(rc == length + EXCEPTION_RC && | |
| 745 | - rsp[offset] == (0x80 + tab_function[i]) && | |
| 746 | - rsp[offset + 1] == MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, ""); | |
| 785 | + ASSERT_TRUE(rc == (backend_length + EXCEPTION_RC) && | |
| 786 | + rsp[backend_offset] == (0x80 + function) && | |
| 787 | + rsp[backend_offset + 1] == MODBUS_EXCEPTION_ILLEGAL_DATA_VALUE, ""); | |
| 747 | 788 | } |
| 748 | 789 | |
| 749 | 790 | return 0; | ... | ... |
tests/unit-test-server.c
| ... | ... | @@ -156,12 +156,15 @@ int main(int argc, char*argv[]) |
| 156 | 156 | |
| 157 | 157 | if (rc == -1) { |
| 158 | 158 | /* Connection closed by the client or error */ |
| 159 | + /* We could answer with an exception on EMBBADDATA to indicate | |
| 160 | + illegal data for example */ | |
| 159 | 161 | break; |
| 160 | 162 | } |
| 161 | 163 | |
| 162 | - | |
| 163 | - /* Read holding registers */ | |
| 164 | + /* Special server behavior to test client */ | |
| 164 | 165 | if (query[header_length] == 0x03) { |
| 166 | + /* Read holding registers */ | |
| 167 | + | |
| 165 | 168 | if (MODBUS_GET_INT16_FROM_INT8(query, header_length + 3) |
| 166 | 169 | == UT_REGISTERS_NB_SPECIAL) { |
| 167 | 170 | printf("Set an incorrect number of values\n"); | ... | ... |