REVERSING WITH IDA FROM SCRATCH (P30)

Posted: June 22, 2020 in IDA Tutorials, REVERSING WITH IDA FROM SCRATCH (P30)
Tags: ,

Trước khi đi vào nội dung chính của bài viết, tôi xin cảm ơn bạn sinh viên nào đó “cuối tháng” vẫn donate vào tài khoản của tôi! Trân trọng!! Số tiền donate mà tôi nhận được, tôi sẽ dành để làm những công việc thiện nguyện hoặc những việc mà tôi thấy có ích và có giá trị. Đơn cử như góp một phần nhỏ bé (https://www.facebook.com/permalink.php?story_fbid=10158548818576598&id=554051597) để mong cháu sẽ khoẻ mạnh bình thường, được đi học, được trưởng thành và đóng góp cho xã hội!

…. Để gió cuốn đi…..

Để giải quyết bài tập ở phần trước, tôi sẽ giải thích thêm về một số điểm có thể các bạn còn chưa rõ và sau đó chúng ta sẽ tìm cách để xây dựng một PoC. Một trong những điểm chưa rõ ràng là nhiều lúc chúng ta thường chấp nhận độ dài của mảng do IDA gợi ý cho chúng ta và đôi khi chúng ta phải tự đặt nghi vấn và tự phân tích, phán đoán như trong phần trước rồi từ đó khẳng định được mảng sẽ có giá trị lớn hơn so với giá trị mà IDA đã gợi ý.

Như vậy, rõ ràng là để làm được thì sẽ phải phụ thuộc rất nhiều vào kinh nghiệm của từng người. Ở phần này tôi sẽ cố gắng giải thích thêm thông qua một vài ví dụ cụ thể.

Hãy xem ví dụ đơn giản sau đây:

Chương trình yêu cầu nhập một số từ bàn phím và lưu vào biến size. Thực hiện kiểm tra và thoát khỏi chương trình nếu size >= 0x200. Sau đó, sử dụng một vòng lặp để đọc kí tự từ bàn phím và gán vào buffer, với số lần lặp phải thỏa mãn điều kiện (i <= size) && ((ch = getchar()) != EOF) && (ch != ‘\n’). Như vậy, có thể thấy nếu bạn nhập giá trị cho size10 nhưng bạn chỉ nhập vào một kí tự và nhấn enter thì buffer cũng chỉ được gán một kí tự đó mà thôi.

Với đoạn code trên thì ta thấy sẽ không có lỗi buffer overflow, vì biến size được khai báo là kiểu unsigned, nên khi so sánh với 0x200 sẽ không có vấn đề gì. Khi ta biên dịch chương trình, sau đó load file vào IDA kèm theo file .pdb của nó thì IDA sẽ nhận diện được chính xác buffer và kích thước của buffer512 (0x200):

Ngược lại, điều gì sẽ xảy ra nếu không load kèm pdb:

Chuyển tới cửa sổ biểu diễn Stack:

Chúng ta thấy rằng lúc này IDA không nhận diện được buffer hoặc bất cứ thông tin gì, do đó bạn phải làm điều đó bằng tay. Ta để ý thấy có không gian trống bên dưới biến var_20C, nên rất có thể là var_20Cbuffer. Kiểm tra các tham chiếu tới biến này, đầu tiên tại đây:

Đoạn code trên khá là rõ ràng khi thực hiện in buffer ra màn hình, sub_4011C0 chính là hàm printf(). Thông thường, khi có một tham chiếu bằng lệnh LEA thì gần như chắc chắn biến đó là một buffer.

Một tham chiếu khác tới buffer này là bên trong vòng lặp, khi nó được gán giá trị là các bytes mà chương trình nhận được thông qua hàm getchar():

Theo đoạn code trên hình, có thể thấy ecx nhận giá trị của biến var_210 (là counter của vòng lặp), do vậy lệnh mov sẽ lấy var_20C làm base của buffer còn ecx chính là index của buffer. Ta đổi tên var_20C thành buffer và quay lại cửa sổ biểu diễn Stack để xem chiều dài/kích thước của buffer này:

IDA cung cấp thông tin như trên hình. Nhưng tại sao lại là 512 mà không phải là một con số khác?

Bởi vì IDA kiểm tra vùng không gian trống bên dưới cho đến khi nó gặp được biến tiếp theo là var_C. Bây giờ, phương pháp để kiểm tra xem var_C có thuộc buffer hay không là xem nơi mà var_C được sử dụng trong code của chương trình (được gán giá trị thế nào, được truy cập ra sao). Xem xét biến var_C:

Có 4 chỗ sử dụng tới biến var_C như trên hình. Quan sát code ta sẽ khẳng định biến var_C là một biến độc lập, không liên quan tới buffer:

Như vậy, độ dài buffer mà IDA đã tự động nhận diện được là hoàn toàn chính xác. Xem xét thử một ví dụ khác:

Về cơ bản ở ví dụ này mọi thứ đều giống với ví dụ trước, các biến và kích thước của buffer vẫn là 0x200, ngoại trừ một vài điểm khác. Chương trình in ra byte thứ tư của buffer và so sánh byte thứ năm với 0, nếu khác sẽ in buffer ra màn hình.

Hãy xem điều gì sẽ xảy ra khi ta biên dịch chương trình, sau đó phân tích trong IDA khi có và không có pdb.

Như các bạn thấy trên hình, khi load kèm theo file pdb thì IDA nhận diện rất chính xác buffer có độ dài là 0x200 và đoạn code in ra byte thứ 4 sẽ như sau:

Chúng ta có thể nghĩ rằng IDA sẽ tạo ra một biến độc lập nhưng không phải như vậy. Để truy xuất đọc byte thứ tư của bộ đệm, chương trình sẽ tính toán bằng cách cộng thêm 4 (gán eax=1, thực hiện shl eax, 2 sẽ tương đương với eax * 4) vào địa chỉ bắt đầu (base) của buffer để tìm giá trị của byte thứ tư.

Để truy cập tới byte thứ năm cũng tương tự. Chương trình thực hiện gán eax = 1, nhân eax với 5 và gán kết quả vào ecx. Lúc đó ecx sẽ bằng 5. Lấy địa chỉ bắt đầu của buffer + ecx ta sẽ truy cập được byte thứ năm của buffer. Kiểm tra xem byte này có bằng 0 hay không? Nếu khác thì sẽ in toàn bộ buffer ra màn hình.

Như vậy, chúng ta thấy rõ rằng buffer không bị ảnh hưởng và IDA vẫn nhận biết được chiều dài của buffer khi load kèm theo file pdb. Giờ ta thử xem khi không có pdb thì code trong IDA sẽ thế nào?

Như trên hình, ta thấy code vẫn giống, IDA không tạo ra các biến riêng cho việc lấy giá trị của byte thứ tư và thứ năm của buffer. Kích thước của buffer vẫn được IDA nhận biết chính xác là 512 bytes vì biến tiếp theo vẫn là var_C (được sử dụng cho việc so sánh). Nếu vì một số lý do nào đó khiến IDA nhận biết các byte thứ tư và thứ năm là các biến, thì rõ ràng chúng sẽ không là các biến độc lập, bởi chúng chỉ nhận được giá trị khi mà bộ đệm được ghi giá trị vào, nếu không thì không có nơi nào khác ghi giá trị vào chúng. Vì vậy, ta xem chúng là một phần của buffer.

Vì không thể biên dịch được một ví dụ mà khiến IDA nhận biết sai, do đó chúng ta so sánh nó với ví dụ về buffer ở trên. Căn cứ vào thực tế khi IDA đề xuất độ dài buffer, ta vẫn phải tiếp tục xem xét biến độc lập đầu tiên bên dưới (truy xuất biến, gán giá trị), từ đó sẽ tìm được giới hạn thực sự của buffer.

Bây giờ, ta quay trở lại với buffer ở phần trước:

Chương trình gọi tới hàm stream_Read, hàm này có thể được sử dụng để đọc nội dung từ một file và lưu tạm thời vào một buffer. Theo code trên hình, giá trị của ecx được truyền cho tham số thứ hai của hàm tại [esp+4], giá trị của ecx lại có được thông qua lệnh LEA, như vậy giá trị tại ECX chính là địa chỉ của một buffer trong Stack. Ta đổi tên var_3C thành buffer.

Theo lập luận và phân tích ở phần đầu của bài viết, ta sẽ xem xét các biến bên dưới buffer này để nhận biết xem đó có phải là biến độc lập đầu tiên sau buffer hay không. Đầu tiên là var_3B:

Tại biến này ta nhấn X, kết quả như sau:

Ta thấy var_3B (cột Direction hiển thị Down, nghĩa là biến được truy cập sau lệnh LEA (lấy địa chỉ của buffer)) cũng là một phần của buffer vì không có giá trị nào được lưu vào nó mà chỉ thấy giá trị của biến được đọc ra và lưu vào thanh ghi. Do đó, chỉ có khả năng khi ta ghi dữ liệu vào buffer thì giá trị nhận được sẽ được lưu vào các biến này. Với các biến khác cũng tương tự:

Biến đầu tiên có một tham chiếu khác so với các biến trước đó là var_30:

Khả năng đây có thể là một buffer vì ta thấy chương trình sử dụng lệnh LEA để lấy địa chỉ biến. Ta đi tới đoạn code liên quan tới var_30:

Nhìn code tại đây, có thể khẳng định nó thực sự là một buffer nhưng là thuộc một phần của buffer trước đó, bởi vì không có chỗ nào ghi dữ liệu vào nó. Lệnh LEA sẽ lấy địa chỉ của biến, thanh ghi esi lúc này được xem là địa chỉ nguồn (source) của lệnh rep movsd. Như vậy, nó đã phải lưu giá trị để được sao chép tới một vị trí khác, hơn nữa nó cũng được đánh dấu là Down, có nghĩa là, nó được sử dụng sau khi điền giá trị vào buffer gốc.

Tiếp tục đi dần xuống để phân tích các biến tiếp theo, ta thấy chương trình sẽ vẫn tiếp tục đọc các biến của buffer, cho tới khi gặp một điểm khác ở biến var_1C:

Theo kết quả trên hình, ta thấy lệnh LEA và ở cột DirectionUp, có nghĩa là nó ở bên trên của vùng buffer trước. Như vậy, đây là một buffer khác và hoàn toàn độc lập với buffer mà ta vừa phân tích. Xem xét đoạn code sử dụng biến này:

Với đoạn code trên hình, buffer này được truyền làm tham số thứ ba của hàm stream_Control và nó được điền giá trị bởi hàm này. Cuối cùng, ta đã tìm được biến độc lập đầu tiên và đó là lý do tại sao buffer ở trên kết thúc ngay trước biến này.

Như vậy ta có được một buffer nhỏ, với kích thước chỉ có 32 bytes:

Một vấn đề khác mà các bạn có thể thắc mắc là làm thế nào tôi lại biết được hàm stream_Read có thể ghi số bytes mà ta truyền vào buffer. Chỉ đơn giản là tên hàm gợi ý cho tôi suy nghĩ đó, cộng với ở phiên bản 0.96 đã patch để giới hạn giá trị ghi vào buffer, nếu lớn hơn 8 khiến tôi nghi ngờ rằng đó là kích thước tối đa mà sẽ sao chép. Đi vào hàm stream_Read ta tới đây:

Follow theo lệnh jmp, ta lại tới đây:

Như vậy, hàm stream_Read là một hàm thuộc thư viện libvlccore.dll. Mở IDA khác và load dll này vào để phân tích, ta sẽ có được hàm stream_Read như sau:

Như trên hình, hàm này phụ thuộc vào một tham số arg_0, là một hằng số đến từ một lệnh CALL. Dựa vào hằng số này chương trình sẽ quyết định được địa chỉ cần rẽ nhánh [eax + 2c]. Tôi có thể reverse để tìm xem nó nhảy tới đâu, nhưng vì tôi cần xây dựng một PoC và để làm được tôi sẽ tiến hành debug.

Sẽ có nhiều bạn hỏi PoC là gì? Nó là viết tắt của Proof Of Concept. PoC là việc tiến hành thử một method hoặc ý tưởng nào đó để chứng minh rằng nó có tính khả thi, hoặc chứng minh về cơ bản một lý thuyết nào đó có tính thực tiễn. Một Proof of Concept thường có quy mô nhỏ, có khi không phải là việc hoàn thiện. Xem thêm tại đây). Trong ngữ cảnh của khai thác lỗ hổng phần mềm thì PoC không phải là một exploit hoàn chỉnh, nó dùng để chứng mình chương trình có lỗ hổng.

