題目給了一個 C 的 struct
typedef struct _llist {
struct _llist *next;
uint32_t tag;
char data[100];
} llist;
並且給了一小段 C 的程式碼
register char *answer;
char *(*func)();
llist *head;
...
func = (char *(*)(llist *))userBuf;
answer = (char *)(*func)(head);
send_string(answer);
exit(0);
此關卡程式會亂數配置若干 linked-list。要解開此關,則要送一段 shellcode 給 Server 執行,即 answer = (char *)(*func)(head); 這行呼叫。
Write me shellcode that traverses the randomly generated linked list, looking for a node with a tag 0x41414100, and returns a pointer to the data associated with that tag, such that the call to send_string will output the answer.
看來只要寫個 shellcode,走訪每個 linked-list,檢查 tag 是否為 0x41414100,若是則將此 linked-list 的位址傳回,應該就能讓 Server 吐出答案。
用 C 來表示大概像這樣:
char *shellcode(llist *head)
{
while(1) {
if (head->tag == 0x41414100)
return head->data;
head = head->next;
}
}
看來很簡單吧!
不過實際轉換成 shellcode 送出才發現,Server 回應 shellcode 只吃 16 bytes,也就是 shellcode 長度不能超過 16 bytes。這個長度限制馬上讓這題變成一個難解之題,不管怎麼縮小,改變 asm 指令等,都相當有難度。
此時,就想到如果不照題目所給的流程走,讓 shellcode 直接吐出走訪 linked-list 的所有資料,那 shellcode 能否控制在 16 bytes 之內呢?
用 C 來表示大概像這樣:
char *shellcode(llist *head)
{
dump:
write(4, head, 100);
head = head->next;
goto dump:
}
若是照正常的系統呼叫設定各暫存器,那鐵定超出長度,因此有些暫存器要故意忽略不設,例如呼叫 write() 的長度,C 這邊我寫 100,實際上完全不確定會 write 多少字元。
最後寫出的 shellcode 長得這樣:
沒幾行,分別解說如下:
7. pop edi
8. pop ecx
此兩行主要是因為 (char *)(*func)(head); ,也就是 call 之前會將參數 head 先放入堆疊(Stack) 再執行 call 時 x86 又會將返回位址放到堆疊,因此 pop edi 則會將返回位址取到 edi,而 pop ecx 則會將 head 存至 ecx。此時 ecx = head
10. mov ecx,[ecx]
此行則為 head = head->next,會放在一開始則完全是 shellcode 長度限制考量。
11. xor eax,eax
12. add eax,4
設定 eax = 4,因為 write 系統呼叫編號為 4。
14. push eax
15. pop ebx
恰好打算要輸出的 socket = 4,所以透過 push/pop 把 eax 複製到 ebx,一樣是長度限制考量。
16. ;mov edx,100
17. int 0x80
18. jmp _run
本來想設定 edx,代表要輸出的長度,不過受到長度限制考量,就完全不理長度,看當時執行此 shellcode 時 edx 暫存器值為何,就輸出多少長度,所以把這行註解掉了。
最後就是系統呼叫 int 0x80 把記憶體內容吐出來,然後 jmp _run 則不斷地走訪 linked-list 不斷地吐資料。
大概收集個100M ~ 900M,完全看運氣,然後再從 dump 出的記憶體中找尋關鍵字 "The key"。
答案出來!