Phong thể và kiểm soát dòng chảy: Từ các byte đến biểu đồ bạn có thể kiểm tra
Type: Build
Languages: Python, C, x86-64 assembly
Prerequisites: Phase 13 · 32 (ELF from scratch)
Time: ~150 minutes
Mục tiêu học tập
- Các bộ phận hướng dẫn x86-64 trong các bộ phận sổ cái được giải mã từ các quy tắc trong tập 2 SDM Intel, Chương 2.
- Cố gắng chứng minh máy giải mã chống lại
llvm-objdump -dvề mọi mệnh lệnh của mọi công trình xây dựng. - Thực hiện một quét tuyến tính và giảm ngược trên cùng một byte và giải thích nơi mỗi một sai.
- Cắt hướng dẫn thành các khối cơ bản và xây dựng biểu đồ kiểm soát dòng chảy với các cạnh được đánh dấu.
- Xét các kẻ thống trị ngay lập tức bằng thuật toán lặp lại của Cooper, Harvey và Kennedy, và tìm các vòng từ các cạnh sau.
- Khôi phục các mục tiêu của một nhảy gián tiếp từ các mục nhập di chuyển đó chỉ ra trên bàn của nó.
Vấn đề
Anh đã biên soạnledger.c-O0và hỏi một đại lý gì gradeĐại lý gọi một người giải thể, đọc danh sách, và trả lời rằng chức năng kết thúc trong jmpq *%raxKhông có mục tiêu nào được biết đến.return 0- Tôi không biết.
Bạn mở ra cùng một vật thể trong llvm-objdump -dvà nhìn thấy bốn khối sau khi nhảy mà cửa hàng 4, 3, 2 và 1. danh sách đã có chúng từ đầu. Không có gì trong danh sách nói cách thực thi đạt đến chúng, do đó công cụ coi chúng là mã chết.
Một danh sách chỉ trả lời một câu hỏi: nào tạo ra các lệnh nào. Nó không nói ra các lệnh chạy, theo thứ tự nào, hoặc từ đâu. Để trả lời chúng bạn cần thêm ba cấu trúc: các byte mà dòng điều khiển có thể đạt được, các khối cơ bản và các cạnh giữa chúng.
rea 6.5.0 không có máy giải thể riêng. Để phân tích sâu sắc, README của nó yêu cầu bạn mang Ghidra, Hopper, hoặc IDA. Để kiểm tra ngoài khơi, rea cũng có thể gọi một GNU objdump hoặc một Rizin mà bạn cài đặt. Hình đồ gọi của nó dừng lại ở các mục tiêu chưa được giải quyết: "Callers/callees chỉ chứa các chức năng được giải quyết.... Phòng chảy máy tính không có mục tiêu vẫn chưa được biết". Bài học này xây dựng phần tạo ra những sự thật đó, để bạn có thể đánh giá chúng khi một công cụ báo cáo chúng.
Các định dạng hướng dẫn có một quy tắc cho mỗi trường
Intel SDM Volume 2, Hình 2-1 cho thấy thứ tự byte của mỗi hướng dẫn. Đầu tiên là các prefix tùy chọn, sau đó là một đến ba byte opcode, sau đó là một byte ModRM tùy chọn và một byte SIB tùy chọn. Một sự di chuyển 0, 1, 2 hoặc 4 byte tiếp theo, sau đó là ngay lập tức 0, 1, 2 hoặc 4 byte. Toàn bộ hướng dẫn là tối đa 15 byte (SDM 2.3.11). AMD APM Volume 3, Chương 1, Phần 1.1 đến 1.7 mô tả cùng một định dạng trong ít trang hơn và là một kiểm tra chéo tốt.
Mỗi trường quyết định nghĩa của trường tiếp theo. Vì vậy, một decoder là một chuỗi các quyết định nhỏ, và mỗi quyết định có một câu trong hướng dẫn đằng sau nó.
Các tiền tố có thể được phân loại thành bốn nhóm
SDM 2.1.1 liệt kê các tiền tố cũ.F0khóa và các dấu tiền lặp lại F2và F3Nhóm 2 giữ sáu phân đoạn vượt trội (2Elà %cs(v) và các gợi ý về ngành.66, kích thước operand. nhóm 4 là67, kích thước địa chỉ vượt qua.
Quyển hướng dẫn cho biết một tiền tố từ mỗi nhóm có thể được sử dụng.ledger-x86_64-O0.otrong sự bù đắp0xf2đọc 66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00, 5 bản sao của 66- Dòng nhau.llvm-objdumpin nó như nopw %cs:(%rax,%rax)Và cũng như decoder bạn xây dựng. decoder thu thập mọi prefix byte nó thấy và hỏi sau đó những gì quan trọng.
66cho các hướng dẫn SSE nó là một tiền tố bắt buộc mà chọn mã op, vì vậy 66 0f ef c0là pxor %xmm0, %xmm0và không phải là 16 bit gì cả.F3 0f 6flà movdqu. Decoder đầu tiên tìm kiếm cặp tiền tố bắt buộc và các byte opcode thứ hai. Chỉ khi cặp đó không có mục nào thì66nghĩa là một opera 16-bit.
REX thêm bốn bit và phải ngồi bên cạnh mã op
SDM 2.2.1 định nghĩa REX là một byte từ 40đến4Fvới hình dạng 0100WRXB- Wchọn một operand 64 bit. Rmở rộng lĩnh vực quy định ModRM. Xmở rộng trường chỉ số SIB. Bmở rộng trường ModRM r/m, trường SIB cơ sở, hoặc sổ đăng ký trong byte opcode (Bảng 2-4). REX phải xuất hiện ngay trước opcode hoặc các byte thoát, và một REX sau đó là một prefix cũ bị bỏ qua.
Trong 48 89 e5byte 48REX có WSet. mã op 89là MOV r/m, r, và ModRM e5chọn %rspvào%rbp. Tắt REX và các byte tương tự là movl %esp, %ebp- Tôi không biết.
REX cũng thay đổi tên của các sổ đăng ký byte. Với bất kỳ REX hiện diện, mã hóa từ 4 đến 7 chọn %spl- %bpl- %sil, và%dilthay vì %ah- %ch- %dh, và%bh- Bộ giải mã giữ hai bàn vì lý do đó.
Các mã hóa có thể được sử dụng trong hai bản đồ
Một mã op một byte là trường hợp phổ biến. 0Fthoát ra bản đồ hai byte, và 0F 38hoặc 0F 3Athoát ra các bản đồ ba byte mà các thiết bị không bao giờ sử dụng (SDM 2.1.2). Một số opcode sử dụng trường ModRM reg như ba bit opcode khác, được viết /digittrong hướng dẫn. 83 /7là cmpvới một 8-bit ngay lập tức, 83 /5là sub, vàFF /4là không liên quanjmp- Tôi không biết.
Bộ giải mã trong code/main.pybao gồm các mã opcode trong bốn vật thể cố định cộng với một vài hàng xóm.mov- lea- movslq- movzbl- imul, các phiên,test, vàidivNó tiếp tục vớipush- pop- call- ret, 16 bước nhảy có điều kiện, nhiềubyte nop, và 5 mã opcode SSE2.DecodeErrorMột máy giải mã đoán là tệ hơn một máy dừng lại.
ModRM và SIB chọn các operand
SDM 2.1.3 chia ModRM thành mod(bits 7:6), reg(5:3), và r/m(2:0). mod = 11nghĩa là r/m là một sổ đăng ký. mod = 00- 01, và10Khoản bộ nhớ trung bình không có chuyển nhượng, chuyển nhượng 8 bit, hoặc chuyển nhượng 32 bit. r/m = 100nghĩa là một byte SIB tiếp theo.scale(7:6), index(5:3), và base(2: 0), và chỉ số của 100nghĩa là không có sổ đăng ký chỉ mục.
Hai hàng đặc biệt trong bảng 2-2 có tính chất trong chế độ 64-bit. mod = 00với r/m = 101Không phải (%rbp). Nó là một sự di chuyển 32 bit so với hướng dẫn tiếp theo (SDM 2.2.1.6). mod = 00với cơ sở SIB của 101là một chuyển động 32 bit mà không có sổ đăng ký cơ sở.
Nhận đi8d 44 08 fatừ grade-O0Mã op.8Dlà lea- ModRM44là 01 000 100: một chuyển động 8 bit, reg %eax, và một SIB tiếp theo.08là 00 001 000: quy mô 1, chỉ số %rcx, cơ sở%rax. Phân tích chuyển độngfalà -6. Văn bản AT&T là leal -0x6(%rax,%rcx), %eax, tính toánscore / 10 - 6, chỉ số chuyển đổi.
Việc di chuyển là nơi biên tập liên kết viết
48 8d 0d 00 00 00 00là leaq (%rip), %rcx. bốn byte không là trường di chuyển của một operand tương quan RIP. Trong một đối tượng có thể di chuyển chúng là người giữ chỗ..rela.texttrong sự bù đắp0x69nói R_X86_64_PC32.rodata-0x4. PsABI (bảng 4.9) xác định R_X86_64_PC32như S + A - P: địa chỉ biểu tượng cộng thêm phụ lục trừ địa chỉ của trường.
Bộ xử lý thêm trường vào địa chỉ của hướng dẫn tiếp theo, do đó mục tiêu cuối cùng là S + A + (end - P)- Vì cái nàyleatrường là bốn byte cuối cùng, vì vậy end - Plà 4 và mục tiêu là .rodata + (-4) + 4 =.rodata + 0x0- 4 bổ sung bù đắp cho bốn byte trước. mỗi RIP tương quan operand trong các thiết bị có -4 này bên trong bổ sung của nó, do đó.rodata+0x10trên -O1 lea- Ý nghĩa.rodata+0x14- Tôi không biết.
Bộ giải mã ghi lại sự thay thế của trường di chuyển bên trong mỗi hướng dẫn. resolve_reference()nhìn được bù đắp trong bảng chuyển đổi và báo cáo nơi mà các operand sẽ chỉ sau khi kết nối.leaq (%rip), %rcxvào bàn ở.rodata+0x0"Điều này là không thể.
Các điểm ngay là 32 bit tối đa, với một ngoại lệ
SDM 2.2.1.5 nói rằng ngay lập tức ở lại tối đa 32 bit trong chế độ 64-bit và được mở rộng bằng dấu hiệu.MOV r64, imm64(B8+rvới REX.W), mà AT&T gọi là cú pháp movabsq. Decoder đọc 8 bit ngay lập tức như ký kết cho toán học (83 c1 falà addl $-0x6, %ecx) và không được ký hiệu cho số lượng thay đổi và chọn trộn (pshufd $0xeeĐó là chia hợp với cái gì?llvm-objdump- Nhìn vân tay.
Gần các nhánh,push, vàpopmặc định cho một hệ điều hành 64 bit và bỏ qua 66(SDM 2.2.1.7). Đó là lý do tại sao decoder đặt tên cho chúng pushq- popq- callq, vàretqmà không đọc một chữ trước kích thước opera.
Tự giải mã một hướng dẫn bằng tay
Trước khi tin vào mã, hãy giải mã 48 69 c0 67 66 66 66từ gradetrên giấy.
48là trong40đến4F, nên nó là REX vớiW = 1- Kích thước của opera là 64 bit.69làIMUL r, r/m, imm32- Tôi không biết.c0là ModRM11 000 000: mẫu đăng ký, quy định%rax, r/m%rax- Tôi không biết.- Bốn byte tiếp theo là ngay lập tức, nhỏ-endian:
0x66666667- Tôi không biết. - AT&T đặt hàng là ngay lập tức, nguồn, đích:
imulq $0x66666667, %rax, %rax- Tôi không biết.
Đó là bảy byte, và llvm-objdumpBền định là tương đối cho phép clang chia bằng 10 với một nhân và sarq $0x22Điều tiếp theo, thay đổi bằng 34, bạn sẽ gặp lại cặp đó trong bài học 34, nơi người nâng biến nó trở lại thànhscore / 10- Tôi không biết.
Bộ giải mã làm 5 bước này trong Decoder.decode()- _State.modrm(), và_State.finish()Đọc chúng theo thứ tự đó và kết hợp từng nhánh với phần SDM được đặt tên trong chuỗi tài liệu của nó.
Đánh sạch tuyến tính
Schwarz, Debray và Andrews mô tả việc lau đường trong Phần 3.1 của "Vô thể tháo gỡ mã thực thi được xem xét" (WCRE 2002). Bắt đầu từ byte đầu tiên của phần mã, giải mã một hướng dẫn, tiếp tục tại byte sau đó, và dừng lại ở cuối. objdumplàm việc theo cách này, và cũng như llvm-objdump- Tôi không biết.
pythondef linear_sweep(code: bytes, start: int = 0, end: int | None = None) -> SweepResult:
position = start
while position < end:
try:
instruction = decode(code[:end], position)
except DecodeError:
undecodable.append((position, code[position]))
position += 1
continue
instructions.append(instruction)
position = instruction.endPhương pháp có một chế độ thất bại, và hình 1 của giấy cho thấy nó. Các byte là dữ liệu được giải mã như là hướng dẫn, và máy giải mã sau đó bỏ lỡ ranh giới hướng dẫn thực sự trong một thời gian. Trên x86, bộ giải mã thường đồng bộ hóa lại trong vòng vài byte, đó là điều khiến lỗi khó nhìn thấy. Việc phát hiện chỉ xảy ra trên một mã op không hợp lệ, và kết luận của báo cáo là các thuật toán này "làm lặng lẽ thất bại".
Việc lau cũng là lời tiên tri cho bài học này.llvm-objdump -dvề mọi hướng dẫn của mọi thứ, vàllvm-objdumpbản thân là một đường thẳng.code/fixtures/*.objdump.txt. Thử nghiệm phân tích từng dòng thành địa chỉ, byte và văn bản, bỏ các bình luận sau #Bốn vật có 86 , 56 , 84 và 11 hướng dẫn, tổng cộng là 237 , và decoder phù hợp với tất cả chúng.
Sự giảm dần
Phần 3.2 của cùng một bài báo gọi phương pháp thứ hai là đi qua nối tiếp. Bắt đầu tại một điểm nhập được biết và giải mã cho đến khi chuyển giao kiểm soát. Đối với một nhảy trực tiếp, tiếp tục tại mục tiêu. Đối với một nhánh có điều kiện, tiếp tục ở cả mục tiêu và hướng dẫn tiếp theo, và cho một cuộc gọi, tiếp tục sau đó, vì người gọi trả lại. Đối với một trở lại, dừng lại. Giữ một bộ địa chỉ được truy cập để bước đi kết thúc.
pythonif instruction.flow == "branch":
worklist.append(instruction.target)
address = instruction.end
elif instruction.flow == "indirect-jump":
table = resolver(instruction, instructions) if resolver else None
if table is None:
unresolved.append(instruction)
breakPhương thức thất bại là ngược lại. Một nhảy gián tiếp không có mục tiêu trong các byte của nó. Đi bộ dừng lại ở đó, và mỗi khối chỉ đạt được thông qua nhảy vẫn không được biết. Hình 2 của giấy cho thấy một nguy cơ thứ hai: một tập mục tiêu bắt nguồn từ việc di chuyển hướng đến giữa một hướng dẫn.main.pyghi lại trường hợp đó như là một xung đột và ngăn chặn con đường đó, vì vậy sự chồng chéo được nhìn thấy thay vì lặng lẽ giải mã hai lần.
Andriesse và những người khác đo chín bộ sụp đổ trên 981 bộ nhị phân được tạo bởi trình biên dịch (USENIX Security 2016). Dữ liệu trong đường và mã chồng chéo ít hơn trong đầu ra trình biên dịch thực tế so với các tài liệu giả định, và nguyên thủy cứng là phục hồi biên giới chức năng. Hãy nhớ cả hai phát hiện. Trạm trong phần tiếp theo là hiếm trong đầu ra trình biên dịch.gradelà phổ biến.
Một khu vực mà việc lau đi sai và hạ xuống đúng
code/fixtures/sweep-trap.slà 24 byte của tập hợp bằng tay với một bảng bốn byte bên trong cơ thể hàm.movzbl (%rcx,%rax), %eaxvà nhảy qua nó.
asmlookup:
movslq %edi, %rax
leaq .Ltable(%rip), %rcx
movzbl (%rcx,%rax), %eax
jmp .Ldone
.Ltable:
.byte 0x31, 0xc0, 0x90, 0x48
.Ldone:
addl $1, %eax
retqHãy xây dựng nó với clang -target x86_64-unknown-linux-gnu -c sweep-trap.sBốn byte bảng được chọn để giải mã như là hướng dẫn hợp lý.31 c0là xorl %eax, %eax- 90là nop- 48là một chữ trước REX, vì vậy, việc quét kết hợp nó với ba byte của thựcaddl $1, %eaxvà dấu vân tayaddq $0x1, %rax- Những kẻ bị bắtllvm-objdumpđầu ra cho thấy chính xác rằng:
text e: eb 04 jmp 0x14 <lookup+0x14>
10: 31 c0 xorl %eax, %eax
12: 90 nop
13: 48 83 c0 01 addq $0x1, %rax
17: c3 retqKhông có gì trong danh sách này trông bị hỏng.retq- Đăng ký vẫn sai 3 cách.0xemục tiêu0x14, và không có hướng dẫn nào trong danh sách bắt đầu từ 0x14- Chỉ dẫn chạy ở0x14là 32 bit addl, và danh sách cho thấy một 64-bit addq4 byte bảng xuất hiện như mã.
Sự giảm dần từ lookupbiểu tượng giải mã sáu hướng dẫn, sau khi nhảy đến 0x14, và báo cáo addl $0x1, %eaxNó không yêu cầu bất kỳ 4 byte bảng và không có 8 byte bọc trước đây .plus_one- trap_report()trong main.pyin so sánh:
json"linearSweep": {
"instructions": 9,
"atJumpTarget": {"starts": "0x13", "text": "addq\t$0x1, %rax", "startsAtTarget": false},
"claimedDataBytes": 4,
"claimedPaddingBytes": 8
},
"recursiveDescent": {
"instructions": 6,
"atJumpTarget": "addl\t$0x1, %eax",
"claimedDataBytes": 0,
"claimedPaddingBytes": 0
}Thử nghiệmtest_sweep_and_objdump_share_the_same_mistakekhẳng định điều đó llvm-objdumpin addq $0x1, %rax0x13- Oracle là một quét tuyến tính và thừa hưởng điểm mù của nó.
Các khối cơ bản
Allen xác định khối cơ bản trong "Control Flow Analysis" (1970). Nó là một chuỗi các hướng dẫn chỉ được nhập vào chỉ tại hướng dẫn đầu tiên và chỉ để lại ở cuối cùng. Yakdan và những người khác tái lập nó để phân tích trong Phần II.A.2 của "No More Gotos" (NDSS 2015), với nút tiêu đề như mục nhập chức năng.
Việc xây dựng là quét hai bước qua các hướng dẫn được giải mã, được sắp xếp theo địa chỉ. Quét đầu tiên thu thập các nhà lãnh đạo. Một nhà lãnh đạo là mục tiêu nhập, một nhánh hoặc nhảy mục tiêu, một mục tiêu bàn hoặc hướng dẫn sau bất kỳ chuyển giao kiểm soát nào. Quét thứ hai bắt đầu một khối mới tại mỗi nhà lãnh đạo và tại mỗi khoảng trống trong chuỗi địa chỉ.
pythonfor instruction in self.instructions:
if instruction.address in leaders or current is None or current.end != instruction.address:
current = Block(instruction.address, [])
self.blocks[instruction.address] = current
current.instructions.append(instruction)Các cạnh theo dõi từ hướng dẫn cuối cùng của mỗi khối. Một trở lại không có người kế nhiệm. Một nhảy có một, được đánh dấu jumpMột nhánh có điều kiện có hai, được đánh dấutakenvà fallthrough. Một hướng dẫn đơn giản ở cuối một khối rơi qua đến khối tiếp theo, xảy ra khi hướng dẫn tiếp theo là một mục tiêu nhánh.table, hoặc không có, và khối được ghi lại trong unresolved- Tôi không biết.
Hai sự thật khác xuất hiện từ cùng một quét. Một khối mà không thể đạt được nhập cảnh là không thể đạt được, và lau qua.grade-O0Một khối có địa chỉ bắt đầu nằm trong một hướng dẫn của danh sách phương pháp khác là trường hợp chồng chéo từ cái bẫy.
Chữ đồ thị kiểm soát dòng chảy tổng
total-O1là 31 byte và 13 hướng dẫn:
text 40: 85 f6 testl %esi, %esi
42: 7e 18 jle 0x5c <total+0x1c>
44: 89 f1 movl %esi, %ecx
46: 31 d2 xorl %edx, %edx
48: 31 c0 xorl %eax, %eax
4a: 66 0f 1f 44 00 00 nopw (%rax,%rax)
50: 03 04 97 addl (%rdi,%rdx,4), %eax
53: 48 ff c2 incq %rdx
56: 48 39 d1 cmpq %rdx, %rcx
59: 75 f5 jne 0x50 <total+0x10>
5b: c3 retq
5c: 31 c0 xorl %eax, %eax
5e: c3 retqCác nhà lãnh đạo là0x40(trình nhập),0x44(sau khi jle), 0x50(Mục tiêu của jne), 0x5b(sau khi jne), và 0x5c(Mục tiêu của jle5 khối, 5 cạnh:
| From | To | Kind | Why |
|---|---|---|---|
0x40 | 0x44 | fallthrough | jle not taken, count is positive |
0x40 | 0x5c | taken | count is zero or negative, return 0 |
0x44 | 0x50 | fallthrough | the nopw is alignment inside the block, execution passes through it |
0x50 | 0x50 | taken | jne 0x50, the loop |
0x50 | 0x5b | fallthrough | loop exit, return the sum |
6 byte nopw0x4alà lấp để vòng lặp đầu vào 0x50Nó nằm trên một ranh giới 16 byte. Nó có thể tiếp cận, vì vậy nó thuộc về khối0x44- Một-bytenop0x5fsau lần cuối cùng retqcũng là nạp, và không có gì đạt đến nó.unreachable()- Đặt nó lên danh sách.
Tại -O2cùng một hàm là 140 byte với hai vòng tròn.0x80thêm tám giá trị cho mỗi lần đi qua với paddd, và phần còn lại của scalar chạy ở 0xc0CFG có hai cạnh sau,0x80 -> 0x80và 0xc0 -> 0xc0, và phòng kiểm tra kiểm tra cả hai.
Các nhà thống trị với thuật toán lặp lại
Một khốidthống trị một khối bKhi mọi con đường từ lối vào đến bđi qua d. người thống trị ngay lập tức của blà chủ quyền của bĐó là gần nhất với bÝ tưởng này bắt đầu từ Prosser (1959) đến Allen (1970), và đó là cấu trúc mà bài học 34 cần đặt ifvà whileđúng rồi.
Cooper, Harvey và Kennedy đã viết "A Simple, Fast Dominance Algorithm" (2001). Họ lập luận rằng định dạng lưu lượng dữ liệu, chạy theo thứ tự hậu ngược với biểu tượng nhỏ gọn, đánh bại Lengauer-Tarjan (1979) trên biểu đồ dưới khoảng một ngàn nút. Bài báo của họ nói mục đích của nó là "không trình bày một thuật toán mới". Nó cho thấy rằng các thuật toán đơn giản có thể chiến thắng trong thực tế và là công cụ giảng dạy tốt. Một chức năng CFG hầu như không bao giờ có một ngàn nút.
pythondef intersect(first: int, second: int) -> int:
while first != second:
while number[first] > number[second]:
first = idom[first]
while number[second] > number[first]:
second = idom[second]
return first
changed = True
while changed:
changed = False
for start in order[1:]:
processed = [p for p in self.predecessors(start) if p in idom]
candidate = processed[0]
for other in processed[1:]:
candidate = intersect(other, candidate)
if idom.get(start) != candidate:
idom[start] = candidate
changed = Truenumbergiữ vị trí hậu trật tự ngược của mỗi khối, với mục nhập ở 0. intersectvà nó đi hai ngón tay lên cây chủ quyền hiện tại cho đến khi chúng gặp nhau.<Mã này sử dụng số hậu lệnh ngược, do đó so sánh là >Mọi thứ khác đều theo mã giả của tờ báo.
Vì total-O1Kết quả là idom(0x44) = 0x40- idom(0x50) = 0x44- idom(0x5b) = 0x50, vàidom(0x5c) = 0x40- Lề cạnh0x50 -> 0x50là một cạnh sau vì mục tiêu của nó thống trị nguồn của nó, và một cạnh sau đánh dấu một vòng lặp (Allen 1970). back_edges()- Thử nghiệm.test_dominators_on_a_hand_built_diamond_with_a_loopchạy cùng một mã trên một chuỗi 16 byte với một điểm kết nối, vì vậy intersectthấy hai người tiền nhiệm.
Bảng nhảy ở điểm -O0
Tại -O0clang biên soạn các switchtrong gradecho ngôn ngữ nhảy bảng độc lập vị trí:
text 5d: 83 e8 04 subl $0x4, %eax
60: 77 38 ja 0x9a <grade+0x6a>
62: 48 8b 45 f0 movq -0x10(%rbp), %rax
66: 48 8d 0d 00 00 00 00 leaq (%rip), %rcx
0000000000000069: R_X86_64_PC32 .rodata-0x4
6d: 48 63 04 81 movslq (%rcx,%rax,4), %rax
71: 48 01 c8 addq %rcx, %rax
74: ff e0 jmpq *%raxHãy đọc nó như bốn sự thật.subl $0x4tiếp theo là jalà kiểm tra giới hạn: chỉ số trên 4 đi đến trường hợp mặc định tại 0x9a, vậy bảng có năm mục.leaqkhi chuyển chỗ của nó đặt địa chỉ bảng trong %rcx, và phần trước đã cho thấy địa chỉ đó là .rodata+0x0- movslqtải một mục 32 bit được ký kết được quy mô bằng 4. addqthêm cơ sở bảng, do đó mỗi mục là khoảng cách từ bảng đến khối trường hợp của nó.
Trong tệp đối tượng mỗi byte bảng là không..rela.rodata- Có thể là:
textRELOCATION RECORDS FOR [.rodata]:
OFFSET TYPE VALUE
0000000000000000 R_X86_64_PC32 .text+0x91
0000000000000004 R_X86_64_PC32 .text+0x8c
0000000000000008 R_X86_64_PC32 .text+0x87
000000000000000c R_X86_64_PC32 .text+0x82
0000000000000010 R_X86_64_PC32 .text+0x86Đơn S + A - Pvào ktrong sự bù đắp4kGiá trị lưu trữ là .text + A - (.rodata + 4k), và mã này thêm .rodata + 0đến nó, vì vậy mục tiêu là .text + A - 4k. Nhập 0 là 0x91 - 0 = 0x91, mục 1 là 0x8c - 4 = 0x88, và mục 2 là 0x87 - 8 = 0x7f. Nhận mục 3 và 4 là 0x82 - 12 = 0x76và 0x86 - 16 = 0x76Các trường hợp 6, 7, 8, 9 và 10 củascore / 10đi đến các khối lưu trữ 1, 2, 3, 4 và 4. Điều đó phù hợp với dòng nguồn C cho dòng, bao gồm cả trường hợp chia sẻ cho 9 và 10.
recover_table()làm toán học này.jump_table_resolver()phù hợp với ngôn ngữ ngược lại từ jmpqNó tìm thấy addqĐó là những gì đã làm ra sổ cái,movslqngười đã tải nó, leaqĐó là nền tảng và đường biên giới.ja. Sự giảm sau đó đẩy bốn mục tiêu khác nhau và tiếp tục. Một trình thực liên kết có giá trị cuối cùng trong bảng và không có di chuyển.evidenceghi lại con đường tạo ra các mục tiêu.
Số cho grade-O0, từ python3 code/main.py cfg code/fixtures/ledger-x86_64-O0.o grade- Có thể là:
| Method | Instructions | Blocks | Edges | Bytes | What is wrong |
|---|---|---|---|---|---|
| linear sweep | 34 | 9 | 7 | 118 code + 10 padding | 5 unreachable blocks, no edge into the four case blocks, padding decoded as nopw |
| recursive descent | 25 | 3 | 2 | 82 code | 36 code bytes never reached, jmpq *%rax at 0x74 unresolved |
| descent with the table | 33 | 8 | 11 | 118 code | nothing |
Với bàn, cây thống trị đã hoàn thành.idomcủa mỗi khối trường hợp là 0x62, khối với nhảy.idomcủa 0xa1là mục nhập 0x30, bởi vì đường mặc định từ 0x9ađạt tới 0xa1Không đi qua 0x62- Tôi không biết.
Điều mà Optimizer đã làm để đánh giá ở -O1
Anh sẽ thấygradeđược mô tả là một chuyển đổi mà tập hợp đến một bảng nhảy.-O1và -O2không có nhảy gián tiếp gradeKhông có:
text 2c: 83 f9 04 cmpl $0x4, %ecx
2f: 77 0c ja 0x3d <grade+0x2d>
31: 89 c8 movl %ecx, %eax
33: 48 8d 0d 00 00 00 00 leaq (%rip), %rcx
0000000000000036: R_X86_64_PC32 .rodata+0x10
3a: 8b 04 81 movl (%rcx,%rax,4), %eax
3d: c3 retqThêm vào việc di chuyển0x10cộng với 4 bình thường đặt bàn ở .rodata+0x14- Lái là một bình thườngmovl, và các byte có01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 04 00 00 00. Bộ biên dịch thay thế năm khối trường hợp bằng một bảng của năm giá trị trả lại. Có một di chuyển, một bảng và kiểm tra giới hạn.find_value_tables()báo cáo như là value-tablevới các mục [1, 2, 3, 4, 4]và evidence: "bytes"- Tôi không biết.
grade_label-O2đi một bước xa hơn.labelsarray của năm chỉ dẫn chuỗi trở thành một bảng của năm 32 bit offsets tại .rodata+0x0Mỗi mục có một R_X86_64_PC32di chuyển đối với phần chuỗi, và hàm này thêm địa chỉ bảng vào mục nó tải. recover_table()đọc được 5 chuyển nhượng và trả lại bù đắp 0, 2, 4, 6, 8vào.rodata.str1.1, giữ được F- D- C- B, vàA. cùng một toán học phục hồi một bảng nhảy phục hồi một bảng dữ liệu tương đối, bởi vì cả hai là S + A - PMột thời gian đáng kể.
Bài học từ hai build là đặt tên cho những gì các byte hiển thị. Một bảng nhảy thay đổi biểu đồ lưu lượng điều khiển. Một bảng giá trị thay đổi tải trọng. gọi cả hai "switch" che giấu sự khác biệt mà bài học 34 phải tái cấu trúc.
Những gì Rea và Ghidra cho bạn ở đây
rea 6.5.0 với nhà cung cấp Ghidra 12.1.4 trả lại văn bản danh sách của một hàm từ procedure_assemblynhư một dây. inspect_native_instructiontrả về các byte, chiều dài, văn bản decoder, mã hiệu operand, tham chiếu được gõ và dòng chảy của một hướng dẫn, với trả về được phân loại từ mã p. xrefstrả lại các địa chỉ nguồn độc đáo được sắp xếp từ Ghidra ReferenceManager.getReferencesTo- analyze_functionmang CFG và bodycủa chức năng. procedure_infođưa ra cơ quan như các phạm vi bao gồm và cảnh báo rằng "Các khoảng trống và đuôi chia sẻ không được coi là các byte sở hữu".
Mỗi một trong những sự kiện đó là Ghidra trở lại dòng dõi cộng với các phân tích của nó, được phân tích theo thứ tự.instruction_flow_evidence- function_references, vàjump_table_evidenceKhi Ghidra không tìm thấy bảng, dòng chảy là "không biết", và các khối trường hợp bị thiếu trong người gọi, gọi và CFG.
Giờ thì anh có công việc rồi.python3 code/main.py cfgtrên cùng một đối tượng và so sánh bắt đầu khối, đếm cạnh, và bảng được phục hồi với những gì nhà cung cấp trả lại. nơi họ đồng ý bạn có hai dẫn xuất độc lập. nơi họ khác nhau bạn có một địa chỉ cụ thể để xem, đó là những gì bài học 35 ghi nhận như là một người không biết.
Một ghi chú về bộ giải mã như một chương trình. Capstone 6.0.0-Alpha10 (2026-07-21) cố định ngoài biên giới đọc và viết dưới CVE-2026-55893 và CVE-2026-55894, và 5.0.9 (2026-05-28) CVE sửa đổi sao lưu. Một bộ giải mã đọc các byte nó không viết. giới hạn 15-byte, kiểm tra cắt giảm trong _Cursor.byte(), và từ chối đoán một mã opcode không rõ bảo vệ main.pytừ đầu vào nó không được xây dựng cho.
Hãy xây dựng nó
Chạy demo:
bashcd phases/13-tools-and-protocols/33-disassembly-and-control-flow
python3 code/main.pyCác kết quả là một báo cáo JSON. Đối với mỗi đối tượng cố định nó cung cấp sự đồng thuận của Oracle. Đối với mỗi chức năng nó cung cấp hướng dẫn, khối và cạnh đếm theo ba phương pháp. Nó cũng liệt kê các khối không thể đạt được, nhảy gián tiếp chưa được giải quyết, cạnh sau, các chủ quyền ngay lập tức, và bất kỳ bảng nào. Nó kết thúc với so sánh bẫy.
Hình dạng dự kiến:
json{
"fixtures": {
"ledger-x86_64-O0.o": {
"oracle": "86/86 instructions agree with llvm-objdump",
"functions": {
"grade": {
"range": "0x30-0xa6",
"paddingBytes": 10,
"linearSweep": {"instructions": 34, "blocks": 9, "edges": 7, "unreachableBlocks": ["0x76", "0x7f", "0x88", "0x91", "0xa6"], "...": "..."},
"recursiveDescent": {"instructions": 25, "blocks": 3, "edges": 2, "unresolvedIndirectJumps": ["0x74"], "...": "..."},
"recursiveDescentWithTables": {"instructions": 33, "blocks": 8, "edges": 11, "...": "..."},
"tables": [{"kind": "jump-table", "table": ".rodata+0x0", "count": 5, "entries": ["0x91", "0x88", "0x7f", "0x76", "0x76"], "evidence": "relocation", "...": "..."}]
}
}
}
},
"sweepTrap": {"...": "..."}
}Đọc code/main.pytheo thứ tự này:
ElfObjectđọc tiêu đề phần, biểu tượng, và.rela.*Các mục, vàsymbolize()đặt tên địa chỉ đườngllvm-objdump- Không.Decoder.decode()đọc tiền đề và REX, sau đó gửi trên bản đồ opcode một-byte hoặc hai-byte._State.modrm()đọc ModRM, SIB, và sự di chuyển, và ghi lại sự di chuyển của trường di chuyển.resolve_reference()áp dụngS + A - Pđến việc di dời trên một khu vực di dời hoặc khu vực gần đó.linear_sweep()vàrecursive_descent()là hai phương pháp, mỗi phương pháp đều có lỗi ghi nhận.recover_table()vàjump_table_resolver()đọc một bảng thông qua các chuyển nhượng hoặc các byte của nó.ControlFlowGraphxây dựng các nhà lãnh đạo, khối, và cạnh đánh dấu, vàdominators()là thuật toán lặp lại.analyze_function()chạy tất cả các phương pháp trên một hàm vàcompare_with_oracle()kiểm tra máy giải mã.
Có hai lệnh nữa.python3 code/main.py disasm FILE.oin một danh sách với mục tiêu di chuyển sau mỗi hướng dẫn có một. python3 code/main.py cfg FILE.o FUNCTIONin phân tích của một hàm.
Sử dụng nó
Sử dụng ba phương pháp theo thứ tự này cho một đối tượng mới.
- Hãy thử so sánh Oracle trước.
llvm-objdumptrên một hướng dẫn đơn giản có một lỗi, và mọi cấu trúc sau đó thừa kế nó. - Chạy sự giảm ngược từ mỗi biểu tượng hàm và đọc
unresolvedMỗi mục nhập là một cuộc gọi không liên quan mà mục tiêu của bạn không có. - Đối với mỗi bước nhảy chưa được giải quyết, hãy tìm kiếm ngôn ngữ bảng và di chuyển hướng về nó. Nếu ngôn ngữ không phù hợp, hãy đọc các byte trước khi nhảy và ghi lại những gì bạn thấy.
- So sánh các bộ sơn và các bộ sơn giảm đi bằng một bộ sơn. các bộ sơn mà chỉ có các bộ sơn yêu cầu là bộ sơn hoặc dữ liệu. các bộ sơn mà không yêu cầu là không thể đạt được hoặc thuộc về một chức năng mà bạn không tìm thấy.
- Xây dựng CFG từ khi xuống với các bảng được phục hồi và đọc các cạnh phía sau.
whilehoặcfor- Tôi không biết.
Giữ các tệp văn bản Oracle với các đối tượng. Khi bạn chuyển sang phiên bản biên dịch mới, lấy lại chúng, bởi vì một danh sách thay đổi là một vật cố định thay đổi.
Phòng thí nghiệm tương tác
Phòng thí nghiệm A: giải mã trên giấy, sau đó kiểm tra
Từ codethư mục, mở Python:
bashcd phases/13-tools-and-protocols/33-disassembly-and-control-flow/code
python3 -qĐi chạy:
pythonfrom main import *
decode(bytes.fromhex("48 8d 0d 00 00 00 00"), 0).text
decode(bytes.fromhex("8d 44 08 fa"), 0).text
decode(bytes.fromhex("66 66 66 66 66 2e 0f 1f 84 00 00 00 00 00"), 0).lengthViết ra những gì bạn mong đợi trước mỗi cuộc gọi.48từ đầu tiên và xem kích thước của operand thay đổi.decode(bytes.fromhex("0f 0a"), 0)và đọc DecodeError- Tôi không biết.
Phòng thí nghiệm B: việc di chuyển trên một
pythonelf = load_fixture("ledger-x86_64-O0.o")
code = elf.section(".text").data
lea = decode(code, 0x66)
lea.displacement_offset
resolve_reference(elf, lea)Bản tham khảo nói:.rodata+0x0Thay đổi địa chỉ thành 0x116trong grade_labelvà đọc tham chiếu vào .dataRồi hãy giải mã callq0x10eVà giải quyết nó.R_X86_64_PLT32 grade-0x4, và mục tiêu được giải quyết là 0x30, đó là grade- Tôi không biết.
Phòng thí nghiệm C: xem hai phương pháp không đồng ý
pythontrap = load_fixture("sweep-trap.o")
text = trap.section(".text").data
[i.text for i in linear_sweep(text, 0, 0x20).instructions]
[i.text for i in recursive_descent(text, [0], 0x20).sorted_instructions()]Đếm các hướng dẫn trong mỗi danh sách và tìm cái bắt đầu từ 0x13Sau đó thay đổi byte cuối cùng trong bảngsweep-trap.stừ 0x48đến0xe8, xây dựng lại, và chạy cả hai lần nữa.addlvà retqvào một callqvới một mục tiêu bên ngoài phần.
Phòng thí nghiệm D: các kẻ thống trị bằng tay
pythonanalysis = analyze_function(load_fixture("ledger-x86_64-O1.o"), "total")
analysis.cfg.edges
analysis.cfg.reverse_postorder()
analysis.cfg.dominators()
analysis.cfg.back_edges()Chụp 5 khối trên giấy và tính toán các nhà thống trị trước khi bạn nhìn vào kết quả.grade-O0và giải thích tại sao idom(0xa1)là 0x30Và không0x62- Tôi không biết.
Phòng thí nghiệm thực hành
Thêm một vị trí gọi gián tiếp vào các thiết bị và mở rộng phân tích.
Yêu cầu:
- Viết một hàm C gọi qua một chỉ số hàm được chọn từ một mảng nhỏ.
-O0và-O1và bắt buộc các đối tượng với họllvm-objdump- Thư. - Làm cho bộ giải mã phù hợp với Oracle trên cả hai đối tượng.
- Tăng
recursive_descent()Vì vậy, một cuộc gọi gián tiếp ghi lại vị trí cuộc gọi trongcallsvớiNonenhư mục tiêu và tiếp tục theo đuổi nó. - Viết một giải pháp đọc các hàm chỉ số dãy thông qua các di chuyển của nó và báo cáo các calles có thể.
- Thêm các thiết lập callie vào báo cáo chức năng như
indirectCallsvới loại bằng chứng cho mỗi mục tiêu. - Viết một bài kiểm tra thất bại khi giải quyết trả lại một mục tiêu không phải là biểu tượng hàm.
Thành công là nghĩa củapython3 code/main.py cfgđặt tên cho mọi người có thể gọi với việc di chuyển chứng minh nó, và sự so sánh của Oracle vẫn truyền trên mọi đối tượng.
Bài học: Cây tạo vật
Bài học này bao gồm:outputs/skill-disassembly-and-control-flow.mdSử dụng nó để biến một đối tượng và một tên chức năng thành một danh sách được kiểm tra, khối, cạnh được gõ, chủ quyền, và mọi chuyển giao chưa được giải quyết. Nó đòi hỏi một so sánh Oracle, cả hai phương pháp giải thể, kế toán byte, bằng chứng di chuyển cho mỗi bảng được phục hồi, và một tuyên bố về những gì vẫn còn không rõ.
Hãy kiểm tra
Chạy demo và bộ xác định:
bashcd phases/13-tools-and-protocols/33-disassembly-and-control-flow
python3 code/main.py
python3 code/tests/test_main.pyViệc kiểm tra phải chứng minh:
- decoder đồng ý với
llvm-objdump -dtrên tất cả 237 hướng dẫn của bốn vật thể cố định - REX.W chọn các operand 64-bit và REX.R và REX.B mở rộng các trường đăng ký
- ModRM và SIB rendering bộ nhớ AT&T, bao gồm quy mô, chỉ mục và chuyển đổi ký hiệu
- Một chuyển động tương quan RIP được ghi lại với sự thay thế trường của nó bên trong hướng dẫn
- lặp đi lặp lại tiền đề thừa kế và một đoạn mã bỏ qua như Oracle
- một yêu cầu bắt buộc
66Prefix chọn mã opcode SSE và để lại các sổ đăng ký 32-bit - 8-bit ngay lập tức là chữ ký mở rộng cho toán học và không chữ ký cho số lượng chuyển tiếp
- một hướng dẫn trên 15 byte, một hướng dẫn cắt ngắn, và một mã opcode không rõ tăng
DecodeError R_X86_64_PC32trên mộtleagiải quyết cho bàn vàR_X86_64_PLT32trên mộtcallgiải quyết cho người gọi- Screep giải mã bảng bẫy như là mã và không có hướng dẫn tại mục tiêu nhảy
- Thì, khi đi xuống, không bao giờ có thể lấy bàn hay đệm và đọc.
addl $0x1, %eaxở mục tiêu llvm-objdumplàm sai lầm giống như việc lau lắp bẫy- một mục nhập bên trong một hướng dẫn được ghi nhận là xung đột
- Một cú nhảy gián tiếp không được giải quyết ngăn chặn sự hạ cánh và được liệt kê
total-O1có năm khối, năm cạnh được đánh dấu, một cạnh sau, và các nhà thống trị ngay lập tức dự kiến- Một viên kim cương được chế tạo bằng tay với một vòng cung cấp cho các nhà thống trị thuật toán xác định
total-O2có hai vònggrade-O0tạo ra một bàn nhảy 5 lần từ.rela.rodatavới mục tiêu0x91, 0x88, 0x7f, 0x76, 0x76- Không có bàn
grade-O0có ba khối và một nhảy không được giải quyết grade-O1và-O2là một bảng giá trị[1, 2, 3, 4, 4]Không có nhảy gián tiếpgrade_label-O2đọc một bảng chuỗi tương đối thông qua năm di chuyển- Hình ảnh tương tác chứa cùng các dữ liệu mà phân tích tạo ra
Các chế độ sản xuất thất bại
| Failure | What the weak tool reports | What the analysis must prove |
|---|---|---|
Data inside .text | A plausible listing with every byte decoded | The descent claims no data byte and the jump target starts an instruction |
| Padding after the last return | A nopw instruction at the end of the function | The bytes are unreachable from the entry |
| Indirect jump through a table | "jmp rax" and four dead blocks | Table address from the lea relocation, entry count from the bounds check, targets from .rela.rodata |
| Switch turned into a value table | "a jump table with a relocation" | A movl load from .rodata+0x14, three blocks, no indirect transfer |
| Call with a zero displacement | callq 0x113, the next instruction | R_X86_64_PLT32 grade-0x4 resolves to 0x30 |
| Decoder guesses an unknown opcode | A mnemonic that looks right | DecodeError with the address, and the sweep records the byte |
| Instruction longer than 15 bytes | A very long "instruction" | The cursor raises before reading byte 16 |
| Branch into the middle of an instruction | Two overlapping listings | A conflict record with both addresses |
| Loop reported as straight-line code | No while in the pseudocode | A back edge whose target dominates its source |
| Oracle captured from a different compiler | Tests pass on stale text | The fixture README names the exact compiler and command |
Quy tắc hoạt động
Khóa mã chống lại một ngôn ngữ, kiểm soát lưu lượng từ các mục nhập được biết, và giải thích cho mỗi byte như được tuyên bố, lấp đầy, dữ liệu hoặc không thể tiếp cận. Khôi phục mỗi mục tiêu gián tiếp từ một nơi di chuyển hoặc một bảng bạn có thể chỉ ra.
Đọc thêm
- Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 2, Chapter 2: Instruction Format
- AMD64 Architecture Programmer's Manual Volume 3, Chapter 1: Instruction Encoding
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, Table 4.9: Relocation Types
- Schwarz, Debray, Andrews, "Disassembly of Executable Code Revisited" (WCRE 2002)
- Andriesse et al., "An In-Depth Analysis of Disassembly on Full-Scale x86/x64 Binaries" (USENIX Security 2016)
- Allen, "Control Flow Analysis" (1970)
- Cooper, Harvey, Kennedy, "A Simple, Fast Dominance Algorithm" (2001)
- Lengauer, Tarjan, "A fast algorithm for finding dominators in a flowgraph" (1979)
- Capstone 6.0.0-Alpha10 release notes
- rea 6.5.0 nguồn tại tag
3b79af2bài học này mô tả:README.md-docs/ghidra-provider.md-docs/ghidra-dos.md-docs/reverse-engineering-tools.md-bridge/ghidra/ReaGhidraBridge.java-src/contracts/officialToolContracts.ts-src/ghidra/GhidraAnalysisProfile.ts- Tôi không biết.
This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.
Browse the complete course catalog or open this lesson on GitHub.