Cụ thể trong ví dụ này, ta sẽ tạo ra một tệp có phần mở rộng là .ty để khi chương trình load file này làm tràn bộ đệm 32 bytes đã phân tích ở trên. Quay trở lại đoạn code liên quan đến buffer đã phân tích, ta thấy lúc này thanh ghi ecx đang giữ địa chỉ của buffer:

Tiếp theo, ta sẽ thấy thanh ghi esi nhận giá trị từ biến có tên là var_58 (sẽ được đổi tên thành size_to_copy).

Xrefs tại biến này, ta thấy nó có được giá trị từ thanh ghi edi và sau đó được tính toán thông qua lệnh IDIV. Quan sát xem giá trị của thanh ghi edi được tính toán từ đâu, ta có kết quả như sau:

Như trên hình, thanh ghi EDI nhận giá trị từ một biến khác là var_5c và sau đó được cộng thêm với 0x8. Ta đổi tên var_5C thành size_to_copy_minus_8:

Nhận thấy, mọi thứ xuất phát từ lời gọi đầu tiên tới hàm stream_Read. Sau đó, chương trình lấy các bytes từ vị trí 0x14, 0x15, 0x16 và 0x17, tính toán theo các lệnh SHLOR để tạo ra một giá trị DWORD được lưu tại biến size_to_copy_minus_8.

