From 894afa96cd14a84cd1a1bcfb9523f10210aebb7c Mon Sep 17 00:00:00 2001 From: Dante Catalfamo Date: Mon, 18 Oct 2021 17:36:47 -0400 Subject: bsd-auth: no longer WIP --- .../WIP-how-bsd-authentication-works/gen_dot.rb | 99 - .../WIP-how-bsd-authentication-works/graph.dot | 152 -- .../WIP-how-bsd-authentication-works/graph.svg | 856 ------ .../WIP-how-bsd-authentication-works/index.org | 2811 -------------------- .../WIP-how-bsd-authentication-works/notes.org | 83 - .../openbsd_internals.gif | Bin 690203 -> 0 bytes 6 files changed, 4001 deletions(-) delete mode 100755 content/posts/WIP-how-bsd-authentication-works/gen_dot.rb delete mode 100644 content/posts/WIP-how-bsd-authentication-works/graph.dot delete mode 100644 content/posts/WIP-how-bsd-authentication-works/graph.svg delete mode 100644 content/posts/WIP-how-bsd-authentication-works/index.org delete mode 100644 content/posts/WIP-how-bsd-authentication-works/notes.org delete mode 100644 content/posts/WIP-how-bsd-authentication-works/openbsd_internals.gif (limited to 'content/posts/WIP-how-bsd-authentication-works') diff --git a/content/posts/WIP-how-bsd-authentication-works/gen_dot.rb b/content/posts/WIP-how-bsd-authentication-works/gen_dot.rb deleted file mode 100755 index 9f71876..0000000 --- a/content/posts/WIP-how-bsd-authentication-works/gen_dot.rb +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Copyright (C) 2021 Dante Catalfamo -# SPDX-License-Identifier: MIT - -require 'digest' -require 'set' - -SOURCE_DIR = File.join Dir.home, 'src', 'github.com', 'openbsd', 'src', 'lib', 'libc', 'gen' -FILENAMES = %w[authenticate.c auth_subr.c login_cap.c].freeze - -FUNCTION_REGEX = /^\w.*?$\n(?!DEF)(\w*)\(.*?\)\n\{(.*?)^\}/m.freeze -ONELINE_FUNCTION_REFGEX = /^\w.*?(\w*)\(.*?\).*?\{(.*?)\}/.freeze -CALL_REGEX = /[^\n](\w+)\(.*?\)/.freeze - -class FunctionDigraph - attr_accessor :pairs, :subgraphs - - class Subgraph - attr_accessor :name, :label, :functions - - def initialize(name, label) - @name = name - @label = label - @functions = [] - end - - def emit - puts "subgraph cluster_#{name} {" - puts "label = \"#{label}\"" - functions.each { |f| puts f unless f == 'DEF_WEAK' } - puts '}' - end - end - - class Pair - attr_accessor :to, :from - - def initialize(from, to) - @from = from - @to = to - end - - def emit - puts "#{from} -> #{to} [color = \"##{color}\"]" - end - - def color - Digest::MD5.hexdigest(from)[..5] - end - end - - def initialize - @pairs = [] - @subgraphs = [] - end - - def emit - puts 'digraph G {' - puts 'rankdir=LR' - puts 'splines=ortho' - puts 'graph [pad="0.5", nodesep="0.5", ranksep="1.5"]' - all_functions = Set.new - @subgraphs.each { |s| all_functions.merge(s.functions) } - @subgraphs.each(&:emit) - @pairs.uniq { |p| [p.to, p.from] }.each do |p| - p.emit if all_functions.include?(p.to) - end - puts '}' - end - - def parse_files(filenames) - filenames.each do |filename| - contents = File.read(filename) - basename = File.basename filename - subgraph = Subgraph.new(basename.gsub(/\..*/, ''), basename) - functions = contents.scan(FUNCTION_REGEX) - oneliners = contents.scan(ONELINE_FUNCTION_REFGEX) - functions.concat(oneliners) unless oneliners.empty? - functions.each do |function| - function_name = function[0] - function_body = function[1] - subgraph.functions << function_name - function_body.scan(CALL_REGEX) do |call| - @pairs << Pair.new(function_name, call[0]) - end - end - @subgraphs << subgraph - end - end -end - -fg = FunctionDigraph.new - -files = FILENAMES.map { |f| File.join(SOURCE_DIR, f) } -fg.parse_files files - -fg.emit diff --git a/content/posts/WIP-how-bsd-authentication-works/graph.dot b/content/posts/WIP-how-bsd-authentication-works/graph.dot deleted file mode 100644 index 9a2be3c..0000000 --- a/content/posts/WIP-how-bsd-authentication-works/graph.dot +++ /dev/null @@ -1,152 +0,0 @@ -digraph G { -rankdir=LR -splines=ortho -graph [pad="0.5", nodesep="0.5", ranksep="1.5"] -subgraph cluster_authenticate { -label = "authenticate.c" -auth_mkvalue -auth_checknologin -_auth_checknologin -auth_cat -_auth_validuser -auth_approval -auth_usercheck -auth_userokay -auth_userchallenge -auth_userresponse -auth_verify -} -subgraph cluster_auth_subr { -label = "auth_subr.c" -auth_open -auth_clean -auth_close -auth_challenge -auth_setenv -auth_clrenv -auth_getitem -auth_setitem -auth_setoption -auth_clroptions -auth_clroption -auth_setdata -auth_setpwd -auth_getvalue -auth_check_expire -auth_check_change -auth_call -_recv_fd -_auth_spool -_add_rmlist -_auth_next_arg -auth_setstate -auth_set_va_list -auth_getstate -auth_getpwd -} -subgraph cluster_login_cap { -label = "login_cap.c" -login_getclass -login_getstyle -login_getcapstr -login_getcaptime -login_getcapnum -login_getcapsize -login_getcapbool -login_close -gsetrl -setclasscontext -setusercontext -setuserpath -setuserenv -login_setenv -strtosize -strtolimit -multiply -secure_path -expandstr -} -auth_checknologin -> _auth_checknologin [color = "#f0bfd8"] -_auth_checknologin -> login_getcapbool [color = "#f51748"] -_auth_checknologin -> login_getcapstr [color = "#f51748"] -_auth_checknologin -> auth_cat [color = "#f51748"] -auth_approval -> auth_getitem [color = "#8f94eb"] -auth_approval -> auth_getpwd [color = "#8f94eb"] -auth_approval -> _auth_validuser [color = "#8f94eb"] -auth_approval -> login_getclass [color = "#8f94eb"] -auth_approval -> login_close [color = "#8f94eb"] -auth_approval -> login_getcapstr [color = "#8f94eb"] -auth_approval -> auth_open [color = "#8f94eb"] -auth_approval -> auth_setstate [color = "#8f94eb"] -auth_approval -> auth_setitem [color = "#8f94eb"] -auth_approval -> auth_check_expire [color = "#8f94eb"] -auth_approval -> login_getcapbool [color = "#8f94eb"] -auth_approval -> auth_call [color = "#8f94eb"] -auth_approval -> auth_close [color = "#8f94eb"] -auth_approval -> auth_getstate [color = "#8f94eb"] -auth_usercheck -> _auth_validuser [color = "#96fc6d"] -auth_usercheck -> login_getclass [color = "#96fc6d"] -auth_usercheck -> login_getstyle [color = "#96fc6d"] -auth_usercheck -> login_close [color = "#96fc6d"] -auth_usercheck -> auth_open [color = "#96fc6d"] -auth_usercheck -> auth_setitem [color = "#96fc6d"] -auth_usercheck -> auth_setdata [color = "#96fc6d"] -auth_usercheck -> auth_verify [color = "#96fc6d"] -auth_userokay -> auth_usercheck [color = "#3eb09b"] -auth_userokay -> auth_close [color = "#3eb09b"] -auth_userchallenge -> _auth_validuser [color = "#0db8d4"] -auth_userchallenge -> login_getclass [color = "#0db8d4"] -auth_userchallenge -> login_getstyle [color = "#0db8d4"] -auth_userchallenge -> auth_open [color = "#0db8d4"] -auth_userchallenge -> login_close [color = "#0db8d4"] -auth_userchallenge -> auth_setitem [color = "#0db8d4"] -auth_userchallenge -> auth_close [color = "#0db8d4"] -auth_userchallenge -> auth_challenge [color = "#0db8d4"] -auth_userresponse -> auth_setstate [color = "#cc7ac2"] -auth_userresponse -> auth_getitem [color = "#cc7ac2"] -auth_userresponse -> _auth_validuser [color = "#cc7ac2"] -auth_userresponse -> auth_close [color = "#cc7ac2"] -auth_userresponse -> auth_setdata [color = "#cc7ac2"] -auth_userresponse -> auth_getstate [color = "#cc7ac2"] -auth_userresponse -> auth_check_expire [color = "#cc7ac2"] -auth_verify -> auth_open [color = "#83192f"] -auth_verify -> auth_setstate [color = "#83192f"] -auth_verify -> auth_setitem [color = "#83192f"] -auth_verify -> auth_getitem [color = "#83192f"] -auth_verify -> _auth_validuser [color = "#83192f"] -auth_verify -> auth_set_va_list [color = "#83192f"] -auth_verify -> auth_call [color = "#83192f"] -auth_clean -> auth_clrenv [color = "#759227"] -auth_clean -> auth_setitem [color = "#759227"] -auth_close -> auth_setenv [color = "#4a5505"] -auth_challenge -> _auth_validuser [color = "#5e3ac3"] -auth_challenge -> auth_getvalue [color = "#5e3ac3"] -auth_setitem -> auth_setitem [color = "#e67693"] -auth_setitem -> _auth_validuser [color = "#e67693"] -auth_check_expire -> auth_setpwd [color = "#739550"] -auth_check_change -> auth_setpwd [color = "#902d9d"] -auth_call -> _auth_next_arg [color = "#a9e6c9"] -auth_call -> _auth_spool [color = "#a9e6c9"] -auth_call -> _add_rmlist [color = "#a9e6c9"] -auth_call -> auth_clrenv [color = "#a9e6c9"] -_auth_spool -> _recv_fd [color = "#977e1c"] -login_getstyle -> login_getcapstr [color = "#51a344"] -login_getcapsize -> strtolimit [color = "#97959e"] -gsetrl -> login_getcaptime [color = "#35d53a"] -gsetrl -> login_getcapsize [color = "#35d53a"] -gsetrl -> login_getcapnum [color = "#35d53a"] -setclasscontext -> login_getclass [color = "#5b8e44"] -setclasscontext -> setusercontext [color = "#5b8e44"] -setclasscontext -> login_close [color = "#5b8e44"] -setusercontext -> login_getclass [color = "#7eb75f"] -setusercontext -> login_close [color = "#7eb75f"] -setusercontext -> login_getcapnum [color = "#7eb75f"] -setusercontext -> setuserenv [color = "#7eb75f"] -setusercontext -> setuserpath [color = "#7eb75f"] -setuserpath -> login_setenv [color = "#35ed7d"] -setuserenv -> login_setenv [color = "#a1aa6a"] -login_setenv -> expandstr [color = "#502c54"] -strtosize -> multiply [color = "#a61402"] -strtosize -> strtosize [color = "#a61402"] -strtolimit -> strtosize [color = "#c987ba"] -} diff --git a/content/posts/WIP-how-bsd-authentication-works/graph.svg b/content/posts/WIP-how-bsd-authentication-works/graph.svg deleted file mode 100644 index 94f22d2..0000000 --- a/content/posts/WIP-how-bsd-authentication-works/graph.svg +++ /dev/null @@ -1,856 +0,0 @@ - - - - - - -G - - -cluster_authenticate - -authenticate.c - - -cluster_auth_subr - -auth_subr.c - - -cluster_login_cap - -login_cap.c - - - -auth_mkvalue - -auth_mkvalue - - - -auth_checknologin - -auth_checknologin - - - -_auth_checknologin - -_auth_checknologin - - - -auth_checknologin->_auth_checknologin - - - - - -auth_cat - -auth_cat - - - -_auth_checknologin->auth_cat - - - - - -login_getcapstr - -login_getcapstr - - - -_auth_checknologin->login_getcapstr - - - - - -login_getcapbool - -login_getcapbool - - - -_auth_checknologin->login_getcapbool - - - - - -_auth_validuser - -_auth_validuser - - - -auth_approval - -auth_approval - - - -auth_approval->_auth_validuser - - - - - -auth_open - -auth_open - - - -auth_approval->auth_open - - - - - -auth_close - -auth_close - - - -auth_approval->auth_close - - - - - -auth_getitem - -auth_getitem - - - -auth_approval->auth_getitem - - - - - -auth_setitem - -auth_setitem - - - -auth_approval->auth_setitem - - - - - -auth_check_expire - -auth_check_expire - - - -auth_approval->auth_check_expire - - - - - -auth_call - -auth_call - - - -auth_approval->auth_call - - - - - -auth_setstate - -auth_setstate - - - -auth_approval->auth_setstate - - - - - -auth_getstate - -auth_getstate - - - -auth_approval->auth_getstate - - - - - -auth_getpwd - -auth_getpwd - - - -auth_approval->auth_getpwd - - - - - -login_getclass - -login_getclass - - - -auth_approval->login_getclass - - - - - -auth_approval->login_getcapstr - - - - - -auth_approval->login_getcapbool - - - - - -login_close - -login_close - - - -auth_approval->login_close - - - - - -auth_usercheck - -auth_usercheck - - - -auth_usercheck->_auth_validuser - - - - - -auth_verify - -auth_verify - - - -auth_usercheck->auth_verify - - - - - -auth_usercheck->auth_open - - - - - -auth_usercheck->auth_setitem - - - - - -auth_setdata - -auth_setdata - - - -auth_usercheck->auth_setdata - - - - - -auth_usercheck->login_getclass - - - - - -login_getstyle - -login_getstyle - - - -auth_usercheck->login_getstyle - - - - - -auth_usercheck->login_close - - - - - -auth_userokay - -auth_userokay - - - -auth_userokay->auth_usercheck - - - - - -auth_userokay->auth_close - - - - - -auth_userchallenge - -auth_userchallenge - - - -auth_userchallenge->_auth_validuser - - - - - -auth_userchallenge->auth_open - - - - - -auth_userchallenge->auth_close - - - - - -auth_challenge - -auth_challenge - - - -auth_userchallenge->auth_challenge - - - - - -auth_userchallenge->auth_setitem - - - - - -auth_userchallenge->login_getclass - - - - - -auth_userchallenge->login_getstyle - - - - - -auth_userchallenge->login_close - - - - - -auth_userresponse - -auth_userresponse - - - -auth_userresponse->_auth_validuser - - - - - -auth_userresponse->auth_close - - - - - -auth_userresponse->auth_getitem - - - - - -auth_userresponse->auth_setdata - - - - - -auth_userresponse->auth_check_expire - - - - - -auth_userresponse->auth_setstate - - - - - -auth_userresponse->auth_getstate - - - - - -auth_verify->_auth_validuser - - - - - -auth_verify->auth_open - - - - - -auth_verify->auth_getitem - - - - - -auth_verify->auth_setitem - - - - - -auth_verify->auth_call - - - - - -auth_verify->auth_setstate - - - - - -auth_set_va_list - -auth_set_va_list - - - -auth_verify->auth_set_va_list - - - - - -auth_clean - -auth_clean - - - -auth_clrenv - -auth_clrenv - - - -auth_clean->auth_clrenv - - - - - -auth_clean->auth_setitem - - - - - -auth_setenv - -auth_setenv - - - -auth_close->auth_setenv - - - - - -auth_challenge->_auth_validuser - - - - - -auth_getvalue - -auth_getvalue - - - -auth_challenge->auth_getvalue - - - - - -auth_setitem->_auth_validuser - - - - - -auth_setitem->auth_setitem - - - - - -auth_setoption - -auth_setoption - - - -auth_clroptions - -auth_clroptions - - - -auth_clroption - -auth_clroption - - - -auth_setpwd - -auth_setpwd - - - -auth_check_expire->auth_setpwd - - - - - -auth_check_change - -auth_check_change - - - -auth_check_change->auth_setpwd - - - - - -auth_call->auth_clrenv - - - - - -_auth_spool - -_auth_spool - - - -auth_call->_auth_spool - - - - - -_add_rmlist - -_add_rmlist - - - -auth_call->_add_rmlist - - - - - -_auth_next_arg - -_auth_next_arg - - - -auth_call->_auth_next_arg - - - - - -_recv_fd - -_recv_fd - - - -_auth_spool->_recv_fd - - - - - -login_getstyle->login_getcapstr - - - - - -login_getcaptime - -login_getcaptime - - - -login_getcapnum - -login_getcapnum - - - -login_getcapsize - -login_getcapsize - - - -strtolimit - -strtolimit - - - -login_getcapsize->strtolimit - - - - - -gsetrl - -gsetrl - - - -gsetrl->login_getcaptime - - - - - -gsetrl->login_getcapnum - - - - - -gsetrl->login_getcapsize - - - - - -setclasscontext - -setclasscontext - - - -setclasscontext->login_getclass - - - - - -setclasscontext->login_close - - - - - -setusercontext - -setusercontext - - - -setclasscontext->setusercontext - - - - - -setusercontext->login_getclass - - - - - -setusercontext->login_getcapnum - - - - - -setusercontext->login_close - - - - - -setuserpath - -setuserpath - - - -setusercontext->setuserpath - - - - - -setuserenv - -setuserenv - - - -setusercontext->setuserenv - - - - - -login_setenv - -login_setenv - - - -setuserpath->login_setenv - - - - - -setuserenv->login_setenv - - - - - -expandstr - -expandstr - - - -login_setenv->expandstr - - - - - -strtosize - -strtosize - - - -strtosize->strtosize - - - - - -multiply - -multiply - - - -strtosize->multiply - - - - - -strtolimit->strtosize - - - - - -secure_path - -secure_path - - - diff --git a/content/posts/WIP-how-bsd-authentication-works/index.org b/content/posts/WIP-how-bsd-authentication-works/index.org deleted file mode 100644 index f0623c2..0000000 --- a/content/posts/WIP-how-bsd-authentication-works/index.org +++ /dev/null @@ -1,2811 +0,0 @@ -#+TITLE: How BSD Authentication Works -#+DATE: 2021-10-18T17:27:13-04:00 -#+DRAFT: true -#+SHOWTOC: true -#+DESCRIPTION: A walkthrough of how OpenBSD's BSD Auth framework functions -#+TAGS[]: openbsd security -#+KEYWORDS[]: openbsd security -#+SLUG: -#+SUMMARY: - -#+ATTR_HTML: :title OpenBSD Internals -#+ATTR_HTML: :alt OpenBSD mascot cutaway view with spinning gears inside -[[file:openbsd_internals.gif]] - -* History - :PROPERTIES: - :CUSTOM_ID: history - :END: - - The way OpenBSD authenticates users is quite different from other - Unix-like operating systems. Most other systems like AIX, Solaris, - Linux, the other BSDs, and MacOS, use a framework called [[https://en.wikipedia.org/wiki/Pluggable_authentication_module][Pluggable - Authentication Module]] (PAM). The two main implementations are [[http://www.linux-pam.org/][Linux - PAM]] and [[https://www.openpam.org/][OpenPAM]]. PAM modules are created as dynamically loaded - shared objects, which communicate using a combination of common and - implementation specific interfaces ([[https://linux.die.net/man/3/pam][Linux-PAM]] and [[https://www.freebsd.org/cgi/man.cgi?query=pam&apropos=0&sektion=3&manpath=FreeBSD+12.1-RELEASE+and+Ports&arch=default&format=html][OpenPAM]]). It's - configured using the [[https://linux.die.net/man/5/pam.d][pam.d]] directory and [[https://www.freebsd.org/cgi/man.cgi?query=pam.conf&sektion=5&apropos=0&manpath=FreeBSD+12.1-RELEASE+and+Ports][pam.conf]] file. While it can - be flexible, it's highly complex and very easy to mis-configure, - leaving you open to strange and hard to track down authentication - bugs. On top of that, the fact that it's a shared library means that - any vulnerability in a poorly vetted authentication module gives - attackers direct access to the internals of your application. Author - Michael W. Lucas said it best when he described PAM as - [[https://www.youtube.com/watch?v=-CXp3byvI1g][unstandardized black magic]]. - - OpenBSD on the other hand uses a mechanism called BSD - Authentication. It was originally developed for a now-defunct - proprietary operating system called [[https://en.wikipedia.org/wiki/BSD/OS][BSD/OS]] by [[https://en.wikipedia.org/wiki/Berkeley_Software_Design][Berkeley Software - Design Inc.]], who later donated the system. It was then adopted by - OpenBSD in release 2.9. BSD Auth is comparatively much simpler than - PAM. Modules or, authentication "styles", are instead stand alone - applications or scripts that communicate over IPC. The module has no - ability to interfere with the parent and can very easily revoke - permissions using [[https://man.openbsd.org/pledge][=pledge(2)=]] or [[https://man.openbsd.org/unveil][=unveil(2)=]]. The BSD Authentication - system of configured through [[https://man.openbsd.org/login.conf][=login.conf(5)=]]. - -* Documentation - :PROPERTIES: - :CUSTOM_ID: documentation - :END: - - All of the high level authentication functions are described in - [[https://man.openbsd.org/authenticate][=authenticate(3)=]], with the lower level functions being described in - [[https://man.openbsd.org/auth_subr][=auth_subr(3)=]]. - - Click on any function prototype in this post to see its definition. - - I've also created a [[#graph][graph]] at the bottom of the post to help - visualize the function calls. - - All code snippets from this blog post belong to the OpenBSD - contributors. Please see the [[#copyright][Copyright]] section for details. - -* BSD Auth Modules - :PROPERTIES: - :CUSTOM_ID: modules - :END: - - Modules are located in =/usr/libexec/auth/= with the naming - convention =login_ -#+end_export diff --git a/content/posts/WIP-how-bsd-authentication-works/notes.org b/content/posts/WIP-how-bsd-authentication-works/notes.org deleted file mode 100644 index 9bd67d4..0000000 --- a/content/posts/WIP-how-bsd-authentication-works/notes.org +++ /dev/null @@ -1,83 +0,0 @@ -* Notes - https://web.archive.org/web/20170327150148/http://www.penzin.net/bsdauth/ - - In the man page for [[https://man.openbsd.org/auth_subr.3#auth_call][=auth_call=]] it says - #+begin_src text - path The full path name of the login script to run. The call will - fail if path does not pass the requirements of the secure_path(3) - function. - #+end_src - - However I don't see this enforced anywhere, I even wrote a small test - script to prove it. - - #+CAPTION: =authfail.c= - #+begin_src c - #include - #include - #include - #include - - int main(void) { - auth_session_t *as; - - as = auth_open(); - auth_call(as, "/home/dante/auth_tests/authtest/test", "hello", NULL); - auth_close(as); - } - #+end_src - - Changing ="/home/dante/auth_tests/authtest/test"= to the location - of the =test= binary. - - #+CAPTION: =test.c= - #+begin_src c - #include - - int main(void) { - printf("Hello! I don't have a secure path!\n"); - return 0; - } - #+end_src - - #+CAPTION: =Makefile= - #+begin_src makefile - CFLAGS = -Wall -Wextra - - run: authfail test - ./authfail - - authfail: authfail.c - $(CC) -o $@ $(CFLAGS) $< - - test: test.c - $(CC) -o $@ $(CFLAGS) $< - #+end_src - - Which results in the following: - - #+begin_src text - $ pwd && ls -l && make - /home/dante/auth_tests/authtest - total 12 - -rw-r--r-- 1 dante dante 143 May 30 19:20 Makefile - -rw-r--r-- 1 dante dante 248 May 29 19:30 authfail.c - -rw-r--r-- 1 dante dante 115 May 29 19:22 test.c - cc -o authfail -Wall -Wextra authfail.c - cc -o test -Wall -Wextra test.c - ./authfail - Hello! I don't have a secure path! - #+end_src - - - The manpage also says the path is limited to =/bin/= and =/usr/bin=, - which is also not the case. - - - The man page describes the interface for =auth_getitem= is in the - format of =AUTH_=, but in reality it is =AUTHV_=. - - # Ask jcs about the file descriptor situation, I don't understand it - # after reading both the man page and source. - - - The [[#auth_getchallenge][=auth_getchallenge=]] function in the [[https://man.openbsd.org/auth_subr.3#auth_getchallenge][=auth_subr(3)=]] man page - doesn't seem to exist in the source code. - -** TODO How are these configured in login.conf? diff --git a/content/posts/WIP-how-bsd-authentication-works/openbsd_internals.gif b/content/posts/WIP-how-bsd-authentication-works/openbsd_internals.gif deleted file mode 100644 index 5088082..0000000 Binary files a/content/posts/WIP-how-bsd-authentication-works/openbsd_internals.gif and /dev/null differ -- cgit v1.2.3