Đây là series try hard giải các challenge trên trang web pwnable.kr. Với mỗi chall mình sẽ phân tích hướng giải và những điều mình học được từ những challenge đó :>
fd
Description
1 2 3 4 5 6
Mommy! what is a file descriptor in Linux?
* try to play the wargame your self but if you are ABSOLUTE beginner, follow this tutorial link: https://youtu.be/971eZhMHQQw
#include<stdio.h> #include<stdlib.h> #include<string.h> char buf[32]; intmain(int argc, char* argv[], char* envp[]){ if(argc<2){ printf("pass argv[1] a number\n"); return0; } int fd = atoi( argv[1] ) - 0x1234; int len = 0; len = read(fd, buf, 32); if(!strcmp("LETMEWIN\n", buf)){ printf("good job :)\n"); setregid(getegid(), getegid()); system("/bin/cat flag"); exit(0); } printf("learn about Linux file IO\n"); return0;
}
Solution
Khi gọi hàm read, chương trình truyền vào tham số đầu tiên là fd với fd được tính theo công thức atoi( argv[1] ) - 0x1234 và argv[1] có nghĩa là tham số đầu tiên khi bạn gọi file ( ví dụ như ./fd 123)
Và nếu như read thực thi thành công thì sẽ yêu cầu bạn nhập input và lưu vào biến buf, sau đó đem đi so sánh với chuỗi LETMEWIN\n là đọc được flag.
tìm hiểu fd thì mình biết được rằng đó là chỉ số file description, với đầu vào là input từ bàn phím thì ta cần nó là fd của STDIN tương đương với 0, argv[1] sẽ được atoi() chuyển về số nguyên và đem trừ với 0x1234 thì ta chỉ cần gọi file với tham số đầu là 4660 là được.
Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
from pwn import * import sys
### USING : python3 solve.py fd
p = ssh( user=sys.argv[1], host='pwnable.kr', port=2222, password='guest', ) sh = p.shell()
Bài này yêu cầu đầu vào dài 20 byte. Sau đó làm gì đó với input của mình rồi mới đem so với biến hashcode. Hàm check_password() nhận vào 1 chuỗi dài 20 byte, sau đó gọi 1 con trỏ int để trỏ tới 4 kí tự 1 lần.
Ví dụ mình nhập aaaabbbbccccddddeeee thì con trỏ đầu tiên sẽ trỏ tới địa chỉ mang giá trị 0x4141414141 rồi cộng số đó vào biến res. Nôm na thì là chuỗi dài 20 kí tự, lấy hex của 4 kí tự đầu ra, cộng vào res, tới 4 kí tự tiếp theo và làm hành động đó tổng là 5 lần. Vậy thì ta chỉ cần lấy biến hashcode chia ra làm 4 phần bằng nhau và cộng với phần còn lại, ghép 5 phần thành 1 chuỗi thì ta sẽ thu được 1 chuỗi khi đi qua hàm check_password() sẽ trả về giá trị bằng với hashcode. Mình không chia cho 5 vì khi lấy chuỗi 0x21DD09EC/5thì mình thấy dư nên sẽ chia 4, vừa tròn đẹp :>
Payload
Để cho tiện thì mình in chuỗi đó ra rồi thêm trực tiếp vào chương trình bằng thủ công :
Với biến overflowme được khai báo là 32 byte nhưng gọi tới hàm gets() - một hàm nhập không có điểm dừng :v Nên chỉ cần mình khi tràn xuống biến key ở dưới là được. Chương trình được cho là x86 có nghĩa là tham số key sẽ nằm trên stack khi được khai báo và địa chỉ sẽ nằm cao hơn địa chỉ của biến local của hàm func ⇒ Biến overflowme có thể thay đổi giá trị của biến key khi ghi tràn.
Mommy told me to make a passcode based login system. My first trial C implementation compiled without any error! Well, there were some compiler warnings, but who cares about that?
voidwelcome(){ char name[100]; printf("enter you name : "); scanf("%100s", name); printf("Welcome %s!\n", name); }
intmain(){ printf("Toddler's Secure Login System 1.1 beta.\n");
welcome(); login();
// something after login... printf("Now I can safely trust you that you have credential :)\n"); return0; }
Checksec
1 2 3 4 5 6
Arch: i386 RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE(0x8048000) Stripped: No
Solution
Chạy chương trình và nhập vào biến thứ 2 thì gặp lỗi Segment fault nên phân tích code thì rõ là đoạn code đã code sai đoạn input passcode1 và passcode2 là thay vì truyền vào địa chỉ của 2 biến thì lại truyền vào giá trị, có nghĩa là khi chạy thì nó sẽ lấy giá trị nào nó trên thanh stack để nhập vào. Điều hay là chương trình vô tình nhập được vào passcode1 (author setup :v). Việc nhập vào được vẫn là khả thi nếu như giá trị được trỏ tới là một địa chỉ hợp lệ (một địa chỉ đang tồn tại trong chương trình và có quyền ghi). Và khi debug sẽ thấy passcode1 được nhận 1 địa chỉ hợp lệ, passcode2 nhận một địa chỉ rác (nó là canary nhưng ta không cần quan tâm, sự xuất hiện của nó giúp mình nhìn thấy được các giá trị khác). Và khi debug hàm welcome sẽ thấy 2 giá trị này cũng tồn tại trên stack khi chương tình bảo nhập tên. Thực tế thì 2 giá trị đó đều là giá trị của thanh stack cũ.
Nhập 95 kí tự vào name và kiểm tra trạng thái stack sẽ thấy được 1 địa chỉ hợp lệ và 1 giá trị hợp lệ khi gọi hàm welcome và nhập tên (ảnh 1)
Trạng thái stack của hàm welcome khi nhập 95 kí tự
Khi đó debug cũng sẽ thấy các giá trị này được gọi tới như tham số thứ 2 của hàm scanf
Các tham số được gọi khi gọi hàm scanf cho giá trị của passcode1
Các tham số được gọi khi gọi hàm scanf cho giá trị của passcode2
Nhập tên dài hơn 4 kí tự sẽ khi đè địa chỉ đó, vậy có nghĩa là ta có thể điều khiển địa chỉ nhập vào của passcode1. Vì không có bảo vệ RELRO và địa chỉ PIE tĩnh nên ta sẽ tấn công địa chỉ GOT của 1 hàm nào sau đó khi nhập xong passcode1. Mình chọn hàm fflush là mục tiêu vì là chọn hàm gần nhất thì giảm rủi ro chương trình bị lỗi.
printf("Wrong, maybe you should try 2^32 cases.\n"); return0; }
Solution
Với rand() mà không có seed truyền vào thì nó không thực sự random mà mình có thể biết được đầu ra bằng cách chạy 1 đoạn code với hàm rand() thì sẽ thu được giá trị giống như chương trình. Vậy thì chỉ cần viết lại 1 chương trình khác in ra hàm rand() 1 lần và lấy giá trị đó xor với 0xcafebabe là sẽ thu được kết quả.
Payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
from pwn import * import sys
p = ssh( user=sys.argv[1], host='pwnable.kr', port=2222, password='guest', ) sh = p.shell() sh.recvuntil(f'{sys.argv[1]}@ubuntu:~$')
intmain(int argc, char* argv[], char* envp[]){ printf("Welcome to pwnable.kr\n"); printf("Let's see if you know how to give input to program\n"); printf("Just give me correct inputs then you will get the flag :)\n");
// here's your flag setregid(getegid(), getegid()); system("/bin/cat flag"); return0; }
Solution
Bài này hay ở chỗ nó dạy mình biết rằng các đầu vào của một chương trình có thể lấy như từ biến môi trường, file, stdin,… Cách giải thì chỉ cần research những đầu vào tương ứng như trong source là được. Ở đây mình cần dùng script để setup các tham số, các biến, và các pipline để bypass các stage.
Stage 1 : Argc là số lượng tham số bạn truyền vào, với tham số đầu tiên luôn là tên file. Tham số argv với argv[’A’] tương đương argv[65]. Được truyền vào như sau : ./file a b c với a b c là các tham số argv.
Stage 2 : với fd=0 sẽ là stdin và fd=2 là stderr, ở đây ta cần dùng script để tạo 1 luồng khác, gán stderr vào luồng đó và ghi vào đó giá trị tương ứng.
Stage 3 : Setup biến môi trường.
Stage 4 : Setup file. Bạn có thể tạo và file bằng python hoặc bằng bash script.
Stage 5 : Là stage dùng socket để gửi nhận giá trị, chỉ cần dùng pwntool để mở 1 luồng remote tới chương trình là ok.
######### STAGE 4 ######### with open('\x0a', 'wb') as f: # Ghi dữ liệu vào tệp f.write(b'\x00\x00\x00\x00') # một cách khác đó là tạo 1 file bằng bash : touch $'\x0a' và dùng công cụ như HxD thêm 4 byte \x00 vào file là được
Chương trình gốc hiểu đơn giản là lấy 3 giá trị của 3 hàm key tính toán rồi so với input của mình. Mục tiêu là tính 3 giá trị đó. Không nhận được binary mà là 1 file asm và với địa chỉ như thế mình nghĩ chương trình là địa chỉ tĩnh. Nên mọi thứ có thể tính được dựa vào file asm.
Với các hàm key thì mình thấy nó không được gọi lệnh trả về nhưng vẫn thu được giá trị. Mình nôm na nghĩ tới việc giá trị sẽ được gán vào thanh ghi nào đó tương tự như EAX → File asm sẽ có ích cho việc này. Thì qua tìm hiểu mình biết được rằng giá trị trả về sẽ được gán vào thanh ghi r0, vậy thì với mỗi hàm key mình sẽ follow giá trị r0 là được.
key1() : pc là thanh ghi trỏ tới địa chỉ của câu lệnh hiện tại + 8, khi chạy tới địa chỉ 0x00008cdc thì thanh ghi pc= 0x00008cdc+8 = 0x8ce4 rồi gán cho r3 và rồi gán cho r0
→ r0 = 0x8ce4
1 2
0x00008cdc <+8>: mov r3, pc 0x00008ce0 <+12>: mov r0, r3
key2() : với 2 dòng đầu tiên có chức năng đổi mode hiện tại là ARM sang THUMB, thì pc sẽ bước ngắn hơn, giá trị pc sẽ là +4 thay vì +8. Thêm lệnh adds là +4 cho r3 trước khi gán cho r0
key3() : lr là thanh ghi trỏ tới địa chỉ trả về (giống như 1 thanh ghi mang giá trị RIP). Thì sau khi hàm key3 thực thi xong, nó sẽ tiếp tục trỏ tới 1 lệnh nào đó trong main thì lr sẽ mang giá trị đó.
→ r0 = 0x00008d80
1 2
0x00008d28 <+8>: mov r3, lr 0x00008d2c <+12>: mov r0, r3
Tổng kết giá trị ta có : 0x8ce4 + 0x8d0c + 0x00008d80 = 0x1a770
Mistake
Description
1 2 3 4 5
We all make mistakes, let's move on. (don't take this too seriously, no fancy hacking skill is required at all) This task is based on real event
Lỗi là ở đây : if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0)
Thứ tự thực thi đã bị sai khi nó gọi open xong thì tới phép so sánh < rồi mới tới gán giá trị cho fd.
thành ra khi mở thành công thì sẽ trả về giá trị file descriptor của file vừa mở, đem đi so sánh với 0, giả sử file descriptor ở đây là 3 thì 3 < 0 là sai → trả về 0 và gán 0 cho biến fd, biến câu lệnh sau thành lệnh đọc input từ stdin :
if(!(len=read(fd,pw_buf,PW_LEN) > 0))
Rồi thì với logic là pw_buf là biến do mình kiểm soát thay vì trong file password, đem so sánh với biến pw_buf2 được qua phép xor của mình thì cả 2 giá trị đều do mình kiểm soát. Thế là xong
Payload
ở đây mình chạy lại output của hàm xor với 1 chuỗi dài 10 kí tự bất kì để thu output. Khi đó chạy chương trình và nhập cả 2 giá trị vào thoi :>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include<stdio.h> #include<string.h> #define XORKEY 1 voidxor(char *s, int len) { int i; for (i = 0; i < len; i++) { s[i] ^= XORKEY; } } intmain() { char d[] = "1234567890"; char *c = d; xor(d, strlen(d)); printf("%s", d); //0325476981
}
Ôk hết rồi, hẹn các bạn phần sau :> Phần sau mình sẽ clear toàn bộ Todder :>