Ta đặt một bp tại lệnh LEA ở địa chỉ 0x61401C0A:

Tiếp theo, tôi chạy chương trình vlc.exe và attach nó vào IDA:

Sau khi attach xong, nhấn F9 để cho chương trình thực thi hoàn toàn, sau đó mở file test-dtivo-junkskip.ty+ (link download đã cung cấp ở phần trước). Sau khi chương trình play được một đoạn, nó sẽ dừng lại tại BP ta đã đặt:

Nhấn F7 để trace code, ta có giá trị tại EDX chính là địa chỉ của buffer:

Nhấn chuột vào mũi tên bên cạnh thanh ghi EDX, IDA sẽ đưa chúng ta đến địa chỉ này:

Tại đây, ta tạo một mảng có kích thước 32 bytes trong bộ nhớ:

Kết quả có được như sau:

Tiếp theo, đặt một breakpoint on wirte tại dword đầu tiên của bộ đệm để xem có dừng lại khi chương trình ghi dữ liệu vào buffer thông qua hàm stream_Read.

Sau khi đặt bp xong, nhấn F9 để Run chương trình, nó sẽ dừng lại tại câu lệnh rep movsd trong thư viện msvcrt.dll.

Truy cập menu Debugger > Debugger windows > Module list, ta sẽ thấy một loạt danh sách các modules và tìm thấy msvcrt.dll tương tự như hình.

