2013年6月18日 星期二

Defcon 21 競賽解題思路 - linked


題目給了一個 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 不斷地吐資料。
寫個小程式 dump,把 shellcode 輸出到 stdout,再透過 nc 送出給 Server。


大概收集個100M ~ 900M,完全看運氣,然後再從 dump 出的記憶體中找尋關鍵字 "The key"。
答案出來!


2013年6月17日 星期一

Defcon 21 競賽解題思路 - incest


好久沒來寫點東西啦!前兩天又是一年一度的 Defcon Quals,照例 CHROOT 集合多位朋友一塊解題闖關。今年我沒能花太多時間在解題,只看了較有把握的兩題也順利解出,先來分享 "\xff\xe4\xcc" 的第一題 - incest。

此題目給了兩隻 Linux x64 binary,maw 和 sis 。當然二話不說,先來 strings。

$ strings maw
eth0
Could not open key.
/home/services/sis
$ objdump -D sis > sis.disasm


從 sis.disasm 反組譯的內容中,可以看到此程式有 fork() 產生的子行程,會配置一塊記憶體,再將 recv() 收到的內容放到此記憶體空間,最後直接呼叫 callq *rdx,跳躍到該記憶體區域執行。而父行程也配了一塊記憶體,從某檔案中讀取內容後,就不斷呼叫 sched_yield()。
如果轉寫成 C 程式碼,大概長得像這樣:

很明顯 maw 是一個網路 daemon,accept() 連線後再執行 sis,執行時會先 open() key file,然後將 key file 的 fd 和 socket fd 一塊傳給 sis。根據 strace/ltrace 可以發現,key_fd = 3,sock_fd = 4,而且 sis 的子行程會直接跳到遠端傳送的資料裡執行。注意到,呼叫 mmap() 配置 sock_buffer 時,是用 PROT_READ|PROT_WRITE|PROT_EXEC,可讀可寫可執行,就可以塞 shellcode 執行啦。

但是,要怎麼取得 key file 的資料呢?

原來的想法是透過打造 open("key", O_RDONLY),然後 read(key_fd) 再 write(sock_fd),但測試結果發現會 Permission denied。而根據題目 incest 原意,搭配 sis.c,可以猜想應該是要由子行程透過 ptrace() 來讀取父行程裡的資料,也就是父行程已 read(key_fd, key_buffer),只要能讀取父行程的 key_buffer 內容,再 write(sock_fd) 就能遠端取得 key_buffer 內容啦。

打造 x64_86 的 shellcode,我是參考此文 64-bit-linux-shellcode,然後用 nasm 寫了一小段程式碼。流程大概如下:
shellcode()
{
int ppid;
int key_buffer_address;
int key_buffer;
int i;
char buffer[40];
user_regs_struct regs;
ppid = getppid();

ptrace (PTRACE_ATTACH, ppid, NULL, NULL);
wait(NULL);
ptrace (PTRACE_GETREGS,ppid, NULL, &regs);
key_buffer_address = regs.rbp-0x18;
ptrace (PTRACE_PEEKDATA, ppid, key_buffer_address, &key_buffer);
for(i = 0; i < 10; i++)
ptrace (PTRACE_PEEKDATA, ppid, key_buffer+i*8, buffer+i*8);
write(4, buffer, 40);
}

根據前面所提的 sis.disasm
  1. 400949:       e8 82 fd ff ff          callq  4006d0
  2. 40094e:       48 89 45 e8             mov    %rax,-0x18(%rbp)

可以發現,取得父行程 rbp 暫存器值後去 -0x18,即為存放 calloc() 配置的記憶體位址。所以透過
key_buffer_address = regs.rbp-0x18;
ptrace (PTRACE_PEEKDATA, ppid, key_buffer_address, &key_buffer);

則會得到父行程的 key_buffer 位址啦,接著用一個 for-loop 把父行程的 key_buffer 存的內容讀出來,再透過 write() 輸出到 sock_fd = 4,就能看到解答。
此題  shellcode 的 nasm 如下:

使用 nasm 編譯後,寫了一個小程式 dump,把 shellcode 輸出 stdout。


這樣就過關啦!