Giải pháp 1: C (Mac OS X x86_64), 109 byte
Nguồn cho golf_sol1.c
main[]={142510920,2336753547,3505849471,284148040,2370322315,2314740852,1351437506,1208291319,914962059,195};
Chương trình trên cần được biên dịch với quyền truy cập thực thi trên phân đoạn __DATA.
clang golf_sol1.c -o golf_sol1 -Xlinker -segprot -Xlinker __DATA -Xlinker rwx -Xlinker rwx
Sau đó, để thực hiện chương trình chạy như sau:
./golf_sol1 $(ruby -e 'puts "\xf5\xff\xff\xfe\xff\xff\x44\x82\x57\x7d\xff\x7f"')
Các kết quả:
Thật không may, Valgrind không xem bộ nhớ được phân bổ từ các cuộc gọi hệ thống, vì vậy tôi không thể hiển thị một rò rỉ được phát hiện đẹp.
Tuy nhiên, chúng ta có thể nhìn vào vmmap để thấy phần lớn bộ nhớ được phân bổ (siêu dữ liệu MALLOC).
VIRTUAL REGION
REGION TYPE SIZE COUNT (non-coalesced)
=========== ======= =======
Kernel Alloc Once 4K 2
MALLOC guard page 16K 4
MALLOC metadata 16.2M 7
MALLOC_SMALL 8192K 2 see MALLOC ZONE table below
MALLOC_TINY 1024K 2 see MALLOC ZONE table below
STACK GUARD 56.0M 2
Stack 8192K 3
VM_ALLOCATE (reserved) 520K 3 reserved VM address space (unallocated)
__DATA 684K 42
__LINKEDIT 70.8M 4
__TEXT 5960K 44
shared memory 8K 3
=========== ======= =======
TOTAL 167.0M 106
TOTAL, minus reserved VM space 166.5M 106
Giải trình
Vì vậy, tôi nghĩ rằng tôi cần mô tả những gì thực sự đang diễn ra ở đây, trước khi chuyển sang giải pháp cải tiến.
Hàm chính này đang lạm dụng khai báo kiểu thiếu của C (vì vậy nó mặc định là int mà không cần chúng ta lãng phí các ký tự viết nó), cũng như cách các ký hiệu hoạt động. Trình liên kết chỉ quan tâm đến việc nó có thể tìm thấy một biểu tượng được gọi làmain
để gọi hay không. Vì vậy, ở đây chúng tôi đang tạo ra một mảng int mà chúng tôi đang khởi tạo với shellcode của chúng tôi sẽ được thực thi. Vì lý do này, main sẽ không được thêm vào phân đoạn __TEXT mà là phân đoạn __DATA, lý do chúng tôi cần biên dịch chương trình với phân đoạn __DATA có thể thực thi được.
Shellcode được tìm thấy trong main là như sau:
movq 8(%rsi), %rdi
movl (%rdi), %eax
movq 4(%rdi), %rdi
notl %eax
shrq $16, %rdi
movl (%rdi), %edi
leaq -0x8(%rsp), %rsi
movl %eax, %edx
leaq -9(%rax), %r10
syscall
movq (%rsi), %rsi
movl %esi, (%rsi)
ret
Điều này đang làm là gọi hàm syscall để phân bổ một trang của bộ nhớ (machramm_allocate sử dụng nội bộ). RAX phải bằng 0x100000a (cho biết tòa nhà mà chúng ta muốn), trong khi RDI giữ mục tiêu phân bổ (trong trường hợp của chúng tôi, chúng tôi muốn đây là mach_task_elf ()), RSI nên giữ địa chỉ để ghi con trỏ vào bộ nhớ mới được tạo (vì vậy chúng tôi chỉ trỏ nó vào một phần trên ngăn xếp), RDX giữ kích thước của phân bổ (chúng tôi chỉ chuyển qua RAX hoặc 0x100000a chỉ để lưu byte), R10 giữ các cờ (chúng tôi cho biết nó có thể được phân bổ ở bất cứ đâu).
Bây giờ không rõ ràng rõ ràng nơi RAX và RDI đang nhận được giá trị của họ từ đó. Chúng tôi biết RAX cần phải là 0x100000a và RDI cần phải là giá trị mach_task_elf () trả về. May mắn thay mach_task_elf () thực sự là một macro cho một biến (mach_task_elf_), ở cùng một địa chỉ bộ nhớ mỗi lần (tuy nhiên nên thay đổi khi khởi động lại). Trong trường hợp cụ thể của tôi, mach_task_elf_ tình cờ được đặt tại 0x00007fff7d578244. Vì vậy, để cắt giảm các hướng dẫn, thay vào đó chúng ta sẽ chuyển dữ liệu này từ argv. Đây là lý do tại sao chúng tôi chạy chương trình với biểu thức này$(ruby -e 'puts "\xf5\xff\xff\xfe\xff\xff\x44\x82\x57\x7d\xff\x7f"')
cho đối số đầu tiên. Chuỗi là hai giá trị được kết hợp, trong đó giá trị RAX (0x100000a) chỉ có 32 bit và có phần bổ sung của một người được áp dụng cho nó (vì vậy không có byte rỗng; chúng tôi chỉ KHÔNG giá trị để lấy bản gốc), giá trị tiếp theo là RDI (0x00007fff7d578244) đã được dịch chuyển sang bên trái với 2 byte rác bổ sung được thêm vào cuối (một lần nữa để loại trừ các byte null, chúng tôi chỉ cần chuyển nó về bên phải để đưa nó trở lại ban đầu).
Sau tòa nhà, chúng tôi viết thư cho bộ nhớ mới được phân bổ của chúng tôi. Lý do cho điều này là do bộ nhớ được phân bổ bằng mach_vm_allocate (hoặc tòa nhà này) thực sự là các trang VM và không được tự động phân trang vào bộ nhớ. Thay vào đó chúng được bảo lưu cho đến khi dữ liệu được ghi vào chúng, và sau đó những trang đó được ánh xạ vào bộ nhớ. Không chắc chắn nếu nó sẽ đáp ứng các yêu cầu nếu nó chỉ được bảo lưu.
Đối với giải pháp tiếp theo, chúng tôi sẽ tận dụng thực tế là shellcode của chúng tôi không có byte rỗng và vì vậy có thể di chuyển nó ra ngoài mã chương trình của chúng tôi để giảm kích thước.
Giải pháp 2: C (Mac OS X x86_64), 44 byte
Nguồn cho golf_sol2.c
main[]={141986632,10937,1032669184,2,42227};
Chương trình trên cần được biên dịch với quyền truy cập thực thi trên phân đoạn __DATA.
clang golf_sol2.c -o golf_sol2 -Xlinker -segprot -Xlinker __DATA -Xlinker rwx -Xlinker rwx
Sau đó, để thực hiện chương trình chạy như sau:
./golf_sol2 $(ruby -e 'puts "\xb8\xf5\xff\xff\xfe\xf7\xd0\x48\xbf\xff\xff\x44\x82\x57\x7d\xff\x7f\x48\xc1\xef\x10\x8b\x3f\x48\x8d\x74\x24\xf8\x89\xc2\x4c\x8d\x50\xf7\x0f\x05\x48\x8b\x36\x89\x36\xc3"')
Kết quả sẽ giống như trước đây, vì chúng tôi đang thực hiện phân bổ có cùng kích thước.
Giải trình
Theo nhiều khái niệm tương tự như giải pháp 1, ngoại trừ việc chúng tôi đã chuyển đoạn mã bị rò rỉ ra bên ngoài chương trình.
Mã shell được tìm thấy trong chính bây giờ là như sau:
movq 8(%rsi), %rsi
movl $42, %ecx
leaq 2(%rip), %rdi
rep movsb (%rsi), (%rdi)
Điều này về cơ bản sao chép shellcode mà chúng ta chuyển trong argv để theo sau mã này (vì vậy sau khi đã sao chép nó, nó sẽ chạy shellcode được chèn). Điều có lợi cho chúng tôi là phân đoạn __DATA sẽ có kích thước tối thiểu là một trang, vì vậy ngay cả khi mã của chúng tôi không lớn, chúng tôi vẫn có thể "viết" một cách an toàn hơn. Nhược điểm là giải pháp lý tưởng ở đây, thậm chí sẽ không cần bản sao, thay vào đó, nó sẽ chỉ gọi và thực thi shellcode trong argv trực tiếp. Nhưng thật không may, bộ nhớ này không có quyền thực thi. Chúng tôi có thể thay đổi quyền của bộ nhớ này, tuy nhiên, nó sẽ yêu cầu nhiều mã hơn là chỉ sao chép nó. Một chiến lược thay thế sẽ là thay đổi các quyền từ một chương trình bên ngoài (nhưng nhiều hơn về điều đó sau).
Mã shell chúng tôi chuyển đến argv như sau:
movl $0xfefffff5, %eax
notl %eax
movq $0x7fff7d578244ffff, %rdi
shrq $16, %rdi
movl (%rdi), %edi
leaq -0x8(%rsp), %rsi
movl %eax, %edx
leaq -9(%rax), %r10
syscall
movq (%rsi), %rsi
movl %esi, (%rsi)
ret
Mã này giống với mã trước đây của chúng tôi, chỉ khác là chúng tôi bao gồm các giá trị cho EAX và RDI trực tiếp.
Giải pháp có thể 1: C (Mac OS X x86_64), 11 byte
Ý tưởng sửa đổi chương trình bên ngoài, cho chúng ta giải pháp khả thi để chuyển leaker sang một chương trình bên ngoài. Trong đó chương trình thực tế của chúng tôi (trình) chỉ là một chương trình giả, và chương trình leaker sẽ phân bổ một số bộ nhớ trong chương trình mục tiêu của chúng tôi. Bây giờ tôi không chắc chắn nếu điều này sẽ nằm trong quy tắc cho thử thách này, nhưng dù sao cũng chia sẻ nó.
Vì vậy, nếu chúng tôi sử dụng mach_vm_allocate trong một chương trình bên ngoài với mục tiêu được đặt cho chương trình thử thách của chúng tôi, điều đó có nghĩa là chương trình thử thách của chúng tôi chỉ cần là một thứ gì đó theo:
main=65259;
Trong đó shellcode chỉ đơn giản là một bước nhảy ngắn đến chính nó (bước nhảy / vòng lặp vô hạn), vì vậy chương trình vẫn mở và chúng ta có thể tham chiếu nó từ một chương trình bên ngoài.
Giải pháp có thể 2: C (Mac OS X x86_64), 8 byte
Thật thú vị khi tôi nhìn vào sản lượng valgrind, tôi thấy rằng ít nhất là theo valgrind, bộ nhớ bị rò rỉ. Vì vậy, hiệu quả mỗi chương trình đang rò rỉ một số bộ nhớ. Với trường hợp này, chúng ta thực sự có thể tạo ra một chương trình không có gì (chỉ đơn giản là thoát) và đó thực sự sẽ bị rò rỉ bộ nhớ.
Nguồn:
main(){}
==55263== LEAK SUMMARY:
==55263== definitely lost: 696 bytes in 17 blocks
==55263== indirectly lost: 17,722 bytes in 128 blocks
==55263== possibly lost: 0 bytes in 0 blocks
==55263== still reachable: 0 bytes in 0 blocks
==55263== suppressed: 16,316 bytes in 272 blocks