Nhấn chuột phải tại msvcrt.dll và chọn Analyze module, sau khi IDA phân tích xong ta chọn tiếp Load debug symbols. Hoàn thành bước này ta thấy hàm sẽ trông đẹp hơn và có thể thấy trong ngăn xếp lời gọi hàm nơi chúng ta thực sự đang đứng.

Truy xuất menu Debugger > Debugger windows > Stack trace (tương đương với cửa sổ Call Stack của OllyDbg/x64dbg):

Như vậy, ta đang ở trong hàm memcpy và hàm này thuộc thư viên msvcrt.dll. Lệnh reps movsd như đã biết, nó sẽ sao chép từ địa chỉ nguồn trỏ bởi ESI tới địa chỉ đích trỏ bởi EDI (với nguồn và đích là hai buffer khác nhau), và thanh ghi ECX sẽ chứa số lượng dwords cần sao chép. Hãy xem ESI đang trỏ tới đâu bằng cách nhấn vào  bên cạnh thanh ghi này, nhưng lần này ta sẽ quan sát tại cửa số Hex Dump:

Đây là những giá trị mà chương trình sẽ copy vào trong buffer. Mở tập tin .ty bằng một trình Hex Editor (ví dụ: HxD). Sau đó, lựa chọn một đoạn dữ liệu trên trong IDA (ví dụ: 32 bytes) và chọn Edit > Export data (Shift+E):

Chuyển sang kiểu hex string (spaced) và tích chọn Save data to clipboard:

Khi tích chọn Save data to cliploard thì đoạn dữ liệu này sẽ được tự động copy vào clipboard. Tìm kiếm đoạn dữ liệu này tại màn hình Hex Editor:

Kết quả tìm thấy tại đây:

Như vậy, ta đã tìm thấy các bytes dữ liệu của file mà chương trình sẽ đọc ra. Theo phân tích ở trên, ta biết rằng các bytes tại vị trí 0x14-0x15-0x16 và 0x17 sẽ được sử dụng để tạo ra một giá trị, tạm gọi là X, sau đó lấy X + 8 và thực hiện phép chia để tạo ra giá trị cuối cùng. Nếu tính từ đầu của buffer thì sẽ có được giá trị của các bytes tại vị trí cần tìm như vùng khoanh đỏ trên hình:

Kết quả có được các giá trị 00 00 00 02. Quay trở lại IDA, đặt một bp để chương trình dừng lại sau khi quay về từ hàm stream_Read:

Nhấn F9 để run sẽ dừng lại tại BP. Lúc này buffer của chúng ta đã nhận được dữ liệu sau khi thực hiện hàm stream_Read:

Tại cửa sổ Hex Dump:

Thêm 0x14 vào địa chỉ của buffer, sẽ có được kết quả là 00 00 00 02 giống với kết quả có được khi xem tại Hex Editor. Tiến hành trace code để xem chương trình sẽ làm gì với những giá trị này:

Tóm lại, đoạn code sẽ thực hiện chuyển các giá trị trên thành DWORD (dd 2) và lưu kết quả vào EDI, sau đó sẽ cộng thêm 8 vào EDI.

Cuối cùng sự dụng lệnh IDIV để tính ra giá trị cần. Trace đến đoạn code này:

Lệnh idiv (tương tự như lệnh div) thực hiện phép chia trên hai số có dấu. Căn cứ vào kích thước của toán hạng nguồn mà sẽ sử dụng AX, DX:AX hay EDX:EAX để chia. Do ở đây, toán hạng nguồn (size_to_copy) có kiểu dword nên sẽ lấy EDX:EAX để chia cho size_to_copy. Vấn đề là ở chỗ nếu tôi có thể tăng size_to_copy lên một giá trị rất lớn thì kết quả phép chia sẽ cho giá trị 0 và giá trị này sẽ được truyền cho hàm malloc sau khi nhân nó với 16. Vì vậy, chúng ta phải xử lý phép chia này thật tốt để kết quả của nó không bằng 0.

Như trong code ở trên hình, việc hình thành nên giá trị cho cặp EDX: EAX sẽ thông qua các bytes tại vị trí 0x1C, 0x1D, 0x1F0x1E của buffer. Giá trị này gần ngay cạnh 00 00 00 02:

Tức là 00000000:00000148 sẽ được chia cho 0xA (0x2+0x8). Vì vậy, nếu tôi thay đổi một giá trị khác 02, thì tôi cũng phải thay một giá trị khác 0x148 để phép chia không có kết quả bằng 0. Tôi thử sửa lại như sau:

Bằng cách này, chúng ta sẽ thấy nếu sau khi tính toán và truyền giá trị cho hàm stream_Read là một kích thước lớn hơn 8 sẽ gây tràn buffer. Ta chạy thử lại chương trình với file đã sửa ở trên:

Trên hình, ta thấy thanh ghi EDI trước khi cộng 0x80x4647. Sau khi + 0x8 kết quả là:

Tiếp theo sẽ lấy ra giá trị 0xAA48, tạo cặp thanh ghi EDX:EAX có giá trị 0000000:AA48 rồi đem chia cho 0x464f:

Kết quả sau khi thực hiện lệnh idiv, thanh ghi EAX sẽ lưu kết quả phép chia là 0x2 còn EDX sẽ lưu số dư là 0x1DAA:

Giá trị của EAX sau phép chia được lưu vào trường Maximum, sau đó đem đi nhân với 16 và gọi hàm malloc với tham số là kết quả sau khi nhân với 16. Vì chúng ta không khai thác tràn trên heap nên quá trình cấp phát bộ nhớ sẽ thực hiện bình thường:

Như vậy, chương trình sẽ gọi hàm malloc để cấp phát vùng nhớ có kích thước 0x20 (32 bytes) mà không gặp trở ngại gì. Tiếp trục trace code tới đây:

Đây là vùng code mà ta đã phân tích là có khả năng lỗi, với size_to_copy bằng 0x464f, được gán cho thanh ghi esi. Rõ ràng, giá trị này lớn hơn 8, mà trong phiên bản 0.9.6 ta thấy chương trình rẽ sang nhánh khác khi lớn hơn 8 và tránh được tràn buffer.

Thanh ghi ECX bên dưới sẽ trỏ tới buffer, ta sẽ đặt bp tại buffer này. Ở đây, ta đặt một hwbp on read write để chương trình dừng lại khi bắt đầu việc ghi dữ liệu vào buffer:

Trace code tới lời gọi hàm stream_Read:

Nhấn F9 để thực thi, chương trình sẽ dừng lại tại đoạn code thực hiện sao chép vào buffer:

Lệnh rep movsd có nghĩa là lặp lại việc chuyển doubleword tại địa chỉ DS:(E)SI tới địa chỉ ES:(E)DI, thanh ghi ECX sẽ chứa số lần thực hiện, ở trên hình là 0x1192. Như vây, ta có tổng số bytes sẽ ghi vào buffer có kích thước 32 bytes0x1192 x 4 hay 0x4648 bytes (từ việc làm tròn giá trị 0x4647 mà tôi thay đổi trong file ở trên).

Chọn Debugger > Run until return hay (Ctrl+F7) để thực thi chương trình và quay trở lại hàm nơi mà truy xuất tới buffer (Đoạn này tôi thực hiện trên một máy ảo WinXP vì trên Win10/Win7 có vấn đề, toàn bị văng ra):

Cuộn chuột xuống cuối của hàm và đặt một breakpoint tại lệnh retn và disable toàn bộ các bp khác đã đặt:

Nhấn F9 để run chương trình, tôi nhận thấy rằng Stack bị phá hủy bởi dữ liệu đã ghi đè lên toàn bộ Stack. Chuyển qua cửa sổ Hex view và nhấn  mũi tên bên cạnh thanh ghi ESP:

Tìm chuỗi trên trong file test-dtivo-junkskip.ty+, ta có được kết quả như sau:

Với giá trị này tại đỉnh của Stack, khi ta thực hiện lệnh retn thì nó sẽ được pop vào thanh ghi EIP và nhảy tới địa chỉ 0x0040DD82. Ta sửa lại giá trị như hình dưới và lưu lại thành file khác (ví dụ: POC.ty+):

Làm lại các bước như trên với file POC.ty+. Kết quả sẽ như hình dưới đây:

Trace qua lệnh retn bạn sẽ thấy giá trị 0x44434241 được đẩy vào thanh ghi EIP và lệnh tiếp theo được thực thi sẽ là tại địa chỉ này, nhưng vì nó không tồn tại nên ta sẽ nhận được thông báo sau:

Nếu mọi thứ diễn ra tốt đẹp thì ta có thể khai thác được lỗi của chương trình. Phần 30 đến đây là tạm dừng, trong phần tới chúng ta sẽ tiếp tục với một số lý thuyết còn thiếu và một số ví dụ đơn giản được lập trình để các bạn có thể thực hành nhiều hơn.

Hẹn gặp các bạn ở phần 31!

Xin gửi lời cảm ơn chân thành tới thầy Ricardo Narvaja!

m4n0w4r

Ủng hộ tác giả

Nếu bạn cảm thấy những gì tôi chia sẻ trong bài viết là hữu ích, bạn có thể ủng hộ bằng “bỉm sữa” hoặc “quân huy” qua địa chỉ:

Tên tài khoản: TRAN TRUNG KIEN
Số tài khoản: 0021001560963
Ngân hàng: Vietcombank

Comments
  1. Anh Nguyễn Tuấn says:

    Cảm ơn anh vì bài viết, cũng cảm ơn anh vì đã ủng hộ cho con của đồng nghiệp của em ạ